54U2NPHGIPLDQBJW6AFRUWHP7WTOCMZ2YCESNHNUOK3JPJTTHHHQC leanprover/lean4:v4.28.0
import Lakeopen Lake DSLpackage blikulean_lib Bliku
{"version": "1.1.0","packagesDir": ".lake/packages","packages": [],"name": "bliku","lakeDir": ".lake"}
import Bliku.Tui
import Bliku.Tui.Primitivesimport Bliku.Tui.Searchimport Bliku.Tui.Syntaximport Bliku.Tui.Terminalimport Bliku.Tui.Windowimport Bliku.Tui.Modelimport Bliku.Tui.Overlayimport Bliku.Tui.Render
import Bliku.Tui.Primitivesnamespace Bliku.Tuistructure Cursor whererow : Natcol : Natderiving Repr, BEq, Inhabitedstructure ViewState wherebufferId : Natcursor : CursorscrollRow : NatscrollCol : Natderiving Repr, Inhabitedinductive Layout where| window (id : Nat) (view : ViewState)| hsplit (left right : Layout) (ratio : Float)| vsplit (top bottom : Layout) (ratio : Float)deriving Repr, Inhabitedstructure FloatingWindowBorderFlags wheretop : Bool := trueright : Bool := truebottom : Bool := trueleft : Bool := truederiving Inhabitedstructure WorkspaceView wherename : Stringlayout : LayoutactiveWindowId : NatfloatingWindows : List Nat := []floatingWindowOffsets : List (Nat × Int × Int) := []deriving Inhabiteddef Layout.findView (l : Layout) (id : Nat) : Option ViewState :=match l with| .window wid v => if wid == id then some v else none| .hsplit left right _ => (left.findView id).orElse (fun _ => right.findView id)| .vsplit top bottom _ => (top.findView id).orElse (fun _ => bottom.findView id)def Layout.updateView (l : Layout) (id : Nat) (f : ViewState → ViewState) : Layout :=let rec go (node : Layout) : Layout × Bool :=match node with| .window wid v =>if wid == id then (.window wid (f v), true) else (node, false)| .hsplit left right ratio =>let (left', foundLeft) := go leftif foundLeft then(.hsplit left' right ratio, true)elselet (right', foundRight) := go right(.hsplit left' right' ratio, foundRight)| .vsplit top bottom ratio =>let (top', foundTop) := go topif foundTop then(.vsplit top' bottom ratio, true)elselet (bottom', foundBottom) := go bottom(.vsplit top' bottom' ratio, foundBottom)(go l).1def Layout.containsWindow (l : Layout) (windowId : Nat) : Bool :=match l with| .window wid _ => wid == windowId| .hsplit left right _ => left.containsWindow windowId || right.containsWindow windowId| .vsplit top bottom _ => top.containsWindow windowId || bottom.containsWindow windowIddef Layout.remove (l : Layout) (id : Nat) : Option Layout :=match l with| .window wid _ => if wid == id then none else some l| .hsplit left right ratio =>match left.remove id with| none => some right| some left' =>match right.remove id with| none => some left'| some right' => some (.hsplit left' right' ratio)| .vsplit top bottom ratio =>match top.remove id with| none => some bottom| some top' =>match bottom.remove id with| none => some top'| some bottom' => some (.vsplit top' bottom' ratio)def getWindowIds (l : Layout) : List Nat :=match l with| .window id _ => [id]| .hsplit left right _ => getWindowIds left ++ getWindowIds right| .vsplit top bottom _ => getWindowIds top ++ getWindowIds bottomdef getAllWindowBounds (l : Layout) (h w : Nat) : List (Nat × Nat × Nat × Nat × Nat) :=let rec traverse (node : Layout) (r c h w : Nat) (acc : List (Nat × Nat × Nat × Nat × Nat)) :=match node with| .window id _ => (id, r, c, h, w) :: acc| .hsplit left right ratio =>let leftW := (Float.ofNat w * ratio).toUInt64.toNatlet acc' := traverse left r c h leftW acctraverse right r (c + leftW + 1) h (if w > leftW then w - leftW - 1 else 0) acc'| .vsplit top bottom ratio =>let topH := (Float.ofNat h * ratio).toUInt64.toNatlet acc' := traverse top r c topH w acctraverse bottom (r + topH + 1) c (if h > topH then h - topH - 1 else 0) w acc'traverse l 0 0 h w []def WorkspaceView.isFloatingWindow (ws : WorkspaceView) (windowId : Nat) : Bool :=ws.floatingWindows.contains windowIddef WorkspaceView.getFloatingWindowIds (ws : WorkspaceView) : Array Nat :=ws.floatingWindows.foldl (fun acc wid =>match ws.layout.findView wid with| some _ => acc.push wid| none => acc) #[]def WorkspaceView.floatingWindowIndex? (ws : WorkspaceView) (windowId : Nat) : Option Nat :=let ids := ws.getFloatingWindowIdslet rec loop (i : Nat) : Option Nat :=if i >= ids.size then noneelse if ids[i]! == windowId then some ielse loop (i + 1)loop 0def WorkspaceView.getFloatingWindowOffset (ws : WorkspaceView) (windowId : Nat) : Int × Int :=match ws.floatingWindowOffsets.find? (fun (wid, _, _) => wid == windowId) with| some (_, ro, co) => (ro, co)| none => (0, 0)def computeFloatingWindowBounds (rows cols idx : Nat) : Option (Nat × Nat × Nat × Nat) :=let availableRows := if rows > 1 then rows - 1 else rowsif availableRows < 6 || cols < 16 thennoneelselet hMax := availableRows - 2let wMax := cols - 4let h := min hMax (max 8 ((availableRows * 3) / 5))let w := min wMax (max 24 ((cols * 3) / 5))let topBase := (availableRows - h) / 2let leftBase := (cols - w) / 2let top := min (availableRows - h) (topBase + idx)let left := min (cols - w) (leftBase + (2 * idx))some (top, left, h, w)def getFloatingWindowBounds (ws : WorkspaceView) (windowHeight windowWidth : Nat) (windowId : Nat) : Option (Nat × Nat × Nat × Nat) :=match ws.floatingWindowIndex? windowId with| none => none| some idx =>match computeFloatingWindowBounds windowHeight windowWidth idx with| none => none| some (top, left, h, w) =>let (rowOff, colOff) := ws.getFloatingWindowOffset windowIdlet toNatNonNeg (v : Int) := if v < 0 then 0 else Int.toNat vlet availableRows := if windowHeight > 1 then windowHeight - 1 else windowHeightlet maxTop := if availableRows > h then availableRows - h else 0let maxLeft := if windowWidth > w then windowWidth - w else 0let rawTop := Int.ofNat top + rowOfflet rawLeft := Int.ofNat left + colOfflet top' := min maxTop (toNatNonNeg rawTop)let left' := min maxLeft (toNatNonNeg rawLeft)some (top', left', h, w)def getFloatingWindowBorderFlags (_ws : WorkspaceView) (_windowId : Nat) : FloatingWindowBorderFlags :={ top := true, right := true, bottom := true, left := true }end Bliku.Tui
namespace Bliku.Tui.Terminal/-- Get terminal size (rows, cols). -/def getWindowSize : IO (Nat × Nat) := dolet out ← IO.Process.run { cmd := "stty", args := #["-F", "/dev/tty", "size"] }let parts := out.trimAscii.toString.splitOn " "match parts with| [rows, cols] => pure (rows.toNat!, cols.toNat!)| _ => pure (24, 80)def moveCursorStr (row col : Nat) : String :=s!"\x1b[{row + 1};{col + 1}H"def hideCursorStr : String := "\x1b[?25l"def showCursorStr : String := "\x1b[?25h"def clearLineStr : String := "\x1b[K"def homeCursorStr : String := "\x1b[H"def clearScreenStr : String := "\x1b[2J"end Bliku.Tui.Terminal
import Bliku.Tui.Syntax.Typesimport Bliku.Tui.Syntax.Leanimport Bliku.Tui.Syntax.Markdownnamespace Bliku.Tui.Syntaxdef highlightLine (filename : Option String) (line : String) : Array Span :=match detectLanguage filename with| .plain => #[]| .lean => highlightLeanLine line| .markdown => highlightMarkdownLine lineend Bliku.Tui.Syntax
import Bliku.Tui.Searchnamespace Bliku.Tui.Syntaxinductive Language where| plain| lean| markdownderiving Repr, BEq, Inhabitedstructure Span wherestartByte : NatendByte : Natstyle : Stringderiving Repr, BEq, Inhabited-- ANSI bright colors.def leanKeywordStyle : String := "\x1b[94m"def leanCommentStyle : String := "\x1b[90m"def leanStringStyle : String := "\x1b[92m"def leanNumberStyle : String := "\x1b[95m"def markdownHeadingStyle : String := "\x1b[96m"def markdownCodeStyle : String := "\x1b[93m"def markdownLinkStyle : String := "\x1b[94m"def markdownEmphasisStyle : String := "\x1b[95m"def detectLanguage (filename : Option String) : Language :=match filename with| none => .plain| some name =>if name.endsWith ".lean" then .leanelse if name.endsWith ".md" || name.endsWith ".markdown" then .markdownelse .plaindef styleForByteRange (spans : Array Span) (byteStart byteEnd : Nat) : Option String :=let rec loop (i : Nat) : Option String :=if i >= spans.size thennoneelselet s := spans[i]!if Bliku.Tui.overlapsByteRange (s.startByte, s.endByte) byteStart byteEnd thensome s.styleelseloop (i + 1)loop 0end Bliku.Tui.Syntax
import Bliku.Tui.Syntax.Typesnamespace Bliku.Tui.Syntaxprivate def isMarkdownHeading (bytes : ByteArray) : Bool := Id.run dolet n := bytes.sizelet mut i := 0while i < n && bytes[i]! == 32 doi := i + 1if i >= n || bytes[i]! != 35 thenreturn falselet mut j := iwhile j < n && bytes[j]! == 35 doj := j + 1return j < n && bytes[j]! == 32private def isMarkdownFence (bytes : ByteArray) : Bool :=(bytes.size >= 3 && bytes[0]! == 96 && bytes[1]! == 96 && bytes[2]! == 96) ||(bytes.size >= 3 && bytes[0]! == 126 && bytes[1]! == 126 && bytes[2]! == 126)def highlightMarkdownLine (line : String) : Array Span := Id.run dolet bytes := line.toUTF8let n := bytes.sizeif isMarkdownHeading bytes thenreturn #[{ startByte := 0, endByte := n, style := markdownHeadingStyle }]if isMarkdownFence bytes thenreturn #[{ startByte := 0, endByte := n, style := markdownCodeStyle }]let mut spans : Array Span := #[]let mut i := 0while i < n doif bytes[i]! == 96 thenlet mut j := i + 1while j < n && bytes[j]! != 96 doj := j + 1if j < n thenspans := spans.push { startByte := i, endByte := j + 1, style := markdownCodeStyle }i := j + 1elsei := i + 1else if bytes[i]! == 91 thenlet mut j := i + 1while j < n && bytes[j]! != 93 doj := j + 1if j + 1 < n && bytes[j]! == 93 && bytes[j + 1]! == 40 thenlet mut k := j + 2while k < n && bytes[k]! != 41 dok := k + 1if k < n thenspans := spans.push { startByte := i, endByte := k + 1, style := markdownLinkStyle }i := k + 1elsei := i + 1elsei := i + 1else if bytes[i]! == 42 || bytes[i]! == 95 thenlet delim := bytes[i]!let mut j := i + 1while j < n && bytes[j]! != delim doj := j + 1if j < n && j > i + 1 thenspans := spans.push { startByte := i, endByte := j + 1, style := markdownEmphasisStyle }i := j + 1elsei := i + 1elsei := i + 1return spansend Bliku.Tui.Syntax
import Bliku.Tui.Syntax.Typesnamespace Bliku.Tui.Syntaxprivate def isAsciiLower (n : Nat) : Bool := 97 <= n && n <= 122private def isAsciiUpper (n : Nat) : Bool := 65 <= n && n <= 90private def isAsciiDigit (n : Nat) : Bool := 48 <= n && n <= 57private def isIdentStart (b : UInt8) : Bool :=let n := b.toNatisAsciiLower n || isAsciiUpper n || n == 95private def isIdentCont (b : UInt8) : Bool :=let n := b.toNatisIdentStart b || isAsciiDigit n || n == 39private def leanKeywords : Array String := #["abbrev", "axiom", "by", "class", "constant", "def", "deriving", "do","else", "end", "example", "from", "have", "if", "import", "in", "inductive","instance", "let", "match", "mutual", "namespace", "open", "opaque", "private","protected", "set_option", "show", "structure", "syntax", "termination_by","theorem", "unsafe", "variable", "where", "with"]def highlightLeanLine (line : String) : Array Span := Id.run dolet bytes := line.toUTF8let n := bytes.sizelet mut spans : Array Span := #[]let mut i := 0while i < n dolet b := bytes[i]!if b == 34 thenlet mut j := i + 1let mut escaped := falselet mut closed := falsewhile j < n && !closed dolet c := bytes[j]!if escaped thenescaped := falsej := j + 1else if c == 92 thenescaped := truej := j + 1else if c == 34 thenclosed := truej := j + 1elsej := j + 1spans := spans.push { startByte := i, endByte := j, style := leanStringStyle }i := jelse if i + 1 < n && bytes[i]! == 45 && bytes[i + 1]! == 45 thenspans := spans.push { startByte := i, endByte := n, style := leanCommentStyle }i := nelse if i + 1 < n && bytes[i]! == 47 && bytes[i + 1]! == 45 thenspans := spans.push { startByte := i, endByte := n, style := leanCommentStyle }i := nelse if isAsciiDigit b.toNat thenlet mut j := i + 1while j < n dolet c := bytes[j]!.toNatif isAsciiDigit c || c == 95 || c == 46 || c == 120 || c == 111 || c == 98 ||(65 <= c && c <= 70) || (97 <= c && c <= 102) thenj := j + 1elsebreakspans := spans.push { startByte := i, endByte := j, style := leanNumberStyle }i := jelse if isIdentStart b thenlet mut j := i + 1while j < n && isIdentCont (bytes[j]!) doj := j + 1let tok := String.fromUTF8! (bytes.extract i j)if leanKeywords.contains tok thenspans := spans.push { startByte := i, endByte := j, style := leanKeywordStyle }i := jelsei := i + 1return spansend Bliku.Tui.Syntax
namespace Bliku.Tui/-- Find all occurrences of a byte pattern within a byte array. -/def findAllMatchesBytes (haystack : ByteArray) (needle : ByteArray) : Array (Nat × Nat) := Id.run dolet n := needle.sizelet h := haystack.sizeif n == 0 || h < n thenreturn #[]let limit := h - n + 1let mut out : Array (Nat × Nat) := #[]for i in [0:limit] dolet mut matched := truefor j in [0:n] doif matched && haystack[i + j]! != needle[j]! thenmatched := falseif matched thenout := out.push (i, i + n)return outdef overlapsByteRange (r : Nat × Nat) (byteStart byteEnd : Nat) : Bool :=let (s, e) := rbyteStart < e && byteEnd > sdef activeMatchRange (hitRanges : Array (Nat × Nat)) (cursorByte : Option Nat) : Option (Nat × Nat) :=match cursorByte with| none => none| some cb =>let rec loop (i : Nat) : Option (Nat × Nat) :=if i >= hitRanges.size thennoneelselet m := hitRanges[i]!let (s, e) := mif s <= cb && cb < e thensome melseloop (i + 1)loop 0end Bliku.Tui
import Bliku.Tui.Primitivesimport Bliku.Tui.Modelimport Bliku.Tui.Windowimport Bliku.Tui.Overlayimport Bliku.Tui.Terminalnamespace Bliku.Tuiopen Bliku.Tui.Terminalprivate def renderLineSlice (line : String) (scrollCol width : Nat) : String :=((line.drop scrollCol).take width).toStringprivate def getBufferLine (buf : BufferState) (row : Nat) : String :=buf.lines[row]?.getD ""private def isVisualMode (mode : Mode) : Bool :=mode == .visual || mode == .visualBlockprivate def normalizeCursorRange (a b : Cursor) : Cursor × Cursor :=if a.row < b.row || (a.row == b.row && a.col <= b.col) then (a, b) else (b, a)private def isInSelection (m : Model) (cur : Cursor) (row col : Nat) : Bool :=if !isVisualMode m.mode thenfalseelsematch m.selectionStart with| none => false| some start =>if m.mode == .visualBlock thenlet minRow := min start.row cur.rowlet maxRow := max start.row cur.rowlet minCol := min start.col cur.collet maxCol := max start.col cur.colrow >= minRow && row <= maxRow && col >= minCol && col <= maxColelselet (p1, p2) := normalizeCursorRange start curif row < p1.row || row > p2.row then falseelse if row > p1.row && row < p2.row then trueelse if p1.row == p2.row then col >= p1.col && col <= p2.colelse if row == p1.row then col >= p1.colelse if row == p2.row then col <= p2.colelse falseprivate def styleVisibleLine(m : Model) (windowId lineIdx scrollCol : Nat) (visible : String) (windowCursor : Option Cursor) : String := Id.run dolet activeRow := windowCursor.map (fun c => c.row) |>.getD 0let activeCol := windowCursor.map (fun c => c.col) |>.getD 0let selectionCur := windowCursor.getD { row := lineIdx, col := scrollCol }let mut out : Array String := #[]let mut activeStyle : Option String := nonelet chars := visible.toListlet mut i := 0for ch in chars dolet col := scrollCol + ilet isCursor :=windowId == m.workspace.activeWindowId &&lineIdx == activeRow &&col == activeCollet desiredStyle : Option String :=if isCursor thenif ch == ' ' then some m.config.cursorSpaceStyle else some m.config.cursorCharStyleelse if isInSelection m selectionCur lineIdx col thensome m.config.visualSelectionStyleelsenoneif desiredStyle != activeStyle thenmatch activeStyle with| some _ => out := out.push m.config.resetStyle| none => pure ()match desiredStyle with| some stl => out := out.push stl| none => pure ()activeStyle := desiredStyleout := out.push ch.toStringi := i + 1if activeStyle.isSome thenout := out.push m.config.resetStylereturn String.intercalate "" out.toListprivate def appendCursorOnBlankCell(m : Model) (windowId lineIdx scrollCol availableWidth : Nat)(visible : String) (lineToDraw : String) (windowCursor : Option Cursor) : String := Id.run dolet some cur := windowCursor | return lineToDrawif windowId != m.workspace.activeWindowId || lineIdx != cur.row || cur.col < scrollCol thenreturn lineToDrawlet visCol := cur.col - scrollCollet visLen := visible.toList.lengthif visCol < visLen || visCol >= availableWidth thenreturn lineToDrawlet padCount := visCol - visLenreturn lineToDraw ++ ("".pushn ' ' padCount) ++ m.config.cursorSpaceStyle ++ " " ++ m.config.resetStyleprivate def renderWindow (m : Model) (windowId : Nat) (view : ViewState) (rect : Rect) : (Array String × Option (Nat × Nat)) := Id.run dolet r := rect.rowlet c := rect.collet h := rect.heightlet w := rect.widthlet workH := if h > 0 then h - 1 else 0let buf := getBuffer m view.bufferIdlet mut out : Array String := #[]for i in [0:workH] dolet lineIdx := view.scrollRow + iout := out.push (moveCursorStr (r + i) c)if lineIdx < buf.lines.size thenlet lnWidth := if m.config.showLineNumbers then 4 else 0let availableWidth := if w > lnWidth then w - lnWidth else 0let windowCursor := if windowId == m.workspace.activeWindowId then some view.cursor else noneif m.config.showLineNumbers thenout := out.push s!"{leftPad (toString (lineIdx + 1)) 3} "let visible := renderLineSlice (getBufferLine buf lineIdx) view.scrollCol availableWidthlet styled := styleVisibleLine m windowId lineIdx view.scrollCol visible windowCursorlet lineToDraw := appendCursorOnBlankCell m windowId lineIdx view.scrollCol availableWidth visible styled windowCursorout := out.push lineToDrawelseout := out.push m.config.emptyLineMarkerout := out.push clearLineStrlet statusRow := r + workHout := out.push (moveCursorStr statusRow c)let fileName := buf.filename.getD "[No Name]"let modeStr := if windowId == m.workspace.activeWindowId then s!"-- {m.mode} --" else "--"let eolMark := if buf.missingEol then " [noeol]" else ""let statusStr := s!"{modeStr} {fileName}{eolMark} [W:{windowId} B:{view.bufferId}] [{m.workgroupName}] {m.workspace.name}"out := out.push m.config.statusBarStyleout := out.push statusStrout := out.push clearLineStrout := out.push m.config.resetStylelet cursorPos : Option (Nat × Nat) :=if view.cursor.row < view.scrollRow then noneelselet visRow := view.cursor.row - view.scrollRowif visRow < workH thenlet colOffset := if m.config.showLineNumbers then 4 else 0let visCol := if view.cursor.col >= view.scrollCol then view.cursor.col - view.scrollCol else 0if visCol + colOffset < w thensome (r + visRow, c + visCol + colOffset)else noneelse none(out, cursorPos)private def renderLayout (m : Model) (l : Layout) (r c h w : Nat) : (Array String × Option (Nat × Nat)) := Id.run domatch l with| .window id view =>if m.workspace.isFloatingWindow id thenlet mut blank : Array String := #[]for i in [0:h] doblank := blank.push (moveCursorStr (r + i) c)blank := blank.push ("".pushn ' ' w)return (blank, none)let (buf, cur) := renderWindow m id view { row := r, col := c, height := h, width := w }return (buf, if id == m.workspace.activeWindowId then cur else none)| .hsplit left right ratio =>let leftW := (Float.ofNat w * ratio).toUInt64.toNatlet (leftBuf, leftCur) := renderLayout m left r c h leftWlet mut sep : Array String := #[]if w > leftW thenlet sepCol := c + leftWfor i in [0:h] dosep := sep.push (moveCursorStr (r + i) sepCol)sep := sep.push m.config.vSplitStrlet (rightBuf, rightCur) := renderLayout m right r (c + leftW + 1) h (if w > leftW then w - leftW - 1 else 0)return (leftBuf ++ sep ++ rightBuf, rightCur.orElse (fun _ => leftCur))| .vsplit top bottom ratio =>let topH := (Float.ofNat h * ratio).toUInt64.toNatlet (topBuf, topCur) := renderLayout m top r c topH wlet mut sep : Array String := #[]if h > topH thenlet sepRow := r + topHsep := sep.push (moveCursorStr sepRow c)for _ in [0:w] dosep := sep.push m.config.hSplitStrlet (bottomBuf, bottomCur) := renderLayout m bottom (r + topH + 1) c (if h > topH then h - topH - 1 else 0) wreturn (topBuf ++ sep ++ bottomBuf, bottomCur.orElse (fun _ => topCur))private def renderFloatingWindows (m : Model) : (Array String × Option (Nat × Nat)) := Id.run dolet ws := m.workspacelet mut out : Array String := #[]let mut activeCur : Option (Nat × Nat) := nonelet ids := ws.getFloatingWindowIdsfor i in [0:ids.size] dolet wid := ids[i]!match ws.layout.findView wid, getFloatingWindowBounds ws m.windowHeight m.windowWidth wid with| some view, some (top, left, h, w) =>let (buf, cur) := renderWindow m wid view { row := top, col := left, height := h, width := w }out := out ++ bufif wid == ws.activeWindowId thenactiveCur := cur| _, _ => pure ()(out, activeCur)private def findOverlayCursorPos (rows cols : Nat) (m : Model) : Option (Nat × Nat) :=match m.floatingOverlay with| none => none| some ov =>match computeFloatingOverlayLayout rows cols ov with| none => none| some layout =>let lines := if ov.lines.isEmpty then #[""] else ov.lineslet maxRow := lines.size - 1let rowIdx := min ov.cursorRow maxRowlet lineLen := (lines[rowIdx]!).lengthlet colIdx := min ov.cursorCol lineLenlet visRow := min rowIdx (layout.contentRows - 1)let visCol := min colIdx layout.innerWidthsome (layout.top + 1 + layout.titleRows + visRow, layout.left + 2 + visCol)/-- Render model to terminal and return updated model (size synchronized). -/def render (model : Model) : IO Model := doif !model.dirty thenreturn modellet (rows, cols) ← getWindowSizelet m := { model with windowHeight := rows, windowWidth := cols }let mut buffer : Array String := #[]buffer := buffer.push hideCursorStrlet layoutH := if rows > 0 then rows - 1 else 0let baseLayout :=m.workspace.getFloatingWindowIds.foldl (fun acc wid =>match acc with| some l => l.remove wid| none => none) (some m.workspace.layout)if baseLayout.isNone thenbuffer := buffer.push clearScreenStrbuffer := buffer.push homeCursorStrlet (layoutBuf, activeCur) :=match baseLayout with| some l => renderLayout m l 0 0 layoutH cols| none => (#[], none)buffer := buffer ++ layoutBuflet (floatBuf, floatCur) := renderFloatingWindows mbuffer := buffer ++ floatBufbuffer := buffer.push (moveCursorStr (rows - 1) 0)buffer := buffer.push (renderStatusBar m)buffer := buffer.push clearLineStrlet overlayToRender := m.floatingOverlay.orElse (fun _ => messageOverlayForState m)if let some ov := overlayToRender thenbuffer := buffer ++ renderFloatingOverlay rows cols ovbuffer := buffer ++ renderCompletionPopup rows cols m activeCurlet overlayCur := findOverlayCursorPos rows cols mbuffer := buffer.push (if m.floatingOverlay.isSome && m.mode != .command && m.mode != .searchForward && m.mode != .searchBackward thenmatch overlayCur with| some (pr, pc) => moveCursorStr pr pc| none => ""elsematch floatCur.orElse (fun _ => activeCur) with| some (pr, pc) => moveCursorStr pr pc| none => "")if m.mode == .command || m.mode == .searchForward || m.mode == .searchBackward thenbuffer := buffer.push (moveCursorStr (rows - 1) (1 + m.commandBuffer.length))if m.mode == .visual || m.mode == .visualBlock thenbuffer := buffer.push hideCursorStrelsebuffer := buffer.push showCursorStrIO.print (String.intercalate "" buffer.toList)(← IO.getStdout).flushreturn mend Bliku.Tui
namespace Bliku.Tui/-- Pad a string on the left with spaces until it reaches the given width. -/def leftPad (s : String) (width : Nat) : String :=if s.length >= width then selse "".pushn ' ' (width - s.length) ++ sstructure Rect whererow : Natcol : Natheight : Natwidth : Natderiving Inhabitedend Bliku.Tui
import Bliku.Tui.Primitivesimport Bliku.Tui.Modelimport Bliku.Tui.Terminalnamespace Bliku.Tuiopen Bliku.Tui.Terminaldef shouldRenderMessageAsFloat (msg : String) : Bool :=let m := msg.trimAscii.toStringif m.isEmpty thenfalseelsem.startsWith "Error" ||m.startsWith "Cannot" ||m.startsWith "Invalid" ||m.startsWith "Unknown" ||m.startsWith "No " ||m.startsWith "Empty " ||m.startsWith "Usage:" ||m.startsWith "failed" ||m.startsWith "Failed" ||m.contains "not found"def renderStatusBar (m : Model) : String :=let plain := m.message.trimAscii.toStringlet floatMsg := shouldRenderMessageAsFloat plainmatch m.mode with| .command => s!":{m.commandBuffer}"| .searchForward => s!"/{m.commandBuffer}"| .searchBackward => s!"?{m.commandBuffer}"| _ => if floatMsg then "" else plainstructure FloatingOverlayLayout wheretop : Natleft : NatinnerWidth : NattitleRows : NatcontentRows : Natderiving Inhabiteddef computeFloatingOverlayLayout (rows cols : Nat) (overlay : FloatingOverlay) : Option FloatingOverlayLayout := Id.run dolet availableRows := if rows > 1 then rows - 1 else rowsif cols < 8 || availableRows < 4 thenreturn nonelet lines := if overlay.lines.isEmpty then #[""] else overlay.lineslet titleText := if overlay.title.isEmpty then "" else s!"[{overlay.title}]"let titleRows := if titleText.isEmpty then 0 else 1let naturalWidthContent := lines.foldl (fun acc ln => max acc ln.length) 0let naturalWidth := max naturalWidthContent titleText.lengthlet maxInnerWidth := if cols > 8 then cols - 8 else 1let targetWidth := if overlay.maxWidth > 0 then overlay.maxWidth else naturalWidthlet innerWidth := max 1 (min targetWidth maxInnerWidth)let maxContentRows := if availableRows > titleRows + 2 then availableRows - titleRows - 2 else 0if maxContentRows == 0 then return nonelet contentRows := max 1 (min lines.size maxContentRows)let boxHeight := contentRows + titleRows + 2let boxWidth := innerWidth + 4let top := (availableRows - boxHeight) / 2let left := (cols - boxWidth) / 2return some { top, left, innerWidth, titleRows, contentRows }def renderFloatingOverlay (rows cols : Nat) (overlay : FloatingOverlay) : Array String := Id.run dolet some layout := computeFloatingOverlayLayout rows cols overlay | return #[]let lines := if overlay.lines.isEmpty then #[""] else overlay.lineslet titleText := if overlay.title.isEmpty then "" else s!"[{overlay.title}]"let border := "+" ++ "".pushn '-' (layout.innerWidth + 2) ++ "+"let mut out : Array String := #[]out := out.push (moveCursorStr layout.top layout.left)out := out.push borderif layout.titleRows == 1 thenout := out.push (moveCursorStr (layout.top + 1) layout.left)let clipped := (titleText.take layout.innerWidth).toStringlet pad := if clipped.length < layout.innerWidth then "".pushn ' ' (layout.innerWidth - clipped.length) else ""out := out.push s!"| {clipped}{pad} |"for i in [0:layout.contentRows] dolet raw := lines[i]?.getD ""let clipped := (raw.take layout.innerWidth).toStringlet pad := if clipped.length < layout.innerWidth then "".pushn ' ' (layout.innerWidth - clipped.length) else ""let row := layout.top + 1 + layout.titleRows + iout := out.push (moveCursorStr row layout.left)out := out.push s!"| {clipped}{pad} |"out := out.push (moveCursorStr (layout.top + layout.contentRows + layout.titleRows + 1) layout.left)out := out.push borderoutdef messageOverlayForState (m : Model) : Option FloatingOverlay :=let plain := m.message.trimAscii.toStringlet floatMsg := shouldRenderMessageAsFloat plainif m.floatingOverlay.isNone && m.mode != .command && m.mode != .searchForward && m.mode != .searchBackward && floatMsg thenif plain.isEmpty then noneelse some { title := "Message", lines := (plain.splitOn "\n").toArray }elsenonedef renderCompletionPopup (_rows cols : Nat) (m : Model) (cursorPos : Option (Nat × Nat)) : Array String := Id.run dolet some popup := m.completionPopup | return #[]let some (cursorRow, cursorCol) := cursorPos | return #[]if popup.items.isEmpty then return #[]let visibleMax := min 8 popup.items.sizelet labels := popup.items.extract 0 visibleMax |>.map (fun it => it.label)let naturalW := labels.foldl (fun acc s => max acc s.length) 0let innerWidth := max 8 (min 48 naturalW)let boxWidth := innerWidth + 2let top := cursorRow + 1let left := if cursorCol + boxWidth < cols then cursorCol else (cols - boxWidth)let borderCh := if m.config.hSplitStr.isEmpty then '-' else m.config.hSplitStr.toList[0]!let hline := "".pushn borderCh innerWidthlet border := m.config.vSplitStr ++ hline ++ m.config.vSplitStrlet mut out : Array String := #[]out := out.push (moveCursorStr top left)out := out.push borderlet selected := if popup.selected < visibleMax then popup.selected else 0for i in [0:visibleMax] dolet label := (labels[i]!.take innerWidth).toStringlet pad := if label.length < innerWidth then "".pushn ' ' (innerWidth - label.length) else ""out := out.push (moveCursorStr (top + 1 + i) left)out := out.push m.config.vSplitStrif i == selected thenout := out.push "\x1b[7m"out := out.push labelout := out.push padout := out.push m.config.resetStyleelseout := out.push labelout := out.push padout := out.push m.config.vSplitStrout := out.push (moveCursorStr (top + visibleMax + 1) left)out := out.push borderoutend Bliku.Tui
import Bliku.Tui.Windownamespace Bliku.Tuiinductive Mode where| normal| insert| command| searchForward| searchBackward| visual| visualBlockderiving Repr, BEq, Inhabitedinstance : ToString Mode wheretoString| .normal => "NORMAL"| .insert => "INSERT"| .command => "COMMAND"| .searchForward => "SEARCH"| .searchBackward => "SEARCH"| .visual => "VISUAL"| .visualBlock => "VISUAL BLOCK"structure BufferState whereid : Natfilename : Option Stringlines : Array StringmissingEol : Bool := falsederiving Inhabitedstructure FloatingOverlay wheretitle : Stringlines : Array StringmaxWidth : Nat := 0cursorRow : Nat := 0cursorCol : Nat := 0deriving Repr, Inhabitedstructure CompletionItem wherelabel : StringinsertText : Stringderiving Repr, Inhabitedstructure CompletionPopup whereitems : Array CompletionItemselected : Nat := 0anchorRow : NatanchorCol : Natderiving Repr, Inhabitedstructure UiConfig whereshowLineNumbers : Bool := truevSplitStr : String := "│"hSplitStr : String := "─"emptyLineMarker : String := "~"statusBarStyle : String := ""resetStyle : String := "\x1b[0m"searchHighlightStyle : String := ""searchHighlightCursorStyle : String := ""visualSelectionStyle : String := "\x1b[7m"cursorCharStyle : String := "\x1b[47m\x1b[30m"cursorSpaceStyle : String := "\x1b[47m\x1b[30m"tabStop : Nat := 4deriving Inhabitedstructure Model wheremode : ModeworkgroupName : Stringworkspace : WorkspaceViewbuffers : Array BufferStateselectionStart : Option Cursor := nonemessage : StringcommandBuffer : StringfloatingOverlay : Option FloatingOverlay := nonecompletionPopup : Option CompletionPopup := noneconfig : UiConfig := {}windowHeight : Nat := 24windowWidth : Nat := 80dirty : Bool := truederiving Inhabiteddef getBuffer (m : Model) (id : Nat) : BufferState :=m.buffers.find? (fun b => b.id == id) |>.getD { id := id, filename := none, lines := #[] }end Bliku.Tui