Detailed changes
@@ -123,7 +123,7 @@
"category_encryption": "تشفير التطبيق",
"category_plugins": "الإضافات",
"help_menu": "↑/↓: التنقل • يمين/enter: اختيار • esc: رجوع",
- "help_content": "esc: العودة للقائمة"
+ "help_content": "left/esc: العودة للقائمة"
},
"settings_accounts": {
"title": "إعدادات الحسابات",
@@ -121,7 +121,7 @@
"category_encryption": "App-Verschlüsselung",
"category_plugins": "Plugins",
"help_menu": "↑/↓: navigieren • rechts/enter: auswählen • esc: zurück",
- "help_content": "esc: zurück zum Menü"
+ "help_content": "left/esc: zurück zum Menü"
},
"settings_accounts": {
"title": "Kontoeinstellungen",
@@ -123,7 +123,7 @@
"category_encryption": "App Encryption",
"category_plugins": "Plugins",
"help_menu": "↑/↓: navigate • right/enter: select • esc: go back",
- "help_content": "esc: back to menu"
+ "help_content": "left/esc: back to menu"
},
"settings_accounts": {
"title": "Account Settings",
@@ -121,7 +121,7 @@
"category_encryption": "Cifrado de Aplicación",
"category_plugins": "Plugins",
"help_menu": "↑/↓: navegar • derecha/enter: seleccionar • esc: volver",
- "help_content": "esc: volver al menú"
+ "help_content": "left/esc: volver al menú"
},
"settings_accounts": {
"title": "Configuración de Cuentas",
@@ -121,7 +121,7 @@
"category_encryption": "Chiffrement de l'Application",
"category_plugins": "Plugins",
"help_menu": "↑/↓: naviguer • droite/entrée: sélectionner • esc: retour",
- "help_content": "esc: retour au menu"
+ "help_content": "left/esc: retour au menu"
},
"settings_accounts": {
"title": "Paramètres des Comptes",
@@ -120,7 +120,7 @@
"category_encryption": "アプリの暗号化",
"category_plugins": "プラグイン",
"help_menu": "↑/↓: 移動 • 右/enter: 選択 • esc: 戻る",
- "help_content": "esc: メニューに戻る"
+ "help_content": "left/esc: メニューに戻る"
},
"settings_accounts": {
"title": "アカウント設定",
@@ -123,7 +123,7 @@
"category_encryption": "Szyfrowanie Aplikacji",
"category_plugins": "Wtyczki",
"help_menu": "↑/↓: nawigacja • prawo/enter: wybierz • esc: wstecz",
- "help_content": "esc: powrót do menu"
+ "help_content": "left/esc: powrót do menu"
},
"settings_accounts": {
"title": "Ustawienia Kont",
@@ -121,7 +121,7 @@
"category_encryption": "Criptografia do Aplicativo",
"category_plugins": "Plugins",
"help_menu": "↑/↓: navegar • direita/enter: selecionar • esc: voltar",
- "help_content": "esc: voltar ao menu"
+ "help_content": "left/esc: voltar ao menu"
},
"settings_accounts": {
"title": "Configurações de Contas",
@@ -123,7 +123,7 @@
"category_encryption": "Шифрование Приложения",
"category_plugins": "Плагины",
"help_menu": "↑/↓: навигация • вправо/enter: выбор • esc: назад",
- "help_content": "esc: назад в меню"
+ "help_content": "left/esc: назад в меню"
},
"settings_accounts": {
"title": "Настройки Учётных Записей",
@@ -122,7 +122,7 @@
"category_encryption": "Шифрування додатка",
"category_plugins": "Плагіни",
"help_menu": "↑/↓: навігація • right/enter: вибрати • esc: назад",
- "help_content": "esc: назад до меню"
+ "help_content": "left/esc: назад до меню"
},
"settings_accounts": {
"title": "Налаштування облікових записів",
@@ -120,7 +120,7 @@
"category_encryption": "应用加密",
"category_plugins": "插件",
"help_menu": "↑/↓: 导航 • 右/enter: 选择 • esc: 返回",
- "help_content": "esc: 返回菜单"
+ "help_content": "left/esc: 返回菜单"
},
"settings_accounts": {
"title": "账户设置",
@@ -3,6 +3,7 @@ package tui
const (
keyEnter = "enter"
keyDown = "down"
+ keyLeft = "left"
keyRight = "right"
keyCount = "count"
keyINBOX = "INBOX"
@@ -109,6 +109,106 @@ 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("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
+ 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 does not exit settings from menu", func(t *testing.T) {
+ settings := NewSettings(&config.Config{})
+ settings.activePane = PaneMenu
+
+ 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 settings.activePane != PaneMenu {
+ t.Fatalf("left from menu pane should keep menu focused, got %d", settings.activePane)
+ }
+ })
+}
+
+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 {
@@ -194,34 +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 == 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
@@ -270,6 +243,80 @@ 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:
+ return !m.isCryptoConfig && !m.confirmingDelete
+ case CategoryEncryption:
+ return config.IsSecureModeEnabled() && !m.confirmingDisable
+ case CategoryPlugins:
+ return !m.pluginEditing && m.pluginSelected == ""
+ case CategoryGeneral, CategoryTheme, CategoryMailingLists:
+ return true
+ 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() lipgloss.Style {
+ if m.activePane == PaneContent {
+ return settingsFocusedStyle
+ }
+ return settingsBlurredStyle
+}
+
func (m *Settings) updateMenu(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
categoryCount := int(CategoryPlugins) + 1
@@ -340,7 +387,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")
@@ -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")))
@@ -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().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().Render(saveBtn)
} else {
saveBtn = settingsBlurredStyle.Render(saveBtn)
}
if m.cryptoFocusIndex == 9 {
- cancelBtn = settingsFocusedStyle.Render(cancelBtn)
+ cancelBtn = m.contentFocusStyle().Render(cancelBtn)
} else {
cancelBtn = settingsBlurredStyle.Render(cancelBtn)
}
@@ -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().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().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().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().Render(saveBtn) + "\n")
} else {
b.WriteString(settingsBlurredStyle.Render(saveBtn) + "\n")
}
@@ -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)
@@ -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")))
@@ -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().Render("Edit "+m.pluginEditingKey) + "\n")
b.WriteString(m.pluginInput.View() + "\n")
b.WriteString("\n")
b.WriteString(helpStyle.Render("enter save • esc cancel"))
@@ -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")
}
@@ -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