okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
0b9f48f
main
1package tui 2 3import ( 4"strings" 5 6"github.com/charmbracelet/x/ansi" 7) 8 9func wrapOrChunkLine (line string ,width int ) []string { 10if width <= 0 { 11width = 80 12 } 13if ansi .StringWidth (line )<= width { 14return []string {line } 15 } 16// Match wrap-ansi's hard:true, trim:false word placement: a separator 17// consumes a cell before deciding whether the next word fits. 18words := strings .Split (line ," " ) 19rows := []string {"" } 20used := 0 21for i ,word := range words { 22if i > 0 { 23if used >= width { 24rows = append (rows ,"" ) 25used = 0 26 } 27rows [len (rows )- 1 ]+= " " 28used ++ 29 } 30wordWidth := ansi .StringWidth (word ) 31if wordWidth > width { 32remaining := width - used 33breaksHere := 1 + (wordWidth - remaining - 1 )/ width 34breaksNext := (wordWidth - 1 )/ width 35if breaksNext < breaksHere { 36rows = append (rows ,"" ) 37used = 0 38 } 39wrapped := strings .Split (ansi .Hardwrap (rows [len (rows )- 1 ]+ word ,width ,true ),"\n" ) 40rows [len (rows )- 1 ]= wrapped [0 ] 41rows = append (rows ,wrapped [1 :]... ) 42used = ansi .StringWidth (rows [len (rows )- 1 ]) 43continue 44 } 45if used + wordWidth > width && used > 0 && wordWidth > 0 { 46rows = append (rows ,"" ) 47used = 0 48 } 49rows [len (rows )- 1 ]+= word 50used += wordWidth 51 } 52// Every viewport row can be rendered alone after a history scroll. Reopen 53// styles that cross a physical wrap, as wrap-ansi does for the TS rows. 54style := "" 55for i ,row := range rows { 56if style != "" { 57rows [i ]= style + row 58 } 59for _ ,code := range sgrCode .FindAllString (row ,- 1 ) { 60if code == "\x1b[0m" || code == "\x1b[m" { 61style = "" 62 }else { 63style += code 64 } 65 } 66if style != "" && i < len (rows )- 1 { 67rows [i ]+= "\x1b[0m" 68 } 69 } 70return rows 71} 72 73func (m ChatModel )promptLines ()int { 74lineCount := m .TextArea .LineCount () 75if lineCount <= 1 { 76return max (1 ,m .TextArea .LineInfo ().Height ) 77 } 78cp := m .TextArea 79for cp .Line ()> 0 { 80cp .CursorUp () 81 } 82total := 0 83for line := range lineCount { 84total += cp .LineInfo ().Height 85if line < lineCount - 1 { 86curr := cp .Line () 87for cp .Line ()== curr { 88beforeRow := cp .LineInfo ().RowOffset 89cp .CursorDown () 90if cp .Line ()== curr && cp .LineInfo ().RowOffset == beforeRow { 91break 92 } 93 } 94 } 95 } 96return max (1 ,total ) 97} 98 99func (m ChatModel )maxPromptHeight ()int { 100if m .Height > 0 { 101return max (1 ,min (6 , (m .Height - 8 )/ 2 )) 102 } 103return 6 104} 105 106func (m ChatModel )promptHeight ()int { 107return min (m .promptLines (),m .maxPromptHeight ()) 108} 109 110func (m ChatModel )inputRows ()int { 111if len (m .effortOptions )> 0 { 112return 4 // breathing room, title, choices, keyboard hint; no composer 113 } 114menu := min (4 ,len (m .CommandMenu .Matches (m .TextArea .Value ()))) 115return m .promptHeight ()+ menu + m .chipHeight () 116} 117 118// chipHeight is the rows the attachment strip takes above the composer. 119func (m ChatModel )chipHeight ()int { 120if len (m .chips ())== 0 { 121return 0 122 } 123return chipRows 124} 125 126// pane is the transcript's window: its size and the rows it shows. The rows 127// arrive wrapped to the width, so drawing it only joins them. 128type pane struct { 129rows []string 130width ,height int 131} 132 133func (p pane )Width ()int {return p .width } 134func (p pane )Height ()int {return p .height } 135func (p * pane )SetWidth (w int ) {p .width = w } 136func (p * pane )SetHeight (h int ) {p .height = h } 137 138// lines is exactly height rows: the shown ones, then blank ones. 139func (p pane )lines () []string { 140lines := make ([]string ,p .height ) 141copy (lines ,p .rows ) 142return lines 143} 144 145// View is the pane as one string, as tests and a sidebar join read it. 146func (p pane )View ()string { 147return strings .Join (p .lines (),"\n" ) 148} 149 150func (m ChatModel )chromeRows ()int { 151return m .inputRows ()+ m .Notices .ChromeRows () 152} 153 154func (m * ChatModel )syncLayout () { 155m .TextArea .SetHeight (m .maxPromptHeight ()) 156m .Viewport .SetHeight (max (1 ,m .Height - 6 - m .chromeRows ())) 157m .refreshViewportContent () 158} 159 160func (m * ChatModel )SetSize (width ,height int ) { 161if m == nil || m .History == nil { 162return 163 } 164m .Width ,m .Height = max (1 ,width ),max (1 ,height ) 165 166available := max (1 ,m .Width - 2 * m .padding ()) 167transcriptWidth := available 168m .layoutSidebar = m .sidebarWidth () 169if m .layoutSidebar > 0 { 170transcriptWidth -= m .layoutSidebar + 2 171 } 172 173m .Viewport .SetWidth (transcriptWidth ) 174m .TextArea .SetWidth (available ) 175m .TextArea .SetHeight (m .maxPromptHeight ()) 176m .Viewport .SetHeight (max (1 ,m .Height - 6 - m .chromeRows ())) 177 178// settled rows are rendered at the body width alone, so only a new body 179// width renders them again 180if body := min (100 ,transcriptWidth );body != m .Renderer .BodyWidth { 181m .Renderer .BodyWidth = body 182m .rebuildSettledLines () 183 } 184m .refreshViewportContent () 185}