diff options
| author | Grail Finder <wohilas@gmail.com> | 2026-02-19 08:13:41 +0300 |
|---|---|---|
| committer | Grail Finder <wohilas@gmail.com> | 2026-02-19 08:13:41 +0300 |
| commit | b861b92e5d63dcfc3b06030006668fac1247e5a8 (patch) | |
| tree | 4064a0f594befad25a98f9c52d73d33ef2580988 /tui.go | |
| parent | 17f0afac8021c4e7eb3a4da38cf5ec189c7b852f (diff) | |
Chore: move helpers to helpfuncs
Diffstat (limited to 'tui.go')
| -rw-r--r-- | tui.go | 396 |
1 files changed, 1 insertions, 395 deletions
@@ -7,7 +7,6 @@ import ( _ "image/jpeg" _ "image/png" "os" - "os/exec" "path" "strconv" "strings" @@ -179,396 +178,6 @@ Press <Enter> or 'x' to return } ) -func toggleShellMode() { - shellMode = !shellMode - if shellMode { - // Update input placeholder to indicate shell mode - textArea.SetPlaceholder("SHELL MODE: Enter command and press <Esc> to execute") - } 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") - } - updateStatusLine() -} - -func updateFlexLayout() { - if fullscreenMode { - // flex already contains only focused widget; do nothing - return - } - flex.Clear() - flex.AddItem(textView, 0, 40, false) - flex.AddItem(textArea, 0, 10, false) - if positionVisible { - flex.AddItem(statusLineWidget, 0, 2, false) - } - // Keep focus on currently focused widget - focused := app.GetFocus() - if focused == textView { - app.SetFocus(textView) - } else { - app.SetFocus(textArea) - } -} - -func executeCommandAndDisplay(cmdText string) { - // Parse the command (split by spaces, but handle quoted arguments) - cmdParts := parseCommand(cmdText) - if len(cmdParts) == 0 { - fmt.Fprintf(textView, "\n[red]Error: No command provided[-:-:-]\n") - if scrollToEndEnabled { - textView.ScrollToEnd() - } - colorText() - return - } - command := cmdParts[0] - args := []string{} - if len(cmdParts) > 1 { - args = cmdParts[1:] - } - // Create the command execution - cmd := exec.Command(command, args...) - // Execute the command and get output - output, err := cmd.CombinedOutput() - // Add the command being executed to the chat - fmt.Fprintf(textView, "\n[yellow]$ %s[-:-:-]\n", cmdText) - var outputContent string - if err != nil { - // Include both output and error - errorMsg := "Error: " + err.Error() - fmt.Fprintf(textView, "[red]%s[-:-:-]\n", errorMsg) - if len(output) > 0 { - outputStr := string(output) - fmt.Fprintf(textView, "[red]%s[-:-:-]\n", outputStr) - outputContent = errorMsg + "\n" + outputStr - } else { - outputContent = errorMsg - } - } else { - // Only output if successful - if len(output) > 0 { - outputStr := string(output) - fmt.Fprintf(textView, "[green]%s[-:-:-]\n", outputStr) - outputContent = outputStr - } else { - successMsg := "Command executed successfully (no output)" - fmt.Fprintf(textView, "[green]%s[-:-:-]\n", successMsg) - outputContent = successMsg - } - } - // Combine command and output in a single message for chat history - combinedContent := "$ " + cmdText + "\n\n" + outputContent - combinedMsg := models.RoleMsg{ - Role: cfg.ToolRole, - Content: combinedContent, - } - chatBody.Messages = append(chatBody.Messages, combinedMsg) - // Scroll to end and update colors - if scrollToEndEnabled { - textView.ScrollToEnd() - } - colorText() -} - -// parseCommand splits command string handling quotes properly -func parseCommand(cmd string) []string { - var args []string - var current string - var inQuotes bool - var quoteChar rune - for _, r := range cmd { - switch r { - case '"', '\'': - if inQuotes { - if r == quoteChar { - inQuotes = false - } else { - current += string(r) - } - } else { - inQuotes = true - quoteChar = r - } - case ' ', '\t': - if inQuotes { - current += string(r) - } else if current != "" { - args = append(args, current) - current = "" - } - default: - current += string(r) - } - } - if current != "" { - args = append(args, current) - } - return args -} - -// Global variables for search state -var searchResults []int -var searchResultLengths []int // To store the length of each match in the formatted string -var searchIndex int -var searchText string -var originalTextForSearch string - -// performSearch searches for the given term in the textView content and highlights matches -func performSearch(term string) { - searchText = term - if searchText == "" { - searchResults = nil - searchResultLengths = nil - originalTextForSearch = "" - // Re-render text without highlights - textView.SetText(chatToText(chatBody.Messages, cfg.ShowSys)) - colorText() - return - } - // Get formatted text and search directly in it to avoid mapping issues - formattedText := textView.GetText(true) - originalTextForSearch = formattedText - searchTermLower := strings.ToLower(searchText) - formattedTextLower := strings.ToLower(formattedText) - // Find all occurrences of the search term in the formatted text directly - formattedSearchResults := []int{} - searchStart := 0 - for { - pos := strings.Index(formattedTextLower[searchStart:], searchTermLower) - if pos == -1 { - break - } - absolutePos := searchStart + pos - formattedSearchResults = append(formattedSearchResults, absolutePos) - searchStart = absolutePos + len(searchText) - } - if len(formattedSearchResults) == 0 { - // No matches found - searchResults = nil - searchResultLengths = nil - notification := "Pattern not found: " + term - if err := notifyUser("search", notification); err != nil { - logger.Error("failed to send notification", "error", err) - } - return - } - // Store the formatted text positions and lengths for accurate highlighting - searchResults = formattedSearchResults - // Create lengths array - all matches have the same length as the search term - searchResultLengths = make([]int, len(formattedSearchResults)) - for i := range searchResultLengths { - searchResultLengths[i] = len(searchText) - } - searchIndex = 0 - highlightCurrentMatch() -} - -// highlightCurrentMatch highlights the current search match and scrolls to it -func highlightCurrentMatch() { - if len(searchResults) == 0 || searchIndex >= len(searchResults) { - return - } - // Get the stored formatted text - formattedText := originalTextForSearch - // For tview to properly support highlighting and scrolling, we need to work with its region system - // Instead of just applying highlights, we need to add region tags to the text - highlightedText := addRegionTags(formattedText, searchResults, searchResultLengths, searchIndex, searchText) - // Update the text view with the text that includes region tags - textView.SetText(highlightedText) - // Highlight the current region and scroll to it - // Need to identify which position in the results array corresponds to the current match - // The region ID will be search_<position>_<index> - currentRegion := fmt.Sprintf("search_%d_%d", searchResults[searchIndex], searchIndex) - textView.Highlight(currentRegion).ScrollToHighlight() - // Send notification about which match we're at - notification := fmt.Sprintf("Match %d of %d", searchIndex+1, len(searchResults)) - if err := notifyUser("search", notification); err != nil { - logger.Error("failed to send notification", "error", err) - } -} - -// showSearchBar shows the search input field as an overlay -func showSearchBar() { - // Create a temporary flex to combine search and main content - updatedFlex := tview.NewFlex().SetDirection(tview.FlexRow). - AddItem(searchField, 3, 0, true). // Search field at top - AddItem(flex, 0, 1, false) // Main flex layout below - - // Add the search overlay as a page - pages.AddPage(searchPageName, updatedFlex, true, true) - app.SetFocus(searchField) -} - -// hideSearchBar hides the search input field -func hideSearchBar() { - pages.RemovePage(searchPageName) - // Return focus to the text view - app.SetFocus(textView) - // Clear the search field - searchField.SetText("") -} - -// Global variables for index overlay functionality -var indexPageName = "indexOverlay" - -// showIndexBar shows the index input field as an overlay at the top -func showIndexBar() { - // Create a temporary flex to combine index input and main content - updatedFlex := tview.NewFlex().SetDirection(tview.FlexRow). - AddItem(indexPickWindow, 3, 0, true). // Index field at top - AddItem(flex, 0, 1, false) // Main flex layout below - - // Add the index overlay as a page - pages.AddPage(indexPageName, updatedFlex, true, true) - app.SetFocus(indexPickWindow) -} - -// hideIndexBar hides the index input field -func hideIndexBar() { - pages.RemovePage(indexPageName) - // Return focus to the text view - app.SetFocus(textView) - // Clear the index field - indexPickWindow.SetText("") -} - -// addRegionTags adds region tags to search matches in the text for tview highlighting -func addRegionTags(text string, positions []int, lengths []int, currentIdx int, searchTerm string) string { - if len(positions) == 0 { - return text - } - var result strings.Builder - lastEnd := 0 - for i, pos := range positions { - endPos := pos + lengths[i] - // Add text before this match - if pos > lastEnd { - result.WriteString(text[lastEnd:pos]) - } - // The matched text, which may contain its own formatting tags - actualText := text[pos:endPos] - // Add region tag and highlighting for this match - // Use a unique region id that includes the match index to avoid conflicts - regionId := fmt.Sprintf("search_%d_%d", pos, i) // position + index to ensure uniqueness - var highlightStart, highlightEnd string - if i == currentIdx { - // Current match - use different highlighting - highlightStart = fmt.Sprintf(`["%s"][yellow:blue:b]`, regionId) // Current match with region and special highlight - highlightEnd = `[-:-:-][""]` // Reset formatting and close region - } else { - // Other matches - use regular highlighting - highlightStart = fmt.Sprintf(`["%s"][gold:red:u]`, regionId) // Other matches with region and highlight - highlightEnd = `[-:-:-][""]` // Reset formatting and close region - } - result.WriteString(highlightStart) - result.WriteString(actualText) - result.WriteString(highlightEnd) - lastEnd = endPos - } - // Add the rest of the text after the last processed match - if lastEnd < len(text) { - result.WriteString(text[lastEnd:]) - } - return result.String() -} - -// searchNext finds the next occurrence of the search term -func searchNext() { - if len(searchResults) == 0 { - if err := notifyUser("search", "No search results to navigate"); err != nil { - logger.Error("failed to send notification", "error", err) - } - return - } - searchIndex = (searchIndex + 1) % len(searchResults) - highlightCurrentMatch() -} - -// searchPrev finds the previous occurrence of the search term -func searchPrev() { - if len(searchResults) == 0 { - if err := notifyUser("search", "No search results to navigate"); err != nil { - logger.Error("failed to send notification", "error", err) - } - return - } - if searchIndex == 0 { - searchIndex = len(searchResults) - 1 - } else { - searchIndex-- - } - highlightCurrentMatch() -} - -func scanFiles(dir, filter string) []string { - var files []string - entries, err := os.ReadDir(dir) - if err != nil { - return files - } - for _, entry := range entries { - name := entry.Name() - if strings.HasPrefix(name, ".") { - continue - } - if filter == "" || strings.HasPrefix(strings.ToLower(name), strings.ToLower(filter)) { - if entry.IsDir() { - files = append(files, name+"/") - } else { - files = append(files, name) - } - } - } - return files -} - -func showFileCompletion(filter string) { - baseDir := cfg.CodingDir - if baseDir == "" { - baseDir = "." - } - complMatches = scanFiles(baseDir, filter) - if len(complMatches) == 0 { - hideCompletion() - return - } - if len(complMatches) > 10 { - complMatches = complMatches[:10] - } - complPopup.Clear() - for _, f := range complMatches { - complPopup.AddItem(f, "", 0, nil) - } - complIndex = 0 - complPopup.SetCurrentItem(0) - complActive = true - pages.AddPage("complPopup", complPopup, true, false) - app.SetFocus(complPopup) - app.Draw() -} - -func insertCompletion() { - if complIndex >= 0 && complIndex < len(complMatches) { - match := complMatches[complIndex] - currentText := textArea.GetText() - atIdx := strings.LastIndex(currentText, "@") - if atIdx >= 0 { - before := currentText[:atIdx] - textArea.SetText(before+match, true) - } - } - hideCompletion() -} - -func hideCompletion() { - complActive = false - complMatches = nil - pages.RemovePage("complPopup") - app.SetFocus(textArea) - app.Draw() -} - func init() { tview.Styles = colorschemes["default"] app = tview.NewApplication() @@ -584,7 +193,7 @@ func init() { // Add input capture for @ completion textArea.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyRune && event.Rune() == '@' { + if shellMode && event.Key() == tcell.KeyRune && event.Rune() == '@' { complAtPos = len(textArea.GetText()) showFileCompletion("") return event @@ -625,7 +234,6 @@ func init() { } return event }) - textView = tview.NewTextView(). SetDynamicColors(true). SetRegions(true). @@ -739,7 +347,6 @@ func init() { // colorText() // updateStatusLine() }) - roleEditWindow = tview.NewInputField(). SetLabel("Enter new role: "). SetPlaceholder("e.g., user, assistant, system, tool"). @@ -1433,7 +1040,6 @@ func init() { app.SetFocus(focusSwitcher[currentF]) return nil } - if isASCII(string(event.Rune())) && !botRespMode { return event } |
