From 931c81fcd7290da1be08ecdb96ec35a2dc364b6b Mon Sep 17 00:00:00 2001 From: FromSi Date: Wed, 27 May 2026 15:39:24 +0500 Subject: [PATCH 1/4] fix(tui): settings focus nav --- CONTRIBUTING.md | 2 + i18n/locales/en.json | 4 +- tui/constants.go | 1 + tui/navigation_wrap_test.go | 84 +++++++++++++++++++++++++++++++++++++ tui/settings.go | 47 ++++++++++++++++++++- tui/settings_accounts.go | 18 +++----- tui/settings_crypto.go | 6 +-- tui/settings_encryption.go | 73 +++++++++++++++++++++----------- tui/settings_general.go | 9 ++-- tui/settings_lists.go | 18 +++----- tui/settings_plugins.go | 20 ++++----- tui/settings_theme.go | 9 ++-- tui/theme.go | 4 +- 13 files changed, 212 insertions(+), 83 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b3023c94..6b7ab239 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,6 +60,8 @@ type(scope): short description Common types: `feat`, `fix`, `docs`, `test`, `ci`, `chore`. +Keep commit messages to 40 characters or fewer. + Examples: ``` feat(compose): add CC/BCC field support diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 63b02153..54fdf7b0 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -122,8 +122,8 @@ "category_mailing_lists": "Mailing Lists", "category_encryption": "App Encryption", "category_plugins": "Plugins", - "help_menu": "↑/↓: navigate • right/enter: select • esc: go back", - "help_content": "esc: back to menu" + "help_menu": "↑/↓: navigate • right/enter: select • left/esc: go back", + "help_content": "left/esc: back to menu" }, "settings_accounts": { "title": "Account Settings", diff --git a/tui/constants.go b/tui/constants.go index 15952efb..72d8805c 100644 --- a/tui/constants.go +++ b/tui/constants.go @@ -3,6 +3,7 @@ package tui const ( keyEnter = "enter" keyDown = "down" + keyLeft = "left" keyRight = "right" keyCount = "count" keyINBOX = "INBOX" diff --git a/tui/navigation_wrap_test.go b/tui/navigation_wrap_test.go index b475e850..c7c11a8d 100644 --- a/tui/navigation_wrap_test.go +++ b/tui/navigation_wrap_test.go @@ -109,6 +109,90 @@ func TestSettingsNavigationWraps(t *testing.T) { }) } +func TestSettingsHorizontalPaneFocus(t *testing.T) { + t.Run("right moves focus from menu to content", func(t *testing.T) { + settings := NewSettings(&config.Config{}) + settings.activePane = PaneMenu + settings.menuCursor = int(CategoryGeneral) + + model, _ := settings.Update(tea.KeyPressMsg{Code: tea.KeyRight}) + settings = model.(*Settings) + + if settings.activePane != PaneContent { + t.Fatalf("right from menu pane should focus content, got %d", settings.activePane) + } + }) + + t.Run("left moves focus from content to menu", func(t *testing.T) { + settings := NewSettings(&config.Config{}) + settings.activePane = PaneContent + settings.activeCategory = CategoryGeneral + settings.menuCursor = int(CategoryGeneral) + + model, _ := settings.Update(tea.KeyPressMsg{Code: tea.KeyLeft}) + settings = model.(*Settings) + + if settings.activePane != PaneMenu { + t.Fatalf("left from content pane should focus menu, got %d", settings.activePane) + } + }) + + t.Run("left exits settings from menu", func(t *testing.T) { + settings := NewSettings(&config.Config{}) + settings.activePane = PaneMenu + + _, cmd := settings.Update(tea.KeyPressMsg{Code: tea.KeyLeft}) + if cmd == nil { + t.Fatal("left from menu pane should return to choice menu") + } + if _, ok := cmd().(GoToChoiceMenuMsg); !ok { + t.Fatalf("left from menu pane should emit GoToChoiceMenuMsg") + } + }) +} + +func TestSettingsEncryptionLeftKeyInInput(t *testing.T) { + t.Run("at input start returns to menu", func(t *testing.T) { + settings := NewSettings(&config.Config{}) + settings.activePane = PaneContent + settings.activeCategory = CategoryEncryption + settings.encFocusIndex = 0 + settings.encPasswordInput.SetValue("secret") + settings.encPasswordInput.SetCursor(0) + settings.encPasswordInput.Focus() + + model, _ := settings.Update(tea.KeyPressMsg{Code: tea.KeyLeft}) + settings = model.(*Settings) + + if settings.activePane != PaneMenu { + t.Fatalf("left at start of encryption input should focus menu, got %d", settings.activePane) + } + if settings.encPasswordInput.Value() != "" { + t.Fatal("left at start of encryption input should clear input like esc") + } + }) + + t.Run("inside input moves cursor", func(t *testing.T) { + settings := NewSettings(&config.Config{}) + settings.activePane = PaneContent + settings.activeCategory = CategoryEncryption + settings.encFocusIndex = 0 + settings.encPasswordInput.SetValue("secret") + settings.encPasswordInput.SetCursor(1) + settings.encPasswordInput.Focus() + + model, _ := settings.Update(tea.KeyPressMsg{Code: tea.KeyLeft}) + settings = model.(*Settings) + + if settings.activePane != PaneContent { + t.Fatalf("left inside encryption input should keep content focused, got %d", settings.activePane) + } + if settings.encPasswordInput.Position() != 0 { + t.Fatalf("left inside encryption input should move cursor left, got position %d", settings.encPasswordInput.Position()) + } + }) +} + func TestFilePickerNavigationWraps(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0o600); err != nil { diff --git a/tui/settings.go b/tui/settings.go index 84c8eb02..565f4a50 100644 --- a/tui/settings.go +++ b/tui/settings.go @@ -205,6 +205,11 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + if m.activePane == PaneContent && msg.String() == keyLeft && m.canFocusSettingsMenuWithLeft() { + m.activePane = PaneMenu + return m, nil + } + if m.activePane == PaneMenu { return m.updateMenu(msg) } @@ -270,6 +275,40 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } +func (m *Settings) canFocusSettingsMenuWithLeft() bool { + switch m.activeCategory { + case CategoryAccounts: + return !m.isCryptoConfig && !m.confirmingDelete + case CategoryEncryption: + return config.IsSecureModeEnabled() && !m.confirmingDisable + case CategoryPlugins: + return !m.pluginEditing && m.pluginSelected == "" + default: + return true + } +} + +func (m *Settings) contentItemStyle(selected bool) lipgloss.Style { + if selected && m.activePane == PaneContent { + return selectedAccountItemStyle + } + return accountItemStyle +} + +func (m *Settings) contentCursor(selected bool) string { + if selected && m.activePane == PaneContent { + return "> " + } + return " " +} + +func (m *Settings) contentFocusStyle(selected bool) lipgloss.Style { + if selected && m.activePane == PaneContent { + return settingsFocusedStyle + } + return settingsBlurredStyle +} + func (m *Settings) updateMenu(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { categoryCount := int(CategoryPlugins) + 1 @@ -308,7 +347,7 @@ func (m *Settings) updateMenu(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { } return m, textinput.Blink - case "esc": + case "esc", keyLeft: return m, func() tea.Msg { return GoToChoiceMenuMsg{} } } m.activeCategory = SettingsCategory(m.menuCursor) @@ -340,7 +379,11 @@ func (m *Settings) View() tea.View { style := accountItemStyle if m.menuCursor == i { - style = selectedAccountItemStyle + if m.activePane == PaneMenu { + style = selectedAccountItemStyle + } else { + style = selectedAccountItemStyle.UnsetBold() + } } left.WriteString(style.Render(cursor+c) + "\n") diff --git a/tui/settings_accounts.go b/tui/settings_accounts.go index 3734466d..fb0beb44 100644 --- a/tui/settings_accounts.go +++ b/tui/settings_accounts.go @@ -153,23 +153,17 @@ func (m *Settings) viewAccounts() string { line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(providerInfo)) - cursor := " " - style := accountItemStyle - if m.accountsCursor == i { - cursor = "> " - style = selectedAccountItemStyle - } + selected := m.accountsCursor == i + cursor := m.contentCursor(selected) + style := m.contentItemStyle(selected) b.WriteString(style.Render(cursor+line) + "\n") } // Add Account option - cursor := " " - style := accountItemStyle - if m.accountsCursor == len(m.cfg.Accounts) { - cursor = "> " - style = selectedAccountItemStyle - } + selected := m.accountsCursor == len(m.cfg.Accounts) + cursor := m.contentCursor(selected) + style := m.contentItemStyle(selected) b.WriteString(style.Render(cursor+t("settings_accounts.add_account")) + "\n\n") b.WriteString(helpStyle.Render(t("settings_accounts.help"))) diff --git a/tui/settings_crypto.go b/tui/settings_crypto.go index d15ee6c1..e541a629 100644 --- a/tui/settings_crypto.go +++ b/tui/settings_crypto.go @@ -121,7 +121,7 @@ func (m *Settings) viewSMIMEConfig() string { renderField := func(index int, label, content string) { if m.cryptoFocusIndex == index { - b.WriteString(settingsFocusedStyle.Render(label) + "\n") + b.WriteString(m.contentFocusStyle(true).Render(label) + "\n") } else { b.WriteString(settingsBlurredStyle.Render(label) + "\n") } @@ -162,12 +162,12 @@ func (m *Settings) viewSMIMEConfig() string { saveBtn := "[ Save ]" cancelBtn := "[ Cancel ]" if m.cryptoFocusIndex == 8 { - saveBtn = settingsFocusedStyle.Render(saveBtn) + saveBtn = m.contentFocusStyle(true).Render(saveBtn) } else { saveBtn = settingsBlurredStyle.Render(saveBtn) } if m.cryptoFocusIndex == 9 { - cancelBtn = settingsFocusedStyle.Render(cancelBtn) + cancelBtn = m.contentFocusStyle(true).Render(cancelBtn) } else { cancelBtn = settingsBlurredStyle.Render(cancelBtn) } diff --git a/tui/settings_encryption.go b/tui/settings_encryption.go index 2190a5d1..abae38f0 100644 --- a/tui/settings_encryption.go +++ b/tui/settings_encryption.go @@ -36,15 +36,14 @@ func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "esc": - // Clear inputs and return to menu - m.encPasswordInput.SetValue("") - m.encConfirmInput.SetValue("") - m.encPasswordStrength = "" - m.encPasswordInput.Blur() - m.encConfirmInput.Blur() - m.encError = "" - m.activePane = PaneMenu + m.leaveEncryptionSettings() return m, nil + case keyLeft: + if m.encryptionInputCursorAtStart() { + m.leaveEncryptionSettings() + return m, nil + } + return m.updateFocusedEncryptionInput(msg) case "tab", keyShiftTab, keyDown, "up": if msg.String() == keyShiftTab || msg.String() == "up" { m.encFocusIndex-- @@ -97,23 +96,47 @@ func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { } } default: - // Forward input to focused textinput - var cmd tea.Cmd - switch m.encFocusIndex { - case 0: - before := m.encPasswordInput.Value() - m.encPasswordInput, cmd = m.encPasswordInput.Update(msg) - if m.encPasswordInput.Value() != before { - m.handlePasswordChanged() - } - case 1: - m.encConfirmInput, cmd = m.encConfirmInput.Update(msg) - } - return m, cmd + return m.updateFocusedEncryptionInput(msg) } return m, nil } +func (m *Settings) encryptionInputCursorAtStart() bool { + switch m.encFocusIndex { + case 0: + return m.encPasswordInput.Position() == 0 + case 1: + return m.encConfirmInput.Position() == 0 + default: + return false + } +} + +func (m *Settings) leaveEncryptionSettings() { + m.encPasswordInput.SetValue("") + m.encConfirmInput.SetValue("") + m.encPasswordStrength = "" + m.encPasswordInput.Blur() + m.encConfirmInput.Blur() + m.encError = "" + m.activePane = PaneMenu +} + +func (m *Settings) updateFocusedEncryptionInput(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch m.encFocusIndex { + case 0: + before := m.encPasswordInput.Value() + m.encPasswordInput, cmd = m.encPasswordInput.Update(msg) + if m.encPasswordInput.Value() != before { + m.handlePasswordChanged() + } + case 1: + m.encConfirmInput, cmd = m.encConfirmInput.Update(msg) + } + return m, cmd +} + func (m *Settings) viewEncryption() string { var b strings.Builder isEnabled := config.IsSecureModeEnabled() @@ -131,7 +154,7 @@ func (m *Settings) viewEncryption() string { ) b.WriteString(dialog + "\n") } else { - b.WriteString(settingsFocusedStyle.Render(" "+t("settings_encryption.enabled")) + "\n\n") + b.WriteString(m.contentFocusStyle(true).Render(" "+t("settings_encryption.enabled")) + "\n\n") b.WriteString(accountEmailStyle.Render(" "+t("settings_encryption.disable_button")) + "\n\n") b.WriteString(helpStyle.Render("enter: disable")) } @@ -139,7 +162,7 @@ func (m *Settings) viewEncryption() string { b.WriteString(accountEmailStyle.Render(t("settings_encryption.disabled")) + "\n\n") if m.encFocusIndex == 0 { - b.WriteString(settingsFocusedStyle.Render(t("settings_encryption.password_label") + "\n")) + b.WriteString(m.contentFocusStyle(true).Render(t("settings_encryption.password_label") + "\n")) } else { b.WriteString(settingsBlurredStyle.Render(t("settings_encryption.password_label") + "\n")) } @@ -149,7 +172,7 @@ func (m *Settings) viewEncryption() string { } if m.encFocusIndex == 1 { - b.WriteString(settingsFocusedStyle.Render(t("settings_encryption.confirm_label") + "\n")) + b.WriteString(m.contentFocusStyle(true).Render(t("settings_encryption.confirm_label") + "\n")) } else { b.WriteString(settingsBlurredStyle.Render(t("settings_encryption.confirm_label") + "\n")) } @@ -161,7 +184,7 @@ func (m *Settings) viewEncryption() string { saveBtn := "[ " + t("settings_encryption.enable_button") + " ]" if m.encFocusIndex == 2 { - b.WriteString(settingsFocusedStyle.Render(saveBtn) + "\n") + b.WriteString(m.contentFocusStyle(true).Render(saveBtn) + "\n") } else { b.WriteString(settingsBlurredStyle.Render(saveBtn) + "\n") } diff --git a/tui/settings_general.go b/tui/settings_general.go index d8c6704b..89cfe397 100644 --- a/tui/settings_general.go +++ b/tui/settings_general.go @@ -130,12 +130,9 @@ func (m *Settings) viewGeneral() string { options := m.buildGeneralOptions() for i, opt := range options { - cursor := " " - style := accountItemStyle - if m.generalCursor == i { - cursor = "> " - style = selectedAccountItemStyle - } + selected := m.generalCursor == i + cursor := m.contentCursor(selected) + style := m.contentItemStyle(selected) label := t(opt.labelKey) text := fmt.Sprintf("%s: %s", label, opt.value) diff --git a/tui/settings_lists.go b/tui/settings_lists.go index 724736fc..d6c39d42 100644 --- a/tui/settings_lists.go +++ b/tui/settings_lists.go @@ -74,21 +74,15 @@ func (m *Settings) viewMailingLists() string { }) line := fmt.Sprintf("%s - %s", list.Name, accountEmailStyle.Render(addrCount)) - cursor := " " - style := accountItemStyle - if m.listsCursor == i { - cursor = "> " - style = selectedAccountItemStyle - } + selected := m.listsCursor == i + cursor := m.contentCursor(selected) + style := m.contentItemStyle(selected) b.WriteString(style.Render(cursor+line) + "\n") } - cursor := " " - style := accountItemStyle - if m.listsCursor == len(m.cfg.MailingLists) { - cursor = "> " - style = selectedAccountItemStyle - } + selected := m.listsCursor == len(m.cfg.MailingLists) + cursor := m.contentCursor(selected) + style := m.contentItemStyle(selected) b.WriteString(style.Render(cursor+t("settings_mailing_lists.add_list")) + "\n\n") b.WriteString(helpStyle.Render(t("settings_mailing_lists.help"))) diff --git a/tui/settings_plugins.go b/tui/settings_plugins.go index ecb883b9..353ef21f 100644 --- a/tui/settings_plugins.go +++ b/tui/settings_plugins.go @@ -156,12 +156,9 @@ func (m *Settings) viewPlugins() string { } for i, s := range schemas { - cursor := " " - style := accountItemStyle - if m.pluginListCursor == i { - cursor = "> " - style = selectedAccountItemStyle - } + selected := m.pluginListCursor == i + cursor := m.contentCursor(selected) + style := m.contentItemStyle(selected) line := fmt.Sprintf("%s (%d %s)", s.Plugin, len(s.Defs), pluralSettings(len(s.Defs))) b.WriteString(style.Render(cursor+line) + "\n") } @@ -174,12 +171,9 @@ func (m *Settings) viewPlugins() string { b.WriteString(accountEmailStyle.Render(m.pluginSelected) + "\n\n") for i, def := range defs { - cursor := " " - style := accountItemStyle - if m.pluginSettingCursor == i { - cursor = "> " - style = selectedAccountItemStyle - } + selected := m.pluginSettingCursor == i + cursor := m.contentCursor(selected) + style := m.contentItemStyle(selected) label := def.Label if label == "" { @@ -193,7 +187,7 @@ func (m *Settings) viewPlugins() string { if m.pluginEditing { b.WriteString("\n") - b.WriteString(settingsFocusedStyle.Render("Edit "+m.pluginEditingKey) + "\n") + b.WriteString(m.contentFocusStyle(true).Render("Edit "+m.pluginEditingKey) + "\n") b.WriteString(m.pluginInput.View() + "\n") b.WriteString("\n") b.WriteString(helpStyle.Render("enter save • esc cancel")) diff --git a/tui/settings_theme.go b/tui/settings_theme.go index 9a8fef44..3627b086 100644 --- a/tui/settings_theme.go +++ b/tui/settings_theme.go @@ -46,12 +46,9 @@ func (m *Settings) viewTheme() string { label += " (" + t("settings_theme.current") + ")" } - cursor := " " - style := accountItemStyle - if m.themeCursor == i { - cursor = "> " - style = selectedAccountItemStyle - } + selected := m.themeCursor == i + cursor := m.contentCursor(selected) + style := m.contentItemStyle(selected) b.WriteString(style.Render(cursor+label) + "\n") } diff --git a/tui/theme.go b/tui/theme.go index 12eacaee..38c155b1 100644 --- a/tui/theme.go +++ b/tui/theme.go @@ -47,10 +47,10 @@ func RebuildStyles() { // settings.go accountItemStyle = lipgloss.NewStyle().PaddingLeft(2) - selectedAccountItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(t.Accent) + selectedAccountItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(t.Accent).Bold(true) accountEmailStyle = lipgloss.NewStyle().Foreground(t.Secondary) dangerStyle = lipgloss.NewStyle().Foreground(t.Danger) - settingsFocusedStyle = lipgloss.NewStyle().Foreground(t.Accent) + settingsFocusedStyle = lipgloss.NewStyle().Foreground(t.Accent).Bold(true) settingsBlurredStyle = lipgloss.NewStyle().Foreground(t.Secondary) // composer.go From e3be3c0b2c10fb99d9597f0ffbe9bde6265e1e14 Mon Sep 17 00:00:00 2001 From: FromSi Date: Wed, 27 May 2026 16:07:19 +0500 Subject: [PATCH 2/4] fix(tui): settings focus nav --- tui/settings.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tui/settings.go b/tui/settings.go index 565f4a50..94a2a9b4 100644 --- a/tui/settings.go +++ b/tui/settings.go @@ -283,6 +283,8 @@ func (m *Settings) canFocusSettingsMenuWithLeft() bool { return config.IsSecureModeEnabled() && !m.confirmingDisable case CategoryPlugins: return !m.pluginEditing && m.pluginSelected == "" + case CategoryGeneral, CategoryTheme, CategoryMailingLists: + return true default: return true } From e5e22bc84495740a5097a3680b09647bb41ba717 Mon Sep 17 00:00:00 2001 From: FromSi Date: Wed, 27 May 2026 16:12:52 +0500 Subject: [PATCH 3/4] fix(tui): settings focus nav --- tui/settings.go | 76 ++++++++++++++++++++------------------ tui/settings_crypto.go | 6 +-- tui/settings_encryption.go | 8 ++-- tui/settings_plugins.go | 2 +- 4 files changed, 49 insertions(+), 43 deletions(-) diff --git a/tui/settings.go b/tui/settings.go index 94a2a9b4..3b4d0303 100644 --- a/tui/settings.go +++ b/tui/settings.go @@ -194,39 +194,7 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case tea.KeyPressMsg: - // Global shortcut to return to menu from content pane - if m.activePane == PaneContent && msg.String() == "esc" { - // unless we are in crypto config or encryption editing which have their own esc logic - if (m.activeCategory != CategoryAccounts || !m.isCryptoConfig) && - (m.activeCategory != CategoryEncryption || m.encFocusIndex <= -1) && - (m.activeCategory != CategoryPlugins || (!m.pluginEditing && m.pluginSelected == "")) { - m.activePane = PaneMenu - return m, nil - } - } - - if m.activePane == PaneContent && msg.String() == keyLeft && m.canFocusSettingsMenuWithLeft() { - m.activePane = PaneMenu - return m, nil - } - - if m.activePane == PaneMenu { - return m.updateMenu(msg) - } - switch m.activeCategory { - case CategoryGeneral: - return m.updateGeneral(msg) - case CategoryAccounts: - return m.updateAccounts(msg) - case CategoryTheme: - return m.updateTheme(msg) - case CategoryMailingLists: - return m.updateMailingLists(msg) - case CategoryEncryption: - return m.updateEncryption(msg) - case CategoryPlugins: - return m.updatePlugins(msg) - } + return m.updateKeyPress(msg) case SecureModeEnabledMsg: m.encEnabling = false @@ -275,6 +243,44 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } +func (m *Settings) updateKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + // Global shortcut to return to menu from content pane + if m.activePane == PaneContent && msg.String() == "esc" { + // unless we are in crypto config or encryption editing which have their own esc logic + if (m.activeCategory != CategoryAccounts || !m.isCryptoConfig) && + (m.activeCategory != CategoryEncryption || m.encFocusIndex <= -1) && + (m.activeCategory != CategoryPlugins || (!m.pluginEditing && m.pluginSelected == "")) { + m.activePane = PaneMenu + return m, nil + } + } + + if m.activePane == PaneContent && msg.String() == keyLeft && m.canFocusSettingsMenuWithLeft() { + m.activePane = PaneMenu + return m, nil + } + + if m.activePane == PaneMenu { + return m.updateMenu(msg) + } + switch m.activeCategory { + case CategoryGeneral: + return m.updateGeneral(msg) + case CategoryAccounts: + return m.updateAccounts(msg) + case CategoryTheme: + return m.updateTheme(msg) + case CategoryMailingLists: + return m.updateMailingLists(msg) + case CategoryEncryption: + return m.updateEncryption(msg) + case CategoryPlugins: + return m.updatePlugins(msg) + } + + return m, nil +} + func (m *Settings) canFocusSettingsMenuWithLeft() bool { switch m.activeCategory { case CategoryAccounts: @@ -304,8 +310,8 @@ func (m *Settings) contentCursor(selected bool) string { return " " } -func (m *Settings) contentFocusStyle(selected bool) lipgloss.Style { - if selected && m.activePane == PaneContent { +func (m *Settings) contentFocusStyle() lipgloss.Style { + if m.activePane == PaneContent { return settingsFocusedStyle } return settingsBlurredStyle diff --git a/tui/settings_crypto.go b/tui/settings_crypto.go index e541a629..b4e7361c 100644 --- a/tui/settings_crypto.go +++ b/tui/settings_crypto.go @@ -121,7 +121,7 @@ func (m *Settings) viewSMIMEConfig() string { renderField := func(index int, label, content string) { if m.cryptoFocusIndex == index { - b.WriteString(m.contentFocusStyle(true).Render(label) + "\n") + b.WriteString(m.contentFocusStyle().Render(label) + "\n") } else { b.WriteString(settingsBlurredStyle.Render(label) + "\n") } @@ -162,12 +162,12 @@ func (m *Settings) viewSMIMEConfig() string { saveBtn := "[ Save ]" cancelBtn := "[ Cancel ]" if m.cryptoFocusIndex == 8 { - saveBtn = m.contentFocusStyle(true).Render(saveBtn) + saveBtn = m.contentFocusStyle().Render(saveBtn) } else { saveBtn = settingsBlurredStyle.Render(saveBtn) } if m.cryptoFocusIndex == 9 { - cancelBtn = m.contentFocusStyle(true).Render(cancelBtn) + cancelBtn = m.contentFocusStyle().Render(cancelBtn) } else { cancelBtn = settingsBlurredStyle.Render(cancelBtn) } diff --git a/tui/settings_encryption.go b/tui/settings_encryption.go index abae38f0..ef0903a1 100644 --- a/tui/settings_encryption.go +++ b/tui/settings_encryption.go @@ -154,7 +154,7 @@ func (m *Settings) viewEncryption() string { ) b.WriteString(dialog + "\n") } else { - b.WriteString(m.contentFocusStyle(true).Render(" "+t("settings_encryption.enabled")) + "\n\n") + b.WriteString(m.contentFocusStyle().Render(" "+t("settings_encryption.enabled")) + "\n\n") b.WriteString(accountEmailStyle.Render(" "+t("settings_encryption.disable_button")) + "\n\n") b.WriteString(helpStyle.Render("enter: disable")) } @@ -162,7 +162,7 @@ func (m *Settings) viewEncryption() string { b.WriteString(accountEmailStyle.Render(t("settings_encryption.disabled")) + "\n\n") if m.encFocusIndex == 0 { - b.WriteString(m.contentFocusStyle(true).Render(t("settings_encryption.password_label") + "\n")) + b.WriteString(m.contentFocusStyle().Render(t("settings_encryption.password_label") + "\n")) } else { b.WriteString(settingsBlurredStyle.Render(t("settings_encryption.password_label") + "\n")) } @@ -172,7 +172,7 @@ func (m *Settings) viewEncryption() string { } if m.encFocusIndex == 1 { - b.WriteString(m.contentFocusStyle(true).Render(t("settings_encryption.confirm_label") + "\n")) + b.WriteString(m.contentFocusStyle().Render(t("settings_encryption.confirm_label") + "\n")) } else { b.WriteString(settingsBlurredStyle.Render(t("settings_encryption.confirm_label") + "\n")) } @@ -184,7 +184,7 @@ func (m *Settings) viewEncryption() string { saveBtn := "[ " + t("settings_encryption.enable_button") + " ]" if m.encFocusIndex == 2 { - b.WriteString(m.contentFocusStyle(true).Render(saveBtn) + "\n") + b.WriteString(m.contentFocusStyle().Render(saveBtn) + "\n") } else { b.WriteString(settingsBlurredStyle.Render(saveBtn) + "\n") } diff --git a/tui/settings_plugins.go b/tui/settings_plugins.go index 353ef21f..dd48cfbc 100644 --- a/tui/settings_plugins.go +++ b/tui/settings_plugins.go @@ -187,7 +187,7 @@ func (m *Settings) viewPlugins() string { if m.pluginEditing { b.WriteString("\n") - b.WriteString(m.contentFocusStyle(true).Render("Edit "+m.pluginEditingKey) + "\n") + b.WriteString(m.contentFocusStyle().Render("Edit "+m.pluginEditingKey) + "\n") b.WriteString(m.pluginInput.View() + "\n") b.WriteString("\n") b.WriteString(helpStyle.Render("enter save • esc cancel")) From 52292fa5258064993ba66917f633648e568ccf2f Mon Sep 17 00:00:00 2001 From: FromSi Date: Thu, 28 May 2026 13:46:19 +0500 Subject: [PATCH 4/4] fix(tui): settings focus nav --- i18n/locales/en.json | 2 +- tui/navigation_wrap_test.go | 28 ++++++++++++++++++++++------ tui/settings.go | 2 +- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 54fdf7b0..770d0100 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -122,7 +122,7 @@ "category_mailing_lists": "Mailing Lists", "category_encryption": "App Encryption", "category_plugins": "Plugins", - "help_menu": "↑/↓: navigate • right/enter: select • left/esc: go back", + "help_menu": "↑/↓: navigate • right/enter: select • esc: go back", "help_content": "left/esc: back to menu" }, "settings_accounts": { diff --git a/tui/navigation_wrap_test.go b/tui/navigation_wrap_test.go index c7c11a8d..87793ca7 100644 --- a/tui/navigation_wrap_test.go +++ b/tui/navigation_wrap_test.go @@ -123,6 +123,20 @@ func TestSettingsHorizontalPaneFocus(t *testing.T) { } }) + t.Run("esc moves focus from content to menu", func(t *testing.T) { + settings := NewSettings(&config.Config{}) + settings.activePane = PaneContent + settings.activeCategory = CategoryGeneral + settings.menuCursor = int(CategoryGeneral) + + model, _ := settings.Update(tea.KeyPressMsg{Code: tea.KeyEsc}) + settings = model.(*Settings) + + if settings.activePane != PaneMenu { + t.Fatalf("esc from content pane should focus menu, got %d", settings.activePane) + } + }) + t.Run("left moves focus from content to menu", func(t *testing.T) { settings := NewSettings(&config.Config{}) settings.activePane = PaneContent @@ -137,16 +151,18 @@ func TestSettingsHorizontalPaneFocus(t *testing.T) { } }) - t.Run("left exits settings from menu", func(t *testing.T) { + t.Run("left does not exit settings from menu", func(t *testing.T) { settings := NewSettings(&config.Config{}) settings.activePane = PaneMenu - _, cmd := settings.Update(tea.KeyPressMsg{Code: tea.KeyLeft}) - if cmd == nil { - t.Fatal("left from menu pane should return to choice menu") + model, cmd := settings.Update(tea.KeyPressMsg{Code: tea.KeyLeft}) + settings = model.(*Settings) + + if cmd != nil { + t.Fatal("left from menu pane should not return to choice menu") } - if _, ok := cmd().(GoToChoiceMenuMsg); !ok { - t.Fatalf("left from menu pane should emit GoToChoiceMenuMsg") + if settings.activePane != PaneMenu { + t.Fatalf("left from menu pane should keep menu focused, got %d", settings.activePane) } }) } diff --git a/tui/settings.go b/tui/settings.go index 3b4d0303..e41e37e8 100644 --- a/tui/settings.go +++ b/tui/settings.go @@ -355,7 +355,7 @@ func (m *Settings) updateMenu(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { } return m, textinput.Blink - case "esc", keyLeft: + case "esc": return m, func() tea.Msg { return GoToChoiceMenuMsg{} } } m.activeCategory = SettingsCategory(m.menuCursor)