From a0ff384b815f525bf15e6928e9a00b7019156e41 Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Fri, 27 Feb 2026 07:58:00 +0300 Subject: Enha: shellmode within inputfield --- helpfuncs.go | 20 +++++++++++---- main.go | 8 +++--- popups.go | 60 +++++++++++++++++++++++++++++++++++++++++++++ tui.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 153 insertions(+), 15 deletions(-) diff --git a/helpfuncs.go b/helpfuncs.go index d8bbd78..7b1cec9 100644 --- a/helpfuncs.go +++ b/helpfuncs.go @@ -426,12 +426,11 @@ func deepseekModelValidator() error { func toggleShellMode() { shellMode = !shellMode + setShellMode(shellMode) if shellMode { - // Update input placeholder to indicate shell mode - textArea.SetPlaceholder("SHELL MODE: Enter command and press to execute") + shellInput.SetLabel(fmt.Sprintf("[%s]$ ", cfg.FilePickerDir)) } else { - // Reset to normal mode - textArea.SetPlaceholder("input is multiline; press to start the next line;\npress to send the message. Alt+1 to exit shell mode") + textArea.SetPlaceholder("input is multiline; press to start the next line;\npress to send the message.") } updateStatusLine() } @@ -443,7 +442,11 @@ func updateFlexLayout() { } flex.Clear() flex.AddItem(textView, 0, 40, false) - flex.AddItem(textArea, 0, 10, false) + if shellMode { + flex.AddItem(shellInput, 0, 10, false) + } else { + flex.AddItem(textArea, 0, 10, false) + } if positionVisible { flex.AddItem(statusLineWidget, 0, 2, false) } @@ -451,6 +454,8 @@ func updateFlexLayout() { focused := app.GetFocus() if focused == textView { app.SetFocus(textView) + } else if shellMode { + app.SetFocus(shellInput) } else { app.SetFocus(textArea) } @@ -515,6 +520,11 @@ func executeCommandAndDisplay(cmdText string) { textView.ScrollToEnd() } colorText() + // Add command to history (avoid duplicates at the end) + if len(shellHistory) == 0 || shellHistory[len(shellHistory)-1] != cmdText { + shellHistory = append(shellHistory, cmdText) + } + shellHistoryPos = -1 } // parseCommand splits command string handling quotes properly diff --git a/main.go b/main.go index d90ff4a..cab86a2 100644 --- a/main.go +++ b/main.go @@ -13,9 +13,11 @@ var ( injectRole = true selectedIndex = int(-1) shellMode = false - thinkingCollapsed = false - statusLineTempl = "help (F12) | [%s:-:b]llm writes[-:-:-] (F6 to interrupt) | chat: [orange:-:b]%s[-:-:-] (F1) | [%s:-:b]tool use[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | [%s:-:b]skip LLM resp[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)" - focusSwitcher = map[tview.Primitive]tview.Primitive{} + shellHistory []string + shellHistoryPos int = -1 + thinkingCollapsed = false + statusLineTempl = "help (F12) | [%s:-:b]llm writes[-:-:-] (F6 to interrupt) | chat: [orange:-:b]%s[-:-:-] (F1) | [%s:-:b]tool use[-:-:-] (ctrl+k) | model: [%s:-:b]%s[-:-:-] (ctrl+l) | [%s:-:b]skip LLM resp[-:-:-] (F10)\nAPI: [orange:-:b]%s[-:-:-] (ctrl+v) | writing as: [orange:-:b]%s[-:-:-] (ctrl+q) | bot will write as [orange:-:b]%s[-:-:-] (ctrl+x)" + focusSwitcher = map[tview.Primitive]tview.Primitive{} ) func main() { diff --git a/popups.go b/popups.go index 8338b61..84b13c4 100644 --- a/popups.go +++ b/popups.go @@ -405,6 +405,66 @@ func showFileCompletionPopup(filter string) { app.SetFocus(widget) } +func showShellFileCompletionPopup(filter string) { + baseDir := cfg.FilePickerDir + if baseDir == "" { + baseDir = "." + } + complMatches := scanFiles(baseDir, filter) + if len(complMatches) == 0 { + return + } + if len(complMatches) == 1 { + currentText := shellInput.GetText() + atIdx := strings.LastIndex(currentText, "@") + if atIdx >= 0 { + before := currentText[:atIdx] + shellInput.SetText(before + complMatches[0]) + } + return + } + widget := tview.NewList().ShowSecondaryText(false). + SetSelectedBackgroundColor(tcell.ColorGray) + widget.SetTitle("file completion").SetBorder(true) + for _, m := range complMatches { + widget.AddItem(m, "", 0, nil) + } + widget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) { + currentText := shellInput.GetText() + atIdx := strings.LastIndex(currentText, "@") + if atIdx >= 0 { + before := currentText[:atIdx] + shellInput.SetText(before + mainText) + } + pages.RemovePage("shellFileCompletionPopup") + app.SetFocus(shellInput) + }) + widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyEscape { + pages.RemovePage("shellFileCompletionPopup") + app.SetFocus(shellInput) + return nil + } + if event.Key() == tcell.KeyRune && event.Rune() == 'x' { + pages.RemovePage("shellFileCompletionPopup") + app.SetFocus(shellInput) + return nil + } + return event + }) + modal := func(p tview.Primitive, width, height int) tview.Primitive { + return tview.NewFlex(). + AddItem(nil, 0, 1, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(nil, 0, 1, false). + AddItem(p, height, 1, true). + AddItem(nil, 0, 1, false), width, 1, true). + AddItem(nil, 0, 1, false) + } + pages.AddPage("shellFileCompletionPopup", modal(widget, 80, 20), true, true) + app.SetFocus(widget) +} + func updateWidgetColors(theme *tview.Theme) { bgColor := theme.PrimitiveBackgroundColor fgColor := theme.PrimaryTextColor diff --git a/tui.go b/tui.go index ddddd35..8c90600 100644 --- a/tui.go +++ b/tui.go @@ -34,6 +34,7 @@ var ( indexPickWindow *tview.InputField renameWindow *tview.InputField roleEditWindow *tview.InputField + shellInput *tview.InputField fullscreenMode bool positionVisible bool = true scrollToEndEnabled bool = true @@ -124,12 +125,75 @@ Press or 'x' to return ` ) +func setShellMode(enabled bool) { + shellMode = enabled + go func() { + app.QueueUpdateDraw(func() { + updateFlexLayout() + }) + }() +} + func init() { // Start background goroutine to update model color cache startModelColorUpdater() tview.Styles = colorschemes["default"] app = tview.NewApplication() pages = tview.NewPages() + shellInput = tview.NewInputField(). + SetLabel(fmt.Sprintf("[%s]$ ", cfg.FilePickerDir)). // dynamic prompt + SetFieldWidth(0). + SetDoneFunc(func(key tcell.Key) { + if key == tcell.KeyEnter { + cmd := shellInput.GetText() + if cmd != "" { + executeCommandAndDisplay(cmd) + } + shellInput.SetText("") + } + }) + // Copy your file completion logic to shellInput's InputCapture + shellInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if !shellMode { + return event + } + // Handle Up arrow for history previous + if event.Key() == tcell.KeyUp { + if len(shellHistory) > 0 { + if shellHistoryPos < len(shellHistory)-1 { + shellHistoryPos++ + shellInput.SetText(shellHistory[len(shellHistory)-1-shellHistoryPos]) + } + } + return nil + } + // Handle Down arrow for history next + if event.Key() == tcell.KeyDown { + if shellHistoryPos > 0 { + shellHistoryPos-- + shellInput.SetText(shellHistory[len(shellHistory)-1-shellHistoryPos]) + } else if shellHistoryPos == 0 { + shellHistoryPos = -1 + shellInput.SetText("") + } + return nil + } + // Reset history position when user types + if event.Key() == tcell.KeyRune { + shellHistoryPos = -1 + } + // Handle Tab key for @ file completion + if event.Key() == tcell.KeyTab { + currentText := shellInput.GetText() + atIndex := strings.LastIndex(currentText, "@") + if atIndex >= 0 { + filter := currentText[atIndex+1:] + showShellFileCompletionPopup(filter) + } + return nil + } + return event + }) textArea = tview.NewTextArea(). SetPlaceholder("input is multiline; press to start the next line;\npress to send the message.") textArea.SetBorder(true).SetTitle("input") @@ -948,14 +1012,16 @@ func init() { } // cannot send msg in editMode or botRespMode if event.Key() == tcell.KeyEscape && !editMode && !botRespMode { - msgText := textArea.GetText() - if shellMode && msgText != "" { - // In shell mode, execute command instead of sending to LLM - executeCommandAndDisplay(msgText) - textArea.SetText("", true) // Clear the input area + if shellMode { + cmdText := shellInput.GetText() + if cmdText != "" { + executeCommandAndDisplay(cmdText) + shellInput.SetText("") + } return nil - } else if !shellMode { - // Normal mode - send to LLM + } + msgText := textArea.GetText() + if msgText != "" { nl := "\n\n" // keep empty lines between messages prevText := textView.GetText(true) persona := cfg.UserRole -- cgit v1.2.3