427 lines
8.6 KiB
Go
427 lines
8.6 KiB
Go
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"unicode/utf8"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/versenilvis/iris/commands/core"
|
|
"github.com/versenilvis/iris/logger"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
const (
|
|
boxWidth = 72
|
|
maxItems = 6
|
|
)
|
|
|
|
func ComputeCursorCol(data []byte) int {
|
|
col := 0
|
|
i := 0
|
|
n := len(data)
|
|
for i < n {
|
|
b := data[i]
|
|
if b == '\r' {
|
|
col = 0
|
|
i++
|
|
continue
|
|
}
|
|
if b == '\b' || b == 0x7f {
|
|
col--
|
|
if col < 0 {
|
|
col = 0
|
|
}
|
|
i++
|
|
continue
|
|
}
|
|
if b == '\t' {
|
|
col = (col + 8) &^ 7
|
|
i++
|
|
continue
|
|
}
|
|
if b == '\033' {
|
|
if i+1 < n && data[i+1] == '[' {
|
|
j := i + 2
|
|
for j < n && data[j] >= 0x20 && data[j] <= 0x3F {
|
|
j++
|
|
}
|
|
if j < n {
|
|
cmd := data[j]
|
|
paramsStr := string(data[i+2 : j])
|
|
paramsStr = strings.TrimLeft(paramsStr, "?>=")
|
|
parts := strings.Split(paramsStr, ";")
|
|
getParam := func(idx, def int) int {
|
|
if idx < len(parts) && parts[idx] != "" {
|
|
if v, err := strconv.Atoi(parts[idx]); err == nil && v > 0 {
|
|
return v
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
switch cmd {
|
|
case 'C':
|
|
col += getParam(0, 1)
|
|
case 'D':
|
|
col -= getParam(0, 1)
|
|
if col < 0 {
|
|
col = 0
|
|
}
|
|
case 'G':
|
|
col = getParam(0, 1) - 1
|
|
if col < 0 {
|
|
col = 0
|
|
}
|
|
}
|
|
i = j + 1
|
|
continue
|
|
}
|
|
break
|
|
} else if i+1 < n && data[i+1] == ']' {
|
|
j := i + 2
|
|
for j < n {
|
|
if data[j] == '\007' {
|
|
j++
|
|
break
|
|
}
|
|
if data[j] == '\033' && j+1 < n && data[j+1] == '\\' {
|
|
j += 2
|
|
break
|
|
}
|
|
j++
|
|
}
|
|
i = j
|
|
continue
|
|
} else if i+1 < n && (data[i+1] == 'P' || data[i+1] == 'X' || data[i+1] == '^' || data[i+1] == '_') {
|
|
j := i + 2
|
|
for j < n {
|
|
if data[j] == '\033' && j+1 < n && data[j+1] == '\\' {
|
|
j += 2
|
|
break
|
|
}
|
|
j++
|
|
}
|
|
i = j
|
|
continue
|
|
} else if i+1 < n {
|
|
i += 2
|
|
continue
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if b < 0x20 {
|
|
i++
|
|
continue
|
|
}
|
|
r, size := utf8.DecodeRune(data[i:])
|
|
w := lipgloss.Width(string(r))
|
|
col += w
|
|
i += size
|
|
}
|
|
return col
|
|
}
|
|
|
|
type Overlay struct {
|
|
mu sync.Mutex
|
|
Visible bool
|
|
Items []core.Suggestion
|
|
Cursor int
|
|
StartIdx int
|
|
LastGhostLen int
|
|
TypedQuery string
|
|
UserNavigated bool
|
|
PromptLen int
|
|
}
|
|
|
|
func (o *Overlay) SetPromptLen(l int) {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
if o.PromptLen != l {
|
|
logger.Debugf("SetPromptLen: %d -> %d", o.PromptLen, l)
|
|
o.PromptLen = l
|
|
}
|
|
}
|
|
|
|
var (
|
|
selBgColor = lipgloss.Color("#44475A")
|
|
|
|
iconStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#BD93F9"))
|
|
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#F8F8F2"))
|
|
descStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6272A4"))
|
|
|
|
iconSel = iconStyle.Background(selBgColor)
|
|
titleSel = titleStyle.Background(selBgColor)
|
|
descSel = descStyle.Background(selBgColor)
|
|
padSel = lipgloss.NewStyle().Background(selBgColor)
|
|
|
|
borderColor = lipgloss.Color("#6272A4")
|
|
borderStyle = lipgloss.NewStyle().Foreground(borderColor)
|
|
)
|
|
|
|
func NewOverlay() *Overlay {
|
|
return &Overlay{
|
|
Visible: false,
|
|
Cursor: 0,
|
|
StartIdx: 0,
|
|
}
|
|
}
|
|
|
|
func (o *Overlay) UpdateItems(items []core.Suggestion) {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
o.Items = items
|
|
o.Visible = len(o.Items) > 0
|
|
o.Cursor = 0
|
|
o.StartIdx = 0
|
|
}
|
|
|
|
func fixedWidth(s string, width int) string {
|
|
runes := []rune(s)
|
|
if len(runes) > width {
|
|
return string(runes[:width-1]) + "…"
|
|
}
|
|
if len(runes) < width {
|
|
return s + strings.Repeat(" ", width-len(runes))
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (o *Overlay) RenderGhostText(buffer string, userNavigated bool) string {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if !o.Visible || len(o.Items) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var s strings.Builder
|
|
|
|
ghostText := ""
|
|
if !userNavigated && buffer != "" {
|
|
topCmd := o.Items[0].Cmd
|
|
if strings.HasPrefix(strings.ToLower(topCmd), strings.ToLower(buffer)) {
|
|
ghostText = topCmd[len(buffer):]
|
|
}
|
|
}
|
|
|
|
padLen := o.LastGhostLen - len(ghostText)
|
|
if padLen < 0 {
|
|
padLen = 0
|
|
}
|
|
|
|
// add extra padding to erase any stray characters left by fast backspaces
|
|
// before the debounce timer fired, 10 spaces is safe and won't hit right prompts
|
|
padLen += 10
|
|
|
|
if ghostText != "" || padLen > 0 {
|
|
s.WriteString("\0337") // save cursor at prompt
|
|
if ghostText != "" {
|
|
s.WriteString("\033[90m")
|
|
s.WriteString(ghostText)
|
|
s.WriteString("\033[0m")
|
|
}
|
|
if padLen > 0 {
|
|
s.WriteString(strings.Repeat(" ", padLen))
|
|
}
|
|
s.WriteString("\0338") // restore cursor back to prompt
|
|
o.LastGhostLen = len(ghostText)
|
|
}
|
|
|
|
return s.String()
|
|
}
|
|
|
|
func (o *Overlay) Render() string {
|
|
return o.draw()
|
|
}
|
|
|
|
func (o *Overlay) draw() string {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if !o.Visible || len(o.Items) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var s strings.Builder
|
|
s.WriteString("\033[?7l")
|
|
|
|
typedLen := len([]rune(o.TypedQuery))
|
|
targetCol := o.PromptLen + typedLen
|
|
|
|
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
|
if err != nil || width <= 0 {
|
|
width = 120
|
|
}
|
|
if targetCol+boxWidth > width {
|
|
targetCol = width - boxWidth
|
|
}
|
|
if targetCol < 0 {
|
|
targetCol = 0
|
|
}
|
|
logger.Debugf("Overlay draw: pLen=%d, typedLen=%d, targetCol=%d, width=%d", o.PromptLen, typedLen, targetCol, width)
|
|
|
|
s.WriteString("\0337")
|
|
|
|
windowSize := maxItems
|
|
if len(o.Items) < windowSize {
|
|
windowSize = len(o.Items)
|
|
}
|
|
|
|
scrolloffUp := 1
|
|
scrolloffDown := 0
|
|
if windowSize <= 3 {
|
|
scrolloffUp = 0
|
|
}
|
|
|
|
if o.Cursor < o.StartIdx+scrolloffUp {
|
|
o.StartIdx = o.Cursor - scrolloffUp
|
|
}
|
|
if o.Cursor >= o.StartIdx+windowSize-scrolloffDown {
|
|
o.StartIdx = o.Cursor - windowSize + scrolloffDown + 1
|
|
}
|
|
if o.StartIdx < 0 {
|
|
o.StartIdx = 0
|
|
}
|
|
if o.StartIdx > len(o.Items)-windowSize {
|
|
o.StartIdx = len(o.Items) - windowSize
|
|
}
|
|
if o.StartIdx < 0 {
|
|
o.StartIdx = 0
|
|
}
|
|
|
|
start := o.StartIdx
|
|
end := start + windowSize
|
|
totalLines := windowSize + 2
|
|
|
|
for range totalLines {
|
|
s.WriteByte('\n')
|
|
}
|
|
fmt.Fprintf(&s, "\033[%dA", totalLines)
|
|
|
|
s.WriteString("\0337")
|
|
|
|
moveToTarget := func() {
|
|
s.WriteString("\r")
|
|
if targetCol > 0 {
|
|
fmt.Fprintf(&s, "\033[%dC", targetCol)
|
|
}
|
|
}
|
|
|
|
s.WriteString("\0338")
|
|
fmt.Fprintf(&s, "\033[%dB", 1)
|
|
s.WriteString("\033[2K")
|
|
moveToTarget()
|
|
|
|
scrollInfo := ""
|
|
if len(o.Items) > windowSize {
|
|
scrollInfo = fmt.Sprintf(" %d/%d ", o.Cursor+1, len(o.Items))
|
|
}
|
|
|
|
borderWidth := boxWidth - len(scrollInfo)
|
|
topBorder := "╭" + strings.Repeat("─", borderWidth/2) + scrollInfo + strings.Repeat("─", boxWidth-borderWidth/2-len(scrollInfo)) + "╮"
|
|
s.WriteString(borderStyle.Render(topBorder))
|
|
|
|
iconW := 6
|
|
descW := 22
|
|
titleW := boxWidth - iconW - descW - 3
|
|
|
|
for i := start; i < end; i++ {
|
|
s.WriteString("\0338")
|
|
fmt.Fprintf(&s, "\033[%dB", (i-start)+2)
|
|
s.WriteString("\033[2K")
|
|
moveToTarget()
|
|
|
|
it := o.Items[i]
|
|
rawIcon := fixedWidth(it.Icon, iconW)
|
|
rawTitle := fixedWidth(it.Cmd, titleW)
|
|
rawDesc := fixedWidth(it.Desc, descW)
|
|
|
|
left := borderStyle.Render("│")
|
|
right := borderStyle.Render("│")
|
|
|
|
if i == o.Cursor {
|
|
icon := iconSel.Render(" " + rawIcon + " ")
|
|
title := titleSel.Render(rawTitle)
|
|
pad := padSel.Render(" ")
|
|
desc := descSel.Render(rawDesc)
|
|
fmt.Fprintf(&s, "%s%s%s%s%s%s", left, icon, pad, title, desc, right)
|
|
} else {
|
|
icon := iconStyle.Render(" " + rawIcon + " ")
|
|
title := titleStyle.Render(rawTitle)
|
|
desc := descStyle.Render(rawDesc)
|
|
fmt.Fprintf(&s, "%s%s %s%s%s", left, icon, title, desc, right)
|
|
}
|
|
}
|
|
|
|
s.WriteString("\0338")
|
|
fmt.Fprintf(&s, "\033[%dB", windowSize+2)
|
|
s.WriteString("\033[2K")
|
|
moveToTarget()
|
|
bottomBorder := "╰" + strings.Repeat("─", boxWidth) + "╯"
|
|
s.WriteString(borderStyle.Render(bottomBorder))
|
|
|
|
s.WriteString("\0338")
|
|
s.WriteString("\033[?7h")
|
|
return s.String()
|
|
}
|
|
|
|
func (o *Overlay) Clear() string {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
var s strings.Builder
|
|
s.WriteString("\033[?7l")
|
|
s.WriteString("\0337")
|
|
|
|
for i := 0; i < maxItems+2; i++ {
|
|
s.WriteString("\0338")
|
|
fmt.Fprintf(&s, "\033[%dB", i+1)
|
|
s.WriteString("\r\033[2K")
|
|
}
|
|
|
|
s.WriteString("\0338")
|
|
s.WriteString("\033[?7h")
|
|
return s.String()
|
|
}
|
|
|
|
func (o *Overlay) ClearAndDisable() string {
|
|
o.mu.Lock()
|
|
defer o.mu.Unlock()
|
|
|
|
if !o.Visible && len(o.Items) == 0 && o.LastGhostLen == 0 {
|
|
return ""
|
|
}
|
|
|
|
o.Visible = false
|
|
o.Items = nil
|
|
o.TypedQuery = ""
|
|
o.UserNavigated = false
|
|
|
|
var s strings.Builder
|
|
s.WriteString("\033[?7l")
|
|
|
|
if o.LastGhostLen > 0 {
|
|
s.WriteString("\0337")
|
|
s.WriteString(strings.Repeat(" ", o.LastGhostLen+10))
|
|
s.WriteString("\0338")
|
|
o.LastGhostLen = 0
|
|
}
|
|
|
|
s.WriteString("\0337")
|
|
|
|
for i := 0; i < maxItems+2; i++ {
|
|
s.WriteString("\0338")
|
|
fmt.Fprintf(&s, "\033[%dB", i+1)
|
|
s.WriteString("\r\033[2K")
|
|
}
|
|
|
|
s.WriteString("\0338")
|
|
s.WriteString("\033[?7h")
|
|
return s.String()
|
|
}
|