Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186package tui
import ( "strings"
"github.com/charmbracelet/x/ansi")
func wrapOrChunkLine(line string, width int) []string { if width <= 0 { width = 80 } if ansi.StringWidth(line) <= width { return []string{line} } // Match wrap-ansi's hard:true, trim:false word placement: a separator // consumes a cell before deciding whether the next word fits. words := strings.Split(line, " ") rows := []string{""} used := 0 for i, word := range words { if i > 0 { if used >= width { rows = append(rows, "") used = 0 } rows[len(rows)-1] += " " used++ } wordWidth := ansi.StringWidth(word) if wordWidth > width { remaining := width - used breaksHere := 1 + (wordWidth-remaining-1)/width breaksNext := (wordWidth - 1) / width if breaksNext < breaksHere { rows = append(rows, "") used = 0 } wrapped := strings.Split(ansi.Hardwrap(rows[len(rows)-1]+word, width, true), "\n") rows[len(rows)-1] = wrapped[0] rows = append(rows, wrapped[1:]...) used = ansi.StringWidth(rows[len(rows)-1]) continue } if used+wordWidth > width && used > 0 && wordWidth > 0 { rows = append(rows, "") used = 0 } rows[len(rows)-1] += word used += wordWidth } // Every viewport row can be rendered alone after a history scroll. Reopen // styles that cross a physical wrap, as wrap-ansi does for the TS rows. style := "" for i, row := range rows { if style != "" { rows[i] = style + row } for _, code := range sgrCode.FindAllString(row, -1) { if code == "\x1b[0m" || code == "\x1b[m" { style = "" } else { style += code } } if style != "" && i < len(rows)-1 { rows[i] += "\x1b[0m" } } return rows}
func (m ChatModel) promptLines() int { lineCount := m.TextArea.LineCount() if lineCount <= 1 { return max(1, m.TextArea.LineInfo().Height) } cp := m.TextArea for cp.Line() > 0 { cp.CursorUp() } total := 0 for line := range lineCount { total += cp.LineInfo().Height if line < lineCount-1 { curr := cp.Line() for cp.Line() == curr { beforeRow := cp.LineInfo().RowOffset cp.CursorDown() if cp.Line() == curr && cp.LineInfo().RowOffset == beforeRow { break } } } } return max(1, total)}
func (m ChatModel) maxPromptHeight() int { if m.Height > 0 { return max(1, min(6, (m.Height-8)/2)) } return 6}
func (m ChatModel) promptHeight() int { return min(m.promptLines(), m.maxPromptHeight())}
func (m ChatModel) inputRows() int { if len(m.effortOptions) > 0 { return 4 // breathing room, title, choices, keyboard hint; no composer } menu := min(4, len(m.CommandMenu.Matches(m.TextArea.Value()))) return m.promptHeight() + menu + m.chipHeight() + len(m.direnvRows(m.chatWidth()))}
// chipHeight is the rows the attachment strip takes above the composer.func (m ChatModel) chipHeight() int { if len(m.chips()) == 0 { return 0 } return chipRows}
// pane is the transcript's window: its size and the rows it shows. The rows// arrive wrapped to the width, so drawing it only joins them.type pane struct { rows []string width, height int}
func (p pane) Width() int { return p.width }func (p pane) Height() int { return p.height }func (p *pane) SetWidth(w int) { p.width = w }func (p *pane) SetHeight(h int) { p.height = h }
// lines is exactly height rows: the shown ones, then blank ones.func (p pane) lines() []string { lines := make([]string, p.height) copy(lines, p.rows) return lines}
// View is the pane as one string, as tests and a sidebar join read it.func (p pane) View() string { return strings.Join(p.lines(), "\n")}
func (m ChatModel) chromeRows() int { return m.inputRows() + m.Notices.ChromeRows()}
func (m *ChatModel) syncLayout() { m.TextArea.SetHeight(m.maxPromptHeight()) m.Viewport.SetHeight(max(1, m.Height-6-m.chromeRows())) m.refreshViewportContent()}
func (m *ChatModel) SetSize(width, height int) { if m == nil || m.History == nil { return } m.Width, m.Height = max(1, width), max(1, height)
available := m.chatWidth() transcriptWidth := available m.layoutSidebar = m.sidebarWidth() if m.layoutSidebar > 0 { transcriptWidth -= m.layoutSidebar + 2 }
m.Viewport.SetWidth(transcriptWidth) m.TextArea.SetWidth(available) m.TextArea.SetHeight(m.maxPromptHeight()) m.Viewport.SetHeight(max(1, m.Height-6-m.chromeRows()))
// settled rows are rendered at the body width alone, so only a new body // width renders them again if body := min(100, transcriptWidth); body != m.Renderer.BodyWidth { m.Renderer.BodyWidth = body m.rebuildSettledLines() } m.refreshViewportContent()}