diff --git a/root/init.go b/root/init.go index facc8b3..5414260 100644 --- a/root/init.go +++ b/root/init.go @@ -42,8 +42,14 @@ if [ -n "$IRIS_PID" ] && [ -n "$IRIS_FD" ]; then print -u $IRIS_FD -N -r -- "$LBUFFER" 2>/dev/null } + _iris_sync_cwd() { + print -u $IRIS_FD -N -r -- "IRIS_CWD:$PWD" 2>/dev/null + } + _iris_precmd() { - print -u $IRIS_FD -N -r -- "IRIS_CMD_STOP" 2>/dev/null + local iris_exit_code=$? + _iris_sync_cwd + print -u $IRIS_FD -N -r -- "IRIS_CMD_STOP:$iris_exit_code" 2>/dev/null } _iris_preexec() { @@ -56,6 +62,7 @@ if [ -n "$IRIS_PID" ] && [ -n "$IRIS_FD" ]; then add-zle-hook-widget line-pre-redraw _iris_send_lbuffer add-zsh-hook precmd _iris_precmd add-zsh-hook preexec _iris_preexec + add-zsh-hook chpwd _iris_sync_cwd fi `) case "bash": @@ -75,7 +82,9 @@ fi # Iris Autocomplete Hook if [ -n "$IRIS_PID" ] && [ -n "$IRIS_FD" ]; then _iris_bash_precmd() { - printf "IRIS_CMD_STOP\x00" >&$IRIS_FD 2>/dev/null + local iris_exit_code=$? + printf "IRIS_CWD:%%s\x00" "$PWD" >&$IRIS_FD 2>/dev/null + printf "IRIS_CMD_STOP:%%s\x00" "$iris_exit_code" >&$IRIS_FD 2>/dev/null } if [[ ";$PROMPT_COMMAND;" != *";_iris_bash_precmd;"* ]]; then @@ -103,10 +112,15 @@ end # Iris Autocomplete Hook if set -q IRIS_PID; and set -q IRIS_FD function _iris_fish_postexec --on-event fish_postexec - echo -n -e "IRIS_CMD_STOP\x00" >&$IRIS_FD 2>/dev/null + set -l iris_exit_code $status + printf "IRIS_CWD:%%s\x00" "$PWD" >&$IRIS_FD 2>/dev/null + printf "IRIS_CMD_STOP:%%s\x00" "$iris_exit_code" >&$IRIS_FD 2>/dev/null + end + function _iris_fish_prompt --on-event fish_prompt + printf "IRIS_CWD:%%s\x00" "$PWD" >&$IRIS_FD 2>/dev/null end function _iris_fish_preexec --on-event fish_preexec - echo -n -e "IRIS_CMD_START\x00" >&$IRIS_FD 2>/dev/null + printf "IRIS_CMD_START\x00" >&$IRIS_FD 2>/dev/null end end `) diff --git a/root/wrapper.go b/root/wrapper.go index fd8c742..ea67c9c 100644 --- a/root/wrapper.go +++ b/root/wrapper.go @@ -386,6 +386,11 @@ func runWrapper() { for scanner.Scan() { query := scanner.Text() + if cwd, ok := strings.CutPrefix(query, "IRIS_CWD:"); ok { + spec.SetCWD(cwd) + continue + } + if query == "IRIS_CMD_START" { isCommandActive.Store(true) bufferMu.Lock() diff --git a/spec/filegen.go b/spec/filegen.go index 0a72a9d..bb86cb5 100644 --- a/spec/filegen.go +++ b/spec/filegen.go @@ -6,24 +6,49 @@ import ( "path/filepath" "sort" "strings" + "sync" "github.com/versenilvis/iris/internal/config" ) -// for tracking current working dir -var ShellPID int +var ( + // ShellPID tracks the underlying shell process. + ShellPID int + + shellCWDMu sync.RWMutex + shellCWD string +) + +// SetCWD updates the working directory reported by the underlying shell. +// Shell integrations call this whenever the prompt directory changes. +func SetCWD(cwd string) { + if cwd != "" && !filepath.IsAbs(cwd) { + return + } + + shellCWDMu.Lock() + shellCWD = cwd + shellCWDMu.Unlock() +} // GetCWD returns the current working directory of the underlying shell -// on Linux, it reads it from /proc/[pid]/cwd +// reported by shell integration. Linux can also read it from /proc/[pid]/cwd. func GetCWD() string { + shellCWDMu.RLock() + cwd := shellCWD + shellCWDMu.RUnlock() + if cwd != "" { + return cwd + } + if ShellPID > 0 { path := fmt.Sprintf("/proc/%d/cwd", ShellPID) - cwd, err := os.Readlink(path) + procCWD, err := os.Readlink(path) if err == nil { - return cwd + return procCWD } } - cwd, _ := os.Getwd() + cwd, _ = os.Getwd() return cwd } diff --git a/spec/filegen_test.go b/spec/filegen_test.go index f7c5439..3d6ba00 100644 --- a/spec/filegen_test.go +++ b/spec/filegen_test.go @@ -8,6 +8,8 @@ import ( ) func TestFileGenerator(t *testing.T) { + SetCWD("") + // Setup mock files tmp := t.TempDir() _ = os.MkdirAll(filepath.Join(tmp, "src"), 0755) @@ -92,3 +94,50 @@ func TestFileGenerator(t *testing.T) { } }) } + +func TestGetCWDUsesShellReportedDirectory(t *testing.T) { + launcherDir := t.TempDir() + shellDir := t.TempDir() + if err := os.WriteFile(filepath.Join(shellDir, "from-shell.txt"), nil, 0644); err != nil { + t.Fatal(err) + } + + oldWd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(launcherDir); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + SetCWD("") + _ = os.Chdir(oldWd) + }) + + SetCWD(shellDir) + if got := GetCWD(); got != shellDir { + t.Fatalf("GetCWD() = %q, want shell-reported directory %q", got, shellDir) + } + + results := FileGenerator()([]string{"cat", ""}, "cat ", "") + found := false + for _, result := range results { + if result.Cmd == "from-shell.txt" { + found = true + break + } + } + if !found { + t.Fatalf("FileGenerator() did not read suggestions from shell directory %q", shellDir) + } +} + +func TestSetCWDRejectsRelativePaths(t *testing.T) { + SetCWD("") + t.Cleanup(func() { SetCWD("") }) + + SetCWD("relative/path") + if got := GetCWD(); got == "relative/path" { + t.Fatalf("GetCWD() accepted relative shell directory %q", got) + } +}