From 5fa22b82f18e38482afdbe2105406d4940b3ca1b Mon Sep 17 00:00:00 2001 From: drew Date: Sat, 30 May 2026 18:30:12 +0400 Subject: [PATCH 1/4] chore: add jwz-go and bubble-overlay Signed-off-by: drew --- config/folder_cache.go | 2 +- go.mod | 9 +- go.sum | 4 +- internal/threading/jwz.go | 365 --------------------------------- internal/threading/jwz_test.go | 154 -------------- internal/threading/subject.go | 20 -- tui/composer.go | 3 +- tui/inbox.go | 2 +- tui/overlay.go | 58 ------ 9 files changed, 14 insertions(+), 603 deletions(-) delete mode 100644 internal/threading/jwz.go delete mode 100644 internal/threading/jwz_test.go delete mode 100644 internal/threading/subject.go delete mode 100644 tui/overlay.go diff --git a/config/folder_cache.go b/config/folder_cache.go index 0553ab71..2312a0ca 100644 --- a/config/folder_cache.go +++ b/config/folder_cache.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/floatpane/matcha/internal/threading" + threading "github.com/floatpane/jwz-go" ) // CachedFolders stores folder names for a single account. diff --git a/go.mod b/go.mod index 3c098c25..baf16c49 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,8 @@ require ( github.com/emersion/go-message v0.18.2 github.com/emersion/go-pgpmail v0.2.2 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 + github.com/floatpane/bubble-overlay v0.0.1 + github.com/floatpane/jwz-go v0.0.1 github.com/floatpane/termimage v0.2.0 github.com/google/uuid v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 @@ -56,10 +58,15 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/image v0.28.0 // indirect + golang.org/x/image v0.41.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.45.0 // indirect kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect ) + +replace ( + github.com/floatpane/bubble-overlay => ../bubble-overlay + github.com/floatpane/jwz-go => ../jwz-go +) diff --git a/go.sum b/go.sum index 74ca8cba..14491c16 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,8 @@ golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= -golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= diff --git a/internal/threading/jwz.go b/internal/threading/jwz.go deleted file mode 100644 index bd6c418c..00000000 --- a/internal/threading/jwz.go +++ /dev/null @@ -1,365 +0,0 @@ -package threading - -import ( - "regexp" - "sort" - "strings" - "time" -) - -type EmailHeader struct { - ID string - InReplyTo string - References []string - Subject string - Date time.Time - EmailID string - Sender string -} - -type Thread struct { - Root *ThreadNode - LatestAt time.Time - Count int - Subject string - Senders []string -} - -type ThreadNode struct { - EmailID string - Children []*ThreadNode - Date time.Time - Sender string - Subject string -} - -type container struct { - id string - node *ThreadNode - parent *container - children []*container -} - -var messageIDRE = regexp.MustCompile(`<[^>]+>`) - -func Build(headers []EmailHeader) []Thread { - containers := make(map[string]*container) - ordered := make([]*container, 0, len(headers)) - - get := func(id string) *container { - if c := containers[id]; c != nil { - return c - } - c := &container{id: id} - containers[id] = c - ordered = append(ordered, c) - return c - } - - for _, h := range headers { - msgID := normalizeMessageID(h.ID) - if msgID == "" { - msgID = "email:" + h.EmailID - } - c := get(msgID) - if c.node != nil { - msgID = msgID + "#email:" + h.EmailID - c = get(msgID) - } - c.node = &ThreadNode{ - EmailID: h.EmailID, - Date: h.Date, - Sender: h.Sender, - Subject: h.Subject, - } - - var prev *container - refs := normalizeReferences(h.References) - for _, ref := range refs { - refc := get(ref) - if prev != nil { - link(prev, refc) - } - prev = refc - } - - parentID := normalizeMessageID(h.InReplyTo) - if parentID == "" && len(refs) > 0 { - parentID = refs[len(refs)-1] - } - if parentID != "" { - link(get(parentID), c) - } - } - - var roots []*container - for _, c := range ordered { - if c.parent == nil { - if root := prune(c); root != nil { - roots = append(roots, root) - } - } - } - roots = groupBySubject(roots) - - threads := make([]Thread, 0, len(roots)) - for _, root := range roots { - sortContainer(root) - thread := buildThread(root) - if thread.Count > 0 { - threads = append(threads, thread) - } - } - - sort.SliceStable(threads, func(i, j int) bool { - if !threads[i].LatestAt.Equal(threads[j].LatestAt) { - return threads[i].LatestAt.After(threads[j].LatestAt) - } - return threadKey(threads[i].Root) < threadKey(threads[j].Root) - }) - - return threads -} - -func normalizeReferences(refs []string) []string { - seen := make(map[string]bool) - var out []string - for _, ref := range refs { - for _, id := range extractMessageIDs(ref) { - if !seen[id] { - out = append(out, id) - seen[id] = true - } - } - } - return out -} - -func extractMessageIDs(s string) []string { - matches := messageIDRE.FindAllString(s, -1) - if len(matches) == 0 { - if id := normalizeMessageID(s); id != "" { - return []string{id} - } - return nil - } - ids := make([]string, 0, len(matches)) - for _, match := range matches { - if id := normalizeMessageID(match); id != "" { - ids = append(ids, id) - } - } - return ids -} - -func normalizeMessageID(id string) string { - id = strings.TrimSpace(id) - if id == "" { - return "" - } - if matches := messageIDRE.FindAllString(id, -1); len(matches) > 0 { - id = matches[len(matches)-1] - } - id = strings.TrimSpace(id) - id = strings.TrimPrefix(id, "<") - id = strings.TrimSuffix(id, ">") - id = strings.TrimSpace(id) - return strings.ToLower(id) -} - -func link(parent, child *container) { - if parent == nil || child == nil || parent == child { - return - } - if child.parent != nil || child.hasDescendant(parent) { - return - } - child.parent = parent - for _, existing := range parent.children { - if existing == child { - return - } - } - parent.children = append(parent.children, child) -} - -func (c *container) hasDescendant(target *container) bool { - for _, child := range c.children { - if child == target || child.hasDescendant(target) { - return true - } - } - return false -} - -func prune(c *container) *container { - if c == nil { - return nil - } - var children []*container - for _, child := range c.children { - if pruned := prune(child); pruned != nil { - pruned.parent = c - children = append(children, pruned) - } - } - c.children = children - - if c.node != nil { - return c - } - switch len(c.children) { - case 0: - return nil - case 1: - child := c.children[0] - child.parent = c.parent - return child - default: - return c - } -} - -func groupBySubject(roots []*container) []*container { - subjects := make(map[string]*container) - var grouped []*container - for _, root := range roots { - subject := firstSubject(root) - if subject == "" { - grouped = append(grouped, root) - continue - } - if existing := subjects[subject]; existing != nil { - link(existing, root) - continue - } - subjects[subject] = root - grouped = append(grouped, root) - } - return grouped -} - -func firstSubject(c *container) string { - if c == nil { - return "" - } - if c.node != nil { - return canonicalSubject(c.node.Subject) - } - for _, child := range c.children { - if subject := firstSubject(child); subject != "" { - return subject - } - } - return "" -} - -func sortContainer(c *container) { - for _, child := range c.children { - sortContainer(child) - } - sort.SliceStable(c.children, func(i, j int) bool { - a, b := c.children[i], c.children[j] - ad, bd := containerDate(a), containerDate(b) - if !ad.Equal(bd) { - return ad.Before(bd) - } - return containerKey(a) < containerKey(b) - }) -} - -func buildThread(root *container) Thread { - node := toThreadNode(root) - thread := Thread{Root: node, Subject: canonicalSubject(firstDisplaySubject(node))} - seenSenders := make(map[string]bool) - walkThread(node, &thread, seenSenders) - return thread -} - -func toThreadNode(c *container) *ThreadNode { - node := &ThreadNode{} - if c.node != nil { - *node = *c.node - node.Children = nil - } - for _, child := range c.children { - node.Children = append(node.Children, toThreadNode(child)) - } - return node -} - -func walkThread(node *ThreadNode, thread *Thread, seenSenders map[string]bool) { - if node == nil { - return - } - if node.EmailID != "" { - thread.Count++ - if node.Date.After(thread.LatestAt) { - thread.LatestAt = node.Date - } - if node.Sender != "" && !seenSenders[node.Sender] { - thread.Senders = append(thread.Senders, node.Sender) - seenSenders[node.Sender] = true - } - } - for _, child := range node.Children { - walkThread(child, thread, seenSenders) - } -} - -func containerDate(c *container) time.Time { - if c == nil { - return time.Time{} - } - if c.node != nil { - return c.node.Date - } - var earliest time.Time - for _, child := range c.children { - date := containerDate(child) - if earliest.IsZero() || (!date.IsZero() && date.Before(earliest)) { - earliest = date - } - } - return earliest -} - -func containerKey(c *container) string { - if c == nil { - return "" - } - if c.node != nil && c.node.EmailID != "" { - return c.node.EmailID - } - return c.id -} - -func threadKey(n *ThreadNode) string { - if n == nil { - return "" - } - if n.EmailID != "" { - return n.EmailID - } - for _, child := range n.Children { - if key := threadKey(child); key != "" { - return key - } - } - return "" -} - -func firstDisplaySubject(node *ThreadNode) string { - if node == nil { - return "" - } - if node.Subject != "" { - return node.Subject - } - for _, child := range node.Children { - if subject := firstDisplaySubject(child); subject != "" { - return subject - } - } - return "" -} diff --git a/internal/threading/jwz_test.go b/internal/threading/jwz_test.go deleted file mode 100644 index 7e10e1f6..00000000 --- a/internal/threading/jwz_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package threading - -import ( - "reflect" - "testing" - "time" -) - -func TestBuildThreeMessageChain(t *testing.T) { - base := time.Date(2026, 4, 28, 10, 0, 0, 0, time.UTC) - threads := Build([]EmailHeader{ - {ID: "", Subject: "Foo", Date: base, EmailID: "1", Sender: "a"}, - {ID: "", References: []string{""}, Subject: "Re: Foo", Date: base.Add(time.Minute), EmailID: "2", Sender: "b"}, - {ID: "", References: []string{"", ""}, Subject: "Re: Re: Foo", Date: base.Add(2 * time.Minute), EmailID: "3", Sender: "c"}, //nolint:dupword - }) - - if len(threads) != 1 { - t.Fatalf("got %d threads, want 1", len(threads)) - } - if threads[0].Count != 3 { - t.Fatalf("got count %d, want 3", threads[0].Count) - } - if got := threads[0].Root.Children[0].Children[0].EmailID; got != "3" { - t.Fatalf("got chain leaf %q, want 3", got) - } -} - -func TestBuildForkedThread(t *testing.T) { - base := time.Date(2026, 4, 28, 10, 0, 0, 0, time.UTC) - threads := Build([]EmailHeader{ - {ID: "", Subject: "Foo", Date: base, EmailID: "1"}, - {ID: "", References: []string{""}, Subject: "Re: Foo", Date: base.Add(2 * time.Minute), EmailID: "3"}, - {ID: "", References: []string{""}, Subject: "Re: Foo", Date: base.Add(time.Minute), EmailID: "2"}, - }) - - if len(threads) != 1 { - t.Fatalf("got %d threads, want 1", len(threads)) - } - children := threads[0].Root.Children - if len(children) != 2 { - t.Fatalf("got %d children, want 2", len(children)) - } - if children[0].EmailID != "2" || children[1].EmailID != "3" { - t.Fatalf("got child order %q, %q; want 2, 3", children[0].EmailID, children[1].EmailID) - } -} - -func TestBuildMissingParentPlaceholderRoot(t *testing.T) { - base := time.Date(2026, 4, 28, 10, 0, 0, 0, time.UTC) - threads := Build([]EmailHeader{ - {ID: "", References: []string{""}, Subject: "Re: Foo", Date: base, EmailID: "child"}, - {ID: "", References: []string{""}, Subject: "Re: Foo", Date: base.Add(time.Minute), EmailID: "other"}, - }) - - if len(threads) != 1 { - t.Fatalf("got %d threads, want 1", len(threads)) - } - if threads[0].Root.EmailID != "" { - t.Fatalf("got root EmailID %q, want placeholder", threads[0].Root.EmailID) - } - if len(threads[0].Root.Children) != 2 { - t.Fatalf("got %d placeholder children, want 2", len(threads[0].Root.Children)) - } -} - -func TestBuildSubjectFallbackGroupingForOrphans(t *testing.T) { - base := time.Date(2026, 4, 28, 10, 0, 0, 0, time.UTC) - threads := Build([]EmailHeader{ - {ID: "", Subject: "Re: Foo", Date: base, EmailID: "1"}, - {ID: "", Subject: "Fwd: foo", Date: base.Add(time.Minute), EmailID: "2"}, - {ID: "", Subject: "Bar", Date: base.Add(2 * time.Minute), EmailID: "3"}, - }) - - if len(threads) != 2 { - t.Fatalf("got %d threads, want 2", len(threads)) - } - var grouped Thread - for _, thread := range threads { - if thread.Subject == "foo" { - grouped = thread - break - } - } - if grouped.Count != 2 { - t.Fatalf("got grouped count %d, want 2", grouped.Count) - } -} - -func TestBuildSubjectFallbackGroupsLocalePrefixes(t *testing.T) { - base := time.Date(2026, 4, 28, 10, 0, 0, 0, time.UTC) - threads := Build([]EmailHeader{ - {ID: "", Subject: "Foo", Date: base, EmailID: "1"}, - {ID: "", Subject: "SV: Foo", Date: base.Add(time.Minute), EmailID: "2"}, - {ID: "", Subject: "RV: Foo", Date: base.Add(2 * time.Minute), EmailID: "3"}, - {ID: "", Subject: "Antw: Foo", Date: base.Add(3 * time.Minute), EmailID: "4"}, - }) - - if len(threads) != 1 { - t.Fatalf("got %d threads, want 1", len(threads)) - } - if threads[0].Subject != "foo" { - t.Fatalf("got subject %q, want foo", threads[0].Subject) - } - if threads[0].Count != 4 { - t.Fatalf("got grouped count %d, want 4", threads[0].Count) - } -} - -func TestBuildEmptyReferencesList(t *testing.T) { - threads := Build([]EmailHeader{ - {ID: "", References: nil, Subject: "Foo", Date: time.Now(), EmailID: "1"}, - }) - - if len(threads) != 1 { - t.Fatalf("got %d threads, want 1", len(threads)) - } - if threads[0].Root.EmailID != "1" { - t.Fatalf("got root %q, want 1", threads[0].Root.EmailID) - } -} - -func TestBuildStableOrderingAcrossCalls(t *testing.T) { - base := time.Date(2026, 4, 28, 10, 0, 0, 0, time.UTC) - headers := []EmailHeader{ - {ID: "", Subject: "Foo", Date: base, EmailID: "1"}, - {ID: "", Subject: "Bar", Date: base, EmailID: "2"}, - {ID: "", References: []string{""}, Subject: "Re: Foo", Date: base, EmailID: "3"}, - } - - first := Build(headers) - second := Build(headers) - if !reflect.DeepEqual(first, second) { - t.Fatalf("Build output differed across calls:\n%#v\n%#v", first, second) - } -} - -func TestCanonicalSubjectNormalizesReplyAndForwardPrefixes(t *testing.T) { - tests := map[string]string{ - "Re: Re: Foo": "foo", //nolint:dupword - "Fwd: FW: Foo": "foo", - "AW: WG: Tr: Foo": "foo", - "Reé: Resp: Foo": "foo", - "SV: VS: RV: Foo": "foo", - "ENC: Antw: Foo": "foo", - "Odp: R: I: Foo": "foo", - " Foo ": "foo", - } - - for in, want := range tests { - if got := canonicalSubject(in); got != want { - t.Fatalf("canonicalSubject(%q) = %q, want %q", in, got, want) - } - } -} diff --git a/internal/threading/subject.go b/internal/threading/subject.go deleted file mode 100644 index 1a36dd55..00000000 --- a/internal/threading/subject.go +++ /dev/null @@ -1,20 +0,0 @@ -package threading - -import ( - "regexp" - "strings" -) - -var subjectPrefixRE = regexp.MustCompile(`(?i)^(Re|Fwd|Fw|AW|WG|Tr|Reé|Resp|SV|VS|RV|ENC|Antw|Odp|R|I)\s*:\s*`) - -func canonicalSubject(s string) string { - s = strings.TrimSpace(s) - for { - next := subjectPrefixRE.ReplaceAllString(s, "") - if next == s { - break - } - s = strings.TrimSpace(next) - } - return strings.ToLower(strings.TrimSpace(s)) -} diff --git a/tui/composer.go b/tui/composer.go index 92d2819b..2e032b0a 100644 --- a/tui/composer.go +++ b/tui/composer.go @@ -13,6 +13,7 @@ import ( "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" + overlay "github.com/floatpane/bubble-overlay" "github.com/floatpane/matcha/config" "github.com/floatpane/matcha/spellcheck" "github.com/google/uuid" @@ -1337,7 +1338,7 @@ func (m *Composer) overlaySpellPopup(view string, elementsBeforeBody []string) s anchorCol = max(0, m.width-popupWidth) } - return overlayBlock(view, popup, anchorRow, anchorCol) + return overlay.Block(view, popup, anchorRow, anchorCol) } // renderSpellPopupLines builds the styled, bordered suggestion box and diff --git a/tui/inbox.go b/tui/inbox.go index e80a1851..6587f6b7 100644 --- a/tui/inbox.go +++ b/tui/inbox.go @@ -13,7 +13,7 @@ import ( "charm.land/lipgloss/v2" "github.com/floatpane/matcha/config" "github.com/floatpane/matcha/fetcher" - "github.com/floatpane/matcha/internal/threading" + threading "github.com/floatpane/jwz-go" "github.com/floatpane/matcha/theme" ) diff --git a/tui/overlay.go b/tui/overlay.go deleted file mode 100644 index b263f711..00000000 --- a/tui/overlay.go +++ /dev/null @@ -1,58 +0,0 @@ -package tui - -import ( - "strings" - - "github.com/charmbracelet/x/ansi" -) - -// overlayBlock paints the lines of block on top of base starting at the -// (row, col) cell position. Lines that extend past the bottom of base are -// appended. The result preserves existing ANSI styling around the -// overlaid region. -func overlayBlock(base string, block []string, row, col int) string { - if len(block) == 0 { - return base - } - lines := strings.Split(base, "\n") - for i, overlay := range block { - r := row + i - for r >= len(lines) { - lines = append(lines, "") - } - lines[r] = overlayLine(lines[r], overlay, col) - } - return strings.Join(lines, "\n") -} - -// overlayLine returns base with overlay painted starting at column col. -// Existing cells under the overlay are removed; cells to the left and -// right of the overlay are preserved with their ANSI styling intact. -// When col exceeds the visible width of base the gap is padded with -// spaces. -func overlayLine(base, overlay string, col int) string { - if overlay == "" { - return base - } - overlayWidth := ansi.StringWidth(overlay) - baseWidth := ansi.StringWidth(base) - - left := ansi.Truncate(base, col, "") - leftWidth := ansi.StringWidth(left) - - var pad string - if leftWidth < col { - pad = strings.Repeat(" ", col-leftWidth) - } - - var right string - rightStart := col + overlayWidth - if rightStart < baseWidth { - right = ansi.Cut(base, rightStart, baseWidth) - } - - // Reset SGR after the overlay so the overlay's styles don't bleed - // into the surrounding cells (the rest of the row may inherit ANSI - // from earlier in the string). - return left + pad + overlay + "\x1b[0m" + right -} From 652828708389d75a80882cb93827a43291cd8384 Mon Sep 17 00:00:00 2001 From: drew Date: Sat, 30 May 2026 18:42:10 +0400 Subject: [PATCH 2/4] fix: remove replace Signed-off-by: drew --- go.mod | 5 ----- go.sum | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index baf16c49..6319863d 100644 --- a/go.mod +++ b/go.mod @@ -65,8 +65,3 @@ require ( golang.org/x/sys v0.45.0 // indirect kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect ) - -replace ( - github.com/floatpane/bubble-overlay => ../bubble-overlay - github.com/floatpane/jwz-go => ../jwz-go -) diff --git a/go.sum b/go.sum index 14491c16..7bd26b61 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,10 @@ github.com/emersion/go-pgpmail v0.2.2/go.mod h1:mRB5P7QKiAuOvcT36tdRZvm7nSt7V+f6 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= +github.com/floatpane/bubble-overlay v0.0.1 h1:5xU8cNigDPYegvgGMfOG23fIDXhrqXPvLTaEB7uHGK4= +github.com/floatpane/bubble-overlay v0.0.1/go.mod h1:Csi1byxb9L8EAb8X13XdWF5aX5YiBD5C9WEWACyGa8A= +github.com/floatpane/jwz-go v0.0.1 h1:OAl/vaUYn+/8zFR47WaCybBGoQItb1ZkFplNrmeO3ps= +github.com/floatpane/jwz-go v0.0.1/go.mod h1:1b/JE4K+LBIBGtWbFdM1BXpN6ZbGWIv7IXPRbXD4oRE= github.com/floatpane/termimage v0.2.0 h1:NGjG7VUFAqpuYiPn/Vqcq2eHYqLZNmD3HkAdQTV0Lc0= github.com/floatpane/termimage v0.2.0/go.mod h1:5Mcw99w/AI4pmYVVyZKM4DkldHClH6uYO0eCQQGmaes= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= From 5854a61cd9b34a25157c73ed22b1e439a1718e2d Mon Sep 17 00:00:00 2001 From: drew Date: Sat, 30 May 2026 18:43:18 +0400 Subject: [PATCH 3/4] fix: lint Signed-off-by: drew --- tui/inbox.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tui/inbox.go b/tui/inbox.go index 6587f6b7..1dbb5b20 100644 --- a/tui/inbox.go +++ b/tui/inbox.go @@ -11,9 +11,9 @@ import ( "charm.land/bubbles/v2/list" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" + threading "github.com/floatpane/jwz-go" "github.com/floatpane/matcha/config" "github.com/floatpane/matcha/fetcher" - threading "github.com/floatpane/jwz-go" "github.com/floatpane/matcha/theme" ) From d390772bb6258bb74c888e69caed45c7bb22a971 Mon Sep 17 00:00:00 2001 From: drew Date: Sun, 31 May 2026 09:30:32 +0400 Subject: [PATCH 4/4] tidy mods Signed-off-by: drew --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5d8692e2..c9fc0ef7 100644 --- a/go.mod +++ b/go.mod @@ -20,8 +20,8 @@ require ( github.com/emersion/go-pgpmail v0.2.2 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 github.com/floatpane/bubble-overlay v0.0.1 - github.com/floatpane/jwz-go v0.0.1 github.com/floatpane/go-uds-jsonrpc v0.0.1 + github.com/floatpane/jwz-go v0.0.1 github.com/floatpane/termimage v0.2.0 github.com/google/uuid v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7