summaryrefslogtreecommitdiff
path: root/popups.go
diff options
context:
space:
mode:
Diffstat (limited to 'popups.go')
-rw-r--r--popups.go571
1 files changed, 571 insertions, 0 deletions
diff --git a/popups.go b/popups.go
new file mode 100644
index 0000000..38f42cd
--- /dev/null
+++ b/popups.go
@@ -0,0 +1,571 @@
+package main
+
+import (
+ "gf-lt/models"
+ "slices"
+ "strings"
+
+ "github.com/gdamore/tcell/v2"
+ "github.com/rivo/tview"
+)
+
+// showModelSelectionPopup creates a modal popup to select a model
+func showModelSelectionPopup() {
+ // Helper function to get model list for a given API
+ getModelListForAPI := func(api string) []string {
+ if strings.Contains(api, "api.deepseek.com/") {
+ return []string{"deepseek-chat", "deepseek-reasoner"}
+ } else if strings.Contains(api, "openrouter.ai") {
+ return ORFreeModels
+ }
+ // Assume local llama.cpp - fetch with load status
+ models, err := fetchLCPModelsWithLoadStatus()
+ if err != nil {
+ logger.Error("failed to fetch models with load status", "error", err)
+ return LocalModels
+ }
+ return models
+ }
+ // Get the current model list based on the API
+ modelList := getModelListForAPI(cfg.CurrentAPI)
+ // Check for empty options list
+ if len(modelList) == 0 {
+ logger.Warn("empty model list for", "api", cfg.CurrentAPI, "localModelsLen", len(LocalModels), "orModelsLen", len(ORFreeModels))
+ var message string
+ switch {
+ case strings.Contains(cfg.CurrentAPI, "openrouter.ai"):
+ message = "No OpenRouter models available. Check token and connection."
+ case strings.Contains(cfg.CurrentAPI, "api.deepseek.com"):
+ message = "DeepSeek models should be available. Please report bug."
+ default:
+ message = "No llama.cpp models loaded. Ensure llama.cpp server is running with models."
+ }
+ showToast("Empty list", message)
+ return
+ }
+ // Create a list primitive
+ modelListWidget := tview.NewList().ShowSecondaryText(false).
+ SetSelectedBackgroundColor(tcell.ColorGray)
+ modelListWidget.SetTitle("Select Model").SetBorder(true)
+ // Find the current model index to set as selected
+ currentModelIndex := -1
+ for i, model := range modelList {
+ if strings.TrimPrefix(model, models.LoadedMark) == chatBody.Model {
+ currentModelIndex = i
+ }
+ modelListWidget.AddItem(model, "", 0, nil)
+ }
+ // Set the current selection if found
+ if currentModelIndex != -1 {
+ modelListWidget.SetCurrentItem(currentModelIndex)
+ }
+ modelListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
+ modelName := strings.TrimPrefix(mainText, models.LoadedMark)
+ chatBody.Model = modelName
+ cfg.CurrentModel = chatBody.Model
+ pages.RemovePage("modelSelectionPopup")
+ app.SetFocus(textArea)
+ updateCachedModelColor()
+ updateStatusLine()
+ })
+ modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ if event.Key() == tcell.KeyEscape {
+ pages.RemovePage("modelSelectionPopup")
+ app.SetFocus(textArea)
+ return nil
+ }
+ if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
+ pages.RemovePage("modelSelectionPopup")
+ app.SetFocus(textArea)
+ 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)
+ }
+ // Add modal page and make it visible
+ pages.AddPage("modelSelectionPopup", modal(modelListWidget, 80, 20), true, true)
+ app.SetFocus(modelListWidget)
+}
+
+// showAPILinkSelectionPopup creates a modal popup to select an API link
+func showAPILinkSelectionPopup() {
+ // Prepare API links dropdown - ensure current API is in the list, avoid duplicates
+ apiLinks := make([]string, 0, len(cfg.ApiLinks)+1)
+ // Add current API first if it's not already in ApiLinks
+ foundCurrentAPI := false
+ for _, api := range cfg.ApiLinks {
+ if api == cfg.CurrentAPI {
+ foundCurrentAPI = true
+ }
+ apiLinks = append(apiLinks, api)
+ }
+ // If current API is not in the list, add it at the beginning
+ if !foundCurrentAPI {
+ apiLinks = make([]string, 0, len(cfg.ApiLinks)+1)
+ apiLinks = append(apiLinks, cfg.CurrentAPI)
+ apiLinks = append(apiLinks, cfg.ApiLinks...)
+ }
+ // Check for empty options list
+ if len(apiLinks) == 0 {
+ logger.Warn("no API links available for selection")
+ message := "No API links available. Please configure API links in your config file."
+ showToast("Empty list", message)
+ return
+ }
+ // Create a list primitive
+ apiListWidget := tview.NewList().ShowSecondaryText(false).
+ SetSelectedBackgroundColor(tcell.ColorGray)
+ apiListWidget.SetTitle("Select API Link").SetBorder(true)
+ // Find the current API index to set as selected
+ currentAPIIndex := -1
+ for i, api := range apiLinks {
+ if api == cfg.CurrentAPI {
+ currentAPIIndex = i
+ }
+ apiListWidget.AddItem(api, "", 0, nil)
+ }
+ // Set the current selection if found
+ if currentAPIIndex != -1 {
+ apiListWidget.SetCurrentItem(currentAPIIndex)
+ }
+ apiListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
+ // Update the API in config
+ cfg.CurrentAPI = mainText
+ // updateToolCapabilities()
+ // Update model list based on new API
+ // Helper function to get model list for a given API (same as in props_table.go)
+ getModelListForAPI := func(api string) []string {
+ if strings.Contains(api, "api.deepseek.com/") {
+ return []string{"deepseek-chat", "deepseek-reasoner"}
+ } else if strings.Contains(api, "openrouter.ai") {
+ return ORFreeModels
+ }
+ // Assume local llama.cpp
+ refreshLocalModelsIfEmpty()
+ localModelsMu.RLock()
+ defer localModelsMu.RUnlock()
+ return LocalModels
+ }
+ newModelList := getModelListForAPI(cfg.CurrentAPI)
+ // Ensure chatBody.Model is in the new list; if not, set to first available model
+ if len(newModelList) > 0 && !slices.Contains(newModelList, chatBody.Model) {
+ chatBody.Model = strings.TrimPrefix(newModelList[0], models.LoadedMark)
+ cfg.CurrentModel = chatBody.Model
+ updateToolCapabilities()
+ }
+ pages.RemovePage("apiLinkSelectionPopup")
+ app.SetFocus(textArea)
+ choseChunkParser()
+ updateCachedModelColor()
+ updateStatusLine()
+ })
+ apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ if event.Key() == tcell.KeyEscape {
+ pages.RemovePage("apiLinkSelectionPopup")
+ app.SetFocus(textArea)
+ return nil
+ }
+ if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
+ pages.RemovePage("apiLinkSelectionPopup")
+ app.SetFocus(textArea)
+ 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)
+ }
+ // Add modal page and make it visible
+ pages.AddPage("apiLinkSelectionPopup", modal(apiListWidget, 80, 20), true, true)
+ app.SetFocus(apiListWidget)
+}
+
+// showUserRoleSelectionPopup creates a modal popup to select a user role
+func showUserRoleSelectionPopup() {
+ // Get the list of available roles
+ roles := listRolesWithUser()
+ // Check for empty options list
+ if len(roles) == 0 {
+ logger.Warn("no roles available for selection")
+ message := "No roles available for selection."
+ showToast("Empty list", message)
+ return
+ }
+ // Create a list primitive
+ roleListWidget := tview.NewList().ShowSecondaryText(false).
+ SetSelectedBackgroundColor(tcell.ColorGray)
+ roleListWidget.SetTitle("Select User Role").SetBorder(true)
+ // Find the current role index to set as selected
+ currentRole := cfg.UserRole
+ if cfg.WriteNextMsgAs != "" {
+ currentRole = cfg.WriteNextMsgAs
+ }
+ currentRoleIndex := -1
+ for i, role := range roles {
+ if strings.EqualFold(role, currentRole) {
+ currentRoleIndex = i
+ }
+ roleListWidget.AddItem(role, "", 0, nil)
+ }
+ // Set the current selection if found
+ if currentRoleIndex != -1 {
+ roleListWidget.SetCurrentItem(currentRoleIndex)
+ }
+ roleListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
+ // Update the user role in config
+ cfg.WriteNextMsgAs = mainText
+ // role got switch, update textview with character specific context for user
+ filtered := filterMessagesForCharacter(chatBody.Messages, mainText)
+ textView.SetText(chatToText(filtered, cfg.ShowSys))
+ // Remove the popup page
+ pages.RemovePage("userRoleSelectionPopup")
+ app.SetFocus(textArea)
+ // Update the status line to reflect the change
+ updateStatusLine()
+ colorText()
+ })
+ roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ if event.Key() == tcell.KeyEscape {
+ pages.RemovePage("userRoleSelectionPopup")
+ app.SetFocus(textArea)
+ return nil
+ }
+ if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
+ pages.RemovePage("userRoleSelectionPopup")
+ app.SetFocus(textArea)
+ 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)
+ }
+ // Add modal page and make it visible
+ pages.AddPage("userRoleSelectionPopup", modal(roleListWidget, 80, 20), true, true)
+ app.SetFocus(roleListWidget)
+}
+
+// showBotRoleSelectionPopup creates a modal popup to select a bot role
+func showBotRoleSelectionPopup() {
+ // Get the list of available roles
+ roles := listChatRoles()
+ if len(roles) == 0 {
+ logger.Warn("empty roles in chat")
+ }
+ if !strInSlice(cfg.AssistantRole, roles) {
+ roles = append(roles, cfg.AssistantRole)
+ }
+ // Check for empty options list
+ if len(roles) == 0 {
+ logger.Warn("no roles available for selection")
+ message := "No roles available for selection."
+ showToast("Empty list", message)
+ return
+ }
+ // Create a list primitive
+ roleListWidget := tview.NewList().ShowSecondaryText(false).
+ SetSelectedBackgroundColor(tcell.ColorGray)
+ roleListWidget.SetTitle("Select Bot Role").SetBorder(true)
+ // Find the current role index to set as selected
+ currentRole := cfg.AssistantRole
+ if cfg.WriteNextMsgAsCompletionAgent != "" {
+ currentRole = cfg.WriteNextMsgAsCompletionAgent
+ }
+ currentRoleIndex := -1
+ for i, role := range roles {
+ if strings.EqualFold(role, currentRole) {
+ currentRoleIndex = i
+ }
+ roleListWidget.AddItem(role, "", 0, nil)
+ }
+ // Set the current selection if found
+ if currentRoleIndex != -1 {
+ roleListWidget.SetCurrentItem(currentRoleIndex)
+ }
+ roleListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
+ // Update the bot role in config
+ cfg.WriteNextMsgAsCompletionAgent = mainText
+ // Remove the popup page
+ pages.RemovePage("botRoleSelectionPopup")
+ app.SetFocus(textArea)
+ // Update the status line to reflect the change
+ updateStatusLine()
+ })
+ roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ if event.Key() == tcell.KeyEscape {
+ pages.RemovePage("botRoleSelectionPopup")
+ app.SetFocus(textArea)
+ return nil
+ }
+ if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
+ pages.RemovePage("botRoleSelectionPopup")
+ app.SetFocus(textArea)
+ 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)
+ }
+ // Add modal page and make it visible
+ pages.AddPage("botRoleSelectionPopup", modal(roleListWidget, 80, 20), true, true)
+ app.SetFocus(roleListWidget)
+}
+
+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 showTextAreaFileCompletionPopup(filter string) {
+ baseDir := cfg.FilePickerDir
+ if baseDir == "" {
+ baseDir = "."
+ }
+ complMatches := scanFiles(baseDir, filter)
+ if len(complMatches) == 0 {
+ return
+ }
+ if len(complMatches) == 1 {
+ currentText := textArea.GetText()
+ atIdx := strings.LastIndex(currentText, "@")
+ if atIdx >= 0 {
+ before := currentText[:atIdx]
+ textArea.SetText(before+complMatches[0], true)
+ }
+ 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 := textArea.GetText()
+ atIdx := strings.LastIndex(currentText, "@")
+ if atIdx >= 0 {
+ before := currentText[:atIdx]
+ textArea.SetText(before+mainText, true)
+ }
+ pages.RemovePage("textAreaFileCompletionPopup")
+ app.SetFocus(textArea)
+ })
+ widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ if event.Key() == tcell.KeyEscape {
+ pages.RemovePage("textAreaFileCompletionPopup")
+ app.SetFocus(textArea)
+ return nil
+ }
+ if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
+ pages.RemovePage("textAreaFileCompletionPopup")
+ app.SetFocus(textArea)
+ 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("textAreaFileCompletionPopup", modal(widget, 80, 20), true, true)
+ app.SetFocus(widget)
+}
+
+func updateWidgetColors(theme *tview.Theme) {
+ bgColor := theme.PrimitiveBackgroundColor
+ fgColor := theme.PrimaryTextColor
+ borderColor := theme.BorderColor
+ titleColor := theme.TitleColor
+ textView.SetBackgroundColor(bgColor)
+ textView.SetTextColor(fgColor)
+ textView.SetBorderColor(borderColor)
+ textView.SetTitleColor(titleColor)
+ textArea.SetBackgroundColor(bgColor)
+ textArea.SetBorderColor(borderColor)
+ textArea.SetTitleColor(titleColor)
+ textArea.SetTextStyle(tcell.StyleDefault.Background(bgColor).Foreground(fgColor))
+ textArea.SetPlaceholderStyle(tcell.StyleDefault.Background(bgColor).Foreground(fgColor))
+ textArea.SetText(textArea.GetText(), true)
+ editArea.SetBackgroundColor(bgColor)
+ editArea.SetBorderColor(borderColor)
+ editArea.SetTitleColor(titleColor)
+ editArea.SetTextStyle(tcell.StyleDefault.Background(bgColor).Foreground(fgColor))
+ editArea.SetPlaceholderStyle(tcell.StyleDefault.Background(bgColor).Foreground(fgColor))
+ editArea.SetText(editArea.GetText(), true)
+ statusLineWidget.SetBackgroundColor(bgColor)
+ statusLineWidget.SetTextColor(fgColor)
+ statusLineWidget.SetBorderColor(borderColor)
+ statusLineWidget.SetTitleColor(titleColor)
+ helpView.SetBackgroundColor(bgColor)
+ helpView.SetTextColor(fgColor)
+ helpView.SetBorderColor(borderColor)
+ helpView.SetTitleColor(titleColor)
+ searchField.SetBackgroundColor(bgColor)
+ searchField.SetBorderColor(borderColor)
+ searchField.SetTitleColor(titleColor)
+}
+
+// showColorschemeSelectionPopup creates a modal popup to select a colorscheme
+func showColorschemeSelectionPopup() {
+ // Get the list of available colorschemes
+ schemeNames := make([]string, 0, len(colorschemes))
+ for name := range colorschemes {
+ schemeNames = append(schemeNames, name)
+ }
+ slices.Sort(schemeNames)
+ // Check for empty options list
+ if len(schemeNames) == 0 {
+ logger.Warn("no colorschemes available for selection")
+ message := "No colorschemes available."
+ showToast("Empty list", message)
+ return
+ }
+ // Create a list primitive
+ schemeListWidget := tview.NewList().ShowSecondaryText(false).
+ SetSelectedBackgroundColor(tcell.ColorGray)
+ schemeListWidget.SetTitle("Select Colorscheme").SetBorder(true)
+ currentScheme := "default"
+ for name := range colorschemes {
+ if tview.Styles == colorschemes[name] {
+ currentScheme = name
+ break
+ }
+ }
+ currentSchemeIndex := -1
+ for i, scheme := range schemeNames {
+ if scheme == currentScheme {
+ currentSchemeIndex = i
+ }
+ schemeListWidget.AddItem(scheme, "", 0, nil)
+ }
+ // Set the current selection if found
+ if currentSchemeIndex != -1 {
+ schemeListWidget.SetCurrentItem(currentSchemeIndex)
+ }
+ schemeListWidget.SetSelectedFunc(func(index int, mainText string, secondaryText string, shortcut rune) {
+ // Update the colorscheme
+ if theme, ok := colorschemes[mainText]; ok {
+ tview.Styles = theme
+ go func() {
+ app.QueueUpdateDraw(func() {
+ updateWidgetColors(&theme)
+ })
+ }()
+ }
+ // Remove the popup page
+ pages.RemovePage("colorschemeSelectionPopup")
+ app.SetFocus(textArea)
+ })
+ schemeListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ if event.Key() == tcell.KeyEscape {
+ pages.RemovePage("colorschemeSelectionPopup")
+ app.SetFocus(textArea)
+ return nil
+ }
+ if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
+ pages.RemovePage("colorschemeSelectionPopup")
+ app.SetFocus(textArea)
+ 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)
+ }
+ // Add modal page and make it visible
+ pages.AddPage("colorschemeSelectionPopup", modal(schemeListWidget, 40, len(schemeNames)+2), true, true)
+ app.SetFocus(schemeListWidget)
+}