From 2940711b26693ddd6f6b97d22576108feef4fa06 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 2 Feb 2026 19:39:02 +0100 Subject: [PATCH 1/2] Make task details scrollable --- pages/taskEditor.go | 73 +++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/pages/taskEditor.go b/pages/taskEditor.go index 7eeba70..a7e7c68 100644 --- a/pages/taskEditor.go +++ b/pages/taskEditor.go @@ -41,6 +41,8 @@ type TaskEditorPage struct { area int areaPicker *areaPicker areas []area + + infoViewport viewport.Model } func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPage { @@ -68,6 +70,11 @@ func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPag p.areaPicker = NewAreaPicker(com, []string{"Task", "Tags", "Dates"}) + p.infoViewport = viewport.New(0, 0) + if p.task.Uuid != "" { + p.infoViewport.SetContent(p.common.TW.GetInformation(&p.task)) + } + p.columnCursor = 1 if p.task.Uuid == "" { p.mode = modeInsert @@ -94,6 +101,12 @@ func (p *TaskEditorPage) SetSize(width, height int) { } else { p.colHeight = height - p.common.Styles.ColumnFocused.GetVerticalFrameSize() } + + p.infoViewport.Width = width - p.colWidth - p.common.Styles.ColumnFocused.GetHorizontalFrameSize()*2 - 5 + if p.infoViewport.Width < 0 { + p.infoViewport.Width = 0 + } + p.infoViewport.Height = p.colHeight } func (p *TaskEditorPage) Init() tea.Cmd { @@ -110,12 +123,20 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p.mode = mode(msg) case prevColumnMsg: p.columnCursor-- + maxCols := 2 + if p.task.Uuid != "" { + maxCols = 3 + } if p.columnCursor < 0 { - p.columnCursor = len(p.areas) - 1 + p.columnCursor = maxCols - 1 } case nextColumnMsg: p.columnCursor++ - if p.columnCursor > len(p.areas)-1 { + maxCols := 2 + if p.task.Uuid != "" { + maxCols = 3 + } + if p.columnCursor >= maxCols { p.columnCursor = 0 } case prevAreaMsg: @@ -166,20 +187,26 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { picker, cmd := p.areaPicker.Update(msg) p.areaPicker = picker.(*areaPicker) return p, cmd - } else { + } else if p.columnCursor == 1 { model, cmd := p.areas[p.area].Update(prevFieldMsg{}) p.areas[p.area] = model.(area) return p, cmd + } else if p.columnCursor == 2 { + p.infoViewport.LineUp(1) + return p, nil } case key.Matches(msg, p.common.Keymap.Down): if p.columnCursor == 0 { picker, cmd := p.areaPicker.Update(msg) p.areaPicker = picker.(*areaPicker) return p, cmd - } else { + } else if p.columnCursor == 1 { model, cmd := p.areas[p.area].Update(nextFieldMsg{}) p.areas[p.area] = model.(area) return p, cmd + } else if p.columnCursor == 2 { + p.infoViewport.LineDown(1) + return p, nil } } } @@ -212,21 +239,23 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { picker, cmd := p.areaPicker.Update(msg) p.areaPicker = picker.(*areaPicker) return p, cmd - } else { + } else if p.columnCursor == 1 { model, cmd := p.areas[p.area].Update(prevFieldMsg{}) p.areas[p.area] = model.(area) return p, cmd } + return p, nil case key.Matches(msg, p.common.Keymap.Next): if p.columnCursor == 0 { picker, cmd := p.areaPicker.Update(msg) p.areaPicker = picker.(*areaPicker) return p, cmd - } else { + } else if p.columnCursor == 1 { model, cmd := p.areas[p.area].Update(nextFieldMsg{}) p.areas[p.area] = model.(area) return p, cmd } + return p, nil case key.Matches(msg, p.common.Keymap.Ok): model, cmd := p.areas[p.area].Update(msg) if p.area != 3 { @@ -241,6 +270,10 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { picker, cmd := p.areaPicker.Update(msg) p.areaPicker = picker.(*areaPicker) return p, cmd + } else if p.columnCursor == 2 { + var cmd tea.Cmd + p.infoViewport, cmd = p.infoViewport.Update(msg) + return p, cmd } else { model, cmd := p.areas[p.area].Update(msg) p.areas[p.area] = model.(area) @@ -253,29 +286,31 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (p *TaskEditorPage) View() string { var focusedStyle, blurredStyle lipgloss.Style if p.mode == modeInsert { - focusedStyle = p.common.Styles.ColumnInsert.Width(p.colWidth).Height(p.colHeight) + focusedStyle = p.common.Styles.ColumnInsert } else { - focusedStyle = p.common.Styles.ColumnFocused.Width(p.colWidth).Height(p.colHeight) + focusedStyle = p.common.Styles.ColumnFocused } - blurredStyle = p.common.Styles.ColumnBlurred.Width(p.colWidth).Height(p.colHeight) - // var picker, area string - var area string - if p.columnCursor == 0 { - // picker = focusedStyle.Render(p.areaPicker.View()) - area = blurredStyle.Render(p.areas[p.area].View()) - } else { - // picker = blurredStyle.Render(p.areaPicker.View()) - area = focusedStyle.Render(p.areas[p.area].View()) + blurredStyle = p.common.Styles.ColumnBlurred + var area string + if p.columnCursor == 1 { + area = focusedStyle.Copy().Width(p.colWidth).Height(p.colHeight).Render(p.areas[p.area].View()) + } else { + area = blurredStyle.Copy().Width(p.colWidth).Height(p.colHeight).Render(p.areas[p.area].View()) } if p.task.Uuid != "" { + var infoView string + if p.columnCursor == 2 { + infoView = focusedStyle.Copy().Width(p.infoViewport.Width).Height(p.infoViewport.Height).Render(p.infoViewport.View()) + } else { + infoView = blurredStyle.Copy().Width(p.infoViewport.Width).Height(p.infoViewport.Height).Render(p.infoViewport.View()) + } area = lipgloss.JoinHorizontal( lipgloss.Top, area, - p.common.Styles.ColumnFocused.Render(p.common.TW.GetInformation(&p.task)), + infoView, ) - } tabs := "" From 2baf3859fd65b1877b461b033277763fa8b0bfe7 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 2 Feb 2026 19:47:18 +0100 Subject: [PATCH 2/2] Add tab bar --- common/styles.go | 17 ++++++++++++++++ pages/main.go | 51 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/common/styles.go b/common/styles.go index f9aec89..c660919 100644 --- a/common/styles.go +++ b/common/styles.go @@ -27,6 +27,10 @@ type Styles struct { Form *huh.Theme TableStyle TableStyle + Tab lipgloss.Style + ActiveTab lipgloss.Style + TabBar lipgloss.Style + ColumnFocused lipgloss.Style ColumnBlurred lipgloss.Style ColumnInsert lipgloss.Style @@ -71,6 +75,19 @@ func NewStyles(config *taskwarrior.TWConfig) *Styles { styles.Form = formTheme + styles.Tab = lipgloss.NewStyle(). + Padding(0, 1). + Foreground(lipgloss.Color("240")) + + styles.ActiveTab = styles.Tab. + Foreground(lipgloss.Color("252")). + Bold(true) + + styles.TabBar = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, false, true, false). + BorderForeground(lipgloss.Color("240")). + MarginBottom(1) + styles.ColumnFocused = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1) styles.ColumnBlurred = lipgloss.NewStyle().Border(lipgloss.HiddenBorder(), true).Padding(1) styles.ColumnInsert = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1) diff --git a/pages/main.go b/pages/main.go index f659718..fdb6e3b 100644 --- a/pages/main.go +++ b/pages/main.go @@ -5,6 +5,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) type MainPage struct { @@ -13,6 +14,9 @@ type MainPage struct { taskPage common.Component timePage common.Component + currentTab int + width int + height int } func NewMainPage(common *common.Common) *MainPage { @@ -24,6 +28,7 @@ func NewMainPage(common *common.Common) *MainPage { m.timePage = NewTimePage(common) m.activePage = m.taskPage + m.currentTab = 0 return m } @@ -37,17 +42,39 @@ func (m *MainPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height m.common.SetSize(msg.Width, msg.Height) + + tabHeight := lipgloss.Height(m.renderTabBar()) + contentHeight := msg.Height - tabHeight + if contentHeight < 0 { + contentHeight = 0 + } + + newMsg := tea.WindowSizeMsg{Width: msg.Width, Height: contentHeight} + activePage, cmd := m.activePage.Update(newMsg) + m.activePage = activePage.(common.Component) + return m, cmd + case tea.KeyMsg: // Only handle tab key for page switching when at the top level (no subpages active) if key.Matches(msg, m.common.Keymap.Next) && !m.common.HasSubpages() { if m.activePage == m.taskPage { m.activePage = m.timePage + m.currentTab = 1 } else { m.activePage = m.taskPage + m.currentTab = 0 } - // Re-size the new active page just in case - m.activePage.SetSize(m.common.Width(), m.common.Height()) + + tabHeight := lipgloss.Height(m.renderTabBar()) + contentHeight := m.height - tabHeight + if contentHeight < 0 { + contentHeight = 0 + } + m.activePage.SetSize(m.width, contentHeight) + // Trigger a refresh/init on switch? Maybe not needed if we keep state. // But we might want to refresh data. return m, m.activePage.Init() @@ -60,6 +87,22 @@ func (m *MainPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m *MainPage) View() string { - return m.activePage.View() +func (m *MainPage) renderTabBar() string { + var tabs []string + headers := []string{"Tasks", "Time"} + + for i, header := range headers { + style := m.common.Styles.Tab + if m.currentTab == i { + style = m.common.Styles.ActiveTab + } + tabs = append(tabs, style.Render(header)) + } + + row := lipgloss.JoinHorizontal(lipgloss.Top, tabs...) + return m.common.Styles.TabBar.Width(m.common.Width()).Render(row) +} + +func (m *MainPage) View() string { + return lipgloss.JoinVertical(lipgloss.Left, m.renderTabBar(), m.activePage.View()) }