feat: up/down arrow key now can choose last commands

This commit is contained in:
verse91
2026-04-29 19:35:02 +07:00
parent 23e5b13f6d
commit 3c8eca3a62
2 changed files with 75 additions and 8 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ import (
// mergeResults collects and dedupes suggestions for a query and mode // mergeResults collects and dedupes suggestions for a query and mode
// example: mergeResults("git ", "spec") // example: mergeResults("git ", "spec")
func mergeResults(query string, mode string) []core.Suggestion { func mergeResults(query string, mode string) []core.Suggestion {
if query == "" { if query == "" && mode != "history" {
debugLog("[Merge] Query empty, returning nil") debugLog("[Merge] Query empty, returning nil")
return nil return nil
} }
+74 -7
View File
@@ -166,14 +166,18 @@ func runWrapper() {
var disableGhostText atomic.Bool var disableGhostText atomic.Bool
var userNavigated bool var userNavigated bool
var renderOverlay func()
isExecuting := func() bool { isExecuting := func() bool {
pgrp, err := unix.IoctlGetInt(int(ptmx.Fd()), unix.TIOCGPGRP) pgrp, err := unix.IoctlGetInt(int(ptmx.Fd()), unix.TIOCGPGRP)
if err != nil { if err != nil {
return false return false
} }
// when another command starts, it puts it in a new process group and gives it the PTY shellPGID, err := unix.Getpgid(core.ShellPID)
return pgrp != core.ShellPID if err != nil {
return pgrp != core.ShellPID
}
return pgrp != shellPGID
} }
// listen for suggestion requests from shell scripts via the ipc pipe // listen for suggestion requests from shell scripts via the ipc pipe
@@ -227,7 +231,7 @@ func runWrapper() {
var renderMu sync.Mutex 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() renderMu.Lock()
defer renderMu.Unlock() defer renderMu.Unlock()
@@ -243,7 +247,7 @@ func runWrapper() {
modeCopy := mode modeCopy := mode
navCopy := userNavigated navCopy := userNavigated
if bufCopy == "" { if bufCopy == "" && !navCopy {
os.Stdout.Write([]byte(overlay.ClearAndDisable())) os.Stdout.Write([]byte(overlay.ClearAndDisable()))
return return
} }
@@ -255,7 +259,7 @@ func runWrapper() {
if isExecuting() { if isExecuting() {
return return
} }
var b strings.Builder var b strings.Builder
if !navCopy { if !navCopy {
debugLog("[Render] query: '%s', mode: %s", bufCopy, modeCopy) debugLog("[Render] query: '%s', mode: %s", bufCopy, modeCopy)
@@ -288,6 +292,23 @@ func runWrapper() {
renderOverlay() renderOverlay()
renderNow := func() {
renderMu.Lock()
if renderTimer != nil {
renderTimer.Stop()
}
renderMu.Unlock()
var b strings.Builder
if overlay.Visible {
if !disableGhostText.Load() {
b.WriteString(overlay.RenderGhostText(naiveBuffer, userNavigated))
}
b.WriteString(overlay.Render())
}
os.Stdout.Write([]byte(b.String()))
}
// reads from stdin and decides what to forward or intercept // reads from stdin and decides what to forward or intercept
// for most cases, I just handle the already have terminal shortcuts // for most cases, I just handle the already have terminal shortcuts
// for some shortcuts like tab, enter, shift tab, ctrl r, // for some shortcuts like tab, enter, shift tab, ctrl r,
@@ -329,6 +350,9 @@ func runWrapper() {
if overlay.Visible && (inputSlice[i+2] == 'A' || inputSlice[i+2] == 'B') { if overlay.Visible && (inputSlice[i+2] == 'A' || inputSlice[i+2] == 'B') {
intercepted = true intercepted = true
userNavigated = true userNavigated = true
os.Stdout.Write([]byte(overlay.Clear())) // clear old menu
if inputSlice[i+2] == 'A' { // up arrow if inputSlice[i+2] == 'A' { // up arrow
overlay.Cursor-- overlay.Cursor--
if overlay.Cursor < 0 { if overlay.Cursor < 0 {
@@ -340,13 +364,56 @@ func runWrapper() {
overlay.Cursor = len(overlay.Items) - 1 overlay.Cursor = len(overlay.Items) - 1
} }
} }
selected := overlay.Items[overlay.Cursor].Cmd selected := overlay.Items[overlay.Cursor].Cmd
ptmx.Write([]byte{0x15}) // ctrl+u to clear line ptmx.Write([]byte{0x15}) // ctrl+u to clear line
ptmx.Write([]byte(selected)) ptmx.Write([]byte(selected))
naiveBuffer = selected naiveBuffer = selected
shouldOverlayDraw = true renderNow()
i += 2
continue
} else if !overlay.Visible && naiveBuffer == "" && (inputSlice[i+2] == 'A' || inputSlice[i+2] == 'B') { // up/down arrow on empty prompt
intercepted = true
mode = "history"
saveMode(mode)
results := mergeResults("", "history")
if len(results) > 0 {
limit := 10
if len(results) < limit {
limit = len(results)
}
var top10 []core.Suggestion
if inputSlice[i+2] == 'A' {
// Up arrow: Reverse the list so newest is at the bottom
for j := limit - 1; j >= 0; j-- {
top10 = append(top10, results[j])
}
} else {
// Down arrow: Normal order, newest is at the top
for j := 0; j < limit; j++ {
top10 = append(top10, results[j])
}
}
overlay.UpdateItems(top10)
if inputSlice[i+2] == 'A' {
overlay.Cursor = len(top10) - 1 // Up arrow: Start at the bottom
} else {
overlay.Cursor = 0 // Down arrow: Start at the top
}
selected := overlay.Items[overlay.Cursor].Cmd
ptmx.Write([]byte{0x15}) // ctrl+u to clear line
ptmx.Write([]byte(selected))
naiveBuffer = selected
userNavigated = true
renderNow()
}
i += 2 i += 2
continue continue
} else if overlay.Visible && !disableGhostText.Load() && inputSlice[i+2] == 'C' { // right arrow } else if overlay.Visible && !disableGhostText.Load() && inputSlice[i+2] == 'C' { // right arrow