diff --git a/internal/config/config.go b/internal/config/config.go index 432ba6e086f..195fd45ce33 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,7 +91,8 @@ type Config struct { DebugLSP bool `json:"debugLSP,omitempty"` ContextPaths []string `json:"contextPaths,omitempty"` TUI TUIConfig `json:"tui"` - Shell ShellConfig `json:"shell,omitempty"` + Shell ShellConfig `json:"shell"` + KeyMaps *KeyMapConfig `json:"keyMaps,omitempty"` } // Application constants @@ -155,6 +156,15 @@ func Load(workingDir string, debug bool, lvl *slog.LevelVar) (*Config, error) { return cfg, fmt.Errorf("failed to unmarshal config: %w", err) } + // Set default keymaps or merge with user-provided keymaps + if cfg.KeyMaps == nil { + cfg.KeyMaps = DefaultKeyMapConfig() + } else { + // Merge user-provided keymaps with default keymaps + defaultKeyMaps := DefaultKeyMapConfig() + mergeKeyMaps(cfg.KeyMaps, defaultKeyMaps) + } + applyDefaultValues() defaultLevel := slog.LevelInfo diff --git a/internal/config/keymaps.go b/internal/config/keymaps.go new file mode 100644 index 00000000000..f022fdf042a --- /dev/null +++ b/internal/config/keymaps.go @@ -0,0 +1,206 @@ +package config + +import ( + "github.com/charmbracelet/bubbles/key" +) + +// KeyMapConfig defines the configuration for keyboard shortcuts +type KeyMapConfig struct { + // Chat keymaps + NewSession []string `json:"newSession,omitempty"` + Cancel []string `json:"cancel,omitempty"` + ToggleTools []string `json:"toggleTools,omitempty"` + ShowCompletionDialog []string `json:"showCompletionDialog,omitempty"` + + // Global keymaps + ViewLogs []string `json:"viewLogs,omitempty"` + Quit []string `json:"quit,omitempty"` + Help []string `json:"help,omitempty"` + SwitchSession []string `json:"switchSession,omitempty"` + Commands []string `json:"commands,omitempty"` + FilePicker []string `json:"filePicker,omitempty"` + Models []string `json:"models,omitempty"` + Theme []string `json:"theme,omitempty"` + Tools []string `json:"tools,omitempty"` +} + +// DefaultKeyMapConfig returns the default keyboard shortcut configuration +func DefaultKeyMapConfig() *KeyMapConfig { + return &KeyMapConfig{ + // Chat keymaps + NewSession: []string{"ctrl+n"}, + Cancel: []string{"esc"}, + ToggleTools: []string{"ctrl+h"}, + ShowCompletionDialog: []string{"/"}, + + // Global keymaps + ViewLogs: []string{"ctrl+l"}, + Quit: []string{"ctrl+c"}, + Help: []string{"ctrl+_"}, + SwitchSession: []string{"ctrl+s"}, + Commands: []string{"ctrl+k"}, + FilePicker: []string{"ctrl+f"}, + Models: []string{"ctrl+o"}, + Theme: []string{"ctrl+t"}, + Tools: []string{"f9"}, + } +} + +func (c *Config) GetAllKeyBinding() []key.Binding { + chatKeyMap := c.GetChatKeyMap() + globalKeyMap := c.GetGlobalKeyMap() + + return []key.Binding{ + chatKeyMap.NewSession, + chatKeyMap.Cancel, + chatKeyMap.ToggleTools, + chatKeyMap.ShowCompletionDialog, + globalKeyMap.ViewLogs, + globalKeyMap.Quit, + globalKeyMap.Help, + globalKeyMap.SwitchSession, + globalKeyMap.Commands, + globalKeyMap.FilePicker, + globalKeyMap.Models, + globalKeyMap.Theme, + globalKeyMap.Tools, + } +} + +// GetChatKeyMap returns a ChatKeyMap with bindings from config +func (c *Config) GetChatKeyMap() ChatKeyMap { + keys := c.KeyMaps + + out := ChatKeyMap{ + NewSession: key.NewBinding( + key.WithKeys(keys.NewSession...), + key.WithHelp(keys.NewSession[0], "new session"), + ), + Cancel: key.NewBinding( + key.WithKeys(keys.Cancel...), + key.WithHelp(keys.Cancel[0], "cancel"), + ), + ToggleTools: key.NewBinding( + key.WithKeys(keys.ToggleTools...), + key.WithHelp(keys.ToggleTools[0], "toggle tools"), + ), + ShowCompletionDialog: key.NewBinding( + key.WithKeys(keys.ShowCompletionDialog...), + key.WithHelp(keys.ShowCompletionDialog[0], "complete"), + ), + } + + return out +} + +// ChatKeyMap defines key bindings for the chat page +type ChatKeyMap struct { + NewSession key.Binding + Cancel key.Binding + ToggleTools key.Binding + ShowCompletionDialog key.Binding +} + +// GlobalKeyMap defines key bindings for global application functions +type GlobalKeyMap struct { + ViewLogs key.Binding + Quit key.Binding + Help key.Binding + SwitchSession key.Binding + Commands key.Binding + FilePicker key.Binding + Models key.Binding + Theme key.Binding + Tools key.Binding +} + +// GetGlobalKeyMap returns a GlobalKeyMap with bindings from config +func (c *Config) GetGlobalKeyMap() GlobalKeyMap { + keys := c.KeyMaps + + return GlobalKeyMap{ + ViewLogs: key.NewBinding( + key.WithKeys(keys.ViewLogs...), + key.WithHelp(keys.ViewLogs[0], "logs"), + ), + Quit: key.NewBinding( + key.WithKeys(keys.Quit...), + key.WithHelp(keys.Quit[0], "quit"), + ), + Help: key.NewBinding( + key.WithKeys(keys.Help...), + key.WithHelp("ctrl+?", "toggle help"), + ), + SwitchSession: key.NewBinding( + key.WithKeys(keys.SwitchSession...), + key.WithHelp(keys.SwitchSession[0], "switch session"), + ), + Commands: key.NewBinding( + key.WithKeys(keys.Commands...), + key.WithHelp(keys.Commands[0], "commands"), + ), + FilePicker: key.NewBinding( + key.WithKeys(keys.FilePicker...), + key.WithHelp(keys.FilePicker[0], "select files to upload"), + ), + Models: key.NewBinding( + key.WithKeys(keys.Models...), + key.WithHelp(keys.Models[0], "model selection"), + ), + Theme: key.NewBinding( + key.WithKeys(keys.Theme...), + key.WithHelp(keys.Theme[0], "switch theme"), + ), + Tools: key.NewBinding( + key.WithKeys(keys.Tools...), + key.WithHelp(keys.Tools[0], "show available tools"), + ), + } +} + +// mergeKeyMaps merges user-provided keymaps with default keymaps +// If a keymap is not provided by the user, the default is used +func mergeKeyMaps(userKeyMaps, defaultKeyMaps *KeyMapConfig) { + // Chat keymaps + if userKeyMaps.NewSession == nil { + userKeyMaps.NewSession = defaultKeyMaps.NewSession + } + if userKeyMaps.Cancel == nil { + userKeyMaps.Cancel = defaultKeyMaps.Cancel + } + if userKeyMaps.ToggleTools == nil { + userKeyMaps.ToggleTools = defaultKeyMaps.ToggleTools + } + if userKeyMaps.ShowCompletionDialog == nil { + userKeyMaps.ShowCompletionDialog = defaultKeyMaps.ShowCompletionDialog + } + + // Global keymaps + if userKeyMaps.ViewLogs == nil { + userKeyMaps.ViewLogs = defaultKeyMaps.ViewLogs + } + if userKeyMaps.Quit == nil { + userKeyMaps.Quit = defaultKeyMaps.Quit + } + if userKeyMaps.Help == nil { + userKeyMaps.Help = defaultKeyMaps.Help + } + if userKeyMaps.SwitchSession == nil { + userKeyMaps.SwitchSession = defaultKeyMaps.SwitchSession + } + if userKeyMaps.Commands == nil { + userKeyMaps.Commands = defaultKeyMaps.Commands + } + if userKeyMaps.FilePicker == nil { + userKeyMaps.FilePicker = defaultKeyMaps.FilePicker + } + if userKeyMaps.Models == nil { + userKeyMaps.Models = defaultKeyMaps.Models + } + if userKeyMaps.Theme == nil { + userKeyMaps.Theme = defaultKeyMaps.Theme + } + if userKeyMaps.Tools == nil { + userKeyMaps.Tools = defaultKeyMaps.Tools + } +} diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index 4c014e45c9e..73fc0d37d12 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -65,11 +65,11 @@ func TestApplyHighlighting(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() result := applyHighlighting(tc.content, tc.segments, tc.segmentType, mockHighlightBg) - + // Verify the result contains the expected sequence - assert.Contains(t, result, tc.expectContains, + assert.Contains(t, result, tc.expectContains, "Result should contain full reset sequence") - + // Print the result for manual inspection if needed if t.Failed() { fmt.Printf("Original: %q\nResult: %q\n", tc.content, result) diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index 0858e22dfa5..b640ac0d700 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -37,10 +37,10 @@ type editorCmp struct { } type EditorKeyMaps struct { - Send key.Binding - OpenEditor key.Binding - Paste key.Binding - HistoryUp key.Binding + Send key.Binding + OpenEditor key.Binding + Paste key.Binding + HistoryUp key.Binding HistoryDown key.Binding } @@ -251,14 +251,14 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() { // Get the current line number currentLine := m.textarea.Line() - + // Only navigate history if we're at the first line if currentLine == 0 && len(m.history) > 0 { // Save current message if we're just starting to navigate if m.historyIndex == len(m.history) { m.currentMessage = m.textarea.Value() } - + // Go to previous message in history if m.historyIndex > 0 { m.historyIndex-- @@ -267,14 +267,14 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } } - + if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() { // Get the current line number and total lines currentLine := m.textarea.Line() value := m.textarea.Value() lines := strings.Split(value, "\n") totalLines := len(lines) - + // Only navigate history if we're at the last line if currentLine == totalLines-1 { if m.historyIndex < len(m.history)-1 { @@ -403,10 +403,10 @@ func CreateTextArea(existing *textarea.Model) textarea.Model { func NewEditorCmp(app *app.App) tea.Model { ta := CreateTextArea(nil) return &editorCmp{ - app: app, - textarea: ta, - history: []string{}, - historyIndex: 0, + app: app, + textarea: ta, + history: []string{}, + historyIndex: 0, currentMessage: "", } } diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go index d6f252aad06..49a2e9e26e4 100644 --- a/internal/tui/components/chat/messages.go +++ b/internal/tui/components/chat/messages.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "strings" "time" "github.com/charmbracelet/bubbles/key" @@ -12,6 +13,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/sst/opencode/internal/app" + "github.com/sst/opencode/internal/config" "github.com/sst/opencode/internal/message" "github.com/sst/opencode/internal/pubsub" "github.com/sst/opencode/internal/session" @@ -370,6 +372,13 @@ func (m *messagesCmp) help() string { text := "" + cfg := config.Get() + if cfg == nil { + return "failed to get config" + } + + toggleTool := cfg.GetChatKeyMap().ToggleTools + if m.app.PrimaryAgent.IsBusy() { text += lipgloss.JoinHorizontal( lipgloss.Left, @@ -388,7 +397,7 @@ func (m *messagesCmp) help() string { baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline,"), baseStyle.Foreground(t.Text()).Bold(true).Render(" ↑↓"), baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for history,"), - baseStyle.Foreground(t.Text()).Bold(true).Render(" ctrl+h"), + baseStyle.Foreground(t.Text()).Bold(true).Render(fmt.Sprintf(" %s", keyBindingString(toggleTool))), baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to toggle tool messages"), ) } @@ -483,3 +492,7 @@ func NewMessagesCmp(app *app.App) tea.Model { showToolMessages: true, } } + +func keyBindingString(binding key.Binding) string { + return strings.Join(binding.Keys(), ", ") +} diff --git a/internal/tui/components/dialog/custom_commands_test.go b/internal/tui/components/dialog/custom_commands_test.go index 3468ac3b0b2..c21eaaa548a 100644 --- a/internal/tui/components/dialog/custom_commands_test.go +++ b/internal/tui/components/dialog/custom_commands_test.go @@ -1,8 +1,8 @@ package dialog import ( - "testing" "regexp" + "testing" ) func TestNamedArgPattern(t *testing.T) { @@ -38,11 +38,11 @@ func TestNamedArgPattern(t *testing.T) { for _, tc := range testCases { matches := namedArgPattern.FindAllStringSubmatch(tc.input, -1) - + // Extract unique argument names argNames := make([]string, 0) argMap := make(map[string]bool) - + for _, match := range matches { argName := match[1] // Group 1 is the name without $ if !argMap[argName] { @@ -50,13 +50,13 @@ func TestNamedArgPattern(t *testing.T) { argNames = append(argNames, argName) } } - + // Check if we got the expected number of arguments if len(argNames) != len(tc.expected) { t.Errorf("Expected %d arguments, got %d for input: %s", len(tc.expected), len(argNames), tc.input) continue } - + // Check if we got the expected argument names for _, expectedArg := range tc.expected { found := false @@ -75,7 +75,7 @@ func TestNamedArgPattern(t *testing.T) { func TestRegexPattern(t *testing.T) { pattern := regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`) - + validMatches := []string{ "$FOO", "$BAR", @@ -83,7 +83,7 @@ func TestRegexPattern(t *testing.T) { "$BAZ123", "$ARGUMENTS", } - + invalidMatches := []string{ "$foo", "$1BAR", @@ -91,16 +91,16 @@ func TestRegexPattern(t *testing.T) { "FOO", "$", } - + for _, valid := range validMatches { if !pattern.MatchString(valid) { t.Errorf("Expected %s to match, but it didn't", valid) } } - + for _, invalid := range invalidMatches { if pattern.MatchString(invalid) { t.Errorf("Expected %s not to match, but it did", invalid) } } -} \ No newline at end of file +} diff --git a/internal/tui/components/dialog/filepicker.go b/internal/tui/components/dialog/filepicker.go index 77e64e16f68..4a6f2cc128e 100644 --- a/internal/tui/components/dialog/filepicker.go +++ b/internal/tui/components/dialog/filepicker.go @@ -42,7 +42,7 @@ type FilePrickerKeyMap struct { OpenFilePicker key.Binding Esc key.Binding InsertCWD key.Binding - Paste key.Binding + Paste key.Binding } var filePickerKeyMap = FilePrickerKeyMap{ diff --git a/internal/tui/components/dialog/help.go b/internal/tui/components/dialog/help.go index 1f7f53e116c..eaf6ee5415f 100644 --- a/internal/tui/components/dialog/help.go +++ b/internal/tui/components/dialog/help.go @@ -74,9 +74,22 @@ func (h *helpCmp) render() string { var ( pairs []string width int - rows = 12 - 2 ) + var rows int + if len(bindings) == 0 { + rows = 1 // Default, loop won't run if len(bindings) is 0. + } else if len(bindings) == 1 { + rows = 1 + } else { // len(bindings) >= 2 + maxItemsVertically := max(h.height-6, 1) + + // Target items per column segment to achieve at least two columns. + // (len(bindings) + numColumns - 1) / numColumns for ceiling division. + itemsToTargetForTwoCols := (len(bindings) + 2 - 1) / 2 + rows = max(min(maxItemsVertically, itemsToTargetForTwoCols), 1) + } + for i := 0; i < len(bindings); i += rows { var ( keys []string diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index e4fb62cafc4..fba8096486c 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/completions" + "github.com/sst/opencode/internal/config" "github.com/sst/opencode/internal/message" "github.com/sst/opencode/internal/session" "github.com/sst/opencode/internal/status" @@ -28,35 +29,10 @@ type chatPage struct { messages layout.Container layout layout.SplitPaneLayout completionDialog dialog.CompletionDialog + keyMap config.ChatKeyMap showCompletionDialog bool } -type ChatKeyMap struct { - NewSession key.Binding - Cancel key.Binding - ToggleTools key.Binding - ShowCompletionDialog key.Binding -} - -var keyMap = ChatKeyMap{ - NewSession: key.NewBinding( - key.WithKeys("ctrl+n"), - key.WithHelp("ctrl+n", "new session"), - ), - Cancel: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("esc", "cancel"), - ), - ToggleTools: key.NewBinding( - key.WithKeys("ctrl+h"), - key.WithHelp("ctrl+h", "toggle tools"), - ), - ShowCompletionDialog: key.NewBinding( - key.WithKeys("/"), - key.WithHelp("/", "Complete"), - ), -} - func (p *chatPage) Init() tea.Cmd { cmds := []tea.Cmd{ p.layout.Init(), @@ -126,24 +102,24 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p.app.SetCompletionDialogOpen(false) case tea.KeyMsg: switch { - case key.Matches(msg, keyMap.ShowCompletionDialog): + case key.Matches(msg, p.keyMap.ShowCompletionDialog): p.showCompletionDialog = true p.app.SetCompletionDialogOpen(true) // Continue sending keys to layout->chat - case key.Matches(msg, keyMap.NewSession): + case key.Matches(msg, p.keyMap.NewSession): p.app.CurrentSession = &session.Session{} return p, tea.Batch( p.clearSidebar(), util.CmdHandler(state.SessionClearedMsg{}), ) - case key.Matches(msg, keyMap.Cancel): + case key.Matches(msg, p.keyMap.Cancel): if p.app.CurrentSession.ID != "" { // Cancel the current session's generation process // This allows users to interrupt long-running operations p.app.PrimaryAgent.Cancel(p.app.CurrentSession.ID) return p, nil } - case key.Matches(msg, keyMap.ToggleTools): + case key.Matches(msg, p.keyMap.ToggleTools): return p, util.CmdHandler(chat.ToggleToolMessagesMsg{}) } } @@ -235,7 +211,7 @@ func (p *chatPage) View() string { } func (p *chatPage) BindingKeys() []key.Binding { - bindings := layout.KeyMapToSlice(keyMap) + bindings := layout.KeyMapToSlice(p.keyMap) bindings = append(bindings, p.messages.BindingKeys()...) bindings = append(bindings, p.editor.BindingKeys()...) return bindings @@ -252,11 +228,20 @@ func NewChatPage(app *app.App) tea.Model { chat.NewEditorCmp(app), layout.WithBorder(true, false, false, false), ) + + // Get keymaps from config + cfg := config.Get() + if cfg == nil { + panic("config is nil") + } + + keyMap := cfg.GetChatKeyMap() return &chatPage{ app: app, editor: editorContainer, messages: messagesContainer, completionDialog: completionDialog, + keyMap: keyMap, layout: layout.NewSplitPane( layout.WithLeftPanel(messagesContainer), layout.WithBottomPanel(editorContainer), diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 56be0461970..a70ff21126b 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -30,66 +30,10 @@ import ( "github.com/sst/opencode/internal/tui/util" ) -type keyMap struct { - Logs key.Binding - Quit key.Binding - Help key.Binding - SwitchSession key.Binding - Commands key.Binding - Filepicker key.Binding - Models key.Binding - SwitchTheme key.Binding - Tools key.Binding -} - const ( quitKey = "q" ) -var keys = keyMap{ - Logs: key.NewBinding( - key.WithKeys("ctrl+l"), - key.WithHelp("ctrl+l", "logs"), - ), - - Quit: key.NewBinding( - key.WithKeys("ctrl+c"), - key.WithHelp("ctrl+c", "quit"), - ), - Help: key.NewBinding( - key.WithKeys("ctrl+_"), - key.WithHelp("ctrl+?", "toggle help"), - ), - - SwitchSession: key.NewBinding( - key.WithKeys("ctrl+s"), - key.WithHelp("ctrl+s", "switch session"), - ), - - Commands: key.NewBinding( - key.WithKeys("ctrl+k"), - key.WithHelp("ctrl+k", "commands"), - ), - Filepicker: key.NewBinding( - key.WithKeys("ctrl+f"), - key.WithHelp("ctrl+f", "select files to upload"), - ), - Models: key.NewBinding( - key.WithKeys("ctrl+o"), - key.WithHelp("ctrl+o", "model selection"), - ), - - SwitchTheme: key.NewBinding( - key.WithKeys("ctrl+t"), - key.WithHelp("ctrl+t", "switch theme"), - ), - - Tools: key.NewBinding( - key.WithKeys("f9"), - key.WithHelp("f9", "show available tools"), - ), -} - var helpEsc = key.NewBinding( key.WithKeys("?"), key.WithHelp("?", "toggle help"), @@ -113,6 +57,7 @@ type appModel struct { loadedPages map[page.PageID]bool status core.StatusCmp app *app.App + keymap config.GlobalKeyMap showPermissions bool permissions dialog.PermissionDialogCmp @@ -144,7 +89,7 @@ type appModel struct { showMultiArgumentsDialog bool multiArgumentsDialog dialog.MultiArgumentsDialogCmp - + showToolsDialog bool toolsDialog dialog.ToolsDialog } @@ -299,11 +244,11 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case dialog.CloseThemeDialogMsg: a.showThemeDialog = false return a, nil - + case dialog.CloseToolsDialogMsg: a.showToolsDialog = false return a, nil - + case dialog.ShowToolsDialogMsg: a.showToolsDialog = msg.Show return a, nil @@ -403,7 +348,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } switch { - case key.Matches(msg, keys.Quit): + case key.Matches(msg, a.keymap.Quit): a.showQuit = !a.showQuit if a.showHelp { a.showHelp = false @@ -429,14 +374,14 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showToolsDialog = false } return a, nil - case key.Matches(msg, keys.SwitchSession): + case key.Matches(msg, a.keymap.SwitchSession): if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog { // Close other dialogs a.showToolsDialog = false a.showThemeDialog = false a.showModelDialog = false a.showFilepicker = false - + // Load sessions and show the dialog sessions, err := a.app.Sessions.List(context.Background()) if err != nil { @@ -452,12 +397,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, nil } return a, nil - case key.Matches(msg, keys.Commands): + case key.Matches(msg, a.keymap.Commands): if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog && !a.showFilepicker { // Close other dialogs a.showToolsDialog = false a.showModelDialog = false - + // Show commands dialog if len(a.commands) == 0 { status.Warn("No commands available") @@ -468,7 +413,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, nil } return a, nil - case key.Matches(msg, keys.Models): + case key.Matches(msg, a.keymap.Models): if a.showModelDialog { a.showModelDialog = false return a, nil @@ -478,28 +423,28 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showToolsDialog = false a.showThemeDialog = false a.showFilepicker = false - + a.showModelDialog = true return a, nil } return a, nil - case key.Matches(msg, keys.SwitchTheme): + case key.Matches(msg, a.keymap.Theme): if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog { // Close other dialogs a.showToolsDialog = false a.showModelDialog = false a.showFilepicker = false - + a.showThemeDialog = true return a, a.themeDialog.Init() } return a, nil - case key.Matches(msg, keys.Tools): + case key.Matches(msg, a.keymap.Tools): // Check if any other dialog is open - if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && - !a.showSessionDialog && !a.showCommandDialog && !a.showThemeDialog && - !a.showFilepicker && !a.showModelDialog && !a.showInitDialog && - !a.showMultiArgumentsDialog { + if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && + !a.showSessionDialog && !a.showCommandDialog && !a.showThemeDialog && + !a.showFilepicker && !a.showModelDialog && !a.showInitDialog && + !a.showMultiArgumentsDialog { // Toggle tools dialog a.showToolsDialog = !a.showToolsDialog if a.showToolsDialog { @@ -548,14 +493,14 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, a.moveToPageUnconditional(page.ChatPage) } } - case key.Matches(msg, keys.Logs): + case key.Matches(msg, a.keymap.ViewLogs): return a, a.moveToPage(page.LogsPage) - case key.Matches(msg, keys.Help): + case key.Matches(msg, a.keymap.Help): if a.showQuit { return a, nil } a.showHelp = !a.showHelp - + // Close other dialogs if opening help if a.showHelp { a.showToolsDialog = false @@ -569,12 +514,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.showHelp = !a.showHelp return a, nil } - case key.Matches(msg, keys.Filepicker): + case key.Matches(msg, a.keymap.FilePicker): // Toggle filepicker a.showFilepicker = !a.showFilepicker a.filepicker.ToggleFilepicker(a.showFilepicker) a.app.SetFilepickerOpen(a.showFilepicker) - + // Close other dialogs if opening filepicker if a.showFilepicker { a.showToolsDialog = false @@ -681,7 +626,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return a, tea.Batch(cmds...) } } - + if a.showToolsDialog { d, toolsCmd := a.toolsDialog.Update(msg) a.toolsDialog = d.(dialog.ToolsDialog) @@ -716,13 +661,13 @@ func getAvailableToolNames(app *app.App) []string { app.History, app.LSPClients, ) - + // Extract tool names var toolNames []string for _, tool := range allTools { toolNames = append(toolNames, tool.Info().Name) } - + return toolNames } @@ -802,7 +747,7 @@ func (a appModel) View() string { } if a.showHelp { - bindings := layout.KeyMapToSlice(keys) + bindings := layout.KeyMapToSlice(a.keymap) if p, ok := a.pages[a.currentPage].(layout.Bindings); ok { bindings = append(bindings, p.BindingKeys()...) } @@ -815,6 +760,7 @@ func (a appModel) View() string { if !a.app.PrimaryAgent.IsBusy() { bindings = append(bindings, helpEsc) } + a.help.SetBindings(bindings) overlay := a.help.View() @@ -931,7 +877,7 @@ func (a appModel) View() string { true, ) } - + if a.showToolsDialog { overlay := a.toolsDialog.View() row := lipgloss.Height(appView) / 2 @@ -952,6 +898,10 @@ func (a appModel) View() string { func New(app *app.App) tea.Model { startPage := page.ChatPage + cfg := config.Get() + if cfg == nil { + panic("config is nil") + } model := &appModel{ currentPage: startPage, loadedPages: make(map[page.PageID]bool), @@ -966,6 +916,7 @@ func New(app *app.App) tea.Model { themeDialog: dialog.NewThemeDialogCmp(), toolsDialog: dialog.NewToolsDialogCmp(), app: app, + keymap: cfg.GetGlobalKeyMap(), commands: []dialog.Command{}, pages: map[page.PageID]tea.Model{ page.ChatPage: page.NewChatPage(app),