fix: iris keep suggesting when press tab continually
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
iris
|
iris
|
||||||
|
iris.log
|
||||||
+143
-23
@@ -1,6 +1,8 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -8,6 +10,14 @@ import (
|
|||||||
"github.com/versenilvis/iris/integration/shell"
|
"github.com/versenilvis/iris/integration/shell"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var DebugWriter io.Writer
|
||||||
|
|
||||||
|
func debugLog(format string, a ...interface{}) {
|
||||||
|
if DebugWriter != nil {
|
||||||
|
fmt.Fprintf(DebugWriter, format+"\n", a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type GeneratorFunc func(tokens []string, prefix string, partial string) []Suggestion
|
type GeneratorFunc func(tokens []string, prefix string, partial string) []Suggestion
|
||||||
|
|
||||||
// Spec defines a top-level CLI command
|
// Spec defines a top-level CLI command
|
||||||
@@ -18,6 +28,7 @@ type Spec struct {
|
|||||||
Subcommands []Subcommand
|
Subcommands []Subcommand
|
||||||
Options []Option
|
Options []Option
|
||||||
Generator GeneratorFunc
|
Generator GeneratorFunc
|
||||||
|
MaxArgs int // 0 means unlimited
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subcommand contains nested subcommands
|
// Subcommand contains nested subcommands
|
||||||
@@ -28,6 +39,7 @@ type Subcommand struct {
|
|||||||
Subcommands []Subcommand
|
Subcommands []Subcommand
|
||||||
Options []Option
|
Options []Option
|
||||||
Generator GeneratorFunc
|
Generator GeneratorFunc
|
||||||
|
MaxArgs int // 0 means unlimited
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option represents a flag or option
|
// Option represents a flag or option
|
||||||
@@ -116,7 +128,37 @@ func Lookup(input string) []Suggestion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(tokens) == 1 {
|
if len(tokens) == 1 {
|
||||||
return topLevelSuggestions(tokens[0])
|
query := tokens[0]
|
||||||
|
results := topLevelSuggestions(query)
|
||||||
|
|
||||||
|
// If query is a full match for a root command, also include its sub-items
|
||||||
|
if spec, exists := registry[query]; exists {
|
||||||
|
// ONLY suggest parameters if there's a trailing space in the query
|
||||||
|
// or if we are filtering. But for 'just' (len=1), if no space, wait.
|
||||||
|
hasTrailingSpace := query != "" && query[len(query)-1] == ' '
|
||||||
|
|
||||||
|
if hasTrailingSpace {
|
||||||
|
partial := ""
|
||||||
|
prefix := query
|
||||||
|
|
||||||
|
// Subcommands
|
||||||
|
for _, sub := range spec.Subcommands {
|
||||||
|
results = append(results, Suggestion{
|
||||||
|
Cmd: strings.TrimSpace(query) + " " + sub.Name, Desc: sub.Description, Icon: query,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Generator (recipes, branches, etc.)
|
||||||
|
if spec.Generator != nil {
|
||||||
|
genResults := spec.Generator(tokens, prefix, partial)
|
||||||
|
for _, g := range genResults {
|
||||||
|
results = append(results, Suggestion{
|
||||||
|
Cmd: strings.TrimSpace(query) + " " + g.Cmd, Desc: g.Desc, Icon: query,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
rootCmdName := tokens[0]
|
rootCmdName := tokens[0]
|
||||||
@@ -128,7 +170,7 @@ func Lookup(input string) []Suggestion {
|
|||||||
currentSubs, currentOpts, currentGen := spec.Subcommands, spec.Options, spec.Generator
|
currentSubs, currentOpts, currentGen := spec.Subcommands, spec.Options, spec.Generator
|
||||||
depth := 1
|
depth := 1
|
||||||
|
|
||||||
for depth < len(tokens) {
|
for depth < len(tokens)-1 {
|
||||||
tok := tokens[depth]
|
tok := tokens[depth]
|
||||||
if tok == "" || strings.HasPrefix(tok, "-") || strings.Contains(tok, "=") {
|
if tok == "" || strings.HasPrefix(tok, "-") || strings.Contains(tok, "=") {
|
||||||
depth++
|
depth++
|
||||||
@@ -153,19 +195,53 @@ func Lookup(input string) []Suggestion {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
depth++
|
depth++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// If not a subcommand, it's an argument. Stop traversal here.
|
||||||
if currentGen != nil && depth < len(tokens)-1 {
|
|
||||||
depth++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
results := []Suggestion{}
|
results := []Suggestion{}
|
||||||
|
|
||||||
|
// Track current max args
|
||||||
|
// Re-track the limit from the actual deepest subcommand reached
|
||||||
|
currentLimit := spec.MaxArgs
|
||||||
|
tempSubs := spec.Subcommands
|
||||||
|
for i := 1; i < depth; i++ {
|
||||||
|
tok := tokens[i]
|
||||||
|
for _, sub := range tempSubs {
|
||||||
|
if sub.Name == tok {
|
||||||
|
currentLimit = sub.MaxArgs
|
||||||
|
tempSubs = sub.Subcommands
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count arguments ALREADY TYPED for this subcommand/spec
|
||||||
|
argCount := 0
|
||||||
|
for i := depth; i < len(tokens)-1; i++ {
|
||||||
|
t := tokens[i]
|
||||||
|
// Skip empty tokens or flags
|
||||||
|
if t != "" && !strings.HasPrefix(t, "-") && !strings.Contains(t, "=") {
|
||||||
|
argCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partial := tokens[len(tokens)-1]
|
||||||
|
// If we hit the limit, don't allow ANY more argument suggestions
|
||||||
|
// (only flags/options which start with '-' are allowed later)
|
||||||
|
allowMoreArgs := currentLimit <= 0 || argCount < currentLimit
|
||||||
|
|
||||||
|
debugLog("[Core] query tokens: %v (partial: '%s')", tokens, partial)
|
||||||
|
debugLog("[Core] depth: %d, argCount: %d, limit: %d, allowMore: %v", depth, argCount, currentLimit, allowMoreArgs)
|
||||||
|
|
||||||
|
rootCmdName = tokens[0]
|
||||||
|
|
||||||
|
// 'prefix' is the breadcrumb for the command (e.g. "git checkout")
|
||||||
prefixBuilder := strings.Builder{}
|
prefixBuilder := strings.Builder{}
|
||||||
for i := 0; i < depth; i++ {
|
for i := 0; i < depth; i++ {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@@ -174,30 +250,74 @@ func Lookup(input string) []Suggestion {
|
|||||||
prefixBuilder.WriteString(tokens[i])
|
prefixBuilder.WriteString(tokens[i])
|
||||||
}
|
}
|
||||||
prefix := prefixBuilder.String()
|
prefix := prefixBuilder.String()
|
||||||
partial := tokens[len(tokens)-1]
|
|
||||||
|
|
||||||
if currentGen != nil {
|
// 'linePrefix' is the full context except the last partial word (e.g. "just run build")
|
||||||
genResults := currentGen(tokens[:depth], prefix, partial)
|
linePrefixBuilder := strings.Builder{}
|
||||||
|
for i := 0; i < len(tokens)-1; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
linePrefixBuilder.WriteByte(' ')
|
||||||
|
}
|
||||||
|
linePrefixBuilder.WriteString(tokens[i])
|
||||||
|
}
|
||||||
|
linePrefix := linePrefixBuilder.String()
|
||||||
|
|
||||||
|
// already have partial from above
|
||||||
|
rootCmdName = tokens[0]
|
||||||
|
|
||||||
|
if currentGen != nil && allowMoreArgs {
|
||||||
|
genResults := currentGen(tokens, prefix, partial)
|
||||||
|
|
||||||
for _, g := range genResults {
|
for _, g := range genResults {
|
||||||
parts := strings.Split(g.Cmd, " ")
|
// Basic filtering based on partial input
|
||||||
name := parts[len(parts)-1]
|
if partial != "" && !hasPrefix(g.Cmd, partial) && !strings.Contains(g.Cmd, partial) {
|
||||||
if partial == "" || hasPrefix(name, partial) {
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
finalCmd := g.Cmd
|
||||||
|
// Smart Absolute Building: join fragments to the line context if needed.
|
||||||
|
// Only add linePrefix if the command doesn't already seem to have it.
|
||||||
|
cleanLinePrefix := strings.TrimSpace(linePrefix)
|
||||||
|
if cleanLinePrefix != "" && !strings.HasPrefix(finalCmd, cleanLinePrefix) && !strings.HasPrefix(finalCmd, rootCmdName) {
|
||||||
|
finalCmd = cleanLinePrefix + " " + g.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// UNIVERSAL DEDUPLICATION:
|
||||||
|
// Check if the NEW word being suggested is already present in the typed tokens.
|
||||||
|
newTokens := tokenize(finalCmd)
|
||||||
|
if len(newTokens) > 0 {
|
||||||
|
lastToken := newTokens[len(newTokens)-1]
|
||||||
|
isDuplicate := false
|
||||||
|
for i := 0; i < len(tokens)-1; i++ {
|
||||||
|
if tokens[i] == lastToken {
|
||||||
|
isDuplicate = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isDuplicate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, Suggestion{
|
||||||
|
Cmd: finalCmd,
|
||||||
|
Desc: g.Desc,
|
||||||
|
Icon: rootCmdName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowMoreArgs {
|
||||||
|
for _, sub := range currentSubs {
|
||||||
|
if partial == "" || hasPrefix(sub.Name, partial) {
|
||||||
results = append(results, Suggestion{
|
results = append(results, Suggestion{
|
||||||
Cmd: g.Cmd, Desc: g.Desc, Icon: rootCmdName,
|
Cmd: prefix + " " + sub.Name, Desc: sub.Description, Icon: rootCmdName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, sub := range currentSubs {
|
// Only suggest options if partial starts with '-'
|
||||||
if partial == "" || hasPrefix(sub.Name, partial) {
|
if len(partial) > 0 && partial[0] == '-' {
|
||||||
results = append(results, Suggestion{
|
|
||||||
Cmd: prefix + " " + sub.Name, Desc: sub.Description, Icon: rootCmdName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if partial == "" || (len(partial) > 0 && partial[0] == '-') {
|
|
||||||
usedOpts := make(map[string]bool)
|
usedOpts := make(map[string]bool)
|
||||||
for _, t := range tokens {
|
for _, t := range tokens {
|
||||||
if strings.HasPrefix(t, "-") {
|
if strings.HasPrefix(t, "-") {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
Name: "build",
|
Name: "build",
|
||||||
Description: "compile packages and dependencies",
|
Description: "compile packages and dependencies",
|
||||||
|
MaxArgs: 1,
|
||||||
Generator: core.FileGenerator(".go"),
|
Generator: core.FileGenerator(".go"),
|
||||||
Options: append(globalBuildOptions,
|
Options: append(globalBuildOptions,
|
||||||
core.Option{Name: ".", Description: "current package"},
|
core.Option{Name: ".", Description: "current package"},
|
||||||
@@ -52,6 +53,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
Name: "run",
|
Name: "run",
|
||||||
Description: "compile and run Go program",
|
Description: "compile and run Go program",
|
||||||
|
MaxArgs: 1,
|
||||||
Generator: core.FileGenerator(".go"),
|
Generator: core.FileGenerator(".go"),
|
||||||
Options: append(globalBuildOptions,
|
Options: append(globalBuildOptions,
|
||||||
core.Option{Name: ".", Description: "current package"},
|
core.Option{Name: ".", Description: "current package"},
|
||||||
@@ -61,6 +63,7 @@ func init() {
|
|||||||
{
|
{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Description: "test packages",
|
Description: "test packages",
|
||||||
|
MaxArgs: 1,
|
||||||
Generator: core.FileGenerator(".go"),
|
Generator: core.FileGenerator(".go"),
|
||||||
Options: append(globalBuildOptions,
|
Options: append(globalBuildOptions,
|
||||||
core.Option{Name: ".", Description: "current package"},
|
core.Option{Name: ".", Description: "current package"},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ func init() {
|
|||||||
core.Register(&core.Spec{
|
core.Register(&core.Spec{
|
||||||
Name: "just",
|
Name: "just",
|
||||||
Description: "command runner",
|
Description: "command runner",
|
||||||
|
MaxArgs: 1,
|
||||||
Generator: func(tokens []string, prefix string, partial string) []core.Suggestion {
|
Generator: func(tokens []string, prefix string, partial string) []core.Suggestion {
|
||||||
file, err := os.Open("justfile")
|
file, err := os.Open("justfile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -54,9 +55,6 @@ func init() {
|
|||||||
seen[recipe] = true
|
seen[recipe] = true
|
||||||
|
|
||||||
cmd := recipe
|
cmd := recipe
|
||||||
if prefix != "" {
|
|
||||||
cmd = prefix + " " + recipe
|
|
||||||
}
|
|
||||||
|
|
||||||
desc := "just recipe"
|
desc := "just recipe"
|
||||||
if lastComment != "" {
|
if lastComment != "" {
|
||||||
|
|||||||
@@ -21,4 +21,10 @@ reload:
|
|||||||
# update pkg
|
# update pkg
|
||||||
[group('dev')]
|
[group('dev')]
|
||||||
pkg:
|
pkg:
|
||||||
@go mod tidy
|
@go mod tidy
|
||||||
|
|
||||||
|
# debugger
|
||||||
|
[group('dev')]
|
||||||
|
debug:
|
||||||
|
@./iris --debug
|
||||||
|
|
||||||
|
|||||||
+145
-53
@@ -33,15 +33,30 @@ var (
|
|||||||
Long: `IRIS (a.k.a Intelligent Real-time Input Suggestion) is a shell auto-autocompletion tool.
|
Long: `IRIS (a.k.a Intelligent Real-time Input Suggestion) is a shell auto-autocompletion tool.
|
||||||
It works exactly like coding editor suggestion menu drop down.`,
|
It works exactly like coding editor suggestion menu drop down.`,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if debugMode {
|
||||||
|
f, _ := os.OpenFile("iris.log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
||||||
|
debugLogger = f
|
||||||
|
core.DebugWriter = f // Link core logger
|
||||||
|
fmt.Fprintf(debugLogger, "--- IRIS Debug Started ---\n")
|
||||||
|
}
|
||||||
runWrapper()
|
runWrapper()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
shellFlag string
|
shellFlag string
|
||||||
isReload bool
|
debugMode bool
|
||||||
|
debugLogger *os.File
|
||||||
|
isReload bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringVarP(&shellFlag, "shell", "s", "", "shell to use (bash, zsh, fish)")
|
rootCmd.PersistentFlags().StringVarP(&shellFlag, "shell", "s", "", "shell to use (bash, zsh, fish)")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&debugMode, "debug", "d", false, "enable debug logging to iris.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugLog(format string, a ...interface{}) {
|
||||||
|
if debugLogger != nil {
|
||||||
|
fmt.Fprintf(debugLogger, format+"\n", a...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
@@ -169,14 +184,16 @@ func runWrapper() {
|
|||||||
c.ExtraFiles = make([]*os.File, 11)
|
c.ExtraFiles = make([]*os.File, 11)
|
||||||
c.ExtraFiles[10] = w
|
c.ExtraFiles[10] = w
|
||||||
c.Env = adapter.GetEnv(10, os.Getpid())
|
c.Env = adapter.GetEnv(10, os.Getpid())
|
||||||
c.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
||||||
|
|
||||||
ptmx, err := pty.Start(c)
|
ptmx, err := pty.Start(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[IRIS] failed to start PTY: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer ptmx.Close()
|
defer ptmx.Close()
|
||||||
|
|
||||||
|
fmt.Printf("\033[36m[IRIS] Active - typing suggestions enabled\033[0m\r\n")
|
||||||
|
|
||||||
_ = pty.InheritSize(os.Stdin, ptmx)
|
_ = pty.InheritSize(os.Stdin, ptmx)
|
||||||
core.ShellPID = c.Process.Pid
|
core.ShellPID = c.Process.Pid
|
||||||
|
|
||||||
@@ -203,10 +220,9 @@ func runWrapper() {
|
|||||||
os.Setenv("IRIS_ACTIVE_SHELL", innerShell)
|
os.Setenv("IRIS_ACTIVE_SHELL", innerShell)
|
||||||
}
|
}
|
||||||
|
|
||||||
// kill the child process group before reloading
|
// kill the child process before reloading
|
||||||
// using negative PID to kill all descendants
|
|
||||||
if c.Process != nil {
|
if c.Process != nil {
|
||||||
_ = syscall.Kill(-c.Process.Pid, syscall.SIGKILL)
|
_ = syscall.Kill(c.Process.Pid, syscall.SIGKILL)
|
||||||
ptmx.Close()
|
ptmx.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,15 +282,22 @@ func runWrapper() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
var naiveBuffer string
|
var naiveBuffer string
|
||||||
|
suggestionsEnabled := true
|
||||||
mode := "spec"
|
mode := "spec"
|
||||||
|
|
||||||
renderOverlay := func() {
|
renderOverlay := func() {
|
||||||
|
if !suggestionsEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
// don't render if shell is starting up
|
// don't render if shell is starting up
|
||||||
if isReload && naiveBuffer == "" {
|
if isReload && naiveBuffer == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugLog("[Render] query: '%s', mode: %s", naiveBuffer, mode)
|
||||||
results := mergeResults(naiveBuffer, mode)
|
results := mergeResults(naiveBuffer, mode)
|
||||||
|
debugLog("[Render] results found: %d", len(results))
|
||||||
|
|
||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
||||||
} else {
|
} else {
|
||||||
@@ -299,40 +322,13 @@ func runWrapper() {
|
|||||||
shouldOverlayDraw := false
|
shouldOverlayDraw := false
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
b := inputSlice[i]
|
b := inputSlice[i]
|
||||||
|
|
||||||
intercepted := false
|
intercepted := false
|
||||||
if b == 0x12 { // ctrl+r
|
|
||||||
intercepted = true
|
|
||||||
if mode == "spec" {
|
|
||||||
mode = "history"
|
|
||||||
} else {
|
|
||||||
mode = "spec"
|
|
||||||
}
|
|
||||||
shouldOverlayDraw = true
|
|
||||||
} else if overlay.Visible && (b == '\r' || b == 0x09) {
|
|
||||||
intercepted = true
|
|
||||||
selected := overlay.Items[overlay.Cursor].Cmd
|
|
||||||
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
|
||||||
if mode == "spec" || b == 0x09 {
|
|
||||||
selected += " "
|
|
||||||
}
|
|
||||||
naiveBuffer = selected
|
|
||||||
mode = "spec"
|
|
||||||
ptmx.Write(adapter.PrepareSelectSequence(selected))
|
|
||||||
if b == '\r' {
|
|
||||||
ptmx.Write([]byte{'\r'})
|
|
||||||
naiveBuffer = ""
|
|
||||||
} else {
|
|
||||||
renderOverlay()
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !intercepted {
|
// Detect Escape sequence (e.g. arrows) early
|
||||||
|
if b == '\033' {
|
||||||
ptmx.Write([]byte{b})
|
ptmx.Write([]byte{b})
|
||||||
|
// arrow key monitoring inside escape
|
||||||
// arrow key monitoring with tagged switch
|
if i+2 < n && inputSlice[i+1] == '[' {
|
||||||
if b == '\033' && i+2 < n && inputSlice[i+1] == '[' {
|
|
||||||
if overlay.Visible {
|
if overlay.Visible {
|
||||||
switch inputSlice[i+2] {
|
switch inputSlice[i+2] {
|
||||||
case 'A': // up
|
case 'A': // up
|
||||||
@@ -350,19 +346,95 @@ func runWrapper() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// skip sequence
|
||||||
switch b {
|
for j := i + 1; j < n; j++ {
|
||||||
|
char := inputSlice[j]
|
||||||
case 0x09: // tab
|
ptmx.Write([]byte{char})
|
||||||
if !overlay.Visible {
|
i = j
|
||||||
shouldOverlayDraw = true
|
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char == '~' {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
case 127: // backspace
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iris shortcuts
|
||||||
|
if b == 0x12 { // ctrl+r
|
||||||
|
intercepted = true
|
||||||
|
if mode == "spec" {
|
||||||
|
mode = "history"
|
||||||
|
} else {
|
||||||
|
mode = "spec"
|
||||||
|
}
|
||||||
|
shouldOverlayDraw = true
|
||||||
|
} else if b == 0x1b && n == 1 { // Standalone ESC (Toggle suggestions)
|
||||||
|
intercepted = true
|
||||||
|
suggestionsEnabled = !suggestionsEnabled
|
||||||
|
if !suggestionsEnabled {
|
||||||
|
debugLog("[Input] ESC pressed: hiding menu")
|
||||||
|
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
||||||
|
shouldOverlayDraw = false
|
||||||
|
} else {
|
||||||
|
debugLog("[Input] ESC pressed: showing menu")
|
||||||
|
shouldOverlayDraw = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else if overlay.Visible && (b == 0x0d || b == 0x0a) { // Enter while menu open
|
||||||
|
intercepted = true
|
||||||
|
debugLog("[Input] Enter pressed: closing menu and executing raw buffer")
|
||||||
|
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
||||||
|
// Send Enter to PTY to execute current buffer as-is
|
||||||
|
ptmx.Write([]byte{0x0d})
|
||||||
|
naiveBuffer = ""
|
||||||
|
shouldOverlayDraw = false
|
||||||
|
continue
|
||||||
|
} else if b == 0x09 { // Tab (Universal Intercept - Select/Commit)
|
||||||
|
intercepted = true
|
||||||
|
if !overlay.Visible {
|
||||||
|
debugLog("[Input] Tab pressed: opening menu")
|
||||||
|
shouldOverlayDraw = true
|
||||||
|
} else {
|
||||||
|
// SELECT and COMMIT
|
||||||
|
selected := overlay.Items[overlay.Cursor].Cmd
|
||||||
|
debugLog("[Input] Tab pressed: committing '%s'", selected)
|
||||||
|
|
||||||
|
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
||||||
|
|
||||||
|
// Auto-add space for spec mode
|
||||||
|
if mode == "spec" {
|
||||||
|
selected = strings.TrimSpace(selected) + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
naiveBuffer = selected
|
||||||
|
mode = "spec"
|
||||||
|
// Sync PTY with the new buffer (Ctrl+A then Ctrl+K to clear rest)
|
||||||
|
ptmx.Write([]byte{0x01, 0x0b}) // bash/zsh style home and clear
|
||||||
|
ptmx.Write([]byte(selected))
|
||||||
|
|
||||||
|
shouldOverlayDraw = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// normal typing
|
||||||
|
if !intercepted {
|
||||||
|
ptmx.Write([]byte{b})
|
||||||
|
switch b {
|
||||||
|
case 127, 0x08: // backspace
|
||||||
if len(naiveBuffer) > 0 {
|
if len(naiveBuffer) > 0 {
|
||||||
naiveBuffer = naiveBuffer[:len(naiveBuffer)-1]
|
naiveBuffer = naiveBuffer[:len(naiveBuffer)-1]
|
||||||
shouldOverlayDraw = true
|
shouldOverlayDraw = true
|
||||||
}
|
}
|
||||||
case '\r', 0x03: // enter, ctrl+c
|
case 0x17: // Ctrl+W: delete one word
|
||||||
|
trimBuf := strings.TrimRight(naiveBuffer, " ")
|
||||||
|
lastSpace := strings.LastIndex(trimBuf, " ")
|
||||||
|
if lastSpace >= 0 {
|
||||||
|
naiveBuffer = trimBuf[:lastSpace+1]
|
||||||
|
} else {
|
||||||
|
naiveBuffer = ""
|
||||||
|
}
|
||||||
|
shouldOverlayDraw = true
|
||||||
|
case '\r', 0x03, 0x15, 0x0C: // Enter, Ctrl+C, Ctrl+U, Ctrl+L
|
||||||
naiveBuffer = ""
|
naiveBuffer = ""
|
||||||
mode = "spec"
|
mode = "spec"
|
||||||
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
os.Stdout.Write([]byte(overlay.ClearAndDisable()))
|
||||||
@@ -383,27 +455,47 @@ func runWrapper() {
|
|||||||
|
|
||||||
func mergeResults(query string, mode string) []core.Suggestion {
|
func mergeResults(query string, mode string) []core.Suggestion {
|
||||||
if query == "" {
|
if query == "" {
|
||||||
|
debugLog("[Merge] Query empty, returning nil")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizedQuery := strings.TrimSpace(query)
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
deduped := []core.Suggestion{}
|
||||||
|
|
||||||
if mode == "history" {
|
if mode == "history" {
|
||||||
histResults, _ := integration.SearchHistory(query)
|
histResults, _ := integration.SearchHistory(query)
|
||||||
cmdResults := []core.Suggestion{}
|
|
||||||
for _, h := range histResults {
|
for _, h := range histResults {
|
||||||
cmdResults = append(cmdResults, core.Suggestion{
|
normalizedCmd := strings.TrimSpace(h.Cmd)
|
||||||
|
if seen[normalizedCmd] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[normalizedCmd] = true
|
||||||
|
deduped = append(deduped, core.Suggestion{
|
||||||
Cmd: h.Cmd,
|
Cmd: h.Cmd,
|
||||||
Desc: " history",
|
Desc: " history",
|
||||||
Icon: fmt.Sprintf("%d", h.ID),
|
Icon: fmt.Sprintf("%d", h.ID),
|
||||||
})
|
})
|
||||||
|
if len(deduped) >= 10 {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(cmdResults) > 100 {
|
debugLog("[Merge] History mode found %d items", len(deduped))
|
||||||
cmdResults = cmdResults[:100]
|
return deduped
|
||||||
}
|
|
||||||
return cmdResults
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugLog("[Merge] Calling Lookup for '%s'", query)
|
||||||
cmdResults := core.Lookup(query)
|
cmdResults := core.Lookup(query)
|
||||||
seen := make(map[string]bool)
|
debugLog("[Merge] Lookup returned %d raw items", len(cmdResults))
|
||||||
deduped := []core.Suggestion{}
|
|
||||||
for _, s := range cmdResults {
|
for _, s := range cmdResults {
|
||||||
|
normalizedCmd := strings.TrimSpace(s.Cmd)
|
||||||
|
// CRITICAL: Filter out exact matches to prevent infinite tab loops
|
||||||
|
if normalizedCmd == normalizedQuery {
|
||||||
|
debugLog("[Merge] Filtered EXACT MATCH: '%s'", normalizedCmd)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if !seen[s.Cmd] {
|
if !seen[s.Cmd] {
|
||||||
seen[s.Cmd] = true
|
seen[s.Cmd] = true
|
||||||
deduped = append(deduped, s)
|
deduped = append(deduped, s)
|
||||||
|
|||||||
Reference in New Issue
Block a user