refactor: shell adaptor
This commit is contained in:
+8
-64
@@ -4,6 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/versenilvis/iris/integration/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GeneratorFunc func(tokens []string, prefix string, partial string) []Suggestion
|
type GeneratorFunc func(tokens []string, prefix string, partial string) []Suggestion
|
||||||
@@ -47,71 +49,9 @@ var (
|
|||||||
shellAliases = make(map[string]string) // alias name -> target command
|
shellAliases = make(map[string]string) // alias name -> target command
|
||||||
)
|
)
|
||||||
|
|
||||||
// scanShellAliases parses shell config files for aliases
|
// scanExternalCommands populates pathCmds
|
||||||
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
|
|
||||||
func scanExternalCommands() {
|
func scanExternalCommands() {
|
||||||
scanPath()
|
scanPath()
|
||||||
scanShellAliases()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// scanPath populates pathCmds with all executable files found in $PATH
|
// scanPath populates pathCmds with all executable files found in $PATH
|
||||||
@@ -150,7 +90,11 @@ func Register(s *Spec) {
|
|||||||
//
|
//
|
||||||
// priority: file/dir -> subcommands -> options
|
// priority: file/dir -> subcommands -> options
|
||||||
func Lookup(input string) []Suggestion {
|
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)
|
pathCmds = make(map[string]bool)
|
||||||
scanExternalCommands()
|
scanExternalCommands()
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -22,6 +22,7 @@ import (
|
|||||||
_ "github.com/versenilvis/iris/commands/search"
|
_ "github.com/versenilvis/iris/commands/search"
|
||||||
_ "github.com/versenilvis/iris/commands/view"
|
_ "github.com/versenilvis/iris/commands/view"
|
||||||
"github.com/versenilvis/iris/integration"
|
"github.com/versenilvis/iris/integration"
|
||||||
|
"github.com/versenilvis/iris/integration/shell"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,12 +55,20 @@ func runWrapper() {
|
|||||||
return
|
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 0, 1, 2 are stdin, stdout, stderr (handled by pty)
|
||||||
// fd 3 is our write pipe
|
// fd 3 is our write pipe
|
||||||
c.ExtraFiles = []*os.File{w}
|
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)
|
ptmx, err := pty.Start(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -224,9 +233,7 @@ func runWrapper() {
|
|||||||
naiveBuffer = selected
|
naiveBuffer = selected
|
||||||
mode = "spec" // reset to spec mode after selection
|
mode = "spec" // reset to spec mode after selection
|
||||||
|
|
||||||
// delete line (ctrl+u)
|
ptmx.Write(adapter.PrepareSelectSequence(selected))
|
||||||
ptmx.Write([]byte{0x15})
|
|
||||||
ptmx.Write([]byte(selected))
|
|
||||||
|
|
||||||
if b == '\r' {
|
if b == '\r' {
|
||||||
ptmx.Write([]byte{'\r'})
|
ptmx.Write([]byte{'\r'})
|
||||||
|
|||||||
Reference in New Issue
Block a user