2022-12-30 21:14:26 -05:00
|
|
|
package helpers
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"strings"
|
|
|
|
|
2022-12-31 14:30:11 -05:00
|
|
|
"github.com/charmbracelet/lipgloss"
|
2022-12-30 21:14:26 -05:00
|
|
|
"github.com/mattn/go-runewidth"
|
|
|
|
"github.com/muesli/ansi"
|
|
|
|
"github.com/muesli/reflow/truncate"
|
|
|
|
"github.com/muesli/termenv"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Most of this code is borrowed from
|
|
|
|
// https://github.com/charmbracelet/lipgloss/pull/102
|
2022-12-31 14:30:11 -05:00
|
|
|
// as well as the lipgloss library, with some modification for what I needed.
|
2022-12-30 21:14:26 -05:00
|
|
|
|
|
|
|
// Split a string into lines, additionally returning the size of the widest
|
|
|
|
// line.
|
|
|
|
func getLines(s string) (lines []string, widest int) {
|
|
|
|
lines = strings.Split(s, "\n")
|
|
|
|
|
|
|
|
for _, l := range lines {
|
|
|
|
w := ansi.PrintableRuneWidth(l)
|
|
|
|
if widest < w {
|
|
|
|
widest = w
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return lines, widest
|
|
|
|
}
|
|
|
|
|
|
|
|
// PlaceOverlay places fg on top of bg.
|
2023-01-01 22:05:50 -05:00
|
|
|
func PlaceOverlay(
|
|
|
|
x, y int,
|
|
|
|
fg, bg string,
|
|
|
|
shadow bool, opts ...WhitespaceOption,
|
|
|
|
) string {
|
2022-12-30 21:14:26 -05:00
|
|
|
fgLines, fgWidth := getLines(fg)
|
|
|
|
bgLines, bgWidth := getLines(bg)
|
|
|
|
bgHeight := len(bgLines)
|
|
|
|
fgHeight := len(fgLines)
|
|
|
|
|
2022-12-31 14:30:11 -05:00
|
|
|
if shadow {
|
|
|
|
var shadowbg string = ""
|
|
|
|
shadowchar := lipgloss.NewStyle().
|
|
|
|
Foreground(lipgloss.Color("#333333")).
|
|
|
|
Render("░")
|
|
|
|
for i := 0; i <= fgHeight; i++ {
|
|
|
|
if i == 0 {
|
|
|
|
shadowbg += " " + strings.Repeat(" ", fgWidth) + "\n"
|
|
|
|
} else {
|
|
|
|
shadowbg += " " + strings.Repeat(shadowchar, fgWidth) + "\n"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
|
|
|
|
fgLines, fgWidth = getLines(fg)
|
|
|
|
fgHeight = len(fgLines)
|
|
|
|
}
|
|
|
|
|
2022-12-30 21:14:26 -05:00
|
|
|
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 clamp(v, lower, upper int) int {
|
|
|
|
return min(max(v, lower), upper)
|
|
|
|
}
|
|
|
|
|
|
|
|
func max(a, b int) int {
|
|
|
|
if a > b {
|
|
|
|
return a
|
|
|
|
}
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
func min(a, b int) int {
|
|
|
|
if a < b {
|
|
|
|
return a
|
|
|
|
}
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
type whitespace struct {
|
|
|
|
style termenv.Style
|
|
|
|
chars string
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.PrintableRuneWidth(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.PrintableRuneWidth(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)
|