mirror of
https://github.com/Kyren223/eko.git
synced 2026-05-02 17:54:40 +00:00
183 lines
4.0 KiB
Go
183 lines
4.0 KiB
Go
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"
|
|
)
|
|
|
|
const DEBUG = true
|
|
|
|
var Center lipgloss.Position = 0.499
|
|
|
|
var Width int
|
|
var Height int
|
|
var Program *tea.Program
|
|
|
|
type ModelTransition struct {
|
|
Model tea.Model
|
|
}
|
|
|
|
func Transition(model tea.Model) tea.Cmd {
|
|
return func() tea.Msg {
|
|
return ModelTransition{Model: model}
|
|
}
|
|
}
|
|
|
|
type QuitMsg struct{}
|
|
|
|
func AddBorderHeader(header string, headerOffset int, style lipgloss.Style, render string) string {
|
|
b := style.GetBorderStyle()
|
|
body := style.UnsetBorderTop().Render(render)
|
|
|
|
bodyWidth, headerWidth := lipgloss.Width(body), lipgloss.Width(header)
|
|
leftCornerWidth, rightCornerWidth := lipgloss.Width(b.TopLeft), lipgloss.Width(b.TopRight)
|
|
topWidth := bodyWidth - leftCornerWidth - rightCornerWidth
|
|
|
|
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()).
|
|
Foreground(style.GetBorderTopForeground())
|
|
|
|
left := b.TopLeft + strings.Repeat(b.Top, leftWidth)
|
|
right := topStyle.Render(strings.Repeat(b.Top, rightWidth) + b.TopRight)
|
|
|
|
borderTop := lipgloss.NewStyle().
|
|
Inline(true).
|
|
MaxWidth(bodyWidth).
|
|
Background(style.GetBorderTopBackground()).
|
|
Foreground(style.GetBorderTopForeground()).
|
|
Render(fmt.Sprintf("%s%s%s", left, header, right))
|
|
|
|
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)
|
|
}
|