diff --git a/internal/tui/config.go b/internal/tui/config.go index 2be2cc3..6058356 100644 --- a/internal/tui/config.go +++ b/internal/tui/config.go @@ -4,6 +4,8 @@ import ( "fmt" "os" "path/filepath" + + "github.com/BurntSushi/toml" ) var ( @@ -14,6 +16,23 @@ var ( ConfigThemesDirPath string ) +// config.toml +type appConfig struct { + Theme string `toml:"theme"` + Keybindings map[string]string `toml:"keybindings"` +} + +func load_config() (*appConfig, error) { + cfgPath := ConfigFilePath + + var cfg appConfig + if _, err := toml.DecodeFile(cfgPath, &cfg); err != nil { + return nil, err + } + + return &cfg, nil +} + func initializeConfig() error { homeDir, err := os.UserHomeDir() if err != nil { @@ -36,7 +55,7 @@ func initializeConfig() error { if _, err := os.Stat(ConfigFilePath); err != nil { if os.IsNotExist(err) { - defaultConfig := fmt.Sprintf("Theme = %q\n", DefaultThemeName) + defaultConfig := fmt.Sprintf("theme = %q\n\n[keybindings]\n", DefaultThemeName) if writeErr := os.WriteFile(ConfigFilePath, []byte(defaultConfig), 0644); writeErr != nil { return fmt.Errorf("failed to create default config file: %w", writeErr) } diff --git a/internal/tui/keys.go b/internal/tui/keys.go index 2fac375..9119a58 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -1,269 +1,229 @@ package tui -import "github.com/charmbracelet/bubbles/key" +import ( + "strings" -// KeyMap defines the keybindings for the application. -type KeyMap struct { - // miscellaneous keybindings - Quit key.Binding - Escape key.Binding - ToggleHelp key.Binding + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) - // keybindings for changing theme - SwitchTheme key.Binding +// KeyMap stores keybindings by action name. +type KeyMap map[string]string - // keybindings for navigation - FocusNext key.Binding - FocusPrev key.Binding - FocusZero key.Binding - FocusOne key.Binding - FocusTwo key.Binding - FocusThree key.Binding - FocusFour key.Binding - FocusFive key.Binding - FocusSix key.Binding - Up key.Binding - Down key.Binding +// HelpSection is a struct to hold a title and keybindings for a help section. +type HelpSection struct { + Title string + Bindings []key.Binding +} - // Keybindings for FilesPanel - StageItem key.Binding - StageAll key.Binding - Discard key.Binding - Stash key.Binding - StashAll key.Binding - Commit key.Binding +var keybindingDescriptions = map[string]string{ + "quit": "quit", + "escape": "cancel", + "toggle_help": "toggle help", + "switch_theme": "switch theme", + "focus_next": "Focus Next Window", + "focus_prev": "Focus Previous Window", + "focus_main": "Focus Main Window", + "focus_status": "Focus Status Window", + "focus_files": "Focus Files Window", + "focus_branches": "Focus Branches Window", + "focus_commits": "Focus Commits Window", + "focus_stash": "Focus Stash Window", + "focus_command_log": "Focus Command log Window", + "up": "up", + "down": "down", + "stage_item": "Stage Item", + "stage_all": "Stage All", + "discard": "Discard", + "stash": "Stash", + "stash_all": "Stash all", + "commit": "Commit", + "checkout": "Checkout", + "new_branch": "New Branch", + "delete_branch": "Delete", + "rename_branch": "Rename", + "amend_commit": "Amend", + "revert": "Revert", + "reset_to_commit": "Reset to Commit", + "stash_apply": "Apply", + "stash_pop": "Pop", + "stash_drop": "Drop", +} - // Keybindings for BranchesPanel - Checkout key.Binding - NewBranch key.Binding - DeleteBranch key.Binding - RenameBranch key.Binding +func keySpec(keys ...string) string { + return strings.Join(keys, ",") +} - // Keybindings for CommitsPanel - AmendCommit key.Binding - Revert key.Binding - ResetToCommit key.Binding +// DefaultKeybindings returns default keybindings for each action. +func DefaultKeybindings() map[string]string { + return map[string]string{ + "quit": keySpec("q", "ctrl+c"), + "escape": keySpec("esc"), + "toggle_help": keySpec("?"), + "switch_theme": keySpec("ctrl+t"), + "focus_next": keySpec("tab"), + "focus_prev": keySpec("shift+tab"), + "focus_main": keySpec("0"), + "focus_status": keySpec("1"), + "focus_files": keySpec("2"), + "focus_branches": keySpec("3"), + "focus_commits": keySpec("4"), + "focus_stash": keySpec("5"), + "focus_command_log": keySpec("6"), + "up": keySpec("k", "up"), + "down": keySpec("j", "down"), + "stage_item": keySpec("a"), + "stage_all": keySpec("space"), + "discard": keySpec("d"), + "stash": keySpec("s"), + "stash_all": keySpec("S"), + "commit": keySpec("c"), + "checkout": keySpec("enter"), + "new_branch": keySpec("n"), + "delete_branch": keySpec("d"), + "rename_branch": keySpec("r"), + "amend_commit": keySpec("A"), + "revert": keySpec("v"), + "reset_to_commit": keySpec("R"), + "stash_apply": keySpec("a"), + "stash_pop": keySpec("p"), + "stash_drop": keySpec("d"), + } +} - // Keybindings for StashPanel - StashApply key.Binding - StashPop key.Binding - StashDrop key.Binding +// DefaultKeyMap returns default keybindings. +func DefaultKeyMap() KeyMap { + defaults := DefaultKeybindings() + result := make(KeyMap, len(defaults)) + for k, v := range defaults { + result[k] = v + } + return result } -// HelpSection is a struct to hold a title and keybindings for a help section. -type HelpSection struct { - Title string - Bindings []key.Binding +// MergeKeybindings merges user overrides into defaults and ignores empty override values. +func MergeKeybindings(defaults, overrides map[string]string) map[string]string { + result := make(map[string]string, len(defaults)+len(overrides)) + for k, v := range defaults { + result[k] = v + } + for k, v := range overrides { + if strings.TrimSpace(v) != "" { + result[k] = v + } + } + return result +} + +// KeyMapFromConfig returns keybindings with user overrides applied on top of defaults. +func KeyMapFromConfig(overrides map[string]string) KeyMap { + merged := MergeKeybindings(DefaultKeybindings(), overrides) + result := make(KeyMap, len(merged)) + for k, v := range merged { + result[k] = v + } + if alias, ok := result["open"]; ok && strings.TrimSpace(result["checkout"]) == "" { + result["checkout"] = alias + } + return result +} + +func parseConfiguredKeys(configured string) ([]string, bool) { + parts := strings.Split(configured, ",") + keys := make([]string, 0, len(parts)) + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + keys = append(keys, trimmed) + } + } + return keys, len(keys) > 0 +} + +func helpLabel(keys []string) string { + return strings.Join(keys, "/") +} + +func (k KeyMap) binding(action string) key.Binding { + spec := strings.TrimSpace(k[action]) + if spec == "" { + spec = DefaultKeybindings()[action] + } + resolvedKeys, ok := parseConfiguredKeys(spec) + if !ok { + resolvedKeys, _ = parseConfiguredKeys(DefaultKeybindings()[action]) + } + desc := keybindingDescriptions[action] + return key.NewBinding( + key.WithKeys(resolvedKeys...), + key.WithHelp(helpLabel(resolvedKeys), desc), + ) +} + +// Matches reports whether the message matches the configured key spec. +func Matches(msg tea.KeyMsg, spec string) bool { + resolvedKeys, ok := parseConfiguredKeys(spec) + if !ok { + return false + } + return key.Matches(msg, key.NewBinding(key.WithKeys(resolvedKeys...))) +} + +func (k KeyMap) bindings(actions ...string) []key.Binding { + result := make([]key.Binding, 0, len(actions)) + for _, action := range actions { + result = append(result, k.binding(action)) + } + return result } // FullHelp returns a structured slice of HelpSection, which is used to build // the full help view. func (k KeyMap) FullHelp() []HelpSection { return []HelpSection{ - { - Title: "Navigation", - Bindings: []key.Binding{ - k.FocusNext, k.FocusPrev, k.FocusZero, k.FocusOne, - k.FocusTwo, k.FocusThree, k.FocusFour, k.FocusFive, - k.FocusSix, k.Up, k.Down, - }, - }, - { - Title: "Files", - Bindings: []key.Binding{ - k.Commit, k.Stash, k.StashAll, k.StageItem, - k.StageAll, k.Discard, - }, - }, - { - Title: "Branches", - Bindings: []key.Binding{k.Checkout, k.NewBranch, k.DeleteBranch, k.RenameBranch}, - }, - { - Title: "Commits", - Bindings: []key.Binding{k.AmendCommit, k.Revert, k.ResetToCommit}, - }, - { - Title: "Stash", - Bindings: []key.Binding{k.StashApply, k.StashPop, k.StashDrop}, - }, - { - Title: "Misc", - Bindings: []key.Binding{k.SwitchTheme, k.ToggleHelp, k.Escape, k.Quit}, - }, + {Title: "Navigation", Bindings: k.bindings( + "focus_next", "focus_prev", "focus_main", "focus_status", + "focus_files", "focus_branches", "focus_commits", "focus_stash", + "focus_command_log", "up", "down", + )}, + {Title: "Files", Bindings: k.bindings("commit", "stash", "stash_all", "stage_item", "stage_all", "discard")}, + {Title: "Branches", Bindings: k.bindings("checkout", "new_branch", "delete_branch", "rename_branch")}, + {Title: "Commits", Bindings: k.bindings("amend_commit", "revert", "reset_to_commit")}, + {Title: "Stash", Bindings: k.bindings("stash_apply", "stash_pop", "stash_drop")}, + {Title: "Misc", Bindings: k.bindings("switch_theme", "toggle_help", "escape", "quit")}, } } // ShortHelp returns a slice of key.Binding containing help for default keybindings. func (k KeyMap) ShortHelp() []key.Binding { - return []key.Binding{k.ToggleHelp, k.Escape, k.Quit} + return k.bindings("toggle_help", "escape", "quit") } // HelpViewHelp returns a slice of key.Binding containing help for keybindings related to Help View. func (k KeyMap) HelpViewHelp() []key.Binding { - return []key.Binding{k.ToggleHelp, k.Escape, k.Quit} + return k.ShortHelp() } // FilesPanelHelp returns a slice of key.Binding containing help for keybindings related to Files Panel. func (k KeyMap) FilesPanelHelp() []key.Binding { - help := []key.Binding{k.Commit, k.Stash, k.Discard, k.StageItem} + help := k.bindings("commit", "stash", "discard", "stage_item") return append(help, k.ShortHelp()...) } // BranchesPanelHelp returns a slice of key.Binding for the Branches Panel help bar. func (k KeyMap) BranchesPanelHelp() []key.Binding { - help := []key.Binding{k.Checkout, k.NewBranch, k.DeleteBranch} + help := k.bindings("checkout", "new_branch", "delete_branch") return append(help, k.ShortHelp()...) } // CommitsPanelHelp returns a slice of key.Binding for the Commits Panel help bar. func (k KeyMap) CommitsPanelHelp() []key.Binding { - help := []key.Binding{k.AmendCommit, k.Revert, k.ResetToCommit} + help := k.bindings("amend_commit", "revert", "reset_to_commit") return append(help, k.ShortHelp()...) } // StashPanelHelp returns a slice of key.Binding for the Stash Panel help bar. func (k KeyMap) StashPanelHelp() []key.Binding { - help := []key.Binding{k.StashApply, k.StashPop, k.StashDrop} + help := k.bindings("stash_apply", "stash_pop", "stash_drop") return append(help, k.ShortHelp()...) } - -// DefaultKeyMap returns a set of default keybindings. -func DefaultKeyMap() KeyMap { - return KeyMap{ - // misc - Quit: key.NewBinding( - key.WithKeys("q", "ctrl+c"), - key.WithHelp("q", "quit"), - ), - Escape: key.NewBinding( - key.WithKeys("esc"), - key.WithHelp("", "cancel"), - ), - ToggleHelp: key.NewBinding( - key.WithKeys("?"), - key.WithHelp("?", "toggle help"), - ), - - // theme - SwitchTheme: key.NewBinding( - key.WithKeys("ctrl+t"), - key.WithHelp("", "switch theme"), - ), - - // navigation - FocusNext: key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", "Focus Next Window"), - ), - FocusPrev: key.NewBinding( - key.WithKeys("shift+tab"), - key.WithHelp("", "Focus Previous Window"), - ), - FocusZero: key.NewBinding( - key.WithKeys("0"), - key.WithHelp("0", "Focus Main Window"), - ), - FocusOne: key.NewBinding( - key.WithKeys("1"), - key.WithHelp("1", "Focus Status Window"), - ), - FocusTwo: key.NewBinding( - key.WithKeys("2"), - key.WithHelp("2", "Focus Files Window"), - ), - FocusThree: key.NewBinding( - key.WithKeys("3"), - key.WithHelp("3", "Focus Branches Window"), - ), - FocusFour: key.NewBinding( - key.WithKeys("4"), - key.WithHelp("4", "Focus Commits Window"), - ), - FocusFive: key.NewBinding( - key.WithKeys("5"), - key.WithHelp("5", "Focus Stash Window"), - ), - FocusSix: key.NewBinding( - key.WithKeys("6"), - key.WithHelp("6", "Focus Command log Window"), - ), - Up: key.NewBinding( - key.WithKeys("k", "up"), - key.WithHelp("k/↑", "up"), - ), - Down: key.NewBinding( - key.WithKeys("j", "down"), - key.WithHelp("j/↓", "down"), - ), - - // FilesPanel - StageItem: key.NewBinding( - key.WithKeys("a"), - key.WithHelp("a", "Stage Item"), - ), - StageAll: key.NewBinding( - key.WithKeys("space"), - key.WithHelp("", "Stage All"), - ), - Discard: key.NewBinding( - key.WithKeys("d"), - key.WithHelp("d", "Discard"), - ), - Stash: key.NewBinding( - key.WithKeys("s"), - key.WithHelp("s", "Stash"), - ), - StashAll: key.NewBinding( - key.WithKeys("S"), - key.WithHelp("S", "Stash all"), - ), - Commit: key.NewBinding( - key.WithKeys("c"), - key.WithHelp("c", "Commit"), - ), - - Checkout: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "Checkout"), - ), - NewBranch: key.NewBinding( - key.WithKeys("n"), - key.WithHelp("n", "New Branch"), - ), - DeleteBranch: key.NewBinding( - key.WithKeys("d"), - key.WithHelp("d", "Delete"), - ), - RenameBranch: key.NewBinding( - key.WithKeys("r"), - key.WithHelp("r", "Rename"), - ), - - AmendCommit: key.NewBinding( - key.WithKeys("A"), - key.WithHelp("A", "Amend"), - ), - Revert: key.NewBinding( - key.WithKeys("v"), - key.WithHelp("v", "Revert"), - ), - ResetToCommit: key.NewBinding( - key.WithKeys("R"), - key.WithHelp("R", "Reset to Commit"), - ), - - StashApply: key.NewBinding( - key.WithKeys("a"), - key.WithHelp("a", "Apply"), - ), - StashPop: key.NewBinding( - key.WithKeys("p"), - key.WithHelp("p", "Pop"), - ), - StashDrop: key.NewBinding( - key.WithKeys("d"), - key.WithHelp("d", "Drop"), - ), - } -} diff --git a/internal/tui/model.go b/internal/tui/model.go index 5f176f5..43cd736 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -49,12 +49,17 @@ type Model struct { confirmCallback func(bool) tea.Cmd // New fields for command history CommandHistory []string + keymap KeyMap } // initialModel creates the initial state of the application. func initialModel() Model { themeNames := ThemeNames() //built-in themes load - cfg, _ := load_config() + cfg, err := load_config() + if err != nil { + cfg = &appConfig{Theme: DefaultThemeName} + } + keymap := KeyMapFromConfig(cfg.Keybindings) var selectedThemeName string if t, ok := Themes[cfg.Theme]; ok { @@ -115,6 +120,7 @@ func initialModel() Model { textInput: ti, descriptionInput: ta, CommandHistory: []string{}, + keymap: keymap, } } @@ -151,14 +157,14 @@ func (m *Model) nextTheme() { func (m *Model) panelShortHelp() []key.Binding { switch m.focusedPanel { case FilesPanel: - return keys.FilesPanelHelp() + return m.keymap.FilesPanelHelp() case BranchesPanel: - return keys.BranchesPanelHelp() + return m.keymap.BranchesPanelHelp() case CommitsPanel: - return keys.CommitsPanelHelp() + return m.keymap.CommitsPanelHelp() case StashPanel: - return keys.StashPanelHelp() + return m.keymap.StashPanelHelp() default: - return keys.ShortHelp() + return m.keymap.ShortHelp() } } diff --git a/internal/tui/model_test.go b/internal/tui/model_test.go index b6cd63c..2de516f 100644 --- a/internal/tui/model_test.go +++ b/internal/tui/model_test.go @@ -173,14 +173,51 @@ func TestModel_KeyFocus(t *testing.T) { func TestModel_contextualHelp(t *testing.T) { m := initialModel() - keys = DefaultKeyMap() t.Run("Files Panel Help", func(t *testing.T) { m.focusedPanel = FilesPanel gotKeys := m.panelShortHelp() - assertKeyBindingsEqual(t, gotKeys, keys.FilesPanelHelp()) + assertKeyBindingsEqual(t, gotKeys, m.keymap.FilesPanelHelp()) }) } +func TestKeyMapFromConfig_OverridesAndFallback(t *testing.T) { + defaults := DefaultKeybindings() + resolved := KeyMapFromConfig(map[string]string{ + "quit": "x", + }) + + if got := resolved["quit"]; got != "x" { + t.Fatalf("expected quit key to be overridden to x, got %q", got) + } + + if got, want := resolved["up"], defaults["up"]; got != want { + t.Fatalf("expected unspecified keybinding to fallback to default, got %q want %q", got, want) + } +} + +func TestKeyMapFromConfig_MultiKeyValue(t *testing.T) { + resolved := KeyMapFromConfig(map[string]string{ + "quit": "x,ctrl+c", + }) + + if got := resolved["quit"]; got != "x,ctrl+c" { + t.Fatalf("expected parsed multi-key binding, got %q", got) + } +} + +func TestKeyMapFromConfig_InvalidValueUsesDefault(t *testing.T) { + defaults := DefaultKeybindings() + resolved := KeyMapFromConfig(map[string]string{ + "quit": " ", + }) + + got := resolved["quit"] + want := defaults["quit"] + if got != want { + t.Fatalf("expected invalid override to keep default, got %q want %q", got, want) + } +} + func TestModel_HelpToggle(t *testing.T) { m := initialModel() t.Run("toggles help on", func(t *testing.T) { diff --git a/internal/tui/theme.go b/internal/tui/theme.go index b8564b3..b1fff93 100644 --- a/internal/tui/theme.go +++ b/internal/tui/theme.go @@ -151,11 +151,6 @@ type TreeStyle struct { Connector, ConnectorLast, Prefix, PrefixLast string } -// config.toml -type themeConfig struct { - Theme string `toml:"theme"` -} - // custom_theme.toml type ThemeFile struct { Fg string `toml:"fg"` @@ -244,17 +239,6 @@ func ThemeNames() []string { return names } -func load_config() (*themeConfig, error) { - cfgPath := ConfigFilePath - - var cfg themeConfig - if _, err := toml.DecodeFile(cfgPath, &cfg); err != nil { - return nil, err - } - - return &cfg, nil -} - func load_custom_theme(name string) (*Palette, error) { themePath := filepath.Join(ConfigThemesDirPath, name+".toml") if _, err := os.Stat(themePath); os.IsNotExist(err) { diff --git a/internal/tui/update.go b/internal/tui/update.go index 287456f..3dbfe06 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -5,15 +5,12 @@ import ( "log" "strings" - "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/gitxtui/gitx/internal/git" zone "github.com/lrstanley/bubblezone" ) -var keys = DefaultKeyMap() - // commandExecutedMsg is sent after a git command has been run successfully. type commandExecutedMsg struct { cmdStr string @@ -194,10 +191,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: if m.showHelp { switch { - case key.Matches(msg, keys.ToggleHelp): + case Matches(msg, m.keymap["toggle_help"]): m.toggleHelp() return m, nil - case key.Matches(msg, keys.Escape): + case Matches(msg, m.keymap["escape"]): m.toggleHelp() return m, nil default: @@ -208,23 +205,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } switch { - case key.Matches(msg, keys.Quit): + case Matches(msg, m.keymap["quit"]): return m, tea.Quit - case key.Matches(msg, keys.Escape): + case Matches(msg, m.keymap["escape"]): return m, nil - case key.Matches(msg, keys.ToggleHelp): + case Matches(msg, m.keymap["toggle_help"]): m.toggleHelp() - case key.Matches(msg, keys.SwitchTheme): + case Matches(msg, m.keymap["switch_theme"]): m.nextTheme() - case key.Matches(msg, keys.FocusNext), key.Matches(msg, keys.FocusPrev), - key.Matches(msg, keys.FocusZero), key.Matches(msg, keys.FocusOne), - key.Matches(msg, keys.FocusTwo), key.Matches(msg, keys.FocusThree), - key.Matches(msg, keys.FocusFour), key.Matches(msg, keys.FocusFive), - key.Matches(msg, keys.FocusSix): + case Matches(msg, m.keymap["focus_next"]), Matches(msg, m.keymap["focus_prev"]), + Matches(msg, m.keymap["focus_main"]), Matches(msg, m.keymap["focus_status"]), + Matches(msg, m.keymap["focus_files"]), Matches(msg, m.keymap["focus_branches"]), + Matches(msg, m.keymap["focus_commits"]), Matches(msg, m.keymap["focus_stash"]), + Matches(msg, m.keymap["focus_command_log"]): m.handleFocusKeys(msg) } @@ -596,7 +593,7 @@ func (m *Model) handleCursorMovement(msg tea.KeyMsg) (bool, tea.Cmd) { p := &m.panels[m.focusedPanel] itemSelected := false switch { - case key.Matches(msg, keys.Up): + case Matches(msg, m.keymap["up"]): if p.cursor > 0 { p.cursor-- if p.cursor < p.viewport.YOffset { @@ -604,7 +601,7 @@ func (m *Model) handleCursorMovement(msg tea.KeyMsg) (bool, tea.Cmd) { } itemSelected = true } - case key.Matches(msg, keys.Down): + case Matches(msg, m.keymap["down"]): if p.cursor < len(p.lines)-1 { p.cursor++ if p.cursor >= p.viewport.YOffset+p.viewport.Height { @@ -637,7 +634,7 @@ func (m *Model) handleFilesPanelKeys(msg tea.KeyMsg) tea.Cmd { filePath := parts[3] switch { - case key.Matches(msg, keys.Commit): + case Matches(msg, m.keymap["commit"]): m.mode = modeCommit m.textInput.SetValue("") m.descriptionInput.SetValue("") @@ -656,7 +653,7 @@ func (m *Model) handleFilesPanelKeys(msg tea.KeyMsg) tea.Cmd { } } - case key.Matches(msg, keys.StageItem): + case Matches(msg, m.keymap["stage_item"]): return func() tea.Msg { var cmdStr string var err error @@ -671,7 +668,7 @@ func (m *Model) handleFilesPanelKeys(msg tea.KeyMsg) tea.Cmd { return commandExecutedMsg{cmdStr} } - case key.Matches(msg, keys.StageAll): + case Matches(msg, m.keymap["stage_all"]): return func() tea.Msg { _, cmdStr, err := m.git.AddFiles([]string{"."}) if err != nil { @@ -680,7 +677,7 @@ func (m *Model) handleFilesPanelKeys(msg tea.KeyMsg) tea.Cmd { return commandExecutedMsg{cmdStr} } - case key.Matches(msg, keys.Discard): + case Matches(msg, m.keymap["discard"]): m.mode = modeConfirm m.confirmMessage = fmt.Sprintf("Discard changes to %s?", filePath) m.confirmCallback = func(confirmed bool) tea.Cmd { @@ -700,7 +697,7 @@ func (m *Model) handleFilesPanelKeys(msg tea.KeyMsg) tea.Cmd { } } - case key.Matches(msg, keys.StashAll): + case Matches(msg, m.keymap["stash_all"]): return func() tea.Msg { _, cmdStr, err := m.git.StashAll() if err != nil { @@ -728,7 +725,7 @@ func (m *Model) handleBranchesPanelKeys(msg tea.KeyMsg) tea.Cmd { branchName := strings.TrimSpace(strings.TrimPrefix(parts[1], "(*) → ")) switch { - case key.Matches(msg, keys.Checkout): + case Matches(msg, m.keymap["checkout"]): return func() tea.Msg { _, cmdStr, err := m.git.Checkout(branchName) if err != nil { @@ -737,7 +734,7 @@ func (m *Model) handleBranchesPanelKeys(msg tea.KeyMsg) tea.Cmd { return commandExecutedMsg{cmdStr} } - case key.Matches(msg, keys.NewBranch): + case Matches(msg, m.keymap["new_branch"]): m.mode = modeInput m.promptTitle = "New Branch Name" m.textInput.SetValue("") @@ -765,7 +762,7 @@ func (m *Model) handleBranchesPanelKeys(msg tea.KeyMsg) tea.Cmd { ) } - case key.Matches(msg, keys.DeleteBranch): + case Matches(msg, m.keymap["delete_branch"]): m.mode = modeConfirm m.confirmMessage = fmt.Sprintf("Delete branch %s?", branchName) m.confirmCallback = func(confirmed bool) tea.Cmd { @@ -782,7 +779,7 @@ func (m *Model) handleBranchesPanelKeys(msg tea.KeyMsg) tea.Cmd { } } - case key.Matches(msg, keys.RenameBranch): + case Matches(msg, m.keymap["rename_branch"]): m.mode = modeInput m.promptTitle = "New Branch Name" m.textInput.SetValue(branchName) @@ -820,7 +817,7 @@ func (m *Model) handleCommitsPanelKeys(msg tea.KeyMsg) tea.Cmd { sha := parts[1] switch { - case key.Matches(msg, keys.AmendCommit): + case Matches(msg, m.keymap["amend_commit"]): m.mode = modeCommit m.textInput.SetValue("") m.descriptionInput.SetValue("") @@ -839,7 +836,7 @@ func (m *Model) handleCommitsPanelKeys(msg tea.KeyMsg) tea.Cmd { } } - case key.Matches(msg, keys.Revert): + case Matches(msg, m.keymap["revert"]): m.mode = modeConfirm m.confirmMessage = fmt.Sprintf("Revert commit %s?", sha) m.confirmCallback = func(confirmed bool) tea.Cmd { @@ -856,7 +853,7 @@ func (m *Model) handleCommitsPanelKeys(msg tea.KeyMsg) tea.Cmd { } } - case key.Matches(msg, keys.ResetToCommit): + case Matches(msg, m.keymap["reset_to_commit"]): m.mode = modeConfirm m.confirmMessage = fmt.Sprintf("Hard reset to commit %s? This will discard all changes!", sha) m.confirmCallback = func(confirmed bool) tea.Cmd { @@ -892,7 +889,7 @@ func (m *Model) handleStashPanelKeys(msg tea.KeyMsg) tea.Cmd { stashID := parts[0] switch { - case key.Matches(msg, keys.StashApply): + case Matches(msg, m.keymap["stash_apply"]): return func() tea.Msg { _, cmdStr, err := m.git.Stash(git.StashOptions{Apply: true, StashID: stashID}) if err != nil { @@ -901,7 +898,7 @@ func (m *Model) handleStashPanelKeys(msg tea.KeyMsg) tea.Cmd { return commandExecutedMsg{cmdStr} } - case key.Matches(msg, keys.StashPop): + case Matches(msg, m.keymap["stash_pop"]): return func() tea.Msg { _, cmdStr, err := m.git.Stash(git.StashOptions{Pop: true, StashID: stashID}) if err != nil { @@ -910,7 +907,7 @@ func (m *Model) handleStashPanelKeys(msg tea.KeyMsg) tea.Cmd { return commandExecutedMsg{cmdStr} } - case key.Matches(msg, keys.StashDrop): + case Matches(msg, m.keymap["stash_drop"]): m.mode = modeConfirm m.confirmMessage = fmt.Sprintf("Drop stash %s?", stashID) m.confirmCallback = func(confirmed bool) tea.Cmd { @@ -937,23 +934,23 @@ func (m *Model) handleStashPanelKeys(msg tea.KeyMsg) tea.Cmd { // handleFocusKeys changes the focused panel based on keyboard shortcuts. func (m *Model) handleFocusKeys(msg tea.KeyMsg) { switch { - case key.Matches(msg, keys.FocusNext): + case Matches(msg, m.keymap["focus_next"]): m.nextPanel() - case key.Matches(msg, keys.FocusPrev): + case Matches(msg, m.keymap["focus_prev"]): m.prevPanel() - case key.Matches(msg, keys.FocusZero): + case Matches(msg, m.keymap["focus_main"]): m.focusedPanel = MainPanel - case key.Matches(msg, keys.FocusOne): + case Matches(msg, m.keymap["focus_status"]): m.focusedPanel = StatusPanel - case key.Matches(msg, keys.FocusTwo): + case Matches(msg, m.keymap["focus_files"]): m.focusedPanel = FilesPanel - case key.Matches(msg, keys.FocusThree): + case Matches(msg, m.keymap["focus_branches"]): m.focusedPanel = BranchesPanel - case key.Matches(msg, keys.FocusFour): + case Matches(msg, m.keymap["focus_commits"]): m.focusedPanel = CommitsPanel - case key.Matches(msg, keys.FocusFive): + case Matches(msg, m.keymap["focus_stash"]): m.focusedPanel = StashPanel - case key.Matches(msg, keys.FocusSix): + case Matches(msg, m.keymap["focus_command_log"]): m.focusedPanel = SecondaryPanel } } diff --git a/internal/tui/view.go b/internal/tui/view.go index ba36302..752ea14 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -232,7 +232,7 @@ func (m Model) renderHelpBar() string { if !m.showHelp { helpBindings = m.panelShortHelp() } else { - helpBindings = keys.ShortHelp() + helpBindings = m.keymap.ShortHelp() } shortHelp := m.help.ShortHelpView(helpBindings) helpButton := m.theme.HelpButton.Render(" help:? ") @@ -290,7 +290,7 @@ func renderBox(title string, titleStyle lipgloss.Style, borderStyle BorderStyle, // generateHelpContent builds the formatted help string from the application's keymap. func (m Model) generateHelpContent() string { - helpSections := keys.FullHelp() + helpSections := m.keymap.FullHelp() var renderedSections []string for _, section := range helpSections { title := m.theme.HelpTitle.