From acb4b5ed4d730bda65ef01cbed5e9dbb3bbc4df8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:04:05 +0000 Subject: [PATCH 1/3] fix(deps): update module github.com/charmbracelet/bubbletea to v2 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e136c62..155f09d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26.0 require ( github.com/PuerkitoBio/goquery v1.11.0 github.com/charmbracelet/bubbles v1.0.0 - github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/bubbletea/v2 v2.0.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/emersion/go-imap v1.2.1 github.com/emersion/go-message v0.18.2 From 115512133b99cf821971e3bfe5492635d7b89ee1 Mon Sep 17 00:00:00 2001 From: drew Date: Wed, 25 Feb 2026 21:55:47 +0400 Subject: [PATCH 2/3] chore(scope): migrate to charm v2 --- go.mod | 18 ++++++++++----- go.sum | 40 +++++++++++++++++++++----------- main.go | 12 ++++++---- tui/choice.go | 10 ++++---- tui/composer.go | 55 +++++++++++++++++--------------------------- tui/drafts.go | 19 +++++++-------- tui/email_view.go | 31 ++++++++++++++----------- tui/filepicker.go | 10 ++++---- tui/inbox.go | 19 +++++++-------- tui/login.go | 23 +++++++++--------- tui/settings.go | 16 ++++++------- tui/signature.go | 20 ++++++++-------- tui/styles.go | 10 ++++---- tui/trash_archive.go | 16 ++++++------- view/html.go | 2 +- 15 files changed, 156 insertions(+), 145 deletions(-) diff --git a/go.mod b/go.mod index 155f09d..7347409 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/floatpane/matcha go 1.26.0 require ( + charm.land/bubbletea/v2 v2.0.0 + charm.land/lipgloss/v2 v2.0.0 github.com/PuerkitoBio/goquery v1.11.0 github.com/charmbracelet/bubbles v1.0.0 - github.com/charmbracelet/bubbletea/v2 v2.0.0 + github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/emersion/go-imap v1.2.1 github.com/emersion/go-message v0.18.2 @@ -16,22 +18,25 @@ require ( ) require ( + charm.land/bubbles/v2 v2.0.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/colorprofile v0.4.1 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/clipperhouse/displaywidth v0.9.0 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.5.0 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect @@ -39,4 +44,5 @@ require ( github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/net v0.47.0 // indirect + golang.org/x/sync v0.19.0 // indirect ) diff --git a/go.sum b/go.sum index 7ac02f1..235cffc 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,9 @@ +charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= +charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= +charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ= +charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= +charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo= +charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= @@ -8,30 +14,34 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= -github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM= +github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= -github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= +github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= -github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= -github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U= -github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= @@ -54,8 +64,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -102,6 +112,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/main.go b/main.go index 0730746..01e55b1 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,7 @@ import ( "sync" "time" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/floatpane/matcha/config" "github.com/floatpane/matcha/fetcher" "github.com/floatpane/matcha/sender" @@ -112,7 +112,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height return m, nil - case tea.KeyMsg: + case tea.KeyPressMsg: if msg.String() == "ctrl+c" { return m, tea.Quit } @@ -856,8 +856,10 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } -func (m *mainModel) View() string { - return m.current.View() +func (m *mainModel) View() tea.View { + v := m.current.View() + v.AltScreen = true + return v } func (m *mainModel) getEmailByIndex(index int, mailbox tui.MailboxKind) *fetcher.Email { @@ -1881,7 +1883,7 @@ func main() { initialModel = newInitialModel(cfg) } - p := tea.NewProgram(initialModel, tea.WithAltScreen()) + p := tea.NewProgram(initialModel) if _, err := p.Run(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) diff --git a/tui/choice.go b/tui/choice.go index f9890e9..56ccbbe 100644 --- a/tui/choice.go +++ b/tui/choice.go @@ -5,8 +5,8 @@ import ( "reflect" "strings" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/floatpane/matcha/config" ) @@ -61,7 +61,7 @@ func (m Choice) Init() tea.Cmd { func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "up", "k": if m.cursor > 0 { @@ -115,7 +115,7 @@ func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m Choice) View() string { +func (m Choice) View() tea.View { var b strings.Builder b.WriteString(logoStyle.Render(choiceLogo)) @@ -147,5 +147,5 @@ func (m Choice) View() string { b.WriteString("\n\n") b.WriteString(helpStyle.Render("Use ↑/↓ to navigate, enter to select, and ctrl+c to quit.")) - return docStyle.Render(b.String()) + return tea.NewView(docStyle.Render(b.String())) } diff --git a/tui/composer.go b/tui/composer.go index 5c9d137..3b9bb34 100644 --- a/tui/composer.go +++ b/tui/composer.go @@ -4,10 +4,10 @@ import ( "fmt" "strings" - "github.com/charmbracelet/bubbles/textarea" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/textarea" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/floatpane/matcha/config" "github.com/google/uuid" ) @@ -22,7 +22,6 @@ var ( var ( focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")) blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - cursorStyle = focusedStyle.Copy() noStyle = lipgloss.NewStyle() helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) focusedButton = focusedStyle.Copy().Render("[ Send ]") @@ -87,41 +86,34 @@ func NewComposer(from, to, subject, body string) *Composer { } m.toInput = textinput.New() - m.toInput.Cursor.Style = cursorStyle m.toInput.Placeholder = "To" m.toInput.SetValue(to) m.toInput.Prompt = "> " m.toInput.CharLimit = 256 m.ccInput = textinput.New() - m.ccInput.Cursor.Style = cursorStyle m.ccInput.Placeholder = "Cc" m.ccInput.Prompt = "> " m.ccInput.CharLimit = 256 m.bccInput = textinput.New() - m.bccInput.Cursor.Style = cursorStyle m.bccInput.Placeholder = "Bcc" m.bccInput.Prompt = "> " m.bccInput.CharLimit = 256 m.subjectInput = textinput.New() - m.subjectInput.Cursor.Style = cursorStyle m.subjectInput.Placeholder = "Subject" m.subjectInput.SetValue(subject) m.subjectInput.Prompt = "> " m.subjectInput.CharLimit = 256 m.bodyInput = textarea.New() - m.bodyInput.Cursor.Style = cursorStyle m.bodyInput.Placeholder = "Body (Markdown supported)..." m.bodyInput.SetValue(body) m.bodyInput.Prompt = "> " m.bodyInput.SetHeight(10) - m.bodyInput.SetCursor(0) m.signatureInput = textarea.New() - m.signatureInput.Cursor.Style = cursorStyle m.signatureInput.Placeholder = "Signature (optional)..." m.signatureInput.Prompt = "> " m.signatureInput.SetHeight(3) @@ -153,7 +145,7 @@ func NewComposerWithAccounts(accounts []config.Account, selectedAccountID string return m } -// ResetConfirmation ensures a restored draft isn't stuck in the exit prompt. +// ResetConfirmation ensures a restored draft isnt stuck in the exit prompt. func (m *Composer) ResetConfirmation() { m.confirmingExit = false } @@ -189,22 +181,18 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.width = msg.Width m.height = msg.Height inputWidth := msg.Width - 6 - m.toInput.Width = inputWidth - m.ccInput.Width = inputWidth - m.bccInput.Width = inputWidth - m.subjectInput.Width = inputWidth + m.toInput.SetWidth(inputWidth) + m.ccInput.SetWidth(inputWidth) + m.bccInput.SetWidth(inputWidth) + m.subjectInput.SetWidth(inputWidth) m.bodyInput.SetWidth(inputWidth) m.signatureInput.SetWidth(inputWidth) - case SetComposerCursorToStartMsg: - m.bodyInput.SetCursor(0) - return m, nil - case FileSelectedMsg: m.attachmentPath = msg.Path return m, nil - case tea.KeyMsg: + case tea.KeyPressMsg: // Handle contact suggestions mode if m.showSuggestions && len(m.suggestions) > 0 { switch msg.String() { @@ -236,7 +224,7 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } // For shift+tab, close suggestions and let it fall through to normal handling - if msg.Type == tea.KeyShiftTab { + if msg.String() == "shift+tab" { m.showSuggestions = false m.suggestions = nil } @@ -273,15 +261,15 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - switch msg.Type { - case tea.KeyCtrlC: + switch msg.String() { + case "ctrl+c": return m, tea.Quit - case tea.KeyEsc: + case "esc": m.confirmingExit = true return m, nil - case tea.KeyTab, tea.KeyShiftTab: - if msg.Type == tea.KeyShiftTab { + case "tab", "shift+tab": + if msg.String() == "shift+tab" { m.focusIndex-- } else { m.focusIndex++ @@ -318,13 +306,12 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, m.subjectInput.Focus()) case focusBody: cmds = append(cmds, m.bodyInput.Focus()) - cmds = append(cmds, func() tea.Msg { return SetComposerCursorToStartMsg{} }) case focusSignature: cmds = append(cmds, m.signatureInput.Focus()) } return m, tea.Batch(cmds...) - case tea.KeyEnter: + case "enter": switch m.focusIndex { case focusFrom: if len(m.accounts) > 1 { @@ -396,7 +383,7 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } -func (m *Composer) View() string { +func (m *Composer) View() tea.View { var composerView strings.Builder var button string @@ -494,7 +481,7 @@ func (m *Composer) View() string { accountList.WriteString(HelpStyle.Render("↑/↓: navigate • enter: select • esc: cancel")) dialog := DialogBoxStyle.Render(accountList.String()) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)) } if m.confirmingExit { @@ -504,10 +491,10 @@ func (m *Composer) View() string { HelpStyle.Render("\n(y/n)"), ), ) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)) } - return composerView.String() + return tea.NewView(composerView.String()) } // SetAccounts sets the available accounts for sending. diff --git a/tui/drafts.go b/tui/drafts.go index b401112..afdd282 100644 --- a/tui/drafts.go +++ b/tui/drafts.go @@ -5,10 +5,10 @@ import ( "strings" "time" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/key" + "charm.land/bubbles/v2/list" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/floatpane/matcha/config" ) @@ -116,7 +116,7 @@ func (m *Drafts) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.list.SetHeight(msg.Height - 4) return m, nil - case tea.KeyMsg: + case tea.KeyPressMsg: // Handle delete confirmation if m.confirmDelete { switch msg.String() { @@ -183,7 +183,7 @@ func (m *Drafts) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m *Drafts) View() string { +func (m *Drafts) View() tea.View { var b strings.Builder if m.confirmDelete { @@ -193,18 +193,19 @@ func (m *Drafts) View() string { HelpStyle.Render("\n(y/n)"), ), ) - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)) } if len(m.drafts) == 0 { emptyMsg := lipgloss.NewStyle(). Foreground(lipgloss.Color("240")). Render("No drafts saved.\n\nPress esc to go back.") - return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, emptyMsg) + return tea.NewView(lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, emptyMsg)) } + // list.View() still returns string in v2 b.WriteString(m.list.View()) - return b.String() + return tea.NewView(b.String()) } // SetDrafts updates the drafts list diff --git a/tui/email_view.go b/tui/email_view.go index 2da090e..5ed92fc 100644 --- a/tui/email_view.go +++ b/tui/email_view.go @@ -6,9 +6,9 @@ import ( "os" "strings" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/floatpane/matcha/fetcher" "github.com/floatpane/matcha/view" ) @@ -59,8 +59,10 @@ func NewEmailView(email fetcher.Email, emailIndex, width, height int, mailbox Ma } // Build viewport with initial size and set wrapped content. - vp := viewport.New(width, height-headerHeight-attachmentHeight) - wrapped := wrapBodyToWidth(body, vp.Width) + vp := viewport.New() + vp.SetWidth(width) + vp.SetHeight(height - headerHeight - attachmentHeight) + wrapped := wrapBodyToWidth(body, vp.Width()) vp.SetContent("\x1b_Ga=d\x1b\\\n" + wrapped + "\n") return &EmailView{ @@ -83,9 +85,9 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: // Handle 'esc' key locally - if msg.Type == tea.KeyEsc { + if msg.String() == "esc" { if m.focusOnAttachments { m.focusOnAttachments = false return m, nil @@ -136,7 +138,7 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if err != nil { body = fmt.Sprintf("Error rendering body: %v", err) } - wrapped := wrapBodyToWidth(body, m.viewport.Width) + wrapped := wrapBodyToWidth(body, m.viewport.Width()) m.viewport.SetContent("\x1b_Ga=d\x1b\\\n" + wrapped + "\n") return m, nil } @@ -178,8 +180,8 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { attachmentHeight = len(m.email.Attachments) + 2 } // Update viewport dimensions - m.viewport.Width = msg.Width - m.viewport.Height = msg.Height - headerHeight - attachmentHeight + m.viewport.SetWidth(msg.Width) + m.viewport.SetHeight(msg.Height - headerHeight - attachmentHeight) // When the window size changes, wrap and clear kitty images to keep placement stable inlineImages := inlineImagesFromAttachments(m.email.Attachments) @@ -187,7 +189,7 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if err != nil { body = fmt.Sprintf("Error rendering body: %v", err) } - wrapped := wrapBodyToWidth(body, m.viewport.Width) + wrapped := wrapBodyToWidth(body, m.viewport.Width()) m.viewport.SetContent("\x1b_Ga=d\x1b\\\n" + wrapped + "\n") } @@ -197,14 +199,14 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } -func (m *EmailView) View() string { +func (m *EmailView) View() tea.View { // Clear all Kitty graphics before rendering to prevent image stacking on scroll. // This must be done synchronously via stdout before the frame is drawn, // as escape sequences in the return string execute too late. clearKittyGraphics() header := fmt.Sprintf("From: %s | Subject: %s", m.email.From, m.email.Subject) - styledHeader := emailHeaderStyle.Width(m.viewport.Width).Render(header) + styledHeader := emailHeaderStyle.Width(m.viewport.Width()).Render(header) var help string if m.focusOnAttachments { @@ -234,7 +236,8 @@ func (m *EmailView) View() string { attachmentView = attachmentBoxStyle.Render(b.String()) } - return fmt.Sprintf("%s\n%s\n%s\n%s", styledHeader, m.viewport.View(), attachmentView, help) + // m.viewport.View() returns a string in Bubbles v2 viewport + return tea.NewView(fmt.Sprintf("%s\n%s\n%s\n%s", styledHeader, m.viewport.View(), attachmentView, help)) } // GetAccountID returns the account ID for this email diff --git a/tui/filepicker.go b/tui/filepicker.go index 115cbd2..9aa2a33 100644 --- a/tui/filepicker.go +++ b/tui/filepicker.go @@ -7,8 +7,8 @@ import ( "path/filepath" "strings" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" ) var ( @@ -52,7 +52,7 @@ func (m *FilePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.width = msg.Width m.height = msg.Height - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "up", "k": if m.cursor > 0 { @@ -92,7 +92,7 @@ func (m *FilePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m *FilePicker) View() string { +func (m *FilePicker) View() tea.View { var b strings.Builder b.WriteString(titleStyle.Render("Select a File") + "\n") @@ -121,5 +121,5 @@ func (m *FilePicker) View() string { b.WriteString("\n" + helpStyle.Render("↑/↓: navigate • enter: select • backspace: up • esc: cancel")) - return docStyle.Render(b.String()) + return tea.NewView(docStyle.Render(b.String())) } diff --git a/tui/inbox.go b/tui/inbox.go index b6c98db..1f1643b 100644 --- a/tui/inbox.go +++ b/tui/inbox.go @@ -5,17 +5,18 @@ import ( "io" "strings" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/key" + "charm.land/bubbles/v2/list" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/floatpane/matcha/config" "github.com/floatpane/matcha/fetcher" ) var ( - paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) - inboxHelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) + // In bubbles v2, list.DefaultStyles() takes a boolean for hasDarkBackground + paginationStyle = list.DefaultStyles(true).PaginationStyle.PaddingLeft(4) + inboxHelpStyle = list.DefaultStyles(true).HelpStyle.PaddingLeft(4).PaddingBottom(1) tabStyle = lipgloss.NewStyle().Padding(0, 2) activeTabStyle = lipgloss.NewStyle().Padding(0, 2).Foreground(lipgloss.Color("42")).Bold(true).Underline(true) tabBarStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).PaddingBottom(1).MarginBottom(1) @@ -299,7 +300,7 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: if m.list.FilterState() == list.Filtering { break } @@ -490,7 +491,7 @@ func (m *Inbox) fetchMoreCmds() []tea.Cmd { return cmds } -func (m *Inbox) View() string { +func (m *Inbox) View() tea.View { var b strings.Builder // Render tabs if there are multiple accounts @@ -564,7 +565,7 @@ func (m *Inbox) View() string { b.WriteString(helpView) - return b.String() + return tea.NewView(b.String()) } // GetCurrentAccountID returns the currently selected account ID diff --git a/tui/login.go b/tui/login.go index 348097d..d6d85d1 100644 --- a/tui/login.go +++ b/tui/login.go @@ -3,9 +3,9 @@ package tui import ( "strconv" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" ) // Login holds the state for the login/add account form. @@ -41,7 +41,6 @@ func NewLogin() *Login { var t textinput.Model for i := range m.inputs { t = textinput.New() - t.Cursor.Style = focusedStyle t.CharLimit = 128 switch i { @@ -93,15 +92,15 @@ func (m *Login) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.width = msg.Width m.height = msg.Height for i := range m.inputs { - m.inputs[i].Width = msg.Width - 6 + m.inputs[i].SetWidth(msg.Width - 6) } - case tea.KeyMsg: - switch msg.Type { - case tea.KeyEsc: + case tea.KeyPressMsg: + switch msg.String() { + case "esc": return m, func() tea.Msg { return GoToChoiceMenuMsg{} } - case tea.KeyEnter: + case "enter": // Check if provider is "custom" to show/hide custom fields provider := m.inputs[inputProvider].Value() if provider == "custom" { @@ -146,7 +145,7 @@ func (m *Login) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } fallthrough - case tea.KeyTab, tea.KeyShiftTab, tea.KeyUp, tea.KeyDown: + case "tab", "shift+tab", "up", "down": s := msg.String() // Check provider to update showCustom @@ -205,7 +204,7 @@ func (m *Login) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the login form. -func (m *Login) View() string { +func (m *Login) View() tea.View { title := "Add Account" if m.isEditMode { title = "Edit Account" @@ -240,7 +239,7 @@ func (m *Login) View() string { views = append(views, helpStyle.Render("\nenter: save • tab: next field • esc: back to menu")) - return lipgloss.JoinVertical(lipgloss.Left, views...) + return tea.NewView(lipgloss.JoinVertical(lipgloss.Left, views...)) } // SetEditMode sets the login form to edit an existing account. diff --git a/tui/settings.go b/tui/settings.go index 7df506a..c363d1b 100644 --- a/tui/settings.go +++ b/tui/settings.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/floatpane/matcha/config" ) @@ -58,7 +58,7 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height return m, nil - case tea.KeyMsg: + case tea.KeyPressMsg: if m.state == SettingsMain { return m.updateMain(msg) } else { @@ -68,7 +68,7 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m *Settings) updateMain(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Settings) updateMain(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "up", "k": if m.cursor > 0 { @@ -99,7 +99,7 @@ func (m *Settings) updateMain(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } -func (m *Settings) updateAccounts(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Settings) updateAccounts(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if m.confirmingDelete { switch msg.String() { case "y", "Y": @@ -146,11 +146,11 @@ func (m *Settings) updateAccounts(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // View renders the settings screen. -func (m *Settings) View() string { +func (m *Settings) View() tea.View { if m.state == SettingsMain { - return m.viewMain() + return tea.NewView(m.viewMain()) } - return m.viewAccounts() + return tea.NewView(m.viewAccounts()) } func (m *Settings) viewMain() string { diff --git a/tui/signature.go b/tui/signature.go index f9da04b..c434716 100644 --- a/tui/signature.go +++ b/tui/signature.go @@ -1,9 +1,9 @@ package tui import ( - "github.com/charmbracelet/bubbles/textarea" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/textarea" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/floatpane/matcha/config" ) @@ -48,11 +48,11 @@ func (m *SignatureEditor) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.textarea.SetHeight(msg.Height - 10) return m, nil - case tea.KeyMsg: - switch msg.Type { - case tea.KeyCtrlC: + case tea.KeyPressMsg: + switch msg.String() { + case "ctrl+c": return m, tea.Quit - case tea.KeyEsc: + case "esc": // Save and go back to settings signature := m.textarea.Value() go config.SaveSignature(signature) @@ -65,16 +65,16 @@ func (m *SignatureEditor) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the signature editor screen. -func (m *SignatureEditor) View() string { +func (m *SignatureEditor) View() tea.View { title := titleStyle.Render("Email Signature") hint := accountEmailStyle.Render("This signature will be appended to your emails.") - return lipgloss.JoinVertical(lipgloss.Left, + return tea.NewView(lipgloss.JoinVertical(lipgloss.Left, title, hint, "", m.textarea.View(), "", helpStyle.Render("esc: save & back"), - ) + )) } diff --git a/tui/styles.go b/tui/styles.go index 6a82cee..c987d31 100644 --- a/tui/styles.go +++ b/tui/styles.go @@ -3,9 +3,9 @@ package tui import ( "fmt" - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/spinner" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" ) // ASCII logo for Matcha displayed during loading screens @@ -68,11 +68,11 @@ func (m Status) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m Status) View() string { +func (m Status) View() tea.View { logoStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("42")) styledLogo := logoStyle.Render(asciiLogo) spinnerLine := fmt.Sprintf(" %s %s", m.spinner.View(), m.message) - return fmt.Sprintf("%s\n%s\n\n", styledLogo, spinnerLine) + return tea.NewView(fmt.Sprintf("%s\n%s\n\n", styledLogo, spinnerLine)) } diff --git a/tui/trash_archive.go b/tui/trash_archive.go index 0f1cd9c..00a225a 100644 --- a/tui/trash_archive.go +++ b/tui/trash_archive.go @@ -3,9 +3,9 @@ package tui import ( "strings" - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/key" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/floatpane/matcha/config" "github.com/floatpane/matcha/fetcher" ) @@ -42,7 +42,7 @@ func (m *TrashArchive) Init() tea.Cmd { func (m *TrashArchive) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: switch msg.String() { case "tab": // Toggle between trash and archive @@ -81,7 +81,7 @@ func (m *TrashArchive) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m *TrashArchive) View() string { +func (m *TrashArchive) View() tea.View { var b strings.Builder // Render the mailbox toggle tabs @@ -104,12 +104,12 @@ func (m *TrashArchive) View() string { // Render the active inbox if m.activeView == MailboxTrash { - b.WriteString(m.trashInbox.View()) + b.WriteString(m.trashInbox.View().Content) } else { - b.WriteString(m.archiveInbox.View()) + b.WriteString(m.archiveInbox.View().Content) } - return b.String() + return tea.NewView(b.String()) } // GetActiveMailbox returns the currently active mailbox kind diff --git a/view/html.go b/view/html.go index 6512849..ae25e63 100644 --- a/view/html.go +++ b/view/html.go @@ -17,8 +17,8 @@ import ( _ "image/gif" _ "image/jpeg" + "charm.land/lipgloss/v2" "github.com/PuerkitoBio/goquery" - "github.com/charmbracelet/lipgloss" "github.com/yuin/goldmark" "github.com/yuin/goldmark/renderer/html" "golang.org/x/sys/unix" From 65b7f1710f3ab498557662b1863cb05b1a583a6c Mon Sep 17 00:00:00 2001 From: drew Date: Wed, 25 Feb 2026 22:27:34 +0400 Subject: [PATCH 3/3] test: upgrade tests to v2, correct html_test --- tui/composer_test.go | 52 +++++++++++++++++++-------------------- tui/email_view_test.go | 24 +++++++++--------- tui/inbox_test.go | 32 ++++++++++++------------ tui/trash_archive_test.go | 12 ++++----- view/html_test.go | 42 ++++++++++++++++++++----------- 5 files changed, 88 insertions(+), 74 deletions(-) diff --git a/tui/composer_test.go b/tui/composer_test.go index 9a49143..ca3a9b8 100644 --- a/tui/composer_test.go +++ b/tui/composer_test.go @@ -3,7 +3,7 @@ package tui import ( "testing" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/floatpane/matcha/config" ) @@ -23,49 +23,49 @@ func TestComposerUpdate(t *testing.T) { } // Simulate pressing Tab to move to the 'Cc' field. - model, _ := composer.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ := composer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) composer = model.(*Composer) if composer.focusIndex != focusCc { t.Errorf("After one Tab, focusIndex should be %d (focusCc), got %d", focusCc, composer.focusIndex) } // Simulate pressing Tab to move to the 'Bcc' field. - model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ = composer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) composer = model.(*Composer) if composer.focusIndex != focusBcc { t.Errorf("After two Tabs, focusIndex should be %d (focusBcc), got %d", focusBcc, composer.focusIndex) } // Simulate pressing Tab to move to the 'Subject' field. - model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ = composer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) composer = model.(*Composer) if composer.focusIndex != focusSubject { t.Errorf("After three Tabs, focusIndex should be %d (focusSubject), got %d", focusSubject, composer.focusIndex) } // Simulate pressing Tab again to move to the 'Body' field. - model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ = composer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) composer = model.(*Composer) if composer.focusIndex != focusBody { t.Errorf("After four Tabs, focusIndex should be %d (focusBody), got %d", focusBody, composer.focusIndex) } // Simulate pressing Tab again to move to the 'Signature' field. - model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ = composer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) composer = model.(*Composer) if composer.focusIndex != focusSignature { t.Errorf("After five Tabs, focusIndex should be %d (focusSignature), got %d", focusSignature, composer.focusIndex) } // Simulate pressing Tab again to move to the 'Attachment' field. - model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ = composer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) composer = model.(*Composer) if composer.focusIndex != focusAttachment { t.Errorf("After six Tabs, focusIndex should be %d (focusAttachment), got %d", focusAttachment, composer.focusIndex) } // Simulate pressing Tab again to move to the 'Send' button. - model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ = composer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) composer = model.(*Composer) if composer.focusIndex != focusSend { t.Errorf("After seven Tabs, focusIndex should be %d (focusSend), got %d", focusSend, composer.focusIndex) @@ -73,7 +73,7 @@ func TestComposerUpdate(t *testing.T) { // Simulate one more Tab to wrap around. // With single account, From field is skipped, so it wraps to focusTo. - model, _ = composer.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ = composer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) composer = model.(*Composer) if composer.focusIndex != focusTo { t.Errorf("After eight Tabs, focusIndex should wrap to %d (focusTo) since single account skips From, got %d", focusTo, composer.focusIndex) @@ -92,7 +92,7 @@ func TestComposerUpdate(t *testing.T) { composer.focusIndex = focusSend // Simulate pressing Enter to send the email. - _, cmd := composer.Update(tea.KeyMsg{Type: tea.KeyEnter}) + _, cmd := composer.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) if cmd == nil { t.Fatal("Expected a command to be returned, but got nil.") } @@ -130,7 +130,7 @@ func TestComposerUpdate(t *testing.T) { multiComposer.focusIndex = focusFrom // Press Enter to open account picker - model, _ := multiComposer.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ := multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) multiComposer = model.(*Composer) if !multiComposer.showAccountPicker { @@ -138,7 +138,7 @@ func TestComposerUpdate(t *testing.T) { } // Navigate down to select second account - model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyDown}) + model, _ = multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyDown}) multiComposer = model.(*Composer) if multiComposer.selectedAccountIdx != 1 { @@ -146,7 +146,7 @@ func TestComposerUpdate(t *testing.T) { } // Press Enter to confirm selection - model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ = multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) multiComposer = model.(*Composer) if multiComposer.showAccountPicker { @@ -169,7 +169,7 @@ func TestComposerUpdate(t *testing.T) { singleComposer.focusIndex = focusFrom // Press Enter - should not open picker with single account - model, _ := singleComposer.Update(tea.KeyMsg{Type: tea.KeyEnter}) + model, _ := singleComposer.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) singleComposer = model.(*Composer) if singleComposer.showAccountPicker { @@ -190,23 +190,23 @@ func TestComposerUpdate(t *testing.T) { } // Tab through all fields: To -> Cc -> Bcc -> Subject -> Body -> Signature -> Attachment -> Send -> From (wrap) - model, _ := multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // To -> Cc + model, _ := multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) // To -> Cc multiComposer = model.(*Composer) - model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Cc -> Bcc + model, _ = multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) // Cc -> Bcc multiComposer = model.(*Composer) - model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Bcc -> Subject + model, _ = multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) // Bcc -> Subject multiComposer = model.(*Composer) - model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Subject -> Body + model, _ = multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) // Subject -> Body multiComposer = model.(*Composer) - model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Body -> Signature + model, _ = multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) // Body -> Signature multiComposer = model.(*Composer) - model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Signature -> Attachment + model, _ = multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) // Signature -> Attachment multiComposer = model.(*Composer) - model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Attachment -> Send + model, _ = multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) // Attachment -> Send multiComposer = model.(*Composer) - model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // Send -> From (wrap) + model, _ = multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) // Send -> From (wrap) multiComposer = model.(*Composer) - model, _ = multiComposer.Update(tea.KeyMsg{Type: tea.KeyTab}) // From -> To (wrap) + model, _ = multiComposer.Update(tea.KeyPressMsg{Code: tea.KeyTab}) // From -> To (wrap) multiComposer = model.(*Composer) // With multiple accounts, From field should be included in tab order @@ -257,9 +257,9 @@ func TestComposerGetFromAddress(t *testing.T) { // TestComposerSetSelectedAccount verifies account selection. func TestComposerSetSelectedAccount(t *testing.T) { accounts := []config.Account{ - {ID: "account-1", Email: "test1@example.com"}, - {ID: "account-2", Email: "test2@example.com"}, - {ID: "account-3", Email: "test3@example.com"}, + {ID: "account-1", FetchEmail: "test1@example.com"}, + {ID: "account-2", FetchEmail: "test2@example.com"}, + {ID: "account-3", FetchEmail: "test3@example.com"}, } composer := NewComposerWithAccounts(accounts, "account-1", "", "", "") diff --git a/tui/email_view_test.go b/tui/email_view_test.go index 3af1a26..80233a2 100644 --- a/tui/email_view_test.go +++ b/tui/email_view_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/floatpane/matcha/fetcher" ) @@ -34,7 +34,7 @@ func TestEmailViewUpdate(t *testing.T) { } // Tab to focus on attachments - model, _ := emailView.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ := emailView.Update(tea.KeyPressMsg{Code: tea.KeyTab}) emailView = model.(*EmailView) if !emailView.focusOnAttachments { @@ -42,7 +42,7 @@ func TestEmailViewUpdate(t *testing.T) { } // Tab back to body - model, _ = emailView.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ = emailView.Update(tea.KeyPressMsg{Code: tea.KeyTab}) emailView = model.(*EmailView) if emailView.focusOnAttachments { t.Error("focusOnAttachments should be false after tabbing again") @@ -55,7 +55,7 @@ func TestEmailViewUpdate(t *testing.T) { t.Error("focusOnAttachments should be initially false") } // Tab - model, _ := emailView.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ := emailView.Update(tea.KeyPressMsg{Code: tea.KeyTab}) emailView = model.(*EmailView) if emailView.focusOnAttachments { t.Error("focusOnAttachments should remain false when there are no attachments") @@ -65,7 +65,7 @@ func TestEmailViewUpdate(t *testing.T) { t.Run("Navigate attachments", func(t *testing.T) { emailView := NewEmailView(emailWithAttachments, 0, 80, 24, MailboxInbox, false) // Focus on attachments - model, _ := emailView.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ := emailView.Update(tea.KeyPressMsg{Code: tea.KeyTab}) emailView = model.(*EmailView) if emailView.attachmentCursor != 0 { @@ -73,21 +73,21 @@ func TestEmailViewUpdate(t *testing.T) { } // Move down - model, _ = emailView.Update(tea.KeyMsg{Type: tea.KeyDown}) + model, _ = emailView.Update(tea.KeyPressMsg{Code: tea.KeyDown}) emailView = model.(*EmailView) if emailView.attachmentCursor != 1 { t.Errorf("After one down arrow, attachmentCursor should be 1, got %d", emailView.attachmentCursor) } // Move down again (should not go past the end) - model, _ = emailView.Update(tea.KeyMsg{Type: tea.KeyDown}) + 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) } // Move up - model, _ = emailView.Update(tea.KeyMsg{Type: tea.KeyUp}) + 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) @@ -97,15 +97,15 @@ func TestEmailViewUpdate(t *testing.T) { t.Run("Download attachment", func(t *testing.T) { emailView := NewEmailView(emailWithAttachments, 0, 80, 24, MailboxInbox, false) // Focus on attachments - model, _ := emailView.Update(tea.KeyMsg{Type: tea.KeyTab}) + model, _ := emailView.Update(tea.KeyPressMsg{Code: tea.KeyTab}) emailView = model.(*EmailView) // Move to the second attachment - model, _ = emailView.Update(tea.KeyMsg{Type: tea.KeyDown}) + model, _ = emailView.Update(tea.KeyPressMsg{Code: tea.KeyDown}) emailView = model.(*EmailView) // Press enter - _, cmd := emailView.Update(tea.KeyMsg{Type: tea.KeyEnter}) + _, cmd := emailView.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) if cmd == nil { t.Fatal("Expected a command, but got nil") } @@ -126,7 +126,7 @@ func TestEmailViewUpdate(t *testing.T) { t.Run("Reply to email", func(t *testing.T) { emailView := NewEmailView(emailWithAttachments, 0, 80, 24, MailboxInbox, false) - _, cmd := emailView.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("r")}) + _, cmd := emailView.Update(tea.KeyPressMsg{Code: 'r', Text: "r"}) if cmd == nil { t.Fatal("Expected a command, but got nil") } diff --git a/tui/inbox_test.go b/tui/inbox_test.go index 84881a5..a24c40f 100644 --- a/tui/inbox_test.go +++ b/tui/inbox_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/floatpane/matcha/config" "github.com/floatpane/matcha/fetcher" ) @@ -14,21 +14,21 @@ func collectMsgs(cmd tea.Cmd) []tea.Msg { return nil } msg := cmd() - switch batch := msg.(type) { - case tea.BatchMsg: + if msg == nil { + return nil + } + + // Try type assertion to see if it's a BatchMsg + if batch, ok := msg.(tea.BatchMsg); ok { var msgs []tea.Msg for _, m := range batch { - if m != nil { - msgs = append(msgs, m) - } + msgs = append(msgs, collectMsgs(m)...) } return msgs - default: - if msg != nil { - return []tea.Msg{msg} - } } - return nil + + // Otherwise it's a regular message + return []tea.Msg{msg} } // TestInboxUpdate verifies the state transitions in the inbox view. @@ -51,10 +51,10 @@ func TestInboxUpdate(t *testing.T) { t.Run("Select email to view", func(t *testing.T) { // By default, the first item is selected (index 0). // Move down to the second item (index 1). - inbox.list, _ = inbox.list.Update(tea.KeyMsg{Type: tea.KeyDown}) + inbox.list, _ = inbox.list.Update(tea.KeyPressMsg{Code: tea.KeyDown}) // Simulate pressing Enter to view the selected email. - _, cmd := inbox.Update(tea.KeyMsg{Type: tea.KeyEnter}) + _, cmd := inbox.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) if cmd == nil { t.Fatal("Expected a command, but got nil.") } @@ -157,7 +157,7 @@ func TestInboxDeleteEmailMsg(t *testing.T) { inbox := NewInbox(emails, accounts) // Simulate pressing 'd' to delete - _, cmd := inbox.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}}) + _, cmd := inbox.Update(tea.KeyPressMsg{Code: 'd', Text: "d"}) if cmd == nil { t.Fatal("Expected a command, but got nil.") } @@ -190,7 +190,7 @@ func TestInboxArchiveEmailMsg(t *testing.T) { inbox := NewInbox(emails, accounts) // Simulate pressing 'a' to archive - _, cmd := inbox.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}}) + _, cmd := inbox.Update(tea.KeyPressMsg{Code: 'a', Text: "a"}) if cmd == nil { t.Fatal("Expected a command, but got nil.") } @@ -283,7 +283,7 @@ func TestFetchMoreTriggeredAtListEnd(t *testing.T) { inbox := NewInbox(emails, accounts) - _, cmd := inbox.Update(tea.KeyMsg{Type: tea.KeyDown}) + _, cmd := inbox.Update(tea.KeyPressMsg{Code: tea.KeyDown}) msgs := collectMsgs(cmd) var fetchMsg FetchMoreEmailsMsg diff --git a/tui/trash_archive_test.go b/tui/trash_archive_test.go index 79f6d08..bd1cda9 100644 --- a/tui/trash_archive_test.go +++ b/tui/trash_archive_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - tea "github.com/charmbracelet/bubbletea" + tea "charm.land/bubbletea/v2" "github.com/floatpane/matcha/config" "github.com/floatpane/matcha/fetcher" ) @@ -63,7 +63,7 @@ func TestTrashArchiveToggle(t *testing.T) { } // Press tab to switch to Archive - ta.Update(tea.KeyMsg{Type: tea.KeyTab}) + ta.Update(tea.KeyPressMsg{Code: tea.KeyTab}) if ta.activeView != MailboxArchive { t.Errorf("Expected view to be MailboxArchive after tab, got %v", ta.activeView) @@ -74,7 +74,7 @@ func TestTrashArchiveToggle(t *testing.T) { } // Press tab again to switch back to Trash - ta.Update(tea.KeyMsg{Type: tea.KeyTab}) + ta.Update(tea.KeyPressMsg{Code: tea.KeyTab}) if ta.activeView != MailboxTrash { t.Errorf("Expected view to be MailboxTrash after second tab, got %v", ta.activeView) @@ -180,7 +180,7 @@ func TestTrashArchiveViewEmailMsg(t *testing.T) { ta := NewTrashArchive(trashEmails, nil, accounts) // Simulate pressing Enter on trash view - _, cmd := ta.Update(tea.KeyMsg{Type: tea.KeyEnter}) + _, cmd := ta.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) if cmd == nil { t.Fatal("Expected a command, but got nil") } @@ -213,10 +213,10 @@ func TestTrashArchiveDeleteEmailMsg(t *testing.T) { ta := NewTrashArchive(nil, archiveEmails, accounts) // Switch to archive view - ta.Update(tea.KeyMsg{Type: tea.KeyTab}) + ta.Update(tea.KeyPressMsg{Code: tea.KeyTab}) // Simulate pressing 'd' to delete - _, cmd := ta.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'d'}}) + _, cmd := ta.Update(tea.KeyPressMsg{Code: 'd', Text: "d"}) if cmd == nil { t.Fatal("Expected a command, but got nil") } diff --git a/view/html_test.go b/view/html_test.go index 6f78586..36c0a90 100644 --- a/view/html_test.go +++ b/view/html_test.go @@ -2,10 +2,11 @@ package view import ( "os" + "regexp" "strings" "testing" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" ) // clearAllTerminalEnv clears all environment variables that could indicate terminal capabilities @@ -478,7 +479,7 @@ func TestProcessBodyWithHyperlinkSupport(t *testing.T) { }, input: `Click here`, expectedContains: "Click here", - expectedNotContains: "<http://example.com>", + expectedNotContains: "", }, { name: "Link without hyperlink support", @@ -497,7 +498,7 @@ func TestProcessBodyWithHyperlinkSupport(t *testing.T) { }, input: `alt text`, expectedContains: "[Click here to view image: alt text]", - expectedNotContains: "<http://example.com/img.png>", + expectedNotContains: "", }, { name: "Image link without hyperlink support", @@ -509,6 +510,9 @@ func TestProcessBodyWithHyperlinkSupport(t *testing.T) { }, } + // Regex to strip out ANSI SGR escape codes (e.g. \x1b[38;2;...m) + ansiEscapeRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`) + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tc.setupHyperlinks() @@ -518,12 +522,14 @@ func TestProcessBodyWithHyperlinkSupport(t *testing.T) { t.Fatalf("ProcessBody() failed: %v", err) } - if !strings.Contains(processed, tc.expectedContains) { - t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", processed, tc.expectedContains) + cleanProcessed := ansiEscapeRegex.ReplaceAllString(processed, "") + + if !strings.Contains(cleanProcessed, tc.expectedContains) { + t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", cleanProcessed, tc.expectedContains) } - if tc.expectedNotContains != "" && strings.Contains(processed, tc.expectedNotContains) { - t.Errorf("Processed body contains unexpected text.\nGot: %q\nShould not contain: %q", processed, tc.expectedNotContains) + if tc.expectedNotContains != "" && strings.Contains(cleanProcessed, tc.expectedNotContains) { + t.Errorf("Processed body contains unexpected text.\nGot: %q\nShould not contain: %q", cleanProcessed, tc.expectedNotContains) } }) } @@ -630,6 +636,8 @@ func TestProcessBodyWithImageProtocol(t *testing.T) { }, } + ansiEscapeRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`) + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tc.clearAllImageEnv() @@ -640,12 +648,14 @@ func TestProcessBodyWithImageProtocol(t *testing.T) { t.Fatalf("ProcessBody() failed: %v", err) } - if !strings.Contains(processed, tc.expectedContains) { - t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", processed, tc.expectedContains) + cleanProcessed := ansiEscapeRegex.ReplaceAllString(processed, "") + + if !strings.Contains(cleanProcessed, tc.expectedContains) { + t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", cleanProcessed, tc.expectedContains) } - if tc.expectedNotContains != "" && strings.Contains(processed, tc.expectedNotContains) { - t.Errorf("Processed body contains unexpected text.\nGot: %q\nShould not contain: %q", processed, tc.expectedNotContains) + if tc.expectedNotContains != "" && strings.Contains(cleanProcessed, tc.expectedNotContains) { + t.Errorf("Processed body contains unexpected text.\nGot: %q\nShould not contain: %q", cleanProcessed, tc.expectedNotContains) } }) } @@ -683,15 +693,19 @@ func TestProcessBody(t *testing.T) { }, } + ansiEscapeRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`) + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { processed, err := ProcessBody(tc.input, h1Style, h2Style, bodyStyle, false) if err != nil { t.Fatalf("ProcessBody() failed: %v", err) } - // Use Contains because styles add ANSI codes - if !strings.Contains(processed, tc.expected) { - t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", processed, tc.expected) + + cleanProcessed := ansiEscapeRegex.ReplaceAllString(processed, "") + + if !strings.Contains(cleanProcessed, tc.expected) { + t.Errorf("Processed body does not contain expected text.\nGot: %q\nWant to contain: %q", cleanProcessed, tc.expected) } }) }