diff --git a/tui/email_view.go b/tui/email_view.go index 978092a85dbc774ecaeb1c3bb3806e6fa11f1259..c21a2b667877b4efb5348f9241c3962443b3c0cc 100644 --- a/tui/email_view.go +++ b/tui/email_view.go @@ -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": diff --git a/tui/email_view_test.go b/tui/email_view_test.go index 18f825ff3c62cc122951c89dde247708a30f2032..e55f6050532866fbe4fabaea280bb54a169e5158 100644 --- a/tui/email_view_test.go +++ b/tui/email_view_test.go @@ -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) } }) diff --git a/tui/filepicker.go b/tui/filepicker.go index 883deede025311124b8d3e49b152cc35025e233c..44ee88d084a3c83e4b4b14ae959e7673b93dc324 100644 --- a/tui/filepicker.go +++ b/tui/filepicker.go @@ -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 diff --git a/tui/navigation_wrap_test.go b/tui/navigation_wrap_test.go new file mode 100644 index 0000000000000000000000000000000000000000..dd906f93b74a3d198d8be36e42ddc7f558fac9ef --- /dev/null +++ b/tui/navigation_wrap_test.go @@ -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) + } +} diff --git a/tui/settings.go b/tui/settings.go index d8e7363ec2839581b5ad85c37de360ed6d3c2f2e..7f20e803ed24f868964313f9ba86c8bff6241294 100644 --- a/tui/settings.go +++ b/tui/settings.go @@ -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 diff --git a/tui/settings_accounts.go b/tui/settings_accounts.go index 7ca07f3a1442a4893d6448c840334189de4e0291..72fc52fb983189b871d06facb6d4f9c362e1f86a 100644 --- a/tui/settings_accounts.go +++ b/tui/settings_accounts.go @@ -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 diff --git a/tui/settings_general.go b/tui/settings_general.go index 9a06651124afca7332665ff59645bd738623ab8e..d4c89ead8781041ee226f046d35c0baeeb8546de 100644 --- a/tui/settings_general.go +++ b/tui/settings_general.go @@ -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] diff --git a/tui/settings_lists.go b/tui/settings_lists.go index abc44c2b7520e7875627b1a4f39fa5b5988bc854..cdd01098df254621642d1388908a70cdd034e3bb 100644 --- a/tui/settings_lists.go +++ b/tui/settings_lists.go @@ -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 diff --git a/tui/settings_theme.go b/tui/settings_theme.go index d3cc2e292b561129dc41a93d65cda47b2284bb6d..bcc4715407b7939e66d10729ad520639d49ff50d 100644 --- a/tui/settings_theme.go +++ b/tui/settings_theme.go @@ -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) {