feat(config): ui width, auto exec, selection and navigation configs (#80)
closes #67 So I made an auto-exec confg `auto-exec = true` (e32c0ba6ed69f3c764353e58b8fbacf160f37d5a) which means: - If you want your command in the prompt to be executed right away, you can set this = false (for example, you are typing `nvim ~/.conf` but IRIS is suggesting `nvim ~/.config`, it will ignore the suggestion and exec what you are typing, this is current default behavior) - If you want to auto execute full command which is being suggested, you can set this = true (this is the opposite of above, when you are typing `nvim ~/.conf` but IRIS is suggesting `nvim ~/.config`, it will auto exec `nvim ~/.config` instead, but sometime you want to exec what you are typing instead of the suggestion right?, just press esc or shift tab to hide the menu and it will) --- Select keybinding to replace tab (0dc01cba8c610e8d6fd8c50f6a71bdcc061a098f) `select = "ctrl+y"` Some users may love nvim keybinding or emacs, so I bring navigation keybinding to be configurable (0dc01cba8c610e8d6fd8c50f6a71bdcc061a098f) ``` navigate-up = "ctrl+k" navigate-down = "ctrl+j" ``` --- I also added a option for UI width (dfa3b810a7be406fe4652b3886020f37bb166811) `max-width = 80` - If you configure `max-width` to be larger than the actual width of the current terminal, Iris will automatically "compress" the window to match the terminal's width (meaning it will never overflow and break the interface) - If you configure `max-width` too small (e.g 10), the interface will have cramped text, making it look very bad. So, I've set a minimum safe min limit of 40. If the configured value is smaller than this, Iris will automatically use 40 Small note: If you set `max-width = 0` (or leave it blank), it will automatically use Iris's default standard size of 76. SO you can confidently customize it without fear of breaking the UI (I hope so lmao) --- Added a fallback to ensure Iris never crashes or loses shortcuts if the user accidentally deletes keybinding configuration lines or intentionally assigns empty strings " " (f682c9bf623f3ed46ad0763221145604dbd91b07) Fix typo `toggle_mode` to `toggle-mode` (b39be6673e552735fa65a6c9807510c83b4d9cdd) if you have a better config design, please tell me, I always listen to your idea
This commit is contained in:
@@ -155,23 +155,28 @@ iris config show
|
|||||||
```toml
|
```toml
|
||||||
[core]
|
[core]
|
||||||
version = 1
|
version = 1
|
||||||
shell = "" # "zsh", "bash", "fish", or empty for auto-detection
|
shell = "" # "zsh", "bash", "fish", or empty for auto-detection
|
||||||
shell-login = false # run the selected shell as a login shell; can also be enabled with iris --shell-login
|
shell-login = false # run the selected shell as a login shell; can also be enabled with iris --shell-login
|
||||||
mode = "last" # "last", "spec", or "history"
|
mode = "last" # "last", "spec", or "history"
|
||||||
debug = false
|
debug = false
|
||||||
expand-alias = true
|
expand-alias = true
|
||||||
|
auto-execute = false
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
style = "modern" # "modern" or "classic"
|
style = "modern" # "modern" or "classic"
|
||||||
ghost-text = true
|
ghost-text = true
|
||||||
hidden-files = false
|
hidden-files = false
|
||||||
max-suggestions = 100
|
max-suggestions = 100
|
||||||
max-height = 15
|
max-height = 15
|
||||||
|
max-width = 0
|
||||||
nerd-fonts = true
|
nerd-fonts = true
|
||||||
|
|
||||||
[keybindings]
|
[keybindings]
|
||||||
toggle-mode = "ctrl+r"
|
toggle-mode = "ctrl+r"
|
||||||
toggle-menu = "ctrl+space"
|
toggle-menu = "shift+tab"
|
||||||
|
select = "tab"
|
||||||
|
navigate-up = "up"
|
||||||
|
navigate-down = "down"
|
||||||
|
|
||||||
[git]
|
[git]
|
||||||
filter-active-branch = true
|
filter-active-branch = true
|
||||||
|
|||||||
+12
-1
@@ -15,7 +15,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
boxWidth = 76 // total visual width, corners included
|
|
||||||
maxItems = 6
|
maxItems = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -569,6 +568,18 @@ func (o *Overlay) draw() string {
|
|||||||
if err != nil || width <= 0 {
|
if err != nil || width <= 0 {
|
||||||
width = 120
|
width = 120
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boxWidth := config.Get().UI.MaxWidth
|
||||||
|
if boxWidth <= 0 {
|
||||||
|
boxWidth = 76 // Default if 0
|
||||||
|
}
|
||||||
|
if boxWidth > width {
|
||||||
|
boxWidth = width // Responsive: don't overflow
|
||||||
|
}
|
||||||
|
if boxWidth < 40 {
|
||||||
|
boxWidth = 40 // Minimum safe width
|
||||||
|
}
|
||||||
|
|
||||||
if targetCol+boxWidth > width {
|
if targetCol+boxWidth > width {
|
||||||
targetCol = width - boxWidth
|
targetCol = width - boxWidth
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type CoreConfig struct {
|
|||||||
Mode string `toml:"mode"`
|
Mode string `toml:"mode"`
|
||||||
Debug bool `toml:"debug"`
|
Debug bool `toml:"debug"`
|
||||||
ExpandAlias bool `toml:"expand-alias"`
|
ExpandAlias bool `toml:"expand-alias"`
|
||||||
|
AutoExecute bool `toml:"auto-execute"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UIConfig struct {
|
type UIConfig struct {
|
||||||
@@ -47,6 +48,7 @@ type UIConfig struct {
|
|||||||
ShowHiddenFiles bool `toml:"hidden-files"`
|
ShowHiddenFiles bool `toml:"hidden-files"`
|
||||||
MaxSuggestions int `toml:"max-suggestions"`
|
MaxSuggestions int `toml:"max-suggestions"`
|
||||||
MaxHeight int `toml:"max-height"`
|
MaxHeight int `toml:"max-height"`
|
||||||
|
MaxWidth int `toml:"max-width"`
|
||||||
NerdFonts bool `toml:"nerd-fonts"`
|
NerdFonts bool `toml:"nerd-fonts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +64,11 @@ type UpdaterConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type KeybindingsConfig struct {
|
type KeybindingsConfig struct {
|
||||||
ToggleMode string `toml:"toggle-mode"`
|
ToggleMode string `toml:"toggle-mode"`
|
||||||
ToggleMenu string `toml:"toggle-menu"`
|
ToggleMenu string `toml:"toggle-menu"`
|
||||||
|
SelectSuggestion string `toml:"select"`
|
||||||
|
NavigateUp string `toml:"navigate-up"`
|
||||||
|
NavigateDown string `toml:"navigate-down"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuggestOnEmptyConfig struct {
|
type SuggestOnEmptyConfig struct {
|
||||||
@@ -186,6 +191,23 @@ func Load() (*Config, error) {
|
|||||||
|
|
||||||
applyEnv(cfg)
|
applyEnv(cfg)
|
||||||
|
|
||||||
|
// fallback for empty keybindings
|
||||||
|
if cfg.Keybindings.ToggleMode == "" {
|
||||||
|
cfg.Keybindings.ToggleMode = "ctrl+r"
|
||||||
|
}
|
||||||
|
if cfg.Keybindings.ToggleMenu == "" {
|
||||||
|
cfg.Keybindings.ToggleMenu = "shift+tab"
|
||||||
|
}
|
||||||
|
if cfg.Keybindings.SelectSuggestion == "" {
|
||||||
|
cfg.Keybindings.SelectSuggestion = "tab"
|
||||||
|
}
|
||||||
|
if cfg.Keybindings.NavigateUp == "" {
|
||||||
|
cfg.Keybindings.NavigateUp = "up"
|
||||||
|
}
|
||||||
|
if cfg.Keybindings.NavigateDown == "" {
|
||||||
|
cfg.Keybindings.NavigateDown = "down"
|
||||||
|
}
|
||||||
|
|
||||||
if err := validate(cfg); err != nil {
|
if err := validate(cfg); err != nil {
|
||||||
return cfg, fmt.Errorf("config: invalid value: %w", err)
|
return cfg, fmt.Errorf("config: invalid value: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ func DefaultConfig() *Config {
|
|||||||
Mode: "last",
|
Mode: "last",
|
||||||
Debug: false,
|
Debug: false,
|
||||||
ExpandAlias: true,
|
ExpandAlias: true,
|
||||||
|
AutoExecute: false,
|
||||||
},
|
},
|
||||||
UI: UIConfig{
|
UI: UIConfig{
|
||||||
Style: "modern",
|
Style: "modern",
|
||||||
@@ -18,6 +19,7 @@ func DefaultConfig() *Config {
|
|||||||
ShowHiddenFiles: false,
|
ShowHiddenFiles: false,
|
||||||
MaxSuggestions: 100,
|
MaxSuggestions: 100,
|
||||||
MaxHeight: 15,
|
MaxHeight: 15,
|
||||||
|
MaxWidth: 0, // 0 means no limit, fallback to terminal width
|
||||||
NerdFonts: true,
|
NerdFonts: true,
|
||||||
},
|
},
|
||||||
Git: GitConfig{
|
Git: GitConfig{
|
||||||
@@ -42,8 +44,11 @@ func DefaultConfig() *Config {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Keybindings: KeybindingsConfig{
|
Keybindings: KeybindingsConfig{
|
||||||
ToggleMode: "ctrl+r",
|
ToggleMode: "ctrl+r",
|
||||||
ToggleMenu: "shift+tab",
|
ToggleMenu: "shift+tab",
|
||||||
|
SelectSuggestion: "tab",
|
||||||
|
NavigateUp: "up",
|
||||||
|
NavigateDown: "down",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -13,6 +13,8 @@ func MatchKey(input []byte, expected string) (matched bool, consumed int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expected = strings.ToLower(strings.TrimSpace(expected))
|
expected = strings.ToLower(strings.TrimSpace(expected))
|
||||||
|
expected = strings.TrimPrefix(expected, "<")
|
||||||
|
expected = strings.TrimSuffix(expected, ">")
|
||||||
|
|
||||||
if strings.HasPrefix(expected, "ctrl+") && len(expected) == 6 {
|
if strings.HasPrefix(expected, "ctrl+") && len(expected) == 6 {
|
||||||
char := expected[5]
|
char := expected[5]
|
||||||
@@ -38,12 +40,24 @@ func MatchKey(input []byte, expected string) (matched bool, consumed int) {
|
|||||||
if len(input) >= 3 && input[0] == 0x1b && input[1] == '[' && input[2] == 'Z' {
|
if len(input) >= 3 && input[0] == 0x1b && input[1] == '[' && input[2] == 'Z' {
|
||||||
return true, 3
|
return true, 3
|
||||||
}
|
}
|
||||||
|
case "up":
|
||||||
|
if len(input) >= 3 && input[0] == 0x1b && (input[1] == '[' || input[1] == 'O') && input[2] == 'A' {
|
||||||
|
return true, 3
|
||||||
|
}
|
||||||
|
case "down":
|
||||||
|
if len(input) >= 3 && input[0] == 0x1b && (input[1] == '[' || input[1] == 'O') && input[2] == 'B' {
|
||||||
|
return true, 3
|
||||||
|
}
|
||||||
case "right":
|
case "right":
|
||||||
// typically \033[C or \033OC
|
// typically \033[C or \033OC
|
||||||
if len(input) >= 3 && input[0] == 0x1b && (input[1] == '[' || input[1] == 'O') && input[2] == 'C' {
|
if len(input) >= 3 && input[0] == 0x1b && (input[1] == '[' || input[1] == 'O') && input[2] == 'C' {
|
||||||
return true, 3
|
return true, 3
|
||||||
}
|
}
|
||||||
case "enter":
|
case "left":
|
||||||
|
if len(input) >= 3 && input[0] == 0x1b && (input[1] == '[' || input[1] == 'O') && input[2] == 'D' {
|
||||||
|
return true, 3
|
||||||
|
}
|
||||||
|
case "enter", "cr", "return":
|
||||||
if input[0] == 0x0d || input[0] == 0x0a {
|
if input[0] == 0x0d || input[0] == 0x0a {
|
||||||
return true, 1
|
return true, 1
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-4
@@ -235,6 +235,9 @@ debug = false
|
|||||||
# automatically expand aliases on space
|
# automatically expand aliases on space
|
||||||
expand-alias = true
|
expand-alias = true
|
||||||
|
|
||||||
|
# automatically execute command after accepting suggestion
|
||||||
|
auto-execute = false
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
# visual style: "modern" (icons, category pills, shortcut footer) or "classic" (minimalist, centered number, no icons)
|
# visual style: "modern" (icons, category pills, shortcut footer) or "classic" (minimalist, centered number, no icons)
|
||||||
style = "modern"
|
style = "modern"
|
||||||
@@ -254,6 +257,9 @@ max-suggestions = 100
|
|||||||
# maximum height of the overlay
|
# maximum height of the overlay
|
||||||
max-height = 15
|
max-height = 15
|
||||||
|
|
||||||
|
# maximum width of the overlay (0 = responsive to terminal)
|
||||||
|
max-width = 0
|
||||||
|
|
||||||
[git]
|
[git]
|
||||||
# hide current branch in checkout/switch list
|
# hide current branch in checkout/switch list
|
||||||
filter-active-branch = true
|
filter-active-branch = true
|
||||||
@@ -272,10 +278,11 @@ channel = "stable"
|
|||||||
check-interval = "24h"
|
check-interval = "24h"
|
||||||
|
|
||||||
[keybindings]
|
[keybindings]
|
||||||
toggle_mode = "ctrl+r"
|
toggle-mode = "ctrl+r"
|
||||||
toggle_menu = "shift+tab"
|
toggle-menu = "shift+tab"
|
||||||
select_suggestion = "tab"
|
select = "tab"
|
||||||
accept_suggestion = "right"
|
navigate-up = "up"
|
||||||
|
navigate-down = "down"
|
||||||
`
|
`
|
||||||
if errWrite := os.WriteFile(path, []byte(defaultContent), 0644); errWrite == nil {
|
if errWrite := os.WriteFile(path, []byte(defaultContent), 0644); errWrite == nil {
|
||||||
fmt.Printf("✓ Initialized default config file at %s\n", path)
|
fmt.Printf("✓ Initialized default config file at %s\n", path)
|
||||||
|
|||||||
+225
-212
@@ -674,7 +674,227 @@ func runWrapper() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMode); matched { // ctrl+r: toggle between command specs and command history
|
||||||
|
i += consumed - 1
|
||||||
|
intercepted = true
|
||||||
|
activeModeMu.Lock()
|
||||||
|
if activeMode == "spec" {
|
||||||
|
activeMode = "history"
|
||||||
|
} else {
|
||||||
|
activeMode = "spec"
|
||||||
|
}
|
||||||
|
saveMode(activeMode)
|
||||||
|
activeModeMu.Unlock()
|
||||||
|
logger.Debugf("Intercepted Ctrl+R, toggled mode to %q", activeMode)
|
||||||
|
if userNavigated.Load() {
|
||||||
|
bufferMu.Lock()
|
||||||
|
naiveBuffer = overlay.GetTypedQuery()
|
||||||
|
cursorOffset = 0
|
||||||
|
bufferMu.Unlock()
|
||||||
|
_, _ = ptmx.Write(append([]byte{0x15}, overlay.GetTypedQuery()...))
|
||||||
|
}
|
||||||
|
userNavigated.Store(false)
|
||||||
|
overlay.Show()
|
||||||
|
shouldOverlayDraw = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var isNavUp, isNavDown bool
|
||||||
|
var navConsumed int
|
||||||
|
if isNavUp, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateUp); !isNavUp {
|
||||||
|
isNavDown, navConsumed = config.MatchKey(inputSlice[i:], config.Get().Keybindings.NavigateDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isNavUp || isNavDown {
|
||||||
|
if overlay.IsVisible() {
|
||||||
|
intercepted = true
|
||||||
|
userNavigated.Store(true)
|
||||||
|
|
||||||
|
arrowDir := "down"
|
||||||
|
if isNavUp {
|
||||||
|
arrowDir = "up"
|
||||||
|
}
|
||||||
|
moved, selectedCmd := overlay.MoveCursor(arrowDir)
|
||||||
|
if !moved {
|
||||||
|
i += navConsumed - 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferMu.Lock()
|
||||||
|
activeModeMu.RLock()
|
||||||
|
isHistMode := activeMode == "history"
|
||||||
|
activeModeMu.RUnlock()
|
||||||
|
var toWrite []byte
|
||||||
|
if isHistMode && selectedCmd != "" {
|
||||||
|
naiveBuffer = selectedCmd
|
||||||
|
cursorOffset = 0
|
||||||
|
toWrite = append([]byte{0x15}, selectedCmd...)
|
||||||
|
}
|
||||||
|
bufCopy := naiveBuffer
|
||||||
|
offsetCopy := cursorOffset
|
||||||
|
bufferMu.Unlock()
|
||||||
|
|
||||||
|
if len(toWrite) > 0 {
|
||||||
|
_, _ = ptmx.Write(toWrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
if !disableGhostText.Load() {
|
||||||
|
b.WriteString(overlay.RenderGhostText(bufCopy, true, offsetCopy == 0))
|
||||||
|
}
|
||||||
|
b.WriteString(overlay.Render())
|
||||||
|
writeStdout([]byte(b.String()))
|
||||||
|
|
||||||
|
i += navConsumed - 1
|
||||||
|
continue
|
||||||
|
} else if naiveBuffer == "" {
|
||||||
|
// up/down arrow on empty prompt
|
||||||
|
intercepted = true
|
||||||
|
activeModeMu.Lock()
|
||||||
|
activeMode = "history"
|
||||||
|
saveMode(activeMode)
|
||||||
|
activeModeMu.Unlock()
|
||||||
|
|
||||||
|
activeModeMu.RLock()
|
||||||
|
currentMode := activeMode
|
||||||
|
activeModeMu.RUnlock()
|
||||||
|
results := MergeResults("", currentMode)
|
||||||
|
if len(results) > 0 {
|
||||||
|
limit := min(len(results), 100)
|
||||||
|
var historyList []spec.Suggestion
|
||||||
|
|
||||||
|
if isNavUp {
|
||||||
|
for j := limit - 1; j >= 0; j-- {
|
||||||
|
historyList = append(historyList, results[j])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for j := range limit {
|
||||||
|
historyList = append(historyList, results[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selected := overlay.SetHistoryList(historyList, isNavUp)
|
||||||
|
if selected != "" {
|
||||||
|
bufferMu.Lock()
|
||||||
|
naiveBuffer = selected
|
||||||
|
cursorOffset = 0
|
||||||
|
bufferMu.Unlock()
|
||||||
|
|
||||||
|
userNavigated.Store(true)
|
||||||
|
writeStdout([]byte(overlay.Render()))
|
||||||
|
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += navConsumed - 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.SelectSuggestion); matched && config.Get().Keybindings.SelectSuggestion != "" {
|
||||||
|
intercepted = true
|
||||||
|
if overlay.IsVisible() {
|
||||||
|
selected := overlay.GetCurrentCmd()
|
||||||
|
if selected != "" {
|
||||||
|
activeModeMu.RLock()
|
||||||
|
currentMode := activeMode
|
||||||
|
activeModeMu.RUnlock()
|
||||||
|
if currentMode == "spec" {
|
||||||
|
s := strings.TrimSpace(selected)
|
||||||
|
if strings.HasSuffix(s, "/") || strings.HasSuffix(s, "\\") {
|
||||||
|
selected = s
|
||||||
|
} else {
|
||||||
|
selected = s + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bufferMu.Lock()
|
||||||
|
naiveBuffer = selected
|
||||||
|
cursorOffset = 0
|
||||||
|
bufferMu.Unlock()
|
||||||
|
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
|
||||||
|
|
||||||
|
overlay.ClearGhostTextState()
|
||||||
|
userNavigated.Store(false)
|
||||||
|
writeStdout([]byte(overlay.Render()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += consumed - 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if b == 0x0d || b == 0x0a { // enter
|
||||||
|
intercepted = true
|
||||||
|
logger.Debugf("Intercepted Enter key")
|
||||||
|
|
||||||
|
var selectedCmd string
|
||||||
|
var shouldAutoExecute bool
|
||||||
|
if config.Get().Core.AutoExecute && overlay.IsVisible() {
|
||||||
|
selectedCmd = overlay.GetCurrentCmd()
|
||||||
|
if selectedCmd != "" {
|
||||||
|
shouldAutoExecute = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeStdout([]byte(overlay.ClearAndDisable()))
|
||||||
|
SetCurrentAISuggestion(nil)
|
||||||
|
renderMu.Lock()
|
||||||
|
if renderTimer != nil {
|
||||||
|
renderTimer.Stop()
|
||||||
|
renderTimer = nil
|
||||||
|
}
|
||||||
|
renderMu.Unlock()
|
||||||
|
|
||||||
|
var cmdToSubmit string
|
||||||
|
if shouldAutoExecute {
|
||||||
|
activeModeMu.RLock()
|
||||||
|
currentMode := activeMode
|
||||||
|
activeModeMu.RUnlock()
|
||||||
|
if currentMode == "spec" {
|
||||||
|
s := strings.TrimSpace(selectedCmd)
|
||||||
|
if strings.HasSuffix(s, "/") || strings.HasSuffix(s, "\\") {
|
||||||
|
selectedCmd = s
|
||||||
|
} else {
|
||||||
|
selectedCmd = s + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update the line first
|
||||||
|
_, _ = ptmx.Write(append([]byte{0x15}, selectedCmd...))
|
||||||
|
cmdToSubmit = selectedCmd
|
||||||
|
} else {
|
||||||
|
bufferMu.Lock()
|
||||||
|
cmdToSubmit = naiveBuffer
|
||||||
|
bufferMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(cmdToSubmit) == "iris reload" {
|
||||||
|
if newCfg, err := config.Load(); err == nil {
|
||||||
|
config.Init(newCfg)
|
||||||
|
disableGhostText.Store(!newCfg.UI.GhostText)
|
||||||
|
}
|
||||||
|
msg := "echo -e '\\033[32m✓ Iris configuration reloaded successfully.\\033[0m'\r"
|
||||||
|
_, _ = ptmx.Write(append([]byte{0x15}, []byte(msg)...))
|
||||||
|
bufferMu.Lock()
|
||||||
|
naiveBuffer = ""
|
||||||
|
cursorOffset = 0
|
||||||
|
bufferMu.Unlock()
|
||||||
|
disableGhostText.Store(false)
|
||||||
|
shouldOverlayDraw = false
|
||||||
|
userNavigated.Store(false)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
integration.RecordSessionCommand(cmdToSubmit)
|
||||||
|
bufferMu.Lock()
|
||||||
|
lastSubmittedCommand = strings.TrimSpace(cmdToSubmit)
|
||||||
|
naiveBuffer = ""
|
||||||
|
cursorOffset = 0
|
||||||
|
bufferMu.Unlock()
|
||||||
|
isCommandActive.Store(true)
|
||||||
|
_, _ = ptmx.Write([]byte{b}) // forward enter to terminal
|
||||||
|
disableGhostText.Store(false)
|
||||||
|
shouldOverlayDraw = false
|
||||||
|
userNavigated.Store(false)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if b == '\033' {
|
if b == '\033' {
|
||||||
// check for bracketed paste start/end
|
// check for bracketed paste start/end
|
||||||
@@ -689,90 +909,6 @@ func runWrapper() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// handle escape sequences like arrow keys and functional shortcuts
|
// handle escape sequences like arrow keys and functional shortcuts
|
||||||
if i+2 < n && (inputSlice[i+1] == '[' || inputSlice[i+1] == 'O') {
|
|
||||||
if overlay.IsVisible() && (inputSlice[i+2] == 'A' || inputSlice[i+2] == 'B') {
|
|
||||||
intercepted = true
|
|
||||||
userNavigated.Store(true)
|
|
||||||
|
|
||||||
arrowDir := "down"
|
|
||||||
if inputSlice[i+2] == 'A' {
|
|
||||||
arrowDir = "up"
|
|
||||||
}
|
|
||||||
moved, selectedCmd := overlay.MoveCursor(arrowDir)
|
|
||||||
if !moved {
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferMu.Lock()
|
|
||||||
activeModeMu.RLock()
|
|
||||||
isHistMode := activeMode == "history"
|
|
||||||
activeModeMu.RUnlock()
|
|
||||||
var toWrite []byte
|
|
||||||
if isHistMode && selectedCmd != "" {
|
|
||||||
naiveBuffer = selectedCmd
|
|
||||||
cursorOffset = 0
|
|
||||||
toWrite = append([]byte{0x15}, selectedCmd...)
|
|
||||||
}
|
|
||||||
bufCopy := naiveBuffer
|
|
||||||
offsetCopy := cursorOffset
|
|
||||||
bufferMu.Unlock()
|
|
||||||
|
|
||||||
if len(toWrite) > 0 {
|
|
||||||
_, _ = ptmx.Write(toWrite)
|
|
||||||
}
|
|
||||||
|
|
||||||
var b strings.Builder
|
|
||||||
if !disableGhostText.Load() {
|
|
||||||
b.WriteString(overlay.RenderGhostText(bufCopy, true, offsetCopy == 0))
|
|
||||||
}
|
|
||||||
b.WriteString(overlay.Render())
|
|
||||||
writeStdout([]byte(b.String()))
|
|
||||||
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
} else if !overlay.IsVisible() && naiveBuffer == "" && (inputSlice[i+2] == 'A' || inputSlice[i+2] == 'B') { // up/down arrow on empty prompt
|
|
||||||
intercepted = true
|
|
||||||
activeModeMu.Lock()
|
|
||||||
activeMode = "history"
|
|
||||||
saveMode(activeMode)
|
|
||||||
activeModeMu.Unlock()
|
|
||||||
|
|
||||||
activeModeMu.RLock()
|
|
||||||
currentMode := activeMode
|
|
||||||
activeModeMu.RUnlock()
|
|
||||||
results := MergeResults("", currentMode)
|
|
||||||
if len(results) > 0 {
|
|
||||||
limit := min(len(results), 100)
|
|
||||||
var historyList []spec.Suggestion
|
|
||||||
|
|
||||||
if inputSlice[i+2] == 'A' {
|
|
||||||
for j := limit - 1; j >= 0; j-- {
|
|
||||||
historyList = append(historyList, results[j])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for j := range limit {
|
|
||||||
historyList = append(historyList, results[j])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selected := overlay.SetHistoryList(historyList, inputSlice[i+2] == 'A')
|
|
||||||
if selected != "" {
|
|
||||||
bufferMu.Lock()
|
|
||||||
naiveBuffer = selected
|
|
||||||
cursorOffset = 0
|
|
||||||
bufferMu.Unlock()
|
|
||||||
|
|
||||||
userNavigated.Store(true)
|
|
||||||
writeStdout([]byte(overlay.Render()))
|
|
||||||
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// left/right arrow cursor tracking
|
// left/right arrow cursor tracking
|
||||||
isLeftRightArrow := false
|
isLeftRightArrow := false
|
||||||
if i+2 < n && (inputSlice[i+1] == '[' || inputSlice[i+1] == 'O') {
|
if i+2 < n && (inputSlice[i+1] == '[' || inputSlice[i+1] == 'O') {
|
||||||
@@ -845,7 +981,10 @@ func runWrapper() {
|
|||||||
if !intercepted {
|
if !intercepted {
|
||||||
writeStdout([]byte(overlay.ClearAndDisable()))
|
writeStdout([]byte(overlay.ClearAndDisable()))
|
||||||
disableGhostText.Store(true)
|
disableGhostText.Store(true)
|
||||||
if !isLeftRightArrow {
|
|
||||||
|
// If it's a standalone ESC (n==1), don't clear the buffer because the user just wanted to hide the menu or enter vi-mode
|
||||||
|
isStandaloneEsc := n == 1 && b == '\033'
|
||||||
|
if !isLeftRightArrow && !isStandaloneEsc {
|
||||||
bufferMu.Lock()
|
bufferMu.Lock()
|
||||||
naiveBuffer = ""
|
naiveBuffer = ""
|
||||||
cursorOffset = 0
|
cursorOffset = 0
|
||||||
@@ -865,101 +1004,7 @@ func runWrapper() {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if b == 0x03 || b == 0x15 { // ctrl+c, ctrl+u
|
||||||
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMode); matched { // ctrl+r: toggle between command specs and command history
|
|
||||||
i += consumed - 1
|
|
||||||
intercepted = true
|
|
||||||
activeModeMu.Lock()
|
|
||||||
if activeMode == "spec" {
|
|
||||||
activeMode = "history"
|
|
||||||
} else {
|
|
||||||
activeMode = "spec"
|
|
||||||
}
|
|
||||||
saveMode(activeMode)
|
|
||||||
activeModeMu.Unlock()
|
|
||||||
logger.Debugf("Intercepted Ctrl+R, toggled mode to %q", activeMode)
|
|
||||||
if userNavigated.Load() {
|
|
||||||
bufferMu.Lock()
|
|
||||||
naiveBuffer = overlay.GetTypedQuery()
|
|
||||||
cursorOffset = 0
|
|
||||||
bufferMu.Unlock()
|
|
||||||
_, _ = ptmx.Write(append([]byte{0x15}, overlay.GetTypedQuery()...))
|
|
||||||
}
|
|
||||||
userNavigated.Store(false)
|
|
||||||
overlay.Show()
|
|
||||||
shouldOverlayDraw = true
|
|
||||||
// enter: enter behavior is a bit different from tab suggestions in code editor
|
|
||||||
// I want it to execute the command anyway and ignore the suggestions
|
|
||||||
// it means only tab to select suggestions, and enter to execute
|
|
||||||
// enter is not used to select suggestions
|
|
||||||
} else if b == 0x0d || b == 0x0a {
|
|
||||||
intercepted = true
|
|
||||||
logger.Debugf("Intercepted Enter key, navigated=%v", overlay.GetUserNavigated())
|
|
||||||
var cmdToSubmit string
|
|
||||||
if overlay.IsVisible() && overlay.GetUserNavigated() {
|
|
||||||
selected := overlay.GetCurrentCmd()
|
|
||||||
if selected != "" {
|
|
||||||
cmdToSubmit = selected
|
|
||||||
activeModeMu.RLock()
|
|
||||||
currentMode := activeMode
|
|
||||||
activeModeMu.RUnlock()
|
|
||||||
if currentMode == "spec" {
|
|
||||||
s := strings.TrimSpace(selected)
|
|
||||||
if strings.HasSuffix(s, "/") || strings.HasSuffix(s, "\\") {
|
|
||||||
selected = s
|
|
||||||
} else {
|
|
||||||
selected = s + " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writeStdout([]byte(overlay.ClearAndDisable()))
|
|
||||||
SetCurrentAISuggestion(nil)
|
|
||||||
renderMu.Lock()
|
|
||||||
if renderTimer != nil {
|
|
||||||
renderTimer.Stop()
|
|
||||||
renderTimer = nil
|
|
||||||
}
|
|
||||||
renderMu.Unlock()
|
|
||||||
|
|
||||||
if cmdToSubmit == "" {
|
|
||||||
bufferMu.Lock()
|
|
||||||
cmdToSubmit = naiveBuffer
|
|
||||||
bufferMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(cmdToSubmit) == "iris reload" {
|
|
||||||
if newCfg, err := config.Load(); err == nil {
|
|
||||||
config.Init(newCfg)
|
|
||||||
disableGhostText.Store(!newCfg.UI.GhostText)
|
|
||||||
}
|
|
||||||
// Clear the shell line with Ctrl+U, then echo message, then Enter
|
|
||||||
msg := "echo -e '\\033[32m✓ Iris configuration reloaded successfully.\\033[0m'\r"
|
|
||||||
_, _ = ptmx.Write(append([]byte{0x15}, []byte(msg)...))
|
|
||||||
bufferMu.Lock()
|
|
||||||
naiveBuffer = ""
|
|
||||||
cursorOffset = 0
|
|
||||||
bufferMu.Unlock()
|
|
||||||
disableGhostText.Store(false)
|
|
||||||
shouldOverlayDraw = false
|
|
||||||
userNavigated.Store(false)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
integration.RecordSessionCommand(cmdToSubmit)
|
|
||||||
bufferMu.Lock()
|
|
||||||
lastSubmittedCommand = strings.TrimSpace(cmdToSubmit)
|
|
||||||
naiveBuffer = ""
|
|
||||||
cursorOffset = 0
|
|
||||||
bufferMu.Unlock()
|
|
||||||
isCommandActive.Store(true)
|
|
||||||
_, _ = ptmx.Write([]byte{b})
|
|
||||||
disableGhostText.Store(false)
|
|
||||||
shouldOverlayDraw = false
|
|
||||||
userNavigated.Store(false)
|
|
||||||
continue
|
|
||||||
} else if b == 0x03 || b == 0x15 { // ctrl+c, ctrl+u
|
|
||||||
intercepted = true
|
intercepted = true
|
||||||
writeStdout([]byte(overlay.ClearAndDisable()))
|
writeStdout([]byte(overlay.ClearAndDisable()))
|
||||||
SetCurrentAISuggestion(nil)
|
SetCurrentAISuggestion(nil)
|
||||||
@@ -979,38 +1024,6 @@ func runWrapper() {
|
|||||||
shouldOverlayDraw = false
|
shouldOverlayDraw = false
|
||||||
userNavigated.Store(false)
|
userNavigated.Store(false)
|
||||||
continue
|
continue
|
||||||
} else if b == 0x09 { // tab: select suggestions
|
|
||||||
intercepted = true
|
|
||||||
logger.Debugf("Intercepted Tab key, visible=%v", overlay.IsVisible())
|
|
||||||
if !overlay.IsVisible() {
|
|
||||||
shouldOverlayDraw = true
|
|
||||||
} else {
|
|
||||||
selected := overlay.GetCurrentCmd()
|
|
||||||
writeStdout([]byte(overlay.ClearAndDisable()))
|
|
||||||
|
|
||||||
activeModeMu.RLock()
|
|
||||||
currentMode := activeMode
|
|
||||||
activeModeMu.RUnlock()
|
|
||||||
if currentMode == "spec" {
|
|
||||||
s := strings.TrimSpace(selected)
|
|
||||||
if strings.HasSuffix(s, "/") || strings.HasSuffix(s, "\\") {
|
|
||||||
selected = s
|
|
||||||
} else {
|
|
||||||
selected = s + " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferMu.Lock()
|
|
||||||
naiveBuffer = selected
|
|
||||||
cursorOffset = 0
|
|
||||||
bufferMu.Unlock()
|
|
||||||
|
|
||||||
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
|
|
||||||
overlay.ResetCursor()
|
|
||||||
shouldOverlayDraw = true
|
|
||||||
userNavigated.Store(false)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !intercepted {
|
if !intercepted {
|
||||||
|
|||||||
Reference in New Issue
Block a user