package tui import ( "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) // EditorOpts configures editor rendering type EditorOpts struct { LineNumbers bool LineNumWidth int // 0 = auto-size WrapLines bool Border LineType Focused bool Style EditorStyle } // EditorStyle defines editor colors type EditorStyle struct { TextFg color.RGB TextBg color.RGB CursorFg color.RGB CursorBg color.RGB LineNumFg color.RGB LineNumBg color.RGB CurrentLineBg color.RGB BorderFg color.RGB } // DefaultEditorStyle returns default colors func DefaultEditorStyle() EditorStyle { return EditorStyle{ TextFg: color.RGB{R: 220, G: 220, B: 220}, TextBg: color.RGB{R: 25, G: 25, B: 35}, CursorFg: color.RGB{R: 0, G: 0, B: 0}, CursorBg: color.RGB{R: 200, G: 200, B: 200}, LineNumFg: color.RGB{R: 100, G: 100, B: 120}, LineNumBg: color.RGB{R: 30, G: 30, B: 40}, CurrentLineBg: color.RGB{R: 35, G: 35, B: 50}, BorderFg: color.RGB{R: 80, G: 80, B: 100}, } } // Editor renders multi-line editor and returns content height used func (r Region) Editor(state *EditorState, opts EditorOpts) int { if r.W < 3 || r.H < 1 { return 0 } style := opts.Style if style == (EditorStyle{}) { style = DefaultEditorStyle() } // Calculate content area accounting for border contentX := 0 contentY := 0 contentW := r.W contentH := r.H if opts.Border != LineNone { if r.H < 3 { return 0 } r.Box(opts.Border, style.BorderFg) contentX = 1 contentY = 1 contentW = r.W - 2 contentH = r.H - 2 } // Line number gutter gutterW := 0 if opts.LineNumbers { gutterW = opts.LineNumWidth if gutterW == 0 { digits := 1 n := len(state.Lines) for n >= 10 { digits++ n /= 10 } gutterW = digits + 1 } contentW -= gutterW } if contentW < 1 || contentH < 1 { return 0 } state.AdjustScroll(contentW, contentH) // Render each visible line for y := 0; y < contentH; y++ { lineIdx := state.ScrollY + y isCurrentLine := lineIdx == state.CursorLine bg := style.TextBg if isCurrentLine && opts.Focused { bg = style.CurrentLineBg } // Line number gutter if opts.LineNumbers { for gx := 0; gx < gutterW-1; gx++ { r.Cell(contentX+gx, contentY+y, ' ', style.LineNumFg, style.LineNumBg, terminal.AttrNone) } r.Cell(contentX+gutterW-1, contentY+y, '│', style.LineNumFg, style.LineNumBg, terminal.AttrDim) if lineIdx < len(state.Lines) { numStr := formatLineNum(lineIdx+1, gutterW-1) for i, ch := range numStr { r.Cell(contentX+i, contentY+y, ch, style.LineNumFg, style.LineNumBg, terminal.AttrNone) } } } textX := contentX + gutterW // Fill text area with background for x := 0; x < contentW; x++ { r.Cell(textX+x, contentY+y, ' ', style.TextFg, bg, terminal.AttrNone) } if lineIdx >= len(state.Lines) { continue } line := []rune(state.Lines[lineIdx]) // Scroll indicator left if state.ScrollX > 0 && len(line) > 0 { r.Cell(textX, contentY+y, '◀', style.LineNumFg, bg, terminal.AttrDim) } // Render visible text for x := 0; x < contentW; x++ { charIdx := state.ScrollX + x if charIdx >= len(line) { break } ch := line[charIdx] fg := style.TextFg cellBg := bg if opts.Focused && lineIdx == state.CursorLine && charIdx == state.CursorCol { fg = style.CursorFg cellBg = style.CursorBg } r.Cell(textX+x, contentY+y, ch, fg, cellBg, terminal.AttrNone) } // Scroll indicator right if state.ScrollX+contentW < len(line) { r.Cell(textX+contentW-1, contentY+y, '▶', style.LineNumFg, bg, terminal.AttrDim) } // Cursor at end of line if opts.Focused && lineIdx == state.CursorLine && state.CursorCol >= len(line) { cursorX := state.CursorCol - state.ScrollX if cursorX >= 0 && cursorX < contentW { r.Cell(textX+cursorX, contentY+y, ' ', style.CursorFg, style.CursorBg, terminal.AttrNone) } } } // Vertical scroll indicators if state.ScrollY > 0 { r.Cell(contentX+gutterW+contentW-1, contentY, '▲', style.LineNumFg, style.TextBg, terminal.AttrDim) } if state.ScrollY+contentH < len(state.Lines) { r.Cell(contentX+gutterW+contentW-1, contentY+contentH-1, '▼', style.LineNumFg, style.TextBg, terminal.AttrDim) } if opts.Border != LineNone { return r.H } return contentH } // formatLineNum formats line number right-aligned to width func formatLineNum(num, width int) string { s := "" for num > 0 { s = string(rune('0'+num%10)) + s num /= 10 } if s == "" { s = "0" } for len(s) < width { s = " " + s } return s }