summaryrefslogtreecommitdiff
path: root/internal/client
diff options
context:
space:
mode:
Diffstat (limited to 'internal/client')
-rw-r--r--internal/client/ui/ui.go125
-rw-r--r--internal/client/ui/whitespace.go89
2 files changed, 214 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)
+}
diff --git a/internal/client/ui/whitespace.go b/internal/client/ui/whitespace.go
new file mode 100644
index 0000000..c7c7d88
--- /dev/null
+++ b/internal/client/ui/whitespace.go
@@ -0,0 +1,89 @@
+package ui
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/x/ansi"
+ "github.com/muesli/termenv"
+)
+
+// whitespace is a whitespace renderer.
+type whitespace struct {
+ re *lipgloss.Renderer
+ style termenv.Style
+ chars string
+}
+
+// newWhitespace creates a new whitespace renderer. The order of the options
+// matters, if you're using WithWhitespaceRenderer, make sure it comes first as
+// other options might depend on it.
+func newWhitespace(r *lipgloss.Renderer, opts ...WhitespaceOption) *whitespace {
+ w := &whitespace{
+ re: r,
+ style: r.ColorProfile().String(),
+ }
+ for _, opt := range opts {
+ opt(w)
+ }
+ return w
+}
+
+// Render whitespaces.
+func (w whitespace) render(width int) string {
+ if w.chars == "" {
+ w.chars = " "
+ }
+
+ r := []rune(w.chars)
+ j := 0
+ b := strings.Builder{}
+
+ // Cycle through runes and print them into the whitespace.
+ for i := 0; i < width; {
+ b.WriteRune(r[j])
+ j++
+ if j >= len(r) {
+ j = 0
+ }
+ i += ansi.StringWidth(string(r[j]))
+ }
+
+ // Fill any extra gaps white spaces. This might be necessary if any runes
+ // are more than one cell wide, which could leave a one-rune gap.
+ short := width - ansi.StringWidth(b.String())
+ if short > 0 {
+ b.WriteString(strings.Repeat(" ", short))
+ }
+
+ return w.style.Styled(b.String())
+}
+
+// WhitespaceOption sets a styling rule for rendering whitespace.
+type WhitespaceOption func(*whitespace)
+
+// WithWhitespaceForeground sets the color of the characters in the whitespace.
+func WithWhitespaceForeground(c lipgloss.TerminalColor) WhitespaceOption {
+ return func(w *whitespace) {
+ w.style = w.style.Foreground(ToTermenvColor(w.re, c))
+ }
+}
+
+// WithWhitespaceBackground sets the background color of the whitespace.
+func WithWhitespaceBackground(c lipgloss.TerminalColor) WhitespaceOption {
+ return func(w *whitespace) {
+ w.style = w.style.Background(ToTermenvColor(w.re, c))
+ }
+}
+
+// WithWhitespaceChars sets the characters to be rendered in the whitespace.
+func WithWhitespaceChars(s string) WhitespaceOption {
+ return func(w *whitespace) {
+ w.chars = s
+ }
+}
+
+func ToTermenvColor(re *lipgloss.Renderer, c lipgloss.TerminalColor) termenv.Color {
+ color := c.(lipgloss.Color)
+ return re.ColorProfile().Color(string(color))
+}