diff --git a/go.mod b/go.mod index bf3b5d650b81c8daf99b1b0a6c44746932bc884e..784977b234d8d0c97341ef39e021eee2c7549907 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect + 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/charmbracelet/colorprofile v0.4.2 // indirect diff --git a/go.sum b/go.sum index 770daab128109234e9f0b68a1ea6a6490e848ee7..3da5507b88f81dfd929589a55bf71d57bfba7734 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,6 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= diff --git a/main.go b/main.go index dc3174ea44f0a07c8dba0469b6bc6190e6e8cc1c..a856e8f3a1ed89cfcf64626dc549b119089283ab 100644 --- a/main.go +++ b/main.go @@ -126,6 +126,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, func() tea.Msg { return tui.CancelFilePickerMsg{} } case *tui.Inbox, *tui.Login, *tui.TrashArchive: m.current = tui.NewChoice() + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) return m, m.current.Init() } } @@ -135,6 +136,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.current = m.inbox } else { m.current = tui.NewChoice() + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) } return m, nil @@ -157,6 +159,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } m.current = tui.NewChoice() + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) return m, nil case tui.DiscardDraftMsg: @@ -170,6 +173,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }() } m.current = tui.NewChoice() + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) return m, m.current.Init() case tui.Credentials: @@ -220,6 +224,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.current = tui.NewChoice() + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) return m, m.current.Init() case tui.GoToInboxMsg: @@ -546,6 +551,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tui.GoToChoiceMenuMsg: m.current = tui.NewChoice() + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) return m, m.current.Init() case tui.DeleteAccountMsg: @@ -734,12 +740,14 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { log.Printf("Failed to send email: %v", msg.Err) m.previousModel = tui.NewChoice() + m.previousModel, _ = m.previousModel.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) m.current = tui.NewStatus(fmt.Sprintf("Error: %v", msg.Err)) return m, tea.Tick(2*time.Second, func(t time.Time) tea.Msg { return tui.RestoreViewMsg{} }) } m.current = tui.NewChoice() + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) return m, m.current.Init() case tui.DeleteEmailMsg: @@ -809,6 +817,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.current.Init() } m.current = tui.NewChoice() + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) return m, m.current.Init() } @@ -820,6 +829,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.current.Init() } m.current = tui.NewChoice() + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) return m, m.current.Init() } @@ -830,6 +840,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.current.Init() } m.current = tui.NewChoice() + m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height}) return m, m.current.Init() case tui.DownloadAttachmentMsg: diff --git a/tui/choice.go b/tui/choice.go index 56ccbbecca4e830d48d24075f86a0de12e8fad36..46cd06695c5312eaf3e88e311dc2e60e4ea6b3a3 100644 --- a/tui/choice.go +++ b/tui/choice.go @@ -36,6 +36,8 @@ type Choice struct { UpdateAvailable bool LatestVersion string CurrentVersion string + width int + height int } func NewChoice() Choice { @@ -61,6 +63,10 @@ func (m Choice) Init() tea.Cmd { func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + return m, nil case tea.KeyPressMsg: switch msg.String() { case "up", "k": @@ -144,8 +150,18 @@ func (m Choice) View() tea.View { b.WriteString("\n") } - b.WriteString("\n\n") - b.WriteString(helpStyle.Render("Use ↑/↓ to navigate, enter to select, and ctrl+c to quit.")) + mainContent := b.String() + helpView := helpStyle.Render("Use ↑/↓ to navigate, enter to select, and ctrl+c to quit.") + + if m.height > 0 { + currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView)) + gap := m.height - currentHeight + if gap > 0 { + mainContent += strings.Repeat("\n", gap) + } + } else { + mainContent += "\n\n" + } - return tea.NewView(docStyle.Render(b.String())) + return tea.NewView(docStyle.Render(mainContent + helpView)) } diff --git a/tui/composer.go b/tui/composer.go index 9cb2447c4e4910f6d905ba598a087f05e03e876c..e523ea21c59da64815c834180168ec4421059ff4 100644 --- a/tui/composer.go +++ b/tui/composer.go @@ -488,9 +488,24 @@ func (m *Composer) View() tea.View { if !m.hideTips && tip != "" { composerViewElements = append(composerViewElements, TipStyle.Render("Tip: "+tip)) } - composerViewElements = append(composerViewElements, helpStyle.Render("Markdown/HTML • tab/shift+tab: navigate • esc: save draft & exit")) - composerView.WriteString(lipgloss.JoinVertical(lipgloss.Left, composerViewElements...)) + mainContent := lipgloss.JoinVertical(lipgloss.Left, composerViewElements...) + helpView := helpStyle.Render("Markdown/HTML • tab/shift+tab: navigate • esc: save draft & exit") + + if m.height > 0 { + currentHeight := lipgloss.Height(mainContent) + lipgloss.Height(helpView) + gap := m.height - currentHeight + if gap >= 0 { + mainContent += strings.Repeat("\n", gap+1) + } else { + mainContent += "\n" + } + } else { + mainContent += "\n\n" + } + + composerView.WriteString(mainContent) + composerView.WriteString(helpView) // Account picker overlay if m.showAccountPicker { diff --git a/tui/inbox.go b/tui/inbox.go index 1f1643b4fcb095aa86e6ce692619ad2ef9ac91a2..630848bf72620a97f40de8c0e85f9d095cb92288 100644 --- a/tui/inbox.go +++ b/tui/inbox.go @@ -516,45 +516,17 @@ func (m *Inbox) View() tea.View { b.WriteString(m.list.View()) - // Calculate remaining height to push help to bottom - // m.height is total height. - // We need to account for tabs (if present) and the list height. - // The list height is set to m.height / 2. - // Tabs take about 3 lines (border + padding + content). + // Ensure we don't start gap calculation on the same line as the list + if !strings.HasSuffix(b.String(), "\n") { + b.WriteString("\n") + } helpView := inboxHelpStyle.Render(m.list.Help.View(m.list)) - // If we have a known height, we can try to fill the space. if m.height > 0 { - // Calculate how many lines we have used - usedHeight := 0 - if len(m.tabs) > 1 { - // Re-render tabs just to measure height - var tabViews []string - for i, tab := range m.tabs { - label := tab.Label - if tab.ID == "" { - label = "ALL" - } - - if i == m.activeTabIndex { - tabViews = append(tabViews, activeTabStyle.Render(label)) - } else { - tabViews = append(tabViews, tabStyle.Render(label)) - } - } - tabBar := tabBarStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, tabViews...)) - usedHeight += lipgloss.Height(tabBar) - } - - // List - usedHeight += m.list.Height() - - // Help - // Use lipgloss to measure help height + usedHeight := lipgloss.Height(b.String()) helpHeight := lipgloss.Height(helpView) - // Calculate gap gap := m.height - usedHeight - helpHeight if gap > 0 { b.WriteString(strings.Repeat("\n", gap)) diff --git a/tui/settings.go b/tui/settings.go index 9c454f2905b451887e50e947a1b622d368fece04..40908910cd195de5ec6f6ae45a6fcb1c34557bef 100644 --- a/tui/settings.go +++ b/tui/settings.go @@ -226,9 +226,20 @@ func (m *Settings) viewMain() string { } } - b.WriteString(helpStyle.Render("↑/↓: navigate • enter: select/toggle • esc: back")) + mainContent := b.String() + helpView := helpStyle.Render("↑/↓: navigate • enter: select/toggle • esc: back") + + if m.height > 0 { + currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView)) + gap := m.height - currentHeight + if gap > 0 { + mainContent += strings.Repeat("\n", gap) + } + } else { + mainContent += "\n\n" + } - return docStyle.Render(b.String()) + return docStyle.Render(mainContent + helpView) } func (m *Settings) viewAccounts() string { @@ -270,9 +281,20 @@ func (m *Settings) viewAccounts() string { } else { b.WriteString(accountItemStyle.Render(fmt.Sprintf(" %s", addAccountText))) } - b.WriteString("\n\n") + b.WriteString("\n") - b.WriteString(helpStyle.Render("↑/↓: navigate • enter: select • d: delete account • esc: back")) + mainContent := b.String() + helpView := helpStyle.Render("↑/↓: navigate • enter: select • d: delete account • esc: back") + + if m.height > 0 { + currentHeight := lipgloss.Height(docStyle.Render(mainContent + helpView)) + gap := m.height - currentHeight + if gap > 0 { + mainContent += strings.Repeat("\n", gap) + } + } else { + mainContent += "\n\n" + } if m.confirmingDelete { accountName := m.cfg.Accounts[m.cursor].Email @@ -286,7 +308,7 @@ func (m *Settings) viewAccounts() string { return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog) } - return docStyle.Render(b.String()) + return docStyle.Render(mainContent + helpView) } // UpdateConfig updates the configuration (used when accounts are deleted).