Detailed changes
@@ -206,13 +206,13 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.focusOnAttachments {
switch msg.String() {
case "up", kb.Global.NavUp:
- if m.attachmentCursor > 0 {
- m.attachmentCursor--
+ if len(m.email.Attachments) > 0 {
+ m.attachmentCursor = (m.attachmentCursor - 1 + len(m.email.Attachments)) % len(m.email.Attachments)
}
return m, nil
case "down", kb.Global.NavDown:
- if m.attachmentCursor < len(m.email.Attachments)-1 {
- m.attachmentCursor++
+ if len(m.email.Attachments) > 0 {
+ m.attachmentCursor = (m.attachmentCursor + 1) % len(m.email.Attachments)
}
return m, nil
case "enter":
@@ -79,18 +79,18 @@ func TestEmailViewUpdate(t *testing.T) {
t.Errorf("After one down arrow, attachmentCursor should be 1, got %d", emailView.attachmentCursor)
}
- // Move down again (should not go past the end)
+ // Move down again (should wrap to the first attachment)
model, _ = emailView.Update(tea.KeyPressMsg{Code: tea.KeyDown})
emailView = model.(*EmailView)
- if emailView.attachmentCursor != 1 {
- t.Errorf("attachmentCursor should not go past the end of the list, got %d", emailView.attachmentCursor)
+ if emailView.attachmentCursor != 0 {
+ t.Errorf("attachmentCursor should wrap to the start of the list, got %d", emailView.attachmentCursor)
}
- // Move up
+ // Move up (should wrap to the last attachment)
model, _ = emailView.Update(tea.KeyPressMsg{Code: tea.KeyUp})
emailView = model.(*EmailView)
- if emailView.attachmentCursor != 0 {
- t.Errorf("After one up arrow, attachmentCursor should be 0, got %d", emailView.attachmentCursor)
+ if emailView.attachmentCursor != 1 {
+ t.Errorf("After one up arrow from the first item, attachmentCursor should be 1, got %d", emailView.attachmentCursor)
}
})
@@ -120,12 +120,12 @@ func (m *FilePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Normal browsing mode
switch msg.String() {
case "up", "k":
- if m.cursor > 0 {
- m.cursor--
+ if len(m.items) > 0 {
+ m.cursor = (m.cursor - 1 + len(m.items)) % len(m.items)
}
case "down", "j":
- if m.cursor < len(m.items)-1 {
- m.cursor++
+ if len(m.items) > 0 {
+ m.cursor = (m.cursor + 1) % len(m.items)
}
case "/":
m.editingPath = true
@@ -0,0 +1,156 @@
+package tui
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ tea "charm.land/bubbletea/v2"
+ "github.com/floatpane/matcha/config"
+ "github.com/floatpane/matcha/theme"
+)
+
+func TestSettingsNavigationWraps(t *testing.T) {
+ settings := NewSettings(&config.Config{
+ Accounts: []config.Account{
+ {ID: "account-1", Email: "one@example.com"},
+ {ID: "account-2", Email: "two@example.com"},
+ },
+ MailingLists: []config.MailingList{
+ {Name: "List One"},
+ {Name: "List Two"},
+ },
+ })
+
+ t.Run("menu", func(t *testing.T) {
+ settings.menuCursor = 0
+ model, _ := settings.updateMenu(tea.KeyPressMsg{Code: tea.KeyUp})
+ settings = model.(*Settings)
+ if settings.menuCursor != int(CategoryEncryption) {
+ t.Fatalf("up from first menu item should wrap to last, got %d", settings.menuCursor)
+ }
+
+ model, _ = settings.updateMenu(tea.KeyPressMsg{Code: tea.KeyDown})
+ settings = model.(*Settings)
+ if settings.menuCursor != 0 {
+ t.Fatalf("down from last menu item should wrap to first, got %d", settings.menuCursor)
+ }
+ })
+
+ t.Run("general", func(t *testing.T) {
+ settings.generalCursor = 0
+ last := len(settings.buildGeneralOptions()) - 1
+
+ model, _ := settings.updateGeneral(tea.KeyPressMsg{Code: tea.KeyUp})
+ settings = model.(*Settings)
+ if settings.generalCursor != last {
+ t.Fatalf("up from first general item should wrap to last, got %d", settings.generalCursor)
+ }
+
+ model, _ = settings.updateGeneral(tea.KeyPressMsg{Code: tea.KeyDown})
+ settings = model.(*Settings)
+ if settings.generalCursor != 0 {
+ t.Fatalf("down from last general item should wrap to first, got %d", settings.generalCursor)
+ }
+ })
+
+ t.Run("accounts", func(t *testing.T) {
+ settings.accountsCursor = 0
+ last := len(settings.cfg.Accounts)
+
+ model, _ := settings.updateAccounts(tea.KeyPressMsg{Code: tea.KeyUp})
+ settings = model.(*Settings)
+ if settings.accountsCursor != last {
+ t.Fatalf("up from first account item should wrap to add account, got %d", settings.accountsCursor)
+ }
+
+ model, _ = settings.updateAccounts(tea.KeyPressMsg{Code: tea.KeyDown})
+ settings = model.(*Settings)
+ if settings.accountsCursor != 0 {
+ t.Fatalf("down from add account should wrap to first, got %d", settings.accountsCursor)
+ }
+ })
+
+ t.Run("mailing lists", func(t *testing.T) {
+ settings.listsCursor = 0
+ last := len(settings.cfg.MailingLists)
+
+ model, _ := settings.updateMailingLists(tea.KeyPressMsg{Code: tea.KeyUp})
+ settings = model.(*Settings)
+ if settings.listsCursor != last {
+ t.Fatalf("up from first mailing list should wrap to add list, got %d", settings.listsCursor)
+ }
+
+ model, _ = settings.updateMailingLists(tea.KeyPressMsg{Code: tea.KeyDown})
+ settings = model.(*Settings)
+ if settings.listsCursor != 0 {
+ t.Fatalf("down from add list should wrap to first, got %d", settings.listsCursor)
+ }
+ })
+
+ t.Run("theme", func(t *testing.T) {
+ themes := theme.AllThemes()
+ if len(themes) < 2 {
+ t.Skip("need at least two themes to test wrap-around")
+ }
+
+ settings.themeCursor = 0
+ model, _ := settings.updateTheme(tea.KeyPressMsg{Code: tea.KeyUp})
+ settings = model.(*Settings)
+ if settings.themeCursor != len(themes)-1 {
+ t.Fatalf("up from first theme should wrap to last, got %d", settings.themeCursor)
+ }
+
+ model, _ = settings.updateTheme(tea.KeyPressMsg{Code: tea.KeyDown})
+ settings = model.(*Settings)
+ if settings.themeCursor != 0 {
+ t.Fatalf("down from last theme should wrap to first, got %d", settings.themeCursor)
+ }
+ })
+}
+
+func TestFilePickerNavigationWraps(t *testing.T) {
+ dir := t.TempDir()
+ if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "b.txt"), []byte("b"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+
+ picker := NewFilePicker(dir)
+ if len(picker.items) != 2 {
+ t.Fatalf("expected two picker items, got %d", len(picker.items))
+ }
+
+ model, _ := picker.Update(tea.KeyPressMsg{Code: tea.KeyUp})
+ picker = model.(*FilePicker)
+ if picker.cursor != len(picker.items)-1 {
+ t.Fatalf("up from first file should wrap to last, got %d", picker.cursor)
+ }
+
+ model, _ = picker.Update(tea.KeyPressMsg{Code: tea.KeyDown})
+ picker = model.(*FilePicker)
+ if picker.cursor != 0 {
+ t.Fatalf("down from last file should wrap to first, got %d", picker.cursor)
+ }
+}
+
+func TestFilePickerNavigationEmptyDirectoryDoesNotWrap(t *testing.T) {
+ picker := NewFilePicker(t.TempDir())
+ if len(picker.items) != 0 {
+ t.Fatalf("expected empty picker, got %d items", len(picker.items))
+ }
+
+ model, _ := picker.Update(tea.KeyPressMsg{Code: tea.KeyUp})
+ picker = model.(*FilePicker)
+ if picker.cursor != 0 {
+ t.Fatalf("empty picker cursor should remain zero after up, got %d", picker.cursor)
+ }
+
+ model, _ = picker.Update(tea.KeyPressMsg{Code: tea.KeyDown})
+ picker = model.(*FilePicker)
+ if picker.cursor != 0 {
+ t.Fatalf("empty picker cursor should remain zero after down, got %d", picker.cursor)
+ }
+}
@@ -237,15 +237,13 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (m *Settings) updateMenu(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
+ categoryCount := int(CategoryEncryption) + 1
+
switch msg.String() {
case "up", "k":
- if m.menuCursor > 0 {
- m.menuCursor--
- }
+ m.menuCursor = (m.menuCursor - 1 + categoryCount) % categoryCount
case "down", "j":
- if m.menuCursor < 4 {
- m.menuCursor++
- }
+ m.menuCursor = (m.menuCursor + 1) % categoryCount
case "right", "l", "enter":
m.activeCategory = SettingsCategory(m.menuCursor)
m.activePane = PaneContent
@@ -37,13 +37,11 @@ func (m *Settings) updateAccounts(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "up", "k":
- if m.accountsCursor > 0 {
- m.accountsCursor--
- }
+ itemCount := len(m.cfg.Accounts) + 1
+ m.accountsCursor = (m.accountsCursor - 1 + itemCount) % itemCount
case "down", "j":
- if m.accountsCursor < len(m.cfg.Accounts) {
- m.accountsCursor++
- }
+ itemCount := len(m.cfg.Accounts) + 1
+ m.accountsCursor = (m.accountsCursor + 1) % itemCount
case "d":
if m.accountsCursor < len(m.cfg.Accounts) && len(m.cfg.Accounts) > 0 {
m.confirmingDelete = true
@@ -51,13 +51,9 @@ func (m *Settings) updateGeneral(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "up", "k":
- if m.generalCursor > 0 {
- m.generalCursor--
- }
+ m.generalCursor = (m.generalCursor - 1 + len(opts)) % len(opts)
case "down", "j":
- if m.generalCursor < len(opts)-1 {
- m.generalCursor++
- }
+ m.generalCursor = (m.generalCursor + 1) % len(opts)
case "enter", "space", "right", "l":
if m.generalCursor < len(opts) {
opt := opts[m.generalCursor]
@@ -30,13 +30,11 @@ func (m *Settings) updateMailingLists(msg tea.KeyPressMsg) (tea.Model, tea.Cmd)
switch msg.String() {
case "up", "k":
- if m.listsCursor > 0 {
- m.listsCursor--
- }
+ itemCount := len(m.cfg.MailingLists) + 1
+ m.listsCursor = (m.listsCursor - 1 + itemCount) % itemCount
case "down", "j":
- if m.listsCursor < len(m.cfg.MailingLists) {
- m.listsCursor++
- }
+ itemCount := len(m.cfg.MailingLists) + 1
+ m.listsCursor = (m.listsCursor + 1) % itemCount
case "d":
if m.listsCursor < len(m.cfg.MailingLists) && len(m.cfg.MailingLists) > 0 {
m.confirmingDelete = true
@@ -14,12 +14,12 @@ func (m *Settings) updateTheme(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "up", "k":
- if m.themeCursor > 0 {
- m.themeCursor--
+ if len(themes) > 0 {
+ m.themeCursor = (m.themeCursor - 1 + len(themes)) % len(themes)
}
case "down", "j":
- if m.themeCursor < len(themes)-1 {
- m.themeCursor++
+ if len(themes) > 0 {
+ m.themeCursor = (m.themeCursor + 1) % len(themes)
}
case "enter", "space", "right", "l":
if m.themeCursor < len(themes) {