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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104package tui
import ( "slices" "strings" "sync/atomic"
"github.com/charmbracelet/x/ansi")
// page is the state the list screens share: a cursor, the terminal size,// the load or save in flight, and the last error and notice. Every request// carries the Generation it was started under; a reply under any other is// stale.type page struct { Error, Notice string Cursor, Width, Height, Generation int Loading, Saving bool}
// Commands outlive closed screens; unique generations reject their replies.var pageGeneration atomic.Int64
func nextPageGeneration() int { return int(pageGeneration.Add(1)) }
func (p *page) SetSize(width, height int) { p.Width, p.Height = width, height }
// settle takes a reply for the request busy tracks: false when it is stale// or failed, with the failure shown.func (p *page) settle(gen int, err error, busy *bool) bool { if gen != p.Generation { return false } *busy = false if err != nil { p.Error = err.Error() return false } return true}
// step moves the cursor for an arrow key over n items.func (p *page) step(key string, n int) bool { switch key { case "up", "ctrl+p": p.Cursor = max(0, p.Cursor-1) case "down", "ctrl+n": p.Cursor = min(max(0, n-1), p.Cursor+1) default: return false } return true}
// reselect keeps the cursor on the same item, by id, across a reload.func reselect[T any](cursor int, before, after []T, id func(T) string) int { if cursor < len(before) { if i := slices.IndexFunc(after, func(item T) bool { return id(item) == id(before[cursor]) }); i >= 0 { return i } } return max(0, min(cursor, len(after)-1))}
// header opens a page with its title rule, the last error and the notice.func (p page) header(command, right string) []string { width := max(1, p.Width) rows := []string{titleRule(width, brand("albedo")+" "+DefaultStyles.Muted.Render(command), DefaultStyles.Faint.Render(right)), ""} if p.Error != "" { rows = append(rows, DefaultStyles.Error.Render(ansi.Truncate(p.Error, width, "…"))) } if p.Notice != "" { rows = append(rows, DefaultStyles.Faint.Render(ansi.Truncate(p.Notice, width, "…"))) } return rows}
// listRow is one list entry, marked and highlighted under the cursor.func listRow(selected bool, row string, width int) string { prefix := " " if selected { prefix = selectBar() + " " } row = ansi.Truncate(prefix+row, width, "…") if selected { return selectedLine(row, width) } return row}
// scrolled is the window of rows lines that keeps line at in view.func scrolled(lines []string, at, rows int) []string { start := max(0, at-rows+1) return lines[start:min(len(lines), start+rows)]}
// fit drops rows past the height, keeping the last one: the key hints.func (p page) fit(rows []string) string { if p.Height > 0 && len(rows) > p.Height { rows = append(rows[:max(1, p.Height-1)], rows[len(rows)-1]) } return strings.Join(rows, "\n")}