Files
iris-context/integration/overlay.go
T
VERSE a885c7462a feat(config): ui width, auto exec, selection and navigation configs (#80)
closes #67

So I made an auto-exec confg `auto-exec = true`
(e32c0ba6ed69f3c764353e58b8fbacf160f37d5a) which means:
- If you want your command in the prompt to be executed right away, you
can set this = false (for example, you are typing `nvim ~/.conf` but
IRIS is suggesting `nvim ~/.config`, it will ignore the suggestion and
exec what you are typing, this is current default behavior)
- If you want to auto execute full command which is being suggested, you
can set this = true (this is the opposite of above, when you are typing
`nvim ~/.conf` but IRIS is suggesting `nvim ~/.config`, it will auto
exec `nvim ~/.config` instead, but sometime you want to exec what you
are typing instead of the suggestion right?, just press esc or shift tab
to hide the menu and it will)

---

Select keybinding to replace tab
(0dc01cba8c610e8d6fd8c50f6a71bdcc061a098f)
`select = "ctrl+y"`

Some users may love nvim keybinding or emacs, so I bring navigation
keybinding to be configurable (0dc01cba8c610e8d6fd8c50f6a71bdcc061a098f)
```
navigate-up = "ctrl+k"
navigate-down = "ctrl+j"
```
---

I also added a option for UI width
(dfa3b810a7be406fe4652b3886020f37bb166811)
`max-width = 80`
- If you configure `max-width` to be larger than the actual width of the
current terminal, Iris will automatically "compress" the window to match
the terminal's width (meaning it will never overflow and break the
interface)
- If you configure `max-width` too small (e.g 10), the interface will
have cramped text, making it look very bad. So, I've set a minimum safe
min limit of 40. If the configured value is smaller than this, Iris will
automatically use 40

Small note: If you set `max-width = 0` (or leave it blank), it will
automatically use Iris's default standard size of 76. SO you can
confidently customize it without fear of breaking the UI (I hope so
lmao)

---

Added a fallback to ensure Iris never crashes or loses shortcuts if the
user accidentally deletes keybinding configuration lines or
intentionally assigns empty strings " "
(f682c9bf623f3ed46ad0763221145604dbd91b07)

Fix typo `toggle_mode` to `toggle-mode`
(b39be6673e552735fa65a6c9807510c83b4d9cdd)

if you have a better config design, please tell me, I always listen to
your idea
2026-07-31 10:49:10 +07:00

912 lines
19 KiB
Go

package integration
import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"unicode/utf8"
"github.com/charmbracelet/lipgloss"
"github.com/versenilvis/iris/internal/config"
"github.com/versenilvis/iris/internal/logger"
"github.com/versenilvis/iris/spec"
"golang.org/x/term"
)
const (
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 = max(getParam(0, 1)-1, 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
}
if b < 0x7f {
col++
i++
continue
}
r, size := utf8.DecodeRune(data[i:])
w := lipgloss.Width(string(r))
col += w
i += size
}
return col
}
type Theme struct {
Border lipgloss.Color
Accent lipgloss.Color
Muted lipgloss.Color
Text lipgloss.Color
TextSel lipgloss.Color
Match lipgloss.Color
Desc lipgloss.Color
DescSel lipgloss.Color
SelBg lipgloss.Color
ScrollInfo lipgloss.Color
GhostText lipgloss.Color
}
var currentTheme = Theme{
Border: lipgloss.Color("#a277ff"),
Accent: lipgloss.Color("#61ffca"),
Muted: lipgloss.Color("#6d6a7f"),
Text: lipgloss.Color("#edecee"),
TextSel: lipgloss.Color("#ffffff"),
Match: lipgloss.Color("#61ffca"),
Desc: lipgloss.Color("#9692a8"),
DescSel: lipgloss.Color("#edecee"),
SelBg: lipgloss.Color("#3d375e"),
ScrollInfo: lipgloss.Color("#a277ff"),
GhostText: lipgloss.Color("#4B4A4C"),
}
func SetTheme(t Theme) {
currentTheme = t
}
type Overlay struct {
mu sync.Mutex
Visible bool
Items []spec.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
}
}
func NewOverlay() *Overlay {
return &Overlay{Visible: false, Cursor: 0, StartIdx: 0}
}
func (o *Overlay) UpdateItems(items []spec.Suggestion) {
o.mu.Lock()
defer o.mu.Unlock()
o.Items = items
o.Visible = len(o.Items) > 0
o.Cursor = 0
o.StartIdx = 0
}
func (o *Overlay) IsVisible() bool {
o.mu.Lock()
defer o.mu.Unlock()
return o.Visible
}
func (o *Overlay) GetUserNavigated() bool {
o.mu.Lock()
defer o.mu.Unlock()
return o.UserNavigated
}
func (o *Overlay) SetUserNavigated(v bool) {
o.mu.Lock()
defer o.mu.Unlock()
o.UserNavigated = v
}
func (o *Overlay) GetTypedQuery() string {
o.mu.Lock()
defer o.mu.Unlock()
return o.TypedQuery
}
func (o *Overlay) GetCurrentCmd() string {
o.mu.Lock()
defer o.mu.Unlock()
if len(o.Items) > 0 && o.Cursor >= 0 && o.Cursor < len(o.Items) {
return o.Items[o.Cursor].Cmd
}
return ""
}
func (o *Overlay) GetTopCmd() string {
o.mu.Lock()
defer o.mu.Unlock()
if len(o.Items) > 0 {
return o.Items[0].Cmd
}
return ""
}
func (o *Overlay) Show() {
o.mu.Lock()
defer o.mu.Unlock()
o.UserNavigated = false
o.Visible = true
}
func (o *Overlay) ResetCursor() {
o.mu.Lock()
defer o.mu.Unlock()
o.Cursor = 0
}
func (o *Overlay) SetQueryAndItems(query string, items []spec.Suggestion) {
o.mu.Lock()
defer o.mu.Unlock()
o.TypedQuery = query
o.UserNavigated = false
o.Items = items
o.Visible = len(o.Items) > 0
o.Cursor = 0
o.StartIdx = 0
}
func (o *Overlay) InjectAISuggestion(sugg spec.Suggestion) bool {
o.mu.Lock()
defer o.mu.Unlock()
if o.TypedQuery == "" {
return false
}
if o.UserNavigated {
return false
}
var currentConf int
if len(o.Items) > 0 {
currentConf = o.Items[0].Confidence
if currentConf == 0 {
if o.Items[0].Source == "history" {
currentConf = 70
} else {
currentConf = 50
}
}
}
if !strings.HasPrefix(strings.ToLower(sugg.Cmd), strings.ToLower(o.TypedQuery)) {
return false
}
if sugg.Confidence <= currentConf && len(o.Items) > 0 {
return false
}
if len(o.Items) == 0 {
o.Items = []spec.Suggestion{sugg}
} else if strings.EqualFold(o.Items[0].Cmd, sugg.Cmd) {
if o.Visible && o.Items[0].Confidence == sugg.Confidence {
return false
}
o.Items[0] = sugg
} else {
o.Items = append([]spec.Suggestion{sugg}, o.Items...)
if len(o.Items) > 100 {
o.Items = o.Items[:100]
}
}
o.Visible = true
o.Cursor = 0
o.StartIdx = 0
return true
}
func (o *Overlay) ClearGhostLen() int {
o.mu.Lock()
defer o.mu.Unlock()
l := o.LastGhostLen
o.LastGhostLen = 0
return l
}
func (o *Overlay) MoveCursor(dir string) (moved bool, selectedCmd string) {
o.mu.Lock()
defer o.mu.Unlock()
if !o.Visible || len(o.Items) == 0 {
return false, ""
}
o.UserNavigated = true
oldCursor := o.Cursor
if dir == "up" {
o.Cursor--
if o.Cursor < 0 {
o.Cursor = 0
}
} else {
o.Cursor++
if o.Cursor >= len(o.Items) {
o.Cursor = len(o.Items) - 1
}
}
if o.Cursor == oldCursor {
return false, ""
}
return true, o.Items[o.Cursor].Cmd
}
func (o *Overlay) SetHistoryList(items []spec.Suggestion, startAtBottom bool) string {
o.mu.Lock()
defer o.mu.Unlock()
o.TypedQuery = ""
o.UserNavigated = true
o.Items = items
o.Visible = len(o.Items) > 0
if startAtBottom && len(o.Items) > 0 {
o.Cursor = len(o.Items) - 1
} else {
o.Cursor = 0
}
o.StartIdx = 0
if len(o.Items) > 0 && o.Cursor >= 0 && o.Cursor < len(o.Items) {
return o.Items[o.Cursor].Cmd
}
return ""
}
func fixedWidth(s string, width int) string {
if width <= 0 {
return ""
}
visualWidth := lipgloss.Width(s)
if visualWidth == width {
return s
}
if visualWidth < width {
return s + strings.Repeat(" ", width-visualWidth)
}
var sb strings.Builder
currentWidth := 0
for _, r := range s {
rw := lipgloss.Width(string(r))
if currentWidth+rw > width-1 {
break
}
sb.WriteRune(r)
currentWidth += rw
}
sb.WriteString("…")
rem := width - lipgloss.Width(sb.String())
if rem > 0 {
sb.WriteString(strings.Repeat(" ", rem))
}
return sb.String()
}
func truncateToWidth(s string, maxW int) string {
if maxW <= 0 {
return ""
}
if lipgloss.Width(s) <= maxW {
return s
}
if maxW == 1 {
return "…"
}
var sb strings.Builder
w := 0
for _, r := range s {
rw := lipgloss.Width(string(r))
if w+rw > maxW-1 { // leave 1 column for '…'
break
}
sb.WriteRune(r)
w += rw
}
sb.WriteRune('…')
return sb.String()
}
func (o *Overlay) GetGhostText(buffer string, cursorAtEnd bool) string {
o.mu.Lock()
defer o.mu.Unlock()
if !o.Visible || len(o.Items) == 0 || !cursorAtEnd || buffer == "" {
return ""
}
var topCmd string
if o.Cursor >= 0 && o.Cursor < len(o.Items) {
topCmd = o.Items[o.Cursor].Cmd
} else {
topCmd = o.Items[0].Cmd
}
if strings.HasPrefix(strings.ToLower(topCmd), strings.ToLower(buffer)) {
return topCmd[len(buffer):]
}
return ""
}
func (o *Overlay) ClearGhostTextState() {
o.mu.Lock()
defer o.mu.Unlock()
o.LastGhostLen = 0
}
func (o *Overlay) HideGhostTextSync() string {
o.mu.Lock()
defer o.mu.Unlock()
if o.LastGhostLen > 0 {
padLen := o.LastGhostLen + 4
res := "\0337" + strings.Repeat(" ", padLen) + "\0338"
o.LastGhostLen = 0
return res
}
return ""
}
func (o *Overlay) RenderGhostText(buffer string, userNavigated bool, cursorAtEnd bool) string {
o.mu.Lock()
defer o.mu.Unlock()
if !o.Visible || len(o.Items) == 0 {
if o.LastGhostLen > 0 {
padLen := o.LastGhostLen + 4
o.LastGhostLen = 0
return "\0337" + strings.Repeat(" ", padLen) + "\0338"
}
return ""
}
var s strings.Builder
ghostText := ""
if cursorAtEnd && buffer != "" {
var topCmd string
if o.Cursor >= 0 && o.Cursor < len(o.Items) {
topCmd = o.Items[o.Cursor].Cmd
} else {
topCmd = o.Items[0].Cmd
}
if strings.HasPrefix(strings.ToLower(topCmd), strings.ToLower(buffer)) {
ghostText = topCmd[len(buffer):]
}
}
if ghostText != "" {
width, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil || width <= 0 {
width = 120
}
cursorCol := o.PromptLen + lipgloss.Width(buffer)
availableCols := width - cursorCol
if availableCols <= 0 {
ghostText = ""
} else if lipgloss.Width(ghostText) > availableCols {
ghostText = truncateToWidth(ghostText, availableCols)
}
}
if ghostText == "" && o.LastGhostLen == 0 {
return ""
}
ghostWidth := lipgloss.Width(ghostText)
padLen := max(o.LastGhostLen-ghostWidth, 0)
if o.LastGhostLen > 0 {
padLen += 4
}
s.WriteString("\0337")
if ghostText != "" {
styled := lipgloss.NewStyle().Foreground(currentTheme.GhostText).Render(ghostText)
s.WriteString(styled)
}
if padLen > 0 {
s.WriteString(strings.Repeat(" ", padLen))
}
s.WriteString("\0338")
o.LastGhostLen = ghostWidth
return s.String()
}
func renderMatchedTitle(title, typed string, selected bool, w int) string {
t := currentTheme
textColor := t.Text
if selected {
textColor = t.TextSel
}
base := lipgloss.NewStyle().Foreground(textColor)
match := lipgloss.NewStyle().Foreground(t.Match).Bold(true)
if selected {
base = base.Background(t.SelBg)
match = match.Background(t.SelBg)
}
display := fixedWidth(title, w)
if typed == "" || !strings.HasPrefix(strings.ToLower(display), strings.ToLower(typed)) {
return base.Render(display)
}
typedRunes := []rune(typed)
displayRunes := []rune(display)
matchLen := min(len(typedRunes), len(displayRunes))
return match.Render(string(displayRunes[:matchLen])) + base.Render(string(displayRunes[matchLen:]))
}
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 ""
}
t := currentTheme
border := lipgloss.NewStyle().Foreground(t.Border)
scrollStyle := lipgloss.NewStyle().Foreground(t.ScrollInfo)
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
}
boxWidth := config.Get().UI.MaxWidth
if boxWidth <= 0 {
boxWidth = 76 // Default if 0
}
if boxWidth > width {
boxWidth = width // Responsive: don't overflow
}
if boxWidth < 40 {
boxWidth = 40 // Minimum safe width
}
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 := min(len(o.Items), maxItems)
scrolloffUp := 1
if windowSize <= 3 {
scrolloffUp = 0
}
if o.Cursor < o.StartIdx+scrolloffUp {
o.StartIdx = o.Cursor - scrolloffUp
}
if o.Cursor >= o.StartIdx+windowSize {
o.StartIdx = o.Cursor - windowSize + 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)
}
}
inner := boxWidth - 2 // width between the two border pipes/corners
style := strings.ToLower(config.Get().UI.Style)
isClassic := style == "classic" || style == "minimal" || style == "minimalist"
// top side border with scroll counter
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))
}
leftDash := 3
if isClassic && scrollInfo != "" {
leftDash = (inner - len(scrollInfo)) / 2
}
rightDash := inner - leftDash - len(scrollInfo)
if scrollInfo == "" {
leftDash = 0
rightDash = inner
}
fmt.Fprintf(&s, "%s%s%s%s%s",
border.Render("╭"),
border.Render(strings.Repeat("─", leftDash)),
scrollStyle.Render(scrollInfo),
border.Render(strings.Repeat("─", rightDash)),
border.Render("╮"),
)
// left and right side border with item rows
descW := 24
padGap := 2
markerW := 1
iconW := 2
if isClassic || !config.Get().UI.NerdFonts {
iconW = 0
}
sidePad := 1
titleW := inner - sidePad*2 - markerW - 1 - iconW
if iconW > 0 {
titleW--
}
titleW = titleW - padGap - descW
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]
selected := i == o.Cursor
left := border.Render("│")
right := border.Render("│")
bg := lipgloss.NewStyle()
if selected {
bg = bg.Background(t.SelBg)
}
marker := " "
markerStyle := bg.Foreground(t.Muted)
if selected {
marker = "▶"
markerStyle = bg.Foreground(t.Accent).Bold(true)
}
iconGlyph := lookupIcon(it.Icon)
iconColor := t.Muted
if selected {
iconColor = t.Accent
}
iconStr := bg.Foreground(iconColor).Render(fixedWidth(iconGlyph, iconW))
title := renderMatchedTitle(it.Cmd, o.TypedQuery, selected, titleW)
descColor := t.Desc
if selected {
descColor = t.DescSel
}
var desc string
if isClassic {
if it.Icon == "alias" {
desc = bg.Foreground(descColor).Render(fixedWidth("alias: "+it.Desc, descW))
} else {
desc = bg.Foreground(descColor).Render(fixedWidth(it.Desc, descW))
}
} else {
switch it.Icon {
case "alias":
boxStyle := lipgloss.NewStyle().Background(lipgloss.Color("#2a2342")).Foreground(lipgloss.Color("#a277ff"))
if selected {
boxStyle = lipgloss.NewStyle().Background(lipgloss.Color("#a277ff")).Foreground(lipgloss.Color("#110f18")).Bold(true)
}
tag := boxStyle.Render(" alias ")
tw := lipgloss.Width(tag)
rem := max(descW-tw-1, 0)
desc = tag + bg.Render(" ") + bg.Foreground(descColor).Render(fixedWidth(it.Desc, rem))
case "history":
boxStyle := lipgloss.NewStyle().Background(lipgloss.Color("#1a2d36")).Foreground(lipgloss.Color("#61ffca"))
if selected {
boxStyle = lipgloss.NewStyle().Background(lipgloss.Color("#61ffca")).Foreground(lipgloss.Color("#110f18")).Bold(true)
}
tag := boxStyle.Render(" history ")
tw := lipgloss.Width(tag)
rem := max(descW-tw, 0)
desc = tag + bg.Render(strings.Repeat(" ", rem))
case "system":
boxStyle := lipgloss.NewStyle().Background(lipgloss.Color("#1e1d28")).Foreground(lipgloss.Color("#a277ff"))
if selected {
boxStyle = lipgloss.NewStyle().Background(lipgloss.Color("#a277ff")).Foreground(lipgloss.Color("#110f18")).Bold(true)
}
tag := boxStyle.Render(" system ")
tw := lipgloss.Width(tag)
rem := max(descW-tw, 0)
desc = tag + bg.Render(strings.Repeat(" ", rem))
default:
desc = bg.Foreground(descColor).Render(fixedWidth(it.Desc, descW))
}
}
iconSection := ""
if iconW > 0 {
iconSection = iconStr + bg.Render(" ")
}
fmt.Fprintf(&s, "%s%s%s%s%s%s%s%s%s%s",
left,
bg.Render(" "),
markerStyle.Render(marker),
bg.Render(" "),
iconSection,
title,
bg.Render(strings.Repeat(" ", padGap)),
desc,
bg.Render(" "),
right,
)
}
// bottom side border with footer shortcut hints
s.WriteString("\0338")
fmt.Fprintf(&s, "\033[%dB", windowSize+2)
s.WriteString("\033[2K")
moveToTarget()
footerInfo := ""
if !isClassic {
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#a277ff")).Bold(true)
tabKey := keyStyle.Render("<Tab>")
ctrlRKey := keyStyle.Render(config.FormatKeyName(config.Get().Keybindings.ToggleMode))
acceptText := lipgloss.NewStyle().Foreground(t.ScrollInfo).Render(" Accept")
modeText := lipgloss.NewStyle().Foreground(t.ScrollInfo).Render(" Mode")
footerInfo = fmt.Sprintf(" %s%s • %s%s ", tabKey, acceptText, ctrlRKey, modeText)
}
footerRunes := lipgloss.Width(footerInfo)
rightDash = 2
leftDash = inner - footerRunes - rightDash
if footerInfo == "" {
leftDash = 0
rightDash = inner
}
if leftDash < 0 {
leftDash = 0
}
fmt.Fprintf(&s, "%s%s%s%s%s",
border.Render("╰"),
border.Render(strings.Repeat("─", leftDash)),
footerInfo,
border.Render(strings.Repeat("─", rightDash)),
border.Render("╯"),
)
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 := range maxItems + 2 {
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) HideMenu(query string) string {
o.mu.Lock()
defer o.mu.Unlock()
o.TypedQuery = query
if !o.Visible && len(o.Items) == 0 && o.LastGhostLen == 0 {
return ""
}
o.Visible = false
o.Items = nil
o.UserNavigated = false
o.Cursor = 0
o.StartIdx = 0
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 := range maxItems + 2 {
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
o.Cursor = 0
o.StartIdx = 0
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 := range maxItems + 2 {
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()
}