A minimal email TUI where you read with Markdown and write in Neovim. neomd.ssp.sh/docs
email markdown neovim tui
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

adding clickable tabs

sspaeti bd5b63af dfc4a4fb

+59 -8
+24 -1
internal/ui/model.go
··· 832 832 func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 833 833 switch msg := msg.(type) { 834 834 835 + case tea.MouseMsg: 836 + if m.state == stateInbox && msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft && msg.Y == 0 { 837 + // Click on tab bar — compute zones and match. 838 + _, zones := folderTabs(m.folders, "", m.folderCounts) 839 + offX := 0 840 + if len(m.accounts) > 1 { 841 + offX = len(" "+m.activeAccount().Name+" ·") + 2 842 + } 843 + clickX := msg.X - offX 844 + for _, z := range zones { 845 + if clickX >= z.xStart && clickX < z.xEnd { 846 + if z.folderIndex == m.activeFolderI && m.offTabFolder == "" { 847 + return m, nil // already on this tab 848 + } 849 + m.activeFolderI = z.folderIndex 850 + m.offTabFolder = "" 851 + m.loading = true 852 + return m, tea.Batch(m.spinner.Tick, m.fetchFolderCmd(m.activeFolder())) 853 + } 854 + } 855 + } 856 + // Fall through — let other components handle mouse events (scroll, etc.) 857 + 835 858 case tea.WindowSizeMsg: 836 859 m.width = msg.Width 837 860 m.height = msg.Height ··· 2747 2770 if m.offTabFolder != "" { 2748 2771 activeTab = "" // deselect all tabs; off-tab folder shown separately 2749 2772 } 2750 - header := folderTabs(m.folders, activeTab, m.folderCounts) 2773 + header, _ := folderTabs(m.folders, activeTab, m.folderCounts) 2751 2774 if m.offTabFolder != "" { 2752 2775 header += styleSeparator.Render(" │ ") + styleHeader.Render(m.offTabFolder) 2753 2776 }
+35 -7
internal/ui/styles.go
··· 113 113 Bold(true) 114 114 ) 115 115 116 - // folderTabs renders the folder switcher bar. 117 - func folderTabs(folders []string, active string, counts map[string]int) string { 118 - var tabs []string 119 - for _, f := range folders { 120 - label := f 116 + // tabZone records the X range for a clickable folder tab. 117 + type tabZone struct { 118 + xStart, xEnd int // character range [xStart, xEnd) 119 + folderIndex int 120 + } 121 + 122 + // folderTabs renders the folder switcher bar and returns click zones. 123 + func folderTabs(folders []string, active string, counts map[string]int) (string, []tabZone) { 124 + // Compute raw label for each tab (before styling) to track character positions. 125 + labels := make([]string, len(folders)) 126 + for i, f := range folders { 127 + labels[i] = f 121 128 if n, ok := counts[f]; ok && n > 0 { 122 - label = fmt.Sprintf("%s (%d)", f, n) 129 + labels[i] = fmt.Sprintf("%s (%d)", f, n) 123 130 } 131 + } 132 + 133 + // styleHeader and styleFolder both add Padding(0,1) = 1 space each side. 134 + const padLeft = 1 135 + const padRight = 1 136 + const sepWidth = 3 // " │ " rendered width 137 + 138 + var zones []tabZone 139 + var tabs []string 140 + x := 0 141 + for i, f := range folders { 142 + label := labels[i] 143 + start := x + padLeft 144 + end := start + len(label) 145 + zones = append(zones, tabZone{xStart: x, xEnd: end + padRight, folderIndex: i}) 146 + 124 147 if f == active { 125 148 tabs = append(tabs, styleHeader.Render(label)) 126 149 } else { 127 150 tabs = append(tabs, styleFolder.Render(label)) 128 151 } 152 + x = end + padRight 153 + if i < len(folders)-1 { 154 + x += sepWidth 155 + } 129 156 } 157 + 130 158 sep := styleSeparator.Render(" │ ") 131 159 result := "" 132 160 for i, t := range tabs { ··· 135 163 } 136 164 result += t 137 165 } 138 - return result 166 + return result, zones 139 167 }