summaryrefslogtreecommitdiff
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
parente29b7ade2f402bf89ca60bcb4a5598310ff809d4 (diff)
feat: added functionality for overlays/popups
-rw-r--r--go.mod6
-rw-r--r--go.sum9
-rw-r--r--internal/client/ui/ui.go125
-rw-r--r--internal/client/ui/whitespace.go89
4 files changed, 227 insertions, 2 deletions
diff --git a/go.mod b/go.mod
index 0387b1d..1a2deb0 100644
--- a/go.mod
+++ b/go.mod
@@ -24,12 +24,14 @@ require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.15.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ golang.org/x/crypto v0.28.0
golang.org/x/sync v0.8.0 // indirect
- golang.org/x/sys v0.24.0 // indirect
- golang.org/x/text v0.14.0 // indirect
+ golang.org/x/sys v0.26.0 // indirect
+ golang.org/x/text v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
diff --git a/go.sum b/go.sum
index 73223d3..b1d6c8b 100644
--- a/go.sum
+++ b/go.sum
@@ -24,6 +24,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
@@ -32,10 +33,13 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -46,14 +50,19 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
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))
+}