- support history suggestions for aliases https://github.com/versenilvis/IRIS/commit/0ae25d05ce0faf8ff2eeee9aa3fbb5889d9ad511 - merge spec and history results (https://github.com/versenilvis/IRIS/commit/315c94f12bb689ad6b39b1f0ce5e0bdef71f38d5) - add 25ms pty throttle and optimize ctrl+l (https://github.com/versenilvis/IRIS/commit/1e603d74c208be00f6b2cb79a628416455711c3a) - align suggestion menu to col 1 (https://github.com/versenilvis/IRIS/commit/e307b210849af65276e93e1cff8895f3b54937ff) - resolve zsh ipc fd mismatch and generate autocomplete hooks (https://github.com/versenilvis/IRIS/pull/12/commits/2199607fad8122ec441ff4b99e562ca62bd948ff) - fix: eliminate prompt duplication and freeze UI movement during menu navigation https://github.com/versenilvis/IRIS/pull/12/commits/6783989a49f0a68d01bad542588983a5c7b597a2
264 lines
5.5 KiB
Go
264 lines
5.5 KiB
Go
package integration
|
|
|
|
import (
|
|
"bufio"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/versenilvis/fuzzy"
|
|
"github.com/versenilvis/iris/integration/shell"
|
|
)
|
|
|
|
var (
|
|
historyCache []string
|
|
idMapCache map[string]int
|
|
searcherCache *fuzzy.Searcher
|
|
mu sync.Mutex
|
|
lastModTime int64
|
|
)
|
|
|
|
type HistResult struct {
|
|
ID int
|
|
Cmd string
|
|
FuzzyScore int
|
|
}
|
|
|
|
func init() {
|
|
idMapCache = make(map[string]int)
|
|
}
|
|
|
|
func SearchHistory(query string, aliases map[string]string) ([]HistResult, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
shellName := "bash"
|
|
if shell.Current != nil {
|
|
shellName = shell.Current.GetName()
|
|
}
|
|
|
|
var histFile string
|
|
switch shellName {
|
|
case "zsh":
|
|
histFile = filepath.Join(home, ".zsh_history")
|
|
case "fish":
|
|
histFile = filepath.Join(home, ".local", "share", "fish", "fish_history")
|
|
default:
|
|
histFile = filepath.Join(home, ".bash_history")
|
|
}
|
|
|
|
if info, err := os.Stat(histFile); err == nil {
|
|
if info.ModTime().UnixNano() > lastModTime {
|
|
historyCache = nil // force reload
|
|
idMapCache = make(map[string]int)
|
|
lastModTime = info.ModTime().UnixNano()
|
|
}
|
|
}
|
|
|
|
// lazy load history if cache is empty
|
|
if len(historyCache) == 0 {
|
|
file, err := os.Open(histFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = file.Close() }()
|
|
|
|
var allCmds []string
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
cmd := line
|
|
|
|
if shellName == "zsh" {
|
|
parts := strings.SplitN(line, ";", 2)
|
|
if len(parts) == 2 {
|
|
cmd = parts[1]
|
|
}
|
|
} else if shellName == "bash" {
|
|
if strings.HasPrefix(line, "#") && len(line) > 1 {
|
|
isTimestamp := true
|
|
for _, c := range line[1:] {
|
|
if c < '0' || c > '9' {
|
|
isTimestamp = false
|
|
break
|
|
}
|
|
}
|
|
if isTimestamp {
|
|
continue
|
|
}
|
|
}
|
|
} else if shellName == "fish" {
|
|
if strings.HasPrefix(line, "- cmd: ") {
|
|
cmd = strings.TrimPrefix(line, "- cmd: ")
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
cmd = strings.TrimSpace(cmd)
|
|
if cmd != "" {
|
|
allCmds = append(allCmds, cmd)
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// build historyCache backwards so newest commands come first
|
|
seen := make(map[string]bool)
|
|
for i := len(allCmds) - 1; i >= 0; i-- {
|
|
cmd := allCmds[i]
|
|
if !seen[cmd] {
|
|
historyCache = append(historyCache, cmd)
|
|
seen[cmd] = true
|
|
// we assign the ID as the original line number (1-indexed based on allCmds length)
|
|
idMapCache[cmd] = i + 1
|
|
}
|
|
}
|
|
|
|
searcherCache = fuzzy.NewPlainSearcher(historyCache)
|
|
}
|
|
|
|
if query == "" {
|
|
var results []HistResult
|
|
limit := 100
|
|
if len(historyCache) < limit {
|
|
limit = len(historyCache)
|
|
}
|
|
|
|
for i := 0; i < limit; i++ {
|
|
cmd := historyCache[i]
|
|
results = append(results, HistResult{
|
|
ID: idMapCache[cmd],
|
|
Cmd: cmd,
|
|
})
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
var alternativeQueries []string
|
|
for name, target := range aliases {
|
|
if target != "" {
|
|
qLow := strings.ToLower(query)
|
|
tLow := strings.ToLower(target)
|
|
nLow := strings.ToLower(name)
|
|
|
|
if qLow == tLow {
|
|
alternativeQueries = append(alternativeQueries, name)
|
|
} else if strings.HasPrefix(qLow, tLow+" ") {
|
|
suffix := query[len(target):]
|
|
alternativeQueries = append(alternativeQueries, name+suffix)
|
|
}
|
|
|
|
if qLow == nLow {
|
|
alternativeQueries = append(alternativeQueries, target)
|
|
} else if strings.HasPrefix(qLow, nLow+" ") {
|
|
suffix := query[len(name):]
|
|
alternativeQueries = append(alternativeQueries, target+suffix)
|
|
}
|
|
}
|
|
}
|
|
|
|
var results []HistResult
|
|
seenCmds := make(map[string]bool)
|
|
|
|
addMatches := func(q string) {
|
|
qLow := strings.ToLower(q)
|
|
queryFirstWord := ""
|
|
if strings.IndexByte(qLow, ' ') != -1 {
|
|
if fields := strings.Fields(qLow); len(fields) > 0 {
|
|
queryFirstWord = fields[0]
|
|
}
|
|
}
|
|
|
|
matches := searcherCache.SearchWithScores(q, &fuzzy.SearchOptions{Limit: 200})
|
|
for _, m := range matches {
|
|
if seenCmds[m.Str] {
|
|
continue
|
|
}
|
|
|
|
// filter results by command name match
|
|
firstWord := m.Str
|
|
if idx := strings.IndexByte(m.Str, ' '); idx != -1 {
|
|
firstWord = m.Str[:idx]
|
|
}
|
|
firstWordLow := strings.ToLower(firstWord)
|
|
|
|
if queryFirstWord != "" {
|
|
if firstWordLow != queryFirstWord {
|
|
continue
|
|
}
|
|
} else {
|
|
if !strings.HasPrefix(firstWordLow, qLow) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
seenCmds[m.Str] = true
|
|
results = append(results, HistResult{
|
|
ID: idMapCache[m.Str],
|
|
Cmd: m.Str,
|
|
FuzzyScore: m.Score,
|
|
})
|
|
}
|
|
}
|
|
|
|
addMatches(query)
|
|
|
|
for _, altQ := range alternativeQueries {
|
|
addMatches(altQ)
|
|
}
|
|
|
|
getTier := func(cmd, q string) int {
|
|
bestTier := 4
|
|
check := func(ql string) {
|
|
cmdLow := strings.ToLower(cmd)
|
|
qlLow := strings.ToLower(ql)
|
|
tier := 4
|
|
if cmdLow == qlLow {
|
|
tier = 1
|
|
} else if strings.HasPrefix(cmdLow, qlLow) {
|
|
tier = 2
|
|
} else if strings.Contains(cmdLow, qlLow) {
|
|
tier = 3
|
|
}
|
|
if tier < bestTier {
|
|
bestTier = tier
|
|
}
|
|
}
|
|
check(q)
|
|
for _, altQ := range alternativeQueries {
|
|
check(altQ)
|
|
}
|
|
return bestTier
|
|
}
|
|
|
|
tiers := make([]int, len(results))
|
|
for i, r := range results {
|
|
tiers[i] = getTier(r.Cmd, query)
|
|
}
|
|
|
|
sort.SliceStable(results, func(i, j int) bool {
|
|
tI := tiers[i]
|
|
tJ := tiers[j]
|
|
if tI != tJ {
|
|
return tI < tJ
|
|
}
|
|
|
|
if tI == 4 && results[i].FuzzyScore != results[j].FuzzyScore {
|
|
return results[i].FuzzyScore > results[j].FuzzyScore
|
|
}
|
|
|
|
return results[i].ID > results[j].ID
|
|
})
|
|
|
|
return results, nil
|
|
}
|