summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrail Finder <wohilas@gmail.com>2026-02-27 07:58:00 +0300
committerGrail Finder <wohilas@gmail.com>2026-02-27 07:58:00 +0300
commita0ff384b815f525bf15e6928e9a00b7019156e41 (patch)
treed13dbc7a5ca79d3d41cf940cc78e78925d13b1eb
parent09b5e0d08f58ff182f30b4d8c4b5a601ff14293d (diff)
Enha: shellmode within inputfield
-rw-r--r--helpfuncs.go20
-rw-r--r--main.go8
-rw-r--r--popups.go60
-rw-r--r--tui.go80
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 <Esc> to execute")
+ shellInput.SetLabel(fmt.Sprintf("[%s]$ ", cfg.FilePickerDir))
} else {
- // Reset to normal mode
- textArea.SetPlaceholder("input is multiline; press <Enter> to start the next line;\npress <Esc> to send the message. Alt+1 to exit shell mode")
+ textArea.SetPlaceholder("input is multiline; press <Enter> to start the next line;\npress <Esc> 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 <Enter> 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 <Enter> to start the next line;\npress <Esc> 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