refactor: shell adaptor

This commit is contained in:
verse91
2026-04-11 22:34:04 +07:00
parent 97d009f7ef
commit 736723fbe8
3 changed files with 172 additions and 69 deletions
+8 -64
View File
@@ -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()
+152
View File
@@ -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
}
+12 -5
View File
@@ -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'})