Files
iris-context/root/wrapper.go
T
VERSE ebde9ba68f refactor(commands): separate commands and its core into two folders (#32)
- I think it would be better if one for commands only, one for the spec
handling core logic so people won't be confused by the core logic inside
the commands folder (cleaner I guess)
- Regen docs
2026-07-05 11:27:30 +07:00

934 lines
25 KiB
Go

package root
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/creack/pty"
"github.com/versenilvis/iris/spec"
"github.com/versenilvis/iris/config"
"github.com/versenilvis/iris/integration"
"github.com/versenilvis/iris/integration/shell"
"github.com/versenilvis/iris/logger"
"golang.org/x/sys/unix"
"golang.org/x/term"
)
func loadMode() string {
mode := config.Get().Core.Mode
if mode == "last" {
state := config.LoadState()
if state.LastMode == "history" || state.LastMode == "spec" {
return state.LastMode
}
return "spec"
}
if mode == "history" || mode == "spec" {
return mode
}
return "spec"
}
func saveMode(mode string) {
state := config.LoadState()
state.LastMode = mode
_ = config.SaveState(state)
}
var (
oldState *term.State
oldStateMu sync.Mutex
activeMode string
activeModeMu sync.RWMutex
stdoutMu sync.Mutex
)
func writeStdout(data []byte) {
if len(data) == 0 {
return
}
stdoutMu.Lock()
defer stdoutMu.Unlock()
_, _ = os.Stdout.Write(data)
}
// restoreTerminal restores the terminal state if needed
func restoreTerminal() {
oldStateMu.Lock()
defer oldStateMu.Unlock()
if oldState != nil {
_ = term.Restore(int(os.Stdin.Fd()), oldState)
oldState = nil
}
}
// runWrapper sets up the pty environment, launches the shell,
// and manages the main input loop to provide real-time suggestions
// it handles raw terminal mode to intercept keystrokes and
// coordinates between the shell process and the suggestion overlay
func runWrapper() {
var naiveBuffer string
cursorOffset := 0
var bufferMu sync.Mutex
var userNavigated atomic.Bool
var renderMenuNow func()
r, w, err := os.Pipe() // pipe for ipc communication from shell to iris
if err != nil {
return
}
var shellName string
if active := os.Getenv("IRIS_ACTIVE_SHELL"); active != "" {
shellName = active
_ = os.Unsetenv("IRIS_ACTIVE_SHELL")
} else if shellFlag != "" {
shellName = shellFlag
} else {
shellName = detectShell()
}
shell.Init(shellName)
adapter := shell.Current
ctx := context.Background()
c := exec.CommandContext(ctx, adapter.GetShellPath())
c.ExtraFiles = make([]*os.File, 11)
// pass write end of pipe to shell as fd 13 (since index 10 maps to 13)
c.ExtraFiles[10] = w
c.Env = adapter.GetEnv(13, os.Getpid())
ptmx, err := pty.Start(c)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "[IRIS] failed to start PTY: %v\n", err)
return
}
defer func() { _ = ptmx.Close() }()
_ = pty.InheritSize(os.Stdin, ptmx)
spec.ShellPID = c.Process.Pid
logger.Infof("PTY child shell started: shell=%s, path=%s, pid=%d", shellName, adapter.GetShellPath(), c.Process.Pid)
// put terminal in raw mode to intercept every keystroke
var errMakeRaw error
oldState, errMakeRaw = term.MakeRaw(int(os.Stdin.Fd()))
if errMakeRaw != nil {
logger.Errorf("Failed to set terminal raw mode: %v", errMakeRaw)
panic(errMakeRaw)
}
logger.Debugf("Terminal set to raw mode successfully")
defer restoreTerminal()
sigCh := make(chan os.Signal, 2)
signal.Notify(sigCh, syscall.SIGWINCH, syscall.SIGUSR1)
go func() {
defer func() {
if r := recover(); r != nil {
WriteCrashLog(r)
restoreTerminal()
printCrashNotice()
startRescueShell()
os.Exit(2)
}
}()
for s := range sigCh {
switch s {
case syscall.SIGWINCH:
logger.Debugf("Received SIGWINCH terminal resize signal")
_ = pty.InheritSize(os.Stdin, ptmx) // handle terminal window resize
// this is the core feature of reloading
// it helps IRIS reload itself that you dont need to restart the shell manually
// SIGUSR1 is the signal to active reload when you type "just reload"
case syscall.SIGUSR1:
// trigger iris reload by executing itself again
exe, _ := os.Executable()
_ = os.Setenv("IRIS_RELOADED", "true")
innerShell := getActiveInnerShell(c.Process.Pid, shellName)
if innerShell != "" {
// to detect which is last shell (bash, zsh, fish)
_ = os.Setenv("IRIS_ACTIVE_SHELL", innerShell)
}
if c.Process != nil {
cwd, linkErr := os.Readlink(fmt.Sprintf("/proc/%d/cwd", c.Process.Pid))
if linkErr != nil {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
out, errCmd := exec.CommandContext(ctx, "lsof", "-p", fmt.Sprintf("%d", c.Process.Pid), "-a", "-d", "cwd", "-F", "n").Output()
cancel()
if errCmd == nil {
for line := range strings.SplitSeq(string(out), "\n") {
if strings.HasPrefix(line, "n") {
cwd = strings.TrimSpace(line[1:])
linkErr = nil
break
}
}
}
}
if linkErr == nil {
_ = os.Chdir(cwd)
}
_ = syscall.Kill(c.Process.Pid, syscall.SIGKILL)
_ = ptmx.Close()
}
restoreTerminal()
execArgs := []string{os.Args[0]}
if logDir, pathErr := config.CachePath(); pathErr == nil {
argsFile := filepath.Join(logDir, "reload-args")
if data, readErr := os.ReadFile(argsFile); readErr == nil {
lines := strings.SplitSeq(string(data), "\n")
for line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" {
execArgs = append(execArgs, trimmed)
}
}
_ = os.Remove(argsFile)
} else {
execArgs = os.Args
}
} else {
execArgs = os.Args
}
_ = syscall.Exec(exe, execArgs, os.Environ())
}
}
}()
overlay := integration.NewOverlay()
// start background update check (async)
pendingUpdate = startBackgroundUpdateCheck()
updatePrinted := false
shellPGID, err := unix.Getpgid(spec.ShellPID)
if err != nil {
shellPGID = spec.ShellPID
}
var isCommandActive atomic.Bool
isExecuting := func() bool {
if isCommandActive.Load() {
return true
}
pgrp, err := unix.IoctlGetInt(int(ptmx.Fd()), unix.TIOCGPGRP)
if err != nil {
return false
}
return pgrp != shellPGID
}
// bridge pty output to actual stdout
go func() {
defer func() {
if r := recover(); r != nil {
WriteCrashLog(r)
restoreTerminal()
printCrashNotice()
startRescueShell()
os.Exit(2)
}
}()
var lastPromptBuf []byte
buf := make([]byte, 4096)
for {
n, err := ptmx.Read(buf)
if err != nil {
if err == io.EOF {
restoreTerminal()
os.Exit(0)
}
continue
}
writeStdout(buf[:n])
bufferMu.Lock()
nbEmpty := naiveBuffer == ""
navigated := userNavigated.Load()
bufferMu.Unlock()
if isExecuting() {
lastPromptBuf = nil
} else if nbEmpty && !navigated {
lastPromptBuf = append(lastPromptBuf, buf[:n]...)
if idx := bytes.LastIndexByte(lastPromptBuf, '\n'); idx >= 0 {
lastPromptBuf = append([]byte(nil), lastPromptBuf[idx+1:]...)
}
pLen := integration.ComputeCursorCol(lastPromptBuf)
if pLen >= 0 {
overlay.SetPromptLen(pLen)
}
}
}
}()
var disableGhostText atomic.Bool
disableGhostText.Store(!config.Get().UI.GhostText)
var renderOverlay func()
// listen for suggestion requests from shell scripts via the ipc pipe
go func() {
defer func() {
if r := recover(); r != nil {
WriteCrashLog(r)
restoreTerminal()
printCrashNotice()
startRescueShell()
os.Exit(2)
}
}()
scanner := bufio.NewScanner(r)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\x00'); i >= 0 {
return i + 1, data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
})
for scanner.Scan() {
query := scanner.Text()
if query == "IRIS_CMD_START" {
isCommandActive.Store(true)
bufferMu.Lock()
naiveBuffer = ""
cursorOffset = 0
bufferMu.Unlock()
writeStdout([]byte(overlay.ClearAndDisable()))
continue
}
if query == "IRIS_CMD_STOP" {
isCommandActive.Store(false)
// hook: after user executes a command, print the update notice exactly once per session
if !updatePrinted {
select {
case result, ok := <-pendingUpdate:
if ok && result.hasUpdate {
printUpdateNotice(result.latestVersion)
updatePrinted = true
}
default:
}
}
continue
}
isCommandActive.Store(false)
if overlay.GetUserNavigated() {
continue
}
if query == "" {
bufferMu.Lock()
wasEmpty := naiveBuffer == ""
naiveBuffer = ""
cursorOffset = 0
bufferMu.Unlock()
if !wasEmpty {
writeStdout([]byte(overlay.ClearAndDisable()))
}
continue
}
bufferMu.Lock()
if naiveBuffer == query {
bufferMu.Unlock()
continue
}
naiveBuffer = query
cursorOffset = 0
bufferMu.Unlock()
renderOverlay()
}
if err := scanner.Err(); err != nil {
logger.Errorf("IPC scanner error: %v", err)
}
}()
suggestionsEnabled := true
activeModeMu.Lock()
activeMode = loadMode()
activeModeMu.Unlock()
writeStdout([]byte(overlay.Clear()))
var renderTimer *time.Timer
var renderMu sync.Mutex
renderMenuNow = func() {
if isExecuting() {
return
}
// copy state safely inside timer
bufferMu.Lock()
bufCopy := naiveBuffer
offsetCopy := cursorOffset
bufferMu.Unlock()
activeModeMu.RLock()
modeCopy := activeMode
activeModeMu.RUnlock()
navCopy := userNavigated.Load()
runes := []rune(bufCopy)
if offsetCopy > 0 && offsetCopy <= len(runes) {
bufCopy = string(runes[:len(runes)-offsetCopy])
}
var b strings.Builder
if !navCopy {
if bufCopy == "" && !overlay.IsVisible() {
writeStdout([]byte(overlay.ClearAndDisable()))
return
}
logger.Debugf("Render query: '%s', mode: %s", bufCopy, modeCopy)
results := MergeResults(bufCopy, modeCopy)
logger.Debugf("Render results found: %d", len(results))
if len(results) == 0 || (len(results) == 1 && strings.TrimSpace(results[0].Cmd) == strings.TrimSpace(bufCopy) && !strings.HasSuffix(bufCopy, " ")) {
b.WriteString(overlay.ClearAndDisable())
writeStdout([]byte(b.String()))
return
}
if overlay.IsVisible() {
b.WriteString(overlay.Clear())
}
overlay.SetQueryAndItems(bufCopy, results)
} else {
if overlay.IsVisible() {
b.WriteString(overlay.Clear())
}
}
overlay.SetUserNavigated(navCopy)
if !disableGhostText.Load() {
b.WriteString(overlay.RenderGhostText(bufCopy, navCopy))
}
currentCmd := overlay.GetCurrentCmd()
logger.Debugf("RenderOverlay nav: %v, typedQuery: '%s', currentCmd: '%s'", navCopy, overlay.GetTypedQuery(), currentCmd)
b.WriteString(overlay.Render())
writeStdout([]byte(b.String()))
}
renderOverlay = func() {
renderMu.Lock()
defer renderMu.Unlock()
if !suggestionsEnabled || isExecuting() {
if renderTimer != nil {
renderTimer.Stop()
renderTimer = nil
}
return
}
if userNavigated.Load() {
return
}
if renderTimer != nil {
renderTimer.Stop()
}
renderTimer = time.AfterFunc(25*time.Millisecond, func() {
renderMu.Lock()
renderTimer = nil
renderMu.Unlock()
renderMenuNow()
})
}
renderOverlay()
// reads from stdin and decides what to forward or intercept
// for most cases, I just handle the already have terminal shortcuts
// for some shortcuts like tab, enter, shift tab, ctrl r,
// they have a little bit different behavior to match our tool
for {
inputSlice := make([]byte, 128)
n, err := os.Stdin.Read(inputSlice)
if err != nil {
break
}
if n > 0 {
if isExecuting() {
_, _ = ptmx.Write(inputSlice[:n])
continue
}
logger.Debugf("Stdin raw input: bytes=%q, hex=%x", inputSlice[:n], inputSlice[:n])
shouldOverlayDraw := false
for i := 0; i < n; i++ {
b := inputSlice[i]
intercepted := false
if b == '\033' {
// check for bracketed paste start/end
if i+5 < n && inputSlice[i+1] == '[' && inputSlice[i+2] == '2' && inputSlice[i+3] == '0' {
if (inputSlice[i+4] == '0' || inputSlice[i+4] == '1') && inputSlice[i+5] == '~' {
intercepted = true
logger.Debugf("Intercepted bracketed paste event")
_, _ = ptmx.Write(inputSlice[i : i+6])
i += 5
continue
}
}
// handle escape sequences like arrow keys and functional shortcuts
if i+2 < n && (inputSlice[i+1] == '[' || inputSlice[i+1] == 'O') {
// shift tab: hide/unhide menu dropdown
if inputSlice[i+1] == '[' && inputSlice[i+2] == 'Z' {
intercepted = true
suggestionsEnabled = !suggestionsEnabled
logger.Debugf("Intercepted Shift+Tab, suggestionsEnabled=%v", suggestionsEnabled)
if !suggestionsEnabled {
writeStdout([]byte(overlay.ClearAndDisable()))
} else {
shouldOverlayDraw = true
}
i += 2
continue
}
if overlay.IsVisible() && (inputSlice[i+2] == 'A' || inputSlice[i+2] == 'B') {
intercepted = true
userNavigated.Store(true)
if l := overlay.ClearGhostLen(); l > 0 {
var gs strings.Builder
gs.WriteString("\0337")
gs.WriteString(strings.Repeat(" ", l+10))
gs.WriteString("\0338")
writeStdout([]byte(gs.String()))
}
arrowDir := "down"
if inputSlice[i+2] == 'A' {
arrowDir = "up"
}
moved, selected := overlay.MoveCursor(arrowDir)
if !moved {
i += 2
continue
}
bufferMu.Lock()
naiveBuffer = selected
cursorOffset = 0
bufferMu.Unlock()
writeStdout([]byte(overlay.Render()))
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
i += 2
continue
} else if !overlay.IsVisible() && naiveBuffer == "" && (inputSlice[i+2] == 'A' || inputSlice[i+2] == 'B') { // up/down arrow on empty prompt
intercepted = true
activeModeMu.Lock()
activeMode = "history"
saveMode(activeMode)
activeModeMu.Unlock()
activeModeMu.RLock()
currentMode := activeMode
activeModeMu.RUnlock()
results := MergeResults("", currentMode)
if len(results) > 0 {
limit := min(len(results), 100)
var historyList []spec.Suggestion
if inputSlice[i+2] == 'A' {
for j := limit - 1; j >= 0; j-- {
historyList = append(historyList, results[j])
}
} else {
for j := 0; j < limit; j++ {
historyList = append(historyList, results[j])
}
}
selected := overlay.SetHistoryList(historyList, inputSlice[i+2] == 'A')
if selected != "" {
bufferMu.Lock()
naiveBuffer = selected
cursorOffset = 0
bufferMu.Unlock()
userNavigated.Store(true)
writeStdout([]byte(overlay.Render()))
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
}
}
i += 2
continue
} else if overlay.IsVisible() && !disableGhostText.Load() && inputSlice[i+2] == 'C' { // right arrow
bufferMu.Lock()
topCmd := overlay.GetTopCmd()
hasMatch := strings.HasPrefix(strings.ToLower(topCmd), strings.ToLower(naiveBuffer))
var ghostText string
if hasMatch {
ghostText = topCmd[len(naiveBuffer):]
}
bufferMu.Unlock()
if hasMatch && len(ghostText) > 0 {
intercepted = true
logger.Debugf("Intercepted Right Arrow (accepted ghost text: %q)", ghostText)
bufferMu.Lock()
naiveBuffer += ghostText
cursorOffset = 0
bufferMu.Unlock()
_, _ = ptmx.Write([]byte(ghostText))
shouldOverlayDraw = true
i += 2
continue
}
}
}
// left/right arrow cursor tracking
isLeftRightArrow := false
if i+2 < n && (inputSlice[i+1] == '[' || inputSlice[i+1] == 'O') {
if inputSlice[i+2] == 'D' {
bufferMu.Lock()
isEmptyQuery := naiveBuffer == "" && (!overlay.IsVisible() || overlay.GetTypedQuery() == "")
bufferMu.Unlock()
if isEmptyQuery {
intercepted = true
i += 2
continue
}
bufferMu.Lock()
if naiveBuffer != "" || overlay.IsVisible() {
cursorOffset++
if cursorOffset > len(naiveBuffer) {
cursorOffset = len(naiveBuffer)
}
shouldOverlayDraw = true
userNavigated.Store(false)
}
bufferMu.Unlock()
isLeftRightArrow = true
} else if inputSlice[i+2] == 'C' {
bufferMu.Lock()
isEmptyQuery := naiveBuffer == "" && (!overlay.IsVisible() || overlay.GetTypedQuery() == "")
bufferMu.Unlock()
if isEmptyQuery {
intercepted = true
i += 2
continue
}
bufferMu.Lock()
if naiveBuffer != "" || overlay.IsVisible() {
cursorOffset--
if cursorOffset < 0 {
cursorOffset = 0
}
shouldOverlayDraw = true
userNavigated.Store(false)
}
bufferMu.Unlock()
isLeftRightArrow = true
}
}
if !intercepted {
writeStdout([]byte(overlay.ClearAndDisable()))
disableGhostText.Store(true)
if !isLeftRightArrow {
bufferMu.Lock()
naiveBuffer = ""
cursorOffset = 0
bufferMu.Unlock()
}
_, _ = ptmx.Write([]byte{b})
// skip remaining bytes of the escape sequence to avoid misinterpretation
for j := i + 1; j < n; j++ {
char := inputSlice[j]
_, _ = ptmx.Write([]byte{char})
i = j
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char == '~' {
break
}
}
}
continue
}
if b == 0x12 { // ctrl+r: toggle between command specs and command history
intercepted = true
activeModeMu.Lock()
if activeMode == "spec" {
activeMode = "history"
} else {
activeMode = "spec"
}
saveMode(activeMode)
activeModeMu.Unlock()
logger.Debugf("Intercepted Ctrl+R, toggled mode to %q", activeMode)
if userNavigated.Load() {
bufferMu.Lock()
naiveBuffer = overlay.GetTypedQuery()
cursorOffset = 0
bufferMu.Unlock()
_, _ = ptmx.Write(append([]byte{0x15}, overlay.GetTypedQuery()...))
}
userNavigated.Store(false)
overlay.Show()
shouldOverlayDraw = true
// enter: enter behavior is a bit different from tab suggestions in code editor
// I want it to execute the command anyway and ignore the suggestions
// it means only tab to select suggestions, and enter to execute
// enter is not used to select suggestions
} else if b == 0x0d || b == 0x0a {
intercepted = true
logger.Debugf("Intercepted Enter key, navigated=%v", overlay.GetUserNavigated())
if overlay.IsVisible() && overlay.GetUserNavigated() {
selected := overlay.GetCurrentCmd()
if selected != "" {
activeModeMu.RLock()
currentMode := activeMode
activeModeMu.RUnlock()
if currentMode == "spec" {
selected = strings.TrimSpace(selected) + " "
}
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
}
}
writeStdout([]byte(overlay.ClearAndDisable()))
renderMu.Lock()
if renderTimer != nil {
renderTimer.Stop()
renderTimer = nil
}
renderMu.Unlock()
isCommandActive.Store(true)
_, _ = ptmx.Write([]byte{b})
bufferMu.Lock()
naiveBuffer = ""
cursorOffset = 0
bufferMu.Unlock()
disableGhostText.Store(false)
shouldOverlayDraw = false
userNavigated.Store(false)
continue
} else if b == 0x03 || b == 0x15 { // ctrl+c, ctrl+u
intercepted = true
writeStdout([]byte(overlay.ClearAndDisable()))
renderMu.Lock()
if renderTimer != nil {
renderTimer.Stop()
renderTimer = nil
}
renderMu.Unlock()
isCommandActive.Store(false)
_, _ = ptmx.Write([]byte{b})
bufferMu.Lock()
naiveBuffer = ""
cursorOffset = 0
bufferMu.Unlock()
disableGhostText.Store(false)
shouldOverlayDraw = false
userNavigated.Store(false)
continue
} else if b == 0x09 { // tab: select suggestions
intercepted = true
logger.Debugf("Intercepted Tab key, visible=%v", overlay.IsVisible())
if !overlay.IsVisible() {
shouldOverlayDraw = true
} else {
selected := overlay.GetCurrentCmd()
writeStdout([]byte(overlay.ClearAndDisable()))
activeModeMu.RLock()
currentMode := activeMode
activeModeMu.RUnlock()
if currentMode == "spec" {
selected = strings.TrimSpace(selected) + " "
}
bufferMu.Lock()
naiveBuffer = selected
cursorOffset = 0
bufferMu.Unlock()
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
overlay.ResetCursor() // this prevents when you tab, it switches between suggestions non-stop
shouldOverlayDraw = true // <- rerender after tab to choose, if you set to false,
// when you press tab continually, it will print all folder from menu suggestions
// and make the cursor jump to next line
userNavigated.Store(false)
}
continue
}
if !intercepted {
_, _ = ptmx.Write([]byte{b})
// we handle line editing keys manually to keep naiveBuffer in sync
// since terminal is in raw mode, we must update our state for every change
switch b {
case 0x01: // ctrl+a: move to beginning of line
bufferMu.Lock()
cursorOffset = len(naiveBuffer)
if naiveBuffer != "" || overlay.IsVisible() {
shouldOverlayDraw = true
}
bufferMu.Unlock()
userNavigated.Store(false)
case 0x05: // ctrl+e: move to end of line
bufferMu.Lock()
cursorOffset = 0
if naiveBuffer != "" || overlay.IsVisible() {
shouldOverlayDraw = true
}
bufferMu.Unlock()
userNavigated.Store(false)
case 127, 0x08: // backspace: remove character
bufferMu.Lock()
wasEmpty := len(naiveBuffer) == 0
if !wasEmpty {
runes := []rune(naiveBuffer)
if cursorOffset <= 0 {
if len(runes) > 0 {
naiveBuffer = string(runes[:len(runes)-1])
}
cursorOffset = 0
} else {
if cursorOffset > len(runes) {
cursorOffset = len(runes)
}
pos := len(runes) - cursorOffset
if pos > 0 && pos <= len(runes) {
naiveBuffer = string(append(runes[:pos-1], runes[pos:]...))
}
}
}
isEmptyNow := len(naiveBuffer) == 0
bufferMu.Unlock()
if wasEmpty || isEmptyNow {
writeStdout([]byte(overlay.ClearAndDisable()))
userNavigated.Store(false)
continue
}
shouldOverlayDraw = true
userNavigated.Store(false)
case 0x17: // ctrl+w: delete the last word in the buffer
bufferMu.Lock()
wasEmpty := len(naiveBuffer) == 0
trimBuf := strings.TrimRight(naiveBuffer, " ")
lastSpace := strings.LastIndex(trimBuf, " ")
if lastSpace >= 0 {
naiveBuffer = trimBuf[:lastSpace+1]
} else {
naiveBuffer = ""
}
cursorOffset = 0
isEmptyNow := len(naiveBuffer) == 0
bufferMu.Unlock()
if wasEmpty || isEmptyNow {
writeStdout([]byte(overlay.ClearAndDisable()))
userNavigated.Store(false)
continue
}
shouldOverlayDraw = true
userNavigated.Store(false)
case 0x0c: // ctrl+l: clear screen but keep buffer and redraw menu
shouldOverlayDraw = true
userNavigated.Store(false)
case '\r', '\n', 0x03, 0x15: // enter, ctrl+c, ctrl+u: clear buffer on line reset
bufferMu.Lock()
naiveBuffer = ""
cursorOffset = 0
bufferMu.Unlock()
activeModeMu.Lock()
activeMode = loadMode()
activeModeMu.Unlock()
disableGhostText.Store(false)
writeStdout([]byte(overlay.ClearAndDisable()))
userNavigated.Store(false)
default:
// track normal printable characters in the buffer for matching
if b >= 32 && b <= 126 {
// if user presses space, check if the current word is an alias
bufferMu.Lock()
isSpaceAlias := b == ' ' && naiveBuffer != "" && !strings.Contains(naiveBuffer, " ")
var target string
var ok bool
if isSpaceAlias {
target, ok = spec.GetAlias(naiveBuffer)
}
bufferMu.Unlock()
if isSpaceAlias && ok {
// clear the current alias and replace it with the full command
_, _ = ptmx.Write(append([]byte{0x15}, target+" "...))
bufferMu.Lock()
naiveBuffer = target + " "
cursorOffset = 0
bufferMu.Unlock()
shouldOverlayDraw = true
continue
}
bufferMu.Lock()
if cursorOffset == 0 {
naiveBuffer += string(b)
} else {
if cursorOffset > len(naiveBuffer) {
cursorOffset = len(naiveBuffer)
}
pos := len(naiveBuffer) - cursorOffset
if pos >= 0 && pos <= len(naiveBuffer) {
naiveBuffer = naiveBuffer[:pos] + string(b) + naiveBuffer[pos:]
} else {
naiveBuffer += string(b)
cursorOffset = 0
}
}
bufferMu.Unlock()
shouldOverlayDraw = true
userNavigated.Store(false)
overlay.SetUserNavigated(false)
}
}
}
}
if shouldOverlayDraw {
renderOverlay()
}
}
}
}