summaryrefslogtreecommitdiff
path: root/internal/client/ui/ui.go
diff options
context:
space:
mode:
authorKyren223 <Kyren223@proton.me>2024-11-07 18:01:10 +0200
committerKyren223 <Kyren223@proton.me>2024-11-07 18:01:10 +0200
commit1a096afcfa1ee0c417f844f1ffd60d1994caede3 (patch)
tree1f3f2c337e2615476162ab7052106d205cc2e967 /internal/client/ui/ui.go
parente29b7ade2f402bf89ca60bcb4a5598310ff809d4 (diff)
feat: added functionality for overlays/popups
Diffstat (limited to 'internal/client/ui/ui.go')
-rw-r--r--internal/client/ui/ui.go125
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)
+}