feat: adopt x/ansi instead of hand rolling escape sequences (#96)
also contains the lipgloss width change from #94
This commit is contained in:
@@ -5,6 +5,7 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
charm.land/lipgloss/v2 v2.0.5
|
charm.land/lipgloss/v2 v2.0.5
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.7
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/versenilvis/fuzzy v0.1.0-rc1.2
|
github.com/versenilvis/fuzzy v0.1.0-rc1.2
|
||||||
@@ -17,7 +18,6 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
|
|||||||
+88
-129
@@ -10,6 +10,7 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"charm.land/lipgloss/v2"
|
"charm.land/lipgloss/v2"
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
"github.com/versenilvis/iris/internal/config"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
"github.com/versenilvis/iris/internal/logger"
|
"github.com/versenilvis/iris/internal/logger"
|
||||||
"github.com/versenilvis/iris/spec"
|
"github.com/versenilvis/iris/spec"
|
||||||
@@ -369,49 +370,20 @@ func fixedWidth(s string, width int) string {
|
|||||||
if visualWidth == width {
|
if visualWidth == width {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
if visualWidth < width {
|
if visualWidth > width {
|
||||||
return s + strings.Repeat(" ", width-visualWidth)
|
s = ansi.Truncate(s, width, "…")
|
||||||
}
|
}
|
||||||
var sb strings.Builder
|
if rem := width - lipgloss.Width(s); rem > 0 {
|
||||||
currentWidth := 0
|
s += strings.Repeat(" ", rem)
|
||||||
for _, r := range s {
|
|
||||||
rw := lipgloss.Width(string(r))
|
|
||||||
if currentWidth+rw > width-1 {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
sb.WriteRune(r)
|
return s
|
||||||
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 {
|
func truncateToWidth(s string, maxW int) string {
|
||||||
if maxW <= 0 {
|
if maxW <= 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if lipgloss.Width(s) <= maxW {
|
return ansi.Truncate(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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// titledEdge renders one horizontal box edge (top or bottom) with styled
|
// titledEdge renders one horizontal box edge (top or bottom) with styled
|
||||||
@@ -469,7 +441,7 @@ func (o *Overlay) HideGhostTextSync() string {
|
|||||||
defer o.mu.Unlock()
|
defer o.mu.Unlock()
|
||||||
if o.LastGhostLen > 0 {
|
if o.LastGhostLen > 0 {
|
||||||
padLen := o.LastGhostLen + 4
|
padLen := o.LastGhostLen + 4
|
||||||
res := "\0337" + strings.Repeat(" ", padLen) + "\0338"
|
res := ansi.SaveCursor + strings.Repeat(" ", padLen) + ansi.RestoreCursor
|
||||||
o.LastGhostLen = 0
|
o.LastGhostLen = 0
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -484,7 +456,7 @@ func (o *Overlay) RenderGhostText(buffer string, userNavigated bool, cursorAtEnd
|
|||||||
if o.LastGhostLen > 0 {
|
if o.LastGhostLen > 0 {
|
||||||
padLen := o.LastGhostLen + 4
|
padLen := o.LastGhostLen + 4
|
||||||
o.LastGhostLen = 0
|
o.LastGhostLen = 0
|
||||||
return "\0337" + strings.Repeat(" ", padLen) + "\0338"
|
return ansi.SaveCursor + strings.Repeat(" ", padLen) + ansi.RestoreCursor
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -504,10 +476,7 @@ func (o *Overlay) RenderGhostText(buffer string, userNavigated bool, cursorAtEnd
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ghostText != "" {
|
if ghostText != "" {
|
||||||
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
width := termWidth()
|
||||||
if err != nil || width <= 0 {
|
|
||||||
width = 120
|
|
||||||
}
|
|
||||||
cursorCol := o.PromptLen + lipgloss.Width(buffer)
|
cursorCol := o.PromptLen + lipgloss.Width(buffer)
|
||||||
availableCols := width - cursorCol
|
availableCols := width - cursorCol
|
||||||
if availableCols <= 0 {
|
if availableCols <= 0 {
|
||||||
@@ -527,7 +496,7 @@ func (o *Overlay) RenderGhostText(buffer string, userNavigated bool, cursorAtEnd
|
|||||||
padLen += 4
|
padLen += 4
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString("\0337")
|
s.WriteString(ansi.SaveCursor)
|
||||||
if ghostText != "" {
|
if ghostText != "" {
|
||||||
styled := lipgloss.NewStyle().Foreground(currentTheme.GhostText).Render(ghostText)
|
styled := lipgloss.NewStyle().Foreground(currentTheme.GhostText).Render(ghostText)
|
||||||
s.WriteString(styled)
|
s.WriteString(styled)
|
||||||
@@ -535,12 +504,32 @@ func (o *Overlay) RenderGhostText(buffer string, userNavigated bool, cursorAtEnd
|
|||||||
if padLen > 0 {
|
if padLen > 0 {
|
||||||
s.WriteString(strings.Repeat(" ", padLen))
|
s.WriteString(strings.Repeat(" ", padLen))
|
||||||
}
|
}
|
||||||
s.WriteString("\0338")
|
s.WriteString(ansi.RestoreCursor)
|
||||||
o.LastGhostLen = ghostWidth
|
o.LastGhostLen = ghostWidth
|
||||||
|
|
||||||
return s.String()
|
return s.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// termWidth returns the terminal width, falling back to 120 when the size
|
||||||
|
// can't be determined (e.g. stdout isn't a TTY in tests).
|
||||||
|
func termWidth() int {
|
||||||
|
w, _, err := term.GetSize(int(os.Stdout.Fd()))
|
||||||
|
if err != nil || w <= 0 {
|
||||||
|
return 120
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourceTag renders a pill-style source label (" alias ") with inverted
|
||||||
|
// colors when selected.
|
||||||
|
func sourceTag(label, bgHex, fgHex string, selected bool) string {
|
||||||
|
style := lipgloss.NewStyle().Background(lipgloss.Color(bgHex)).Foreground(lipgloss.Color(fgHex))
|
||||||
|
if selected {
|
||||||
|
style = lipgloss.NewStyle().Background(lipgloss.Color(fgHex)).Foreground(lipgloss.Color("#110f18")).Bold(true)
|
||||||
|
}
|
||||||
|
return style.Render(" " + label + " ")
|
||||||
|
}
|
||||||
|
|
||||||
func renderMatchedTitle(title, typed string, selected bool, w int) string {
|
func renderMatchedTitle(title, typed string, selected bool, w int) string {
|
||||||
t := currentTheme
|
t := currentTheme
|
||||||
textColor := t.Text
|
textColor := t.Text
|
||||||
@@ -561,10 +550,12 @@ func renderMatchedTitle(title, typed string, selected bool, w int) string {
|
|||||||
return base.Render(display)
|
return base.Render(display)
|
||||||
}
|
}
|
||||||
|
|
||||||
typedRunes := []rune(typed)
|
// Split by display width, not rune count, so a typed prefix containing
|
||||||
displayRunes := []rune(display)
|
// wide runes (CJK, emoji) highlights exactly the matching cells.
|
||||||
matchLen := min(len(typedRunes), len(displayRunes))
|
matchW := lipgloss.Width(typed)
|
||||||
return match.Render(string(displayRunes[:matchLen])) + base.Render(string(displayRunes[matchLen:]))
|
highlighted := ansi.Truncate(display, matchW, "")
|
||||||
|
rest := strings.TrimPrefix(display, highlighted)
|
||||||
|
return match.Render(highlighted) + base.Render(rest)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Overlay) Render() string {
|
func (o *Overlay) Render() string {
|
||||||
@@ -584,15 +575,12 @@ func (o *Overlay) draw() string {
|
|||||||
scrollStyle := lipgloss.NewStyle().Foreground(t.ScrollInfo)
|
scrollStyle := lipgloss.NewStyle().Foreground(t.ScrollInfo)
|
||||||
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
s.WriteString("\033[?7l")
|
s.WriteString(ansi.ResetModeAutoWrap)
|
||||||
|
|
||||||
typedLen := len([]rune(o.TypedQuery))
|
typedLen := lipgloss.Width(o.TypedQuery)
|
||||||
targetCol := o.PromptLen + typedLen
|
targetCol := o.PromptLen + typedLen
|
||||||
|
|
||||||
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
width := termWidth()
|
||||||
if err != nil || width <= 0 {
|
|
||||||
width = 120
|
|
||||||
}
|
|
||||||
|
|
||||||
boxWidth := config.Get().UI.MaxWidth
|
boxWidth := config.Get().UI.MaxWidth
|
||||||
if boxWidth <= 0 {
|
if boxWidth <= 0 {
|
||||||
@@ -613,7 +601,7 @@ func (o *Overlay) draw() string {
|
|||||||
}
|
}
|
||||||
logger.Debugf("Overlay draw: pLen=%d, typedLen=%d, targetCol=%d, width=%d", o.PromptLen, typedLen, targetCol, width)
|
logger.Debugf("Overlay draw: pLen=%d, typedLen=%d, targetCol=%d, width=%d", o.PromptLen, typedLen, targetCol, width)
|
||||||
|
|
||||||
s.WriteString("\0337")
|
s.WriteString(ansi.SaveCursor)
|
||||||
|
|
||||||
windowSize := min(len(o.Items), maxItems)
|
windowSize := min(len(o.Items), maxItems)
|
||||||
|
|
||||||
@@ -642,17 +630,15 @@ func (o *Overlay) draw() string {
|
|||||||
end := start + windowSize
|
end := start + windowSize
|
||||||
totalLines := windowSize + 2
|
totalLines := windowSize + 2
|
||||||
|
|
||||||
for range totalLines {
|
s.WriteString(strings.Repeat("\n", totalLines))
|
||||||
s.WriteByte('\n')
|
s.WriteString(ansi.CursorUp(totalLines))
|
||||||
}
|
|
||||||
fmt.Fprintf(&s, "\033[%dA", totalLines)
|
|
||||||
|
|
||||||
s.WriteString("\0337")
|
s.WriteString(ansi.SaveCursor)
|
||||||
|
|
||||||
moveToTarget := func() {
|
moveToTarget := func() {
|
||||||
s.WriteString("\r")
|
s.WriteString("\r")
|
||||||
if targetCol > 0 {
|
if targetCol > 0 {
|
||||||
fmt.Fprintf(&s, "\033[%dC", targetCol)
|
s.WriteString(ansi.CursorForward(targetCol))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,9 +648,9 @@ func (o *Overlay) draw() string {
|
|||||||
isClassic := style == "classic" || style == "minimal" || style == "minimalist"
|
isClassic := style == "classic" || style == "minimal" || style == "minimalist"
|
||||||
|
|
||||||
// top side border with scroll counter
|
// top side border with scroll counter
|
||||||
s.WriteString("\0338")
|
s.WriteString(ansi.RestoreCursor)
|
||||||
fmt.Fprintf(&s, "\033[%dB", 1)
|
s.WriteString(ansi.CursorDown(1))
|
||||||
s.WriteString("\033[2K")
|
s.WriteString(ansi.EraseEntireLine)
|
||||||
moveToTarget()
|
moveToTarget()
|
||||||
|
|
||||||
scrollInfo := ""
|
scrollInfo := ""
|
||||||
@@ -693,9 +679,9 @@ func (o *Overlay) draw() string {
|
|||||||
titleW = titleW - padGap - descW
|
titleW = titleW - padGap - descW
|
||||||
|
|
||||||
for i := start; i < end; i++ {
|
for i := start; i < end; i++ {
|
||||||
s.WriteString("\0338")
|
s.WriteString(ansi.RestoreCursor)
|
||||||
fmt.Fprintf(&s, "\033[%dB", (i-start)+2)
|
s.WriteString(ansi.CursorDown((i - start) + 2))
|
||||||
s.WriteString("\033[2K")
|
s.WriteString(ansi.EraseEntireLine)
|
||||||
moveToTarget()
|
moveToTarget()
|
||||||
|
|
||||||
it := o.Items[i]
|
it := o.Items[i]
|
||||||
@@ -740,31 +726,16 @@ func (o *Overlay) draw() string {
|
|||||||
} else {
|
} else {
|
||||||
switch it.Icon {
|
switch it.Icon {
|
||||||
case "alias":
|
case "alias":
|
||||||
boxStyle := lipgloss.NewStyle().Background(lipgloss.Color("#2a2342")).Foreground(lipgloss.Color("#a277ff"))
|
tag := sourceTag("alias", "#2a2342", "#a277ff", selected)
|
||||||
if selected {
|
rem := max(descW-lipgloss.Width(tag)-1, 0)
|
||||||
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))
|
desc = tag + bg.Render(" ") + bg.Foreground(descColor).Render(fixedWidth(it.Desc, rem))
|
||||||
case "history":
|
case "history":
|
||||||
boxStyle := lipgloss.NewStyle().Background(lipgloss.Color("#1a2d36")).Foreground(lipgloss.Color("#61ffca"))
|
tag := sourceTag("history", "#1a2d36", "#61ffca", selected)
|
||||||
if selected {
|
rem := max(descW-lipgloss.Width(tag), 0)
|
||||||
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))
|
desc = tag + bg.Render(strings.Repeat(" ", rem))
|
||||||
case "system":
|
case "system":
|
||||||
boxStyle := lipgloss.NewStyle().Background(lipgloss.Color("#1e1d28")).Foreground(lipgloss.Color("#a277ff"))
|
tag := sourceTag("system", "#1e1d28", "#a277ff", selected)
|
||||||
if selected {
|
rem := max(descW-lipgloss.Width(tag), 0)
|
||||||
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))
|
desc = tag + bg.Render(strings.Repeat(" ", rem))
|
||||||
default:
|
default:
|
||||||
desc = bg.Foreground(descColor).Render(fixedWidth(it.Desc, descW))
|
desc = bg.Foreground(descColor).Render(fixedWidth(it.Desc, descW))
|
||||||
@@ -791,9 +762,9 @@ func (o *Overlay) draw() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// bottom side border with footer shortcut hints
|
// bottom side border with footer shortcut hints
|
||||||
s.WriteString("\0338")
|
s.WriteString(ansi.RestoreCursor)
|
||||||
fmt.Fprintf(&s, "\033[%dB", windowSize+2)
|
s.WriteString(ansi.CursorDown(windowSize + 2))
|
||||||
s.WriteString("\033[2K")
|
s.WriteString(ansi.EraseEntireLine)
|
||||||
moveToTarget()
|
moveToTarget()
|
||||||
|
|
||||||
footerInfo := ""
|
footerInfo := ""
|
||||||
@@ -808,27 +779,31 @@ func (o *Overlay) draw() string {
|
|||||||
|
|
||||||
s.WriteString(titledEdge("╰", "╯", inner, footerInfo, border, inner-lipgloss.Width(footerInfo)-2))
|
s.WriteString(titledEdge("╰", "╯", inner, footerInfo, border, inner-lipgloss.Width(footerInfo)-2))
|
||||||
|
|
||||||
s.WriteString("\0338")
|
s.WriteString(ansi.RestoreCursor)
|
||||||
s.WriteString("\033[?7h")
|
s.WriteString(ansi.SetModeAutoWrap)
|
||||||
return s.String()
|
return s.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clearLinesBelow erases n lines below the cursor, leaving the cursor where
|
||||||
|
// it started. Caller is responsible for wrapping/resetting auto-wrap mode.
|
||||||
|
func clearLinesBelow(s *strings.Builder, n int) {
|
||||||
|
s.WriteString(ansi.SaveCursor)
|
||||||
|
for i := range n {
|
||||||
|
s.WriteString(ansi.RestoreCursor)
|
||||||
|
s.WriteString(ansi.CursorDown(i + 1))
|
||||||
|
s.WriteString("\r" + ansi.EraseEntireLine)
|
||||||
|
}
|
||||||
|
s.WriteString(ansi.RestoreCursor)
|
||||||
|
}
|
||||||
|
|
||||||
func (o *Overlay) Clear() string {
|
func (o *Overlay) Clear() string {
|
||||||
o.mu.Lock()
|
o.mu.Lock()
|
||||||
defer o.mu.Unlock()
|
defer o.mu.Unlock()
|
||||||
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
s.WriteString("\033[?7l")
|
s.WriteString(ansi.ResetModeAutoWrap)
|
||||||
s.WriteString("\0337")
|
clearLinesBelow(&s, maxItems+2)
|
||||||
|
s.WriteString(ansi.SetModeAutoWrap)
|
||||||
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()
|
return s.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,25 +823,17 @@ func (o *Overlay) HideMenu(query string) string {
|
|||||||
o.StartIdx = 0
|
o.StartIdx = 0
|
||||||
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
s.WriteString("\033[?7l")
|
s.WriteString(ansi.ResetModeAutoWrap)
|
||||||
|
|
||||||
if o.LastGhostLen > 0 {
|
if o.LastGhostLen > 0 {
|
||||||
s.WriteString("\0337")
|
s.WriteString(ansi.SaveCursor)
|
||||||
s.WriteString(strings.Repeat(" ", o.LastGhostLen+10))
|
s.WriteString(strings.Repeat(" ", o.LastGhostLen+10))
|
||||||
s.WriteString("\0338")
|
s.WriteString(ansi.RestoreCursor)
|
||||||
o.LastGhostLen = 0
|
o.LastGhostLen = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString("\0337")
|
clearLinesBelow(&s, maxItems+2)
|
||||||
|
s.WriteString(ansi.SetModeAutoWrap)
|
||||||
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()
|
return s.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,24 +853,16 @@ func (o *Overlay) ClearAndDisable() string {
|
|||||||
o.StartIdx = 0
|
o.StartIdx = 0
|
||||||
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
s.WriteString("\033[?7l")
|
s.WriteString(ansi.ResetModeAutoWrap)
|
||||||
|
|
||||||
if o.LastGhostLen > 0 {
|
if o.LastGhostLen > 0 {
|
||||||
s.WriteString("\0337")
|
s.WriteString(ansi.SaveCursor)
|
||||||
s.WriteString(strings.Repeat(" ", o.LastGhostLen+10))
|
s.WriteString(strings.Repeat(" ", o.LastGhostLen+10))
|
||||||
s.WriteString("\0338")
|
s.WriteString(ansi.RestoreCursor)
|
||||||
o.LastGhostLen = 0
|
o.LastGhostLen = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
s.WriteString("\0337")
|
clearLinesBelow(&s, maxItems+2)
|
||||||
|
s.WriteString(ansi.SetModeAutoWrap)
|
||||||
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()
|
return s.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
"github.com/versenilvis/iris/spec"
|
"github.com/versenilvis/iris/spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,3 +127,34 @@ func TestHideMenu_PreservesTypedQueryForAI(t *testing.T) {
|
|||||||
t.Fatalf("Expected AI suggestion to be injected into Items[0] and Visible=true")
|
t.Fatalf("Expected AI suggestion to be injected into Items[0] and Visible=true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderMatchedTitle_ASCII(t *testing.T) {
|
||||||
|
out := renderMatchedTitle("git status", "git", false, 40)
|
||||||
|
stripped := ansi.Strip(out)
|
||||||
|
if stripped != "git status"+strings.Repeat(" ", 30) {
|
||||||
|
t.Fatalf("unexpected content: %q", stripped)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "git") {
|
||||||
|
t.Fatalf("missing match segment: %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderMatchedTitle_WideRunes(t *testing.T) {
|
||||||
|
out := renderMatchedTitle("日記を表示する", "日記", false, 20)
|
||||||
|
stripped := ansi.Strip(out)
|
||||||
|
if !strings.HasPrefix(stripped, "日記を表示する") {
|
||||||
|
t.Fatalf("content mangled: %q", stripped)
|
||||||
|
}
|
||||||
|
// split must occur after 日記 (width 4), not mid-grapheme
|
||||||
|
if !strings.Contains(stripped, "を") {
|
||||||
|
t.Fatalf("lost remainder: %q", stripped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderMatchedTitle_CaseInsensitive(t *testing.T) {
|
||||||
|
out := renderMatchedTitle("GitHub CLI", "git", false, 40)
|
||||||
|
stripped := ansi.Strip(out)
|
||||||
|
if !strings.HasPrefix(stripped, "GitHub CLI") {
|
||||||
|
t.Fatalf("case-insensitive match mangled content: %q", stripped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user