From 736723fbe8a6d520ec70ecd939e3a91bf013f624 Mon Sep 17 00:00:00 2001 From: verse91 Date: Sat, 11 Apr 2026 22:34:04 +0700 Subject: [PATCH] refactor: shell adaptor --- commands/core/spec.go | 72 ++--------------- integration/shell/adapter.go | 152 +++++++++++++++++++++++++++++++++++ root/root.go | 17 ++-- 3 files changed, 172 insertions(+), 69 deletions(-) create mode 100644 integration/shell/adapter.go diff --git a/commands/core/spec.go b/commands/core/spec.go index 6191540..56f9e30 100644 --- a/commands/core/spec.go +++ b/commands/core/spec.go @@ -4,6 +4,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/versenilvis/iris/integration/shell" ) type GeneratorFunc func(tokens []string, prefix string, partial string) []Suggestion @@ -47,71 +49,9 @@ var ( shellAliases = make(map[string]string) // alias name -> target command ) -// scanShellAliases parses shell config files for aliases -func scanShellAliases() { - home, err := os.UserHomeDir() - if err != nil { - return - } - - for _, f := range []string{".zshrc", ".bashrc", ".bash_profile", ".bash_aliases"} { - content, err := os.ReadFile(filepath.Join(home, f)) - if err != nil { - continue - } - - for _, line := range strings.Split(string(content), "\n") { - line = strings.TrimSpace(line) - if !strings.HasPrefix(line, "alias") { - continue - } - - body := strings.TrimSpace(strings.TrimPrefix(line, "alias")) - for _, pair := range splitAliasTokens(body) { - if eqIdx := strings.IndexByte(pair, '='); eqIdx > 0 { - k := strings.TrimSpace(pair[:eqIdx]) - v := strings.Trim(strings.TrimSpace(pair[eqIdx+1:]), "\"'") - if k != "" && v != "" { - shellAliases[k] = v - } - } - } - } - } -} - -func splitAliasTokens(s string) []string { - var pairs []string - var cur strings.Builder - inQuote := false - var quote rune - for _, c := range s { - switch { - case !inQuote && (c == '"' || c == '\''): - inQuote, quote = true, c - cur.WriteRune(c) - case inQuote && c == quote: - inQuote = false - cur.WriteRune(c) - case c == ' ' && !inQuote: - if cur.Len() > 0 { - pairs = append(pairs, cur.String()) - cur.Reset() - } - default: - cur.WriteRune(c) - } - } - if cur.Len() > 0 { - pairs = append(pairs, cur.String()) - } - return pairs -} - -// scanExternalCommands populates pathCmds and shellAliases +// scanExternalCommands populates pathCmds func scanExternalCommands() { scanPath() - scanShellAliases() } // scanPath populates pathCmds with all executable files found in $PATH @@ -150,7 +90,11 @@ func Register(s *Spec) { // // priority: file/dir -> subcommands -> options func Lookup(input string) []Suggestion { - shellAliases = make(map[string]string) + if shell.Current != nil { + shellAliases = shell.Current.ScanAliases() + } else { + shellAliases = make(map[string]string) + } pathCmds = make(map[string]bool) scanExternalCommands() diff --git a/integration/shell/adapter.go b/integration/shell/adapter.go new file mode 100644 index 0000000..f15d4a9 --- /dev/null +++ b/integration/shell/adapter.go @@ -0,0 +1,152 @@ +package shell + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// Adapter defines the behavior for different shell environments +type Adapter interface { + GetName() string + GetShellPath() string + GetEnv(fd int, pid int) []string + PrepareSelectSequence(selected string) []byte + // ScanAliases returns a map of alias name to target command + ScanAliases() map[string]string +} + +// Current shell instance +var Current Adapter + +func Init(name string) { + switch name { + case "zsh": + Current = &ZshAdapter{} + case "fish": + Current = &FishAdapter{} + default: + Current = &BashAdapter{} + } +} + +// BashAdapter implementation +type BashAdapter struct{} + +func (b *BashAdapter) GetName() string { return "bash" } +func (b *BashAdapter) GetShellPath() string { return "bash" } +func (b *BashAdapter) GetEnv(fd int, pid int) []string { + return append(os.Environ(), "IRIS_FD="+fmt.Sprint(fd), "IRIS_PID="+fmt.Sprint(pid)) +} +func (b *BashAdapter) PrepareSelectSequence(selected string) []byte { + return append([]byte{0x15}, []byte(selected)...) +} +func (b *BashAdapter) ScanAliases() map[string]string { + return scanPosixAliases([]string{".bashrc", ".bash_profile", ".bash_aliases"}) +} + +// ZshAdapter implementation +type ZshAdapter struct{} + +func (z *ZshAdapter) GetName() string { return "zsh" } +func (z *ZshAdapter) GetShellPath() string { return "zsh" } +func (z *ZshAdapter) GetEnv(fd int, pid int) []string { + return append(os.Environ(), "IRIS_FD="+fmt.Sprint(fd), "IRIS_PID="+fmt.Sprint(pid)) +} +func (z *ZshAdapter) PrepareSelectSequence(selected string) []byte { + return append([]byte{0x15}, []byte(selected)...) +} +func (z *ZshAdapter) ScanAliases() map[string]string { + return scanPosixAliases([]string{".zshrc", ".zshenv", ".zprofile"}) +} + +// FishAdapter implementation +type FishAdapter struct{} + +func (f *FishAdapter) GetName() string { return "fish" } +func (f *FishAdapter) GetShellPath() string { return "fish" } +func (f *FishAdapter) GetEnv(fd int, pid int) []string { + return append(os.Environ(), "IRIS_FD="+fmt.Sprint(fd), "IRIS_PID="+fmt.Sprint(pid)) +} +func (f *FishAdapter) PrepareSelectSequence(selected string) []byte { + return append([]byte{0x15}, []byte(selected)...) +} +func (f *FishAdapter) ScanAliases() map[string]string { + // fish uses 'alias' command in config.fish or separate function files + return scanPosixAliases([]string{filepath.Join(".config", "fish", "config.fish")}) +} + +func scanPosixAliases(files []string) map[string]string { + aliases := make(map[string]string) + home, err := os.UserHomeDir() + if err != nil { + return aliases + } + + for _, f := range files { + path := f + if !filepath.IsAbs(f) { + path = filepath.Join(home, f) + } + data, err := os.ReadFile(path) + if err != nil { + continue + } + + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "alias ") { + continue + } + body := strings.TrimSpace(strings.TrimPrefix(line, "alias")) + if body == "" { + continue + } + + pairs := splitAliasTokens(body) + for _, pair := range pairs { + eqIdx := strings.IndexByte(pair, '=') + if eqIdx < 0 { + continue + } + key := strings.TrimSpace(pair[:eqIdx]) + val := strings.Trim(strings.TrimSpace(pair[eqIdx+1:]), `"'`) + if key != "" && val != "" { + aliases[key] = val + } + } + } + } + return aliases +} + +func splitAliasTokens(s string) []string { + var tokens []string + var cur strings.Builder + inQuote := false + var quoteChar rune + for _, c := range s { + switch { + case !inQuote && (c == '"' || c == '\''): + inQuote = true + quoteChar = c + cur.WriteRune(c) + case inQuote && c == quoteChar: + inQuote = false + cur.WriteRune(c) + case !inQuote && c == ' ': + if cur.Len() > 0 { + tokens = append(tokens, cur.String()) + cur.Reset() + } + default: + cur.WriteRune(c) + } + } + if cur.Len() > 0 { + tokens = append(tokens, cur.String()) + } + return tokens +} diff --git a/root/root.go b/root/root.go index b622126..abdbc32 100644 --- a/root/root.go +++ b/root/root.go @@ -22,6 +22,7 @@ import ( _ "github.com/versenilvis/iris/commands/search" _ "github.com/versenilvis/iris/commands/view" "github.com/versenilvis/iris/integration" + "github.com/versenilvis/iris/integration/shell" "golang.org/x/term" ) @@ -54,12 +55,20 @@ func runWrapper() { return } - c := exec.Command("bash") + // default to bash + shellName := "bash" + if strings.Contains(os.Getenv("SHELL"), "zsh") { + shellName = "zsh" + } + shell.Init(shellName) + adapter := shell.Current + + c := exec.Command(adapter.GetShellPath()) // fd 0, 1, 2 are stdin, stdout, stderr (handled by pty) // fd 3 is our write pipe c.ExtraFiles = []*os.File{w} - c.Env = append(os.Environ(), "IRIS_FD=3", fmt.Sprintf("IRIS_PID=%d", os.Getpid())) + c.Env = adapter.GetEnv(3, os.Getpid()) ptmx, err := pty.Start(c) if err != nil { @@ -224,9 +233,7 @@ func runWrapper() { naiveBuffer = selected mode = "spec" // reset to spec mode after selection - // delete line (ctrl+u) - ptmx.Write([]byte{0x15}) - ptmx.Write([]byte(selected)) + ptmx.Write(adapter.PrepareSelectSequence(selected)) if b == '\r' { ptmx.Write([]byte{'\r'})