diff options
Diffstat (limited to 'popups.go')
| -rw-r--r-- | popups.go | 571 |
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) +} |
