diff options
Diffstat (limited to 'internal/client/ui/ui.go')
| -rw-r--r-- | internal/client/ui/ui.go | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/internal/client/ui/ui.go b/internal/client/ui/ui.go index b03df59..355fbfb 100644 --- a/internal/client/ui/ui.go +++ b/internal/client/ui/ui.go @@ -1,11 +1,18 @@ package ui import ( + "bytes" "fmt" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + charmansi "github.com/charmbracelet/x/ansi" + "github.com/mattn/go-runewidth" + "github.com/muesli/ansi" + "github.com/muesli/reflow/truncate" + + "github.com/kyren223/eko/pkg/assert" ) type ModelTransition struct { @@ -22,6 +29,8 @@ func AddBorderHeader(header string, headerOffset int, style lipgloss.Style, rend leftWidth := headerOffset rightWidth := topWidth - leftWidth - headerWidth + assert.Assert(leftWidth >= 0, "left width cannot be negative", "leftWidth", leftWidth) + assert.Assert(rightWidth >= 0, "right width cannot be negative", "rightWidth", rightWidth) topStyle := lipgloss.NewStyle(). Background(style.GetBorderTopBackground()). @@ -39,3 +48,119 @@ func AddBorderHeader(header string, headerOffset int, style lipgloss.Style, rend return lipgloss.JoinVertical(lipgloss.Left, borderTop, body) } + +// PlaceOverlay places fg on top of bg. +func PlaceOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string { + fgLines, fgWidth := getLines(fg) + bgLines, bgWidth := getLines(bg) + bgHeight := len(bgLines) + fgHeight := len(fgLines) + + if fgWidth >= bgWidth && fgHeight >= bgHeight { + // FIXME: return fg or bg? + return fg + } + // TODO: allow placement outside of the bg box? + x = clamp(x, 0, bgWidth-fgWidth) + y = clamp(y, 0, bgHeight-fgHeight) + + ws := &whitespace{} + for _, opt := range opts { + opt(ws) + } + + var b strings.Builder + for i, bgLine := range bgLines { + if i > 0 { + b.WriteByte('\n') + } + if i < y || i >= y+fgHeight { + b.WriteString(bgLine) + continue + } + + pos := 0 + if x > 0 { + left := truncate.String(bgLine, uint(x)) + pos = ansi.PrintableRuneWidth(left) + b.WriteString(left) + if pos < x { + b.WriteString(ws.render(x - pos)) + pos = x + } + } + + fgLine := fgLines[i-y] + b.WriteString(fgLine) + pos += ansi.PrintableRuneWidth(fgLine) + + right := cutLeft(bgLine, pos) + bgWidth := ansi.PrintableRuneWidth(bgLine) + rightWidth := ansi.PrintableRuneWidth(right) + if rightWidth <= bgWidth-pos { + b.WriteString(ws.render(bgWidth - rightWidth - pos)) + } + + b.WriteString(right) + } + + return b.String() +} + +// cutLeft cuts printable characters from the left. +// This function is heavily based on muesli's ansi and truncate packages. +func cutLeft(s string, cutWidth int) string { + var ( + pos int + isAnsi bool + ab bytes.Buffer + b bytes.Buffer + ) + for _, c := range s { + var w int + if c == ansi.Marker || isAnsi { + isAnsi = true + ab.WriteRune(c) + if ansi.IsTerminator(c) { + isAnsi = false + if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) { + ab.Reset() + } + } + } else { + w = runewidth.RuneWidth(c) + } + + if pos >= cutWidth { + if b.Len() == 0 { + if ab.Len() > 0 { + b.Write(ab.Bytes()) + } + if pos-cutWidth > 1 { + b.WriteByte(' ') + continue + } + } + b.WriteRune(c) + } + pos += w + } + return b.String() +} + +func getLines(s string) (lines []string, widest int) { + lines = strings.Split(s, "\n") + + for _, l := range lines { + w := charmansi.StringWidth(l) + if widest < w { + widest = w + } + } + + return lines, widest +} + +func clamp(v, lower, upper int) int { + return min(max(v, lower), upper) +} |
