feat(overlay): ghost text
This commit is contained in:
+52
-1
@@ -19,6 +19,7 @@ type Overlay struct {
|
|||||||
Visible bool
|
Visible bool
|
||||||
Items []core.Suggestion
|
Items []core.Suggestion
|
||||||
Cursor int
|
Cursor int
|
||||||
|
LastGhostLen int
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -65,6 +66,48 @@ func fixedWidth(s string, width int) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Overlay) RenderGhostText(buffer string) string {
|
||||||
|
o.mu.Lock()
|
||||||
|
defer o.mu.Unlock()
|
||||||
|
|
||||||
|
if !o.Visible || len(o.Items) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var s strings.Builder
|
||||||
|
|
||||||
|
ghostText := ""
|
||||||
|
if 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" + ghostText + "\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 {
|
func (o *Overlay) Render() string {
|
||||||
o.mu.Lock()
|
o.mu.Lock()
|
||||||
defer o.mu.Unlock()
|
defer o.mu.Unlock()
|
||||||
@@ -75,7 +118,7 @@ func (o *Overlay) Render() string {
|
|||||||
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
s.WriteString("\033[?7l")
|
s.WriteString("\033[?7l")
|
||||||
s.WriteString("\0337") // DEC save cursor
|
s.WriteString("\0337") // DEC save cursor for menu drawing
|
||||||
|
|
||||||
windowSize := maxItems
|
windowSize := maxItems
|
||||||
if len(o.Items) < windowSize {
|
if len(o.Items) < windowSize {
|
||||||
@@ -189,6 +232,14 @@ func (o *Overlay) ClearAndDisable() string {
|
|||||||
|
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
s.WriteString("\033[?7l")
|
s.WriteString("\033[?7l")
|
||||||
|
|
||||||
|
if o.LastGhostLen > 0 {
|
||||||
|
s.WriteString("\0337") // save
|
||||||
|
s.WriteString(strings.Repeat(" ", o.LastGhostLen+10))
|
||||||
|
s.WriteString("\0338") // restore
|
||||||
|
o.LastGhostLen = 0
|
||||||
|
}
|
||||||
|
|
||||||
s.WriteString("\0337")
|
s.WriteString("\0337")
|
||||||
|
|
||||||
for i := 0; i < maxItems+2; i++ {
|
for i := 0; i < maxItems+2; i++ {
|
||||||
|
|||||||
+50
-10
@@ -11,7 +11,9 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"github.com/versenilvis/iris/commands/core"
|
"github.com/versenilvis/iris/commands/core"
|
||||||
@@ -185,7 +187,10 @@ func runWrapper() {
|
|||||||
}
|
}
|
||||||
os.Stdout.Write([]byte(overlay.Clear()))
|
os.Stdout.Write([]byte(overlay.Clear()))
|
||||||
overlay.UpdateItems(results)
|
overlay.UpdateItems(results)
|
||||||
os.Stdout.Write([]byte(overlay.Render()))
|
var rBuf strings.Builder
|
||||||
|
rBuf.WriteString(overlay.RenderGhostText(query))
|
||||||
|
rBuf.WriteString(overlay.Render())
|
||||||
|
os.Stdout.Write([]byte(rBuf.String()))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -193,32 +198,51 @@ func runWrapper() {
|
|||||||
suggestionsEnabled := true
|
suggestionsEnabled := true
|
||||||
mode := loadMode()
|
mode := loadMode()
|
||||||
|
|
||||||
|
var renderTimer *time.Timer
|
||||||
|
var renderMu sync.Mutex
|
||||||
|
|
||||||
// renderOverlay decides whether to draw the suggestion menu based on current state
|
// renderOverlay decides whether to draw the suggestion menu based on current state
|
||||||
renderOverlay := func() {
|
renderOverlay := func() {
|
||||||
|
renderMu.Lock()
|
||||||
|
defer renderMu.Unlock()
|
||||||
|
|
||||||
|
if renderTimer != nil {
|
||||||
|
renderTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
if !suggestionsEnabled {
|
if !suggestionsEnabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if naiveBuffer == "" {
|
bufCopy := naiveBuffer
|
||||||
|
modeCopy := mode
|
||||||
|
|
||||||
|
if bufCopy == "" {
|
||||||
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog("[Render] query: '%s', mode: %s", naiveBuffer, mode)
|
// Debounce for 15ms to allow PTY to process the keystroke and update the terminal cursor.
|
||||||
results := mergeResults(naiveBuffer, mode)
|
// This completely prevents the asynchronous ghost text race condition where the PTY echo
|
||||||
|
// overwrites the first letter of our ghost text!
|
||||||
|
renderTimer = time.AfterFunc(15*time.Millisecond, func() {
|
||||||
|
debugLog("[Render] query: '%s', mode: %s", bufCopy, modeCopy)
|
||||||
|
results := mergeResults(bufCopy, modeCopy)
|
||||||
debugLog("[Render] results found: %d", len(results))
|
debugLog("[Render] results found: %d", len(results))
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
b.WriteString(overlay.ClearAndDisable())
|
||||||
} else {
|
} else {
|
||||||
var buf strings.Builder
|
|
||||||
if overlay.Visible {
|
if overlay.Visible {
|
||||||
buf.WriteString(overlay.Clear())
|
b.WriteString(overlay.Clear())
|
||||||
}
|
}
|
||||||
overlay.UpdateItems(results)
|
overlay.UpdateItems(results)
|
||||||
buf.WriteString(overlay.Render())
|
b.WriteString(overlay.RenderGhostText(bufCopy))
|
||||||
os.Stdout.Write([]byte(buf.String()))
|
b.WriteString(overlay.Render())
|
||||||
}
|
}
|
||||||
|
os.Stdout.Write([]byte(b.String()))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
renderOverlay()
|
renderOverlay()
|
||||||
@@ -269,9 +293,25 @@ func runWrapper() {
|
|||||||
overlay.Cursor = len(overlay.Items) - 1
|
overlay.Cursor = len(overlay.Items) - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
os.Stdout.Write([]byte(overlay.Render()))
|
var rBuf strings.Builder
|
||||||
|
rBuf.WriteString(overlay.RenderGhostText(naiveBuffer))
|
||||||
|
rBuf.WriteString(overlay.Render())
|
||||||
|
os.Stdout.Write([]byte(rBuf.String()))
|
||||||
i += 2
|
i += 2
|
||||||
continue
|
continue
|
||||||
|
} else if overlay.Visible && inputSlice[i+2] == 'C' { // right arrow
|
||||||
|
topCmd := overlay.Items[0].Cmd
|
||||||
|
if strings.HasPrefix(strings.ToLower(topCmd), strings.ToLower(naiveBuffer)) {
|
||||||
|
ghostText := topCmd[len(naiveBuffer):]
|
||||||
|
if len(ghostText) > 0 {
|
||||||
|
intercepted = true
|
||||||
|
naiveBuffer += ghostText
|
||||||
|
ptmx.Write([]byte(ghostText))
|
||||||
|
shouldOverlayDraw = true
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user