diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-05 19:24:09 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-05 19:24:09 +0200 |
| commit | 30c955ef113e5e0c99c147ee4e7c8c20b0a7f273 (patch) | |
| tree | f872b7c44e62609a455dbed356fcc28ab3b53531 /internal | |
| parent | 33064821d637aeef94fe6ee96edbac7a503c0692 (diff) | |
Migrate UI stack to Bubble Tea v2
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/atable/table.go | 70 | ||||
| -rw-r--r-- | internal/ui/handlers.go | 146 | ||||
| -rw-r--r-- | internal/ui/keyhandlers.go | 118 | ||||
| -rw-r--r-- | internal/ui/table.go | 172 | ||||
| -rw-r--r-- | internal/ui/table_test.go | 104 | ||||
| -rw-r--r-- | internal/ui/taskdetail.go | 14 |
6 files changed, 328 insertions, 296 deletions
diff --git a/internal/atable/table.go b/internal/atable/table.go index 44f1c9f..d31088e 100644 --- a/internal/atable/table.go +++ b/internal/atable/table.go @@ -4,11 +4,11 @@ package table import ( "strings" - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/help" + "charm.land/bubbles/v2/key" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "github.com/charmbracelet/x/ansi" ) @@ -150,7 +150,7 @@ func New(opts ...Option) Model { m := Model{ cursor: 0, colCursor: 0, - viewport: viewport.New(0, 20), //nolint:mnd + viewport: viewport.New(viewport.WithWidth(0), viewport.WithHeight(20)), //nolint:mnd KeyMap: DefaultKeyMap(), Help: help.New(), @@ -184,14 +184,14 @@ func WithRows(rows []Row) Option { // WithHeight sets the height of the table. func WithHeight(h int) Option { return func(m *Model) { - m.viewport.Height = h - lipgloss.Height(m.headersView()) + m.viewport.SetHeight(h - lipgloss.Height(m.headersView())) } } // WithWidth sets the width of the table. func WithWidth(w int) Option { return func(m *Model) { - m.viewport.Width = w + m.viewport.SetWidth(w) } } @@ -230,20 +230,21 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { } switch msg := msg.(type) { - case tea.KeyMsg: + case tea.KeyPressMsg: + height := m.viewport.Height() switch { case key.Matches(msg, m.KeyMap.LineUp): m.MoveUp(1) case key.Matches(msg, m.KeyMap.LineDown): m.MoveDown(1) case key.Matches(msg, m.KeyMap.PageUp): - m.MoveUp(m.viewport.Height) + m.MoveUp(height) case key.Matches(msg, m.KeyMap.PageDown): - m.MoveDown(m.viewport.Height) + m.MoveDown(height) case key.Matches(msg, m.KeyMap.HalfPageUp): - m.MoveUp(m.viewport.Height / 2) //nolint:mnd + m.MoveUp(height / 2) //nolint:mnd case key.Matches(msg, m.KeyMap.HalfPageDown): - m.MoveDown(m.viewport.Height / 2) //nolint:mnd + m.MoveDown(height / 2) //nolint:mnd case key.Matches(msg, m.KeyMap.GotoTop): m.GotoTop() case key.Matches(msg, m.KeyMap.GotoBottom): @@ -296,16 +297,17 @@ func (m Model) HelpView() string { // columns and rows. func (m *Model) UpdateViewport() { renderedRows := make([]string, 0, len(m.rows)) + height := m.viewport.Height() // Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height // Constant runtime, independent of number of rows in a table. // Limits the number of renderedRows to a maximum of 2*m.viewport.Height if m.cursor >= 0 { - m.start = clamp(m.cursor-m.viewport.Height, 0, m.cursor) + m.start = clamp(m.cursor-height, 0, m.cursor) } else { m.start = 0 } - m.end = clamp(m.cursor+m.viewport.Height, m.cursor, len(m.rows)) + m.end = clamp(m.cursor+height, m.cursor, len(m.rows)) for i := m.start; i < m.end; i++ { renderedRows = append(renderedRows, m.renderRow(i)) } @@ -349,24 +351,24 @@ func (m *Model) SetColumns(c []Column) { // SetWidth sets the width of the viewport of the table. func (m *Model) SetWidth(w int) { - m.viewport.Width = w + m.viewport.SetWidth(w) m.UpdateViewport() } // SetHeight sets the height of the viewport of the table. func (m *Model) SetHeight(h int) { - m.viewport.Height = h - lipgloss.Height(m.headersView()) + m.viewport.SetHeight(h - lipgloss.Height(m.headersView())) m.UpdateViewport() } // Height returns the viewport height of the table. func (m Model) Height() int { - return m.viewport.Height + return m.viewport.Height() } // Width returns the viewport width of the table. func (m Model) Width() int { - return m.viewport.Width + return m.viewport.Width() } // Cursor returns the index of the selected row. @@ -395,13 +397,15 @@ func (m *Model) SetColumnCursor(n int) { // It can not go above the first row. func (m *Model) MoveUp(n int) { m.cursor = clamp(m.cursor-n, 0, len(m.rows)-1) + yOffset := m.viewport.YOffset() + height := m.viewport.Height() switch { case m.start == 0: - m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) - case m.start < m.viewport.Height: - m.viewport.YOffset = (clamp(clamp(m.viewport.YOffset+n, 0, m.cursor), 0, m.viewport.Height)) - case m.viewport.YOffset >= 1: - m.viewport.YOffset = clamp(m.viewport.YOffset+n, 1, m.viewport.Height) + m.viewport.SetYOffset(clamp(yOffset, 0, m.cursor)) + case m.start < height: + m.viewport.SetYOffset(clamp(clamp(yOffset+n, 0, m.cursor), 0, height)) + case yOffset >= 1: + m.viewport.SetYOffset(clamp(yOffset+n, 1, height)) } m.UpdateViewport() } @@ -411,15 +415,17 @@ func (m *Model) MoveUp(n int) { func (m *Model) MoveDown(n int) { m.cursor = clamp(m.cursor+n, 0, len(m.rows)-1) m.UpdateViewport() + yOffset := m.viewport.YOffset() + height := m.viewport.Height() switch { - case m.end == len(m.rows) && m.viewport.YOffset > 0: - m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height)) - case m.cursor > (m.end-m.start)/2 && m.viewport.YOffset > 0: - m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.cursor)) - case m.viewport.YOffset > 1: - case m.cursor > m.viewport.YOffset+m.viewport.Height-1: - m.viewport.SetYOffset(clamp(m.viewport.YOffset+1, 0, 1)) + case m.end == len(m.rows) && yOffset > 0: + m.viewport.SetYOffset(clamp(yOffset-n, 1, height)) + case m.cursor > (m.end-m.start)/2 && yOffset > 0: + m.viewport.SetYOffset(clamp(yOffset-n, 1, m.cursor)) + case yOffset > 1: + case m.cursor > yOffset+height-1: + m.viewport.SetYOffset(clamp(yOffset+1, 0, 1)) } } @@ -529,7 +535,7 @@ func addSpacingStyled(cells []string, style lipgloss.Style) []string { spaced := make([]string, 0, len(cells)*2-1) for i, cell := range cells { if i > 0 { - spaced = append(spaced, style.Copy().Padding(0, 0).Render(" ")) + spaced = append(spaced, style.Padding(0, 0).Render(" ")) } spaced = append(spaced, cell) } diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index 11a78cd..3122a34 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -6,17 +6,17 @@ import ( "strings" "time" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/textinput" + tea "charm.land/bubbletea/v2" "github.com/charmbracelet/x/ansi" "codeberg.org/snonux/tasksamurai/internal/task" ) // handleTextInput provides generic text input handling for all input modes -func (m *Model) handleTextInput(msg tea.KeyMsg, input *textinput.Model, onEnter func(string) error, onExit func()) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handleTextInput(msg tea.KeyPressMsg, input *textinput.Model, onEnter func(string) error, onExit func()) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": value := input.Value() if err := onEnter(value); err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) @@ -29,7 +29,7 @@ func (m *Model) handleTextInput(msg tea.KeyMsg, input *textinput.Model, onEnter onExit() m.updateTableHeight() return m, nil - case tea.KeyEsc: + case "esc": input.Blur() onExit() m.updateTableHeight() @@ -41,13 +41,13 @@ func (m *Model) handleTextInput(msg tea.KeyMsg, input *textinput.Model, onEnter } // handleAnnotationMode handles keyboard input when in annotation mode -func (m *Model) handleAnnotationMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleAnnotationMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { // Annotation can be empty when replacing (to remove all) if !m.replaceAnnotations && strings.TrimSpace(value) == "" { return fmt.Errorf("annotation cannot be empty") } - + if m.replaceAnnotations { if err := task.ReplaceAnnotations(m.annotateID, value); err != nil { return err @@ -61,14 +61,14 @@ func (m *Model) handleAnnotationMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { _ = m.reload() return nil } - + onExit := func() { m.annotating = false m.replaceAnnotations = false } - + model, cmd := m.handleTextInput(msg, &m.annotateInput, onEnter, onExit) - if msg.Type == tea.KeyEnter && m.annotateInput.Value() != "" { + if msg.String() == "enter" && m.annotateInput.Value() != "" { // Start blink after successful annotation return model, m.startBlink(m.annotateID, false) } @@ -76,7 +76,7 @@ func (m *Model) handleAnnotationMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleDescriptionMode handles keyboard input when editing description -func (m *Model) handleDescriptionMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleDescriptionMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { if err := validateDescription(value); err != nil { return err @@ -87,20 +87,20 @@ func (m *Model) handleDescriptionMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { _ = m.reload() return nil } - + onExit := func() { m.descEditing = false } - + model, cmd := m.handleTextInput(msg, &m.descInput, onEnter, onExit) - if msg.Type == tea.KeyEnter { + if msg.String() == "enter" { return model, m.startBlink(m.descID, false) } return model, cmd } // handleTagsMode handles keyboard input when editing tags -func (m *Model) handleTagsMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleTagsMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { words := strings.Fields(value) var adds, removes []string @@ -136,13 +136,13 @@ func (m *Model) handleTagsMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { _ = m.reload() return nil } - + onExit := func() { m.tagsEditing = false } - + model, cmd := m.handleTextInput(msg, &m.tagsInput, onEnter, onExit) - if msg.Type == tea.KeyEnter { + if msg.String() == "enter" { if m.showTaskDetail { // In detail view, blink the tags field return model, m.startDetailBlink(4) // Tags is field index 4 @@ -153,9 +153,9 @@ func (m *Model) handleTagsMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleDueEditMode handles due date editing -func (m *Model) handleDueEditMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handleDueEditMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": if err := task.SetDueDate(m.dueID, m.dueDate.Format("2006-01-02")); err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { @@ -174,12 +174,12 @@ func (m *Model) handleDueEditMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } m.updateTableHeight() return m, cmd - case tea.KeyEsc: + case "esc": m.dueEditing = false m.updateTableHeight() return m, nil } - + switch msg.String() { case "h", "left": m.dueDate = m.dueDate.AddDate(0, 0, -1) @@ -194,7 +194,7 @@ func (m *Model) handleDueEditMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleRecurrenceMode handles recurrence editing -func (m *Model) handleRecurrenceMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleRecurrenceMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { if err := validateRecurrence(value); err != nil { return err @@ -205,13 +205,13 @@ func (m *Model) handleRecurrenceMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { _ = m.reload() return nil } - + onExit := func() { m.recurEditing = false } - + model, cmd := m.handleTextInput(msg, &m.recurInput, onEnter, onExit) - if msg.Type == tea.KeyEnter { + if msg.String() == "enter" { if m.showTaskDetail { // In detail view, blink the recurrence field (dynamic index) // Need to calculate the index based on whether recurrence field exists @@ -226,18 +226,18 @@ func (m *Model) handleRecurrenceMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleProjectMode handles project editing -func (m *Model) handleProjectMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleProjectMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { return task.SetProject(m.projID, value) } - + onExit := func() { m.projEditing = false m.reload() } - + model, cmd := m.handleTextInput(msg, &m.projInput, onEnter, onExit) - if msg.Type == tea.KeyEnter { + if msg.String() == "enter" { if m.showTaskDetail { // In detail view, blink the project field return model, m.startDetailBlink(fieldProject) // Project field index in detail view @@ -248,9 +248,9 @@ func (m *Model) handleProjectMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handlePriorityMode handles priority selection -func (m *Model) handlePriorityMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handlePriorityMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": priority := priorityOptions[m.priorityIndex] if err := validatePriority(priority); err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) @@ -277,12 +277,12 @@ func (m *Model) handlePriorityMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } m.updateTableHeight() return m, cmd - case tea.KeyEsc: + case "esc": m.prioritySelecting = false m.updateTableHeight() return m, nil } - + switch msg.String() { case "h", "left": m.priorityIndex = (m.priorityIndex + len(priorityOptions) - 1) % len(priorityOptions) @@ -293,29 +293,29 @@ func (m *Model) handlePriorityMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } // handleFilterMode handles filter editing -func (m *Model) handleFilterMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleFilterMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { onEnter := func(value string) error { m.filters = strings.Fields(value) _ = m.reload() return nil } - + onExit := func() { m.filterEditing = false } - + return m.handleTextInput(msg, &m.filterInput, onEnter, onExit) } // handleAddTaskMode handles adding a new task -func (m *Model) handleAddTaskMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handleAddTaskMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": oldIDs := make(map[int]struct{}, len(m.tasks)) for _, tsk := range m.tasks { oldIDs[tsk.ID] = struct{}{} } - + if err := task.AddLine(m.addInput.Value()); err != nil { m.statusMsg = fmt.Sprintf("Error: %v", err) cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { @@ -323,11 +323,11 @@ func (m *Model) handleAddTaskMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { }) return m, cmd } - + m.addingTask = false m.addInput.Blur() m.reload() - + // Find the newly added task var newID int row := -1 @@ -338,7 +338,7 @@ func (m *Model) handleAddTaskMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { break } } - + m.updateTableHeight() if row >= 0 { prevRow := m.tbl.Cursor() @@ -349,23 +349,23 @@ func (m *Model) handleAddTaskMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, m.startBlink(newID, false) } return m, nil - - case tea.KeyEsc: + + case "esc": m.addingTask = false m.addInput.Blur() m.updateTableHeight() return m, nil } - + var cmd tea.Cmd m.addInput, cmd = m.addInput.Update(msg) return m, cmd } // handleSearchMode handles search input -func (m *Model) handleSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handleSearchMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": pattern := m.searchInput.Value() if pattern != "" { // Check cache first @@ -388,7 +388,7 @@ func (m *Model) handleSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.searchInput.Blur() m.reload() m.updateTableHeight() - + if len(m.searchMatches) > 0 { match := m.searchMatches[m.searchIndex] prevRow := m.tbl.Cursor() @@ -398,23 +398,23 @@ func (m *Model) handleSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.updateSelectionHighlight(prevRow, m.tbl.Cursor(), prevCol, m.tbl.ColumnCursor()) } return m, nil - - case tea.KeyEsc: + + case "esc": m.searching = false m.searchInput.Blur() m.updateTableHeight() return m, nil } - + var cmd tea.Cmd m.searchInput, cmd = m.searchInput.Update(msg) return m, cmd } // handleHelpSearchMode handles search input in help mode -func (m *Model) handleHelpSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - switch msg.Type { - case tea.KeyEnter: +func (m *Model) handleHelpSearchMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "enter": pattern := m.helpSearchInput.Value() if pattern != "" { // Check cache first @@ -435,7 +435,7 @@ func (m *Model) handleHelpSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } m.helpSearching = false m.helpSearchInput.Blur() - + // Find matching help lines m.helpSearchMatches = nil if m.helpSearchRegex != nil { @@ -451,13 +451,13 @@ func (m *Model) handleHelpSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } } return m, nil - - case tea.KeyEsc: + + case "esc": m.helpSearching = false m.helpSearchInput.Blur() return m, nil } - + var cmd tea.Cmd m.helpSearchInput, cmd = m.helpSearchInput.Update(msg) return m, cmd @@ -465,7 +465,7 @@ func (m *Model) handleHelpSearchMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { // handleBlinkingState handles input when a task is blinking func (m *Model) handleBlinkingState(msg tea.Msg) (tea.Model, tea.Cmd) { - if _, ok := msg.(tea.KeyMsg); ok { + if _, ok := msg.(tea.KeyPressMsg); ok { // Only allow navigation while blinking prevRow := m.tbl.Cursor() prevCol := m.tbl.ColumnCursor() @@ -480,7 +480,7 @@ func (m *Model) handleBlinkingState(msg tea.Msg) (tea.Model, tea.Cmd) { } // handleEditingModes checks if we're in any editing mode and handles it -func (m *Model) handleEditingModes(msg tea.KeyMsg) (handled bool, model tea.Model, cmd tea.Cmd) { +func (m *Model) handleEditingModes(msg tea.KeyPressMsg) (handled bool, model tea.Model, cmd tea.Cmd) { switch { case m.annotating: model, cmd = m.handleAnnotationMode(msg) @@ -539,11 +539,11 @@ func (m *Model) getTaskAtCursor() *task.Task { } // handleTaskDetailMode handles keyboard input in task detail view -func (m *Model) handleTaskDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleTaskDetailMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { if m.detailSearching { var cmd tea.Cmd - switch msg.Type { - case tea.KeyEnter: + switch msg.String() { + case "enter": pattern := m.detailSearchInput.Value() if pattern != "" { re, err := compileAndCacheRegex(pattern) @@ -559,7 +559,7 @@ func (m *Model) handleTaskDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.detailSearching = false m.detailSearchInput.Blur() return m, nil - case tea.KeyEsc, tea.KeyCtrlC: + case "esc", "ctrl+c": m.detailSearching = false m.detailSearchInput.Blur() return m, nil @@ -568,7 +568,7 @@ func (m *Model) handleTaskDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, cmd } } - + // Normal task detail view mode switch msg.String() { case "q", "esc": @@ -605,7 +605,7 @@ func (m *Model) handleTaskDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { // Check if current field is editable return m.handleDetailFieldEdit() } - + return m, nil } @@ -765,4 +765,4 @@ func (m *Model) activateDescriptionEdit(id int, tsk *task.Task) (tea.Model, tea. m.descInput.Focus() m.updateTableHeight() return m, nil -}
\ No newline at end of file +} diff --git a/internal/ui/keyhandlers.go b/internal/ui/keyhandlers.go index 0a2bc62..9eedafa 100644 --- a/internal/ui/keyhandlers.go +++ b/internal/ui/keyhandlers.go @@ -7,15 +7,15 @@ import ( "strings" "time" - "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" + "charm.land/bubbles/v2/textinput" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" "codeberg.org/snonux/tasksamurai/internal/task" ) // handleNormalMode handles keyboard input in normal mode (not editing) -func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { // If help is shown, handle special cases if m.showHelp { switch msg.String() { @@ -36,7 +36,7 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case "pgup", "b": m.helpViewport.PageUp() return m, nil - case "pgdown", " ": + case "pgdown", "space": m.helpViewport.PageDown() return m, nil case "g", "home": @@ -50,7 +50,7 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } } - + switch msg.String() { case "H": return m.handleToggleHelp() @@ -98,7 +98,7 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m.handleToggleDisco() case "B": return m.handleToggleBlink() - case " ": + case "space": return m.handleRefresh() case "/", "?": return m.handleSearch() @@ -123,7 +123,7 @@ func (m *Model) handleNormalMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { func (m *Model) handleToggleHelp() (tea.Model, tea.Cmd) { m.showHelp = true // Initialize help viewport with proper dimensions - width := m.tbl.Width() - 4 // Account for padding + width := m.tbl.Width() - 4 // Account for padding height := m.windowHeight - 6 // Leave room for status bars and search input if width <= 0 { width = 80 // Default width @@ -131,7 +131,7 @@ func (m *Model) handleToggleHelp() (tea.Model, tea.Cmd) { if height <= 0 { height = 20 // Default height } - m.helpViewport = viewport.New(width, height) + m.helpViewport = viewport.New(viewport.WithWidth(width), viewport.WithHeight(height)) // Set the content immediately content := m.buildHelpContent() m.helpViewport.SetContent(content) @@ -187,7 +187,7 @@ func (m *Model) handleToggleStart() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + // Check if task is started started := false for _, tsk := range m.tasks { @@ -196,7 +196,7 @@ func (m *Model) handleToggleStart() (tea.Model, tea.Cmd) { break } } - + if started { if err := task.Stop(id); err != nil { m.showError(err) @@ -208,7 +208,7 @@ func (m *Model) handleToggleStart() (tea.Model, tea.Cmd) { return m, nil } } - + m.reload() return m, m.startBlink(id, false) } @@ -226,17 +226,17 @@ func (m *Model) handleOpenURL() (tea.Model, tea.Cmd) { if task == nil { return m, nil } - + url := urlRegex.FindString(task.Description) if url == "" { return m, nil } - + if err := exec.Command(m.browserCmd, url).Run(); err != nil { m.showError(fmt.Errorf("opening browser: %w", err)) return m, nil } - + return m, m.startBlink(task.ID, false) } @@ -244,21 +244,21 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) { if len(m.undoStack) == 0 { return m, nil } - + uuid := m.undoStack[len(m.undoStack)-1] m.undoStack = m.undoStack[:len(m.undoStack)-1] - + if err := task.SetStatusUUID(uuid, "pending"); err != nil { m.showError(err) return m, nil } - + // Reload the task list to get the updated task with its new ID if err := m.reload(); err != nil { m.showError(err) return m, nil } - + // Find the task ID for blinking var id int var found bool @@ -269,7 +269,7 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) { break } } - + // If task not found or has ID 0, try to get it directly from Taskwarrior if !found || id == 0 { // Use task export with UUID filter to get the specific task @@ -278,7 +278,7 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) { filters = append(filters, m.filters...) } filters = append(filters, "status:pending") - + tasks, err := task.Export(filters...) if err == nil && len(tasks) > 0 { id = tasks[0].ID @@ -291,13 +291,13 @@ func (m *Model) handleUndo() (tea.Model, tea.Cmd) { } } } - + // If we still don't have a valid ID, don't try to blink if id == 0 { m.statusMsg = "Task restored" return m, nil } - + return m, m.startBlink(id, false) } @@ -306,7 +306,7 @@ func (m *Model) handleSetDueDate() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + m.clearEditingModes() m.dueID = id m.dueEditing = true @@ -320,13 +320,13 @@ func (m *Model) handleRemoveDueDate() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + // In Taskwarrior, passing an empty value to due: removes the due date if err := task.SetDueDate(id, ""); err != nil { m.showError(err) return m, nil } - + m.reload() return m, m.startBlink(id, false) } @@ -336,15 +336,15 @@ func (m *Model) handleRandomDueDate() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + days := rand.Intn(31) + 7 due := time.Now().AddDate(0, 0, days).Format("2006-01-02") - + if err := task.SetDueDate(id, due); err != nil { m.showError(err) return m, nil } - + m.reload() return m, m.startBlink(id, false) } @@ -354,12 +354,12 @@ func (m *Model) handleSetRecurrence() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + task := m.getTaskAtCursor() if task == nil { return m, nil } - + m.clearEditingModes() m.recurID = id m.recurEditing = true @@ -374,7 +374,7 @@ func (m *Model) handleSetPriority() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + m.clearEditingModes() m.priorityID = id m.prioritySelecting = true @@ -388,7 +388,7 @@ func (m *Model) handleAnnotate(replace bool) (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + m.clearEditingModes() m.annotateID = id m.annotating = true @@ -422,7 +422,7 @@ func (m *Model) handleEditTags() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + m.clearEditingModes() m.tagsID = id m.tagsEditing = true @@ -437,11 +437,11 @@ func (m *Model) handleEditProject() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + m.clearEditingModes() m.projID = id m.projEditing = true - + // Get current project value task := m.getTaskAtCursor() if task != nil { @@ -459,29 +459,29 @@ func (m *Model) handleTagToProject() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + // Get the task at cursor currentTask := m.getTaskAtCursor() if currentTask == nil || len(currentTask.Tags) == 0 { // No tags to convert return m, nil } - + // Get the first tag firstTag := currentTask.Tags[0] - + // Set the tag as project if err := task.SetProject(id, firstTag); err != nil { m.showError(err) return m, nil } - + // Remove the tag from the task if err := task.RemoveTags(id, []string{firstTag}); err != nil { m.showError(err) return m, nil } - + m.reload() return m, m.startBlink(id, false) } @@ -533,7 +533,7 @@ func (m *Model) handleNextSearchMatch() (tea.Model, tea.Cmd) { if len(m.searchMatches) == 0 { return m, nil } - + m.searchIndex = (m.searchIndex + 1) % len(m.searchMatches) match := m.searchMatches[m.searchIndex] prevRow := m.tbl.Cursor() @@ -548,7 +548,7 @@ func (m *Model) handlePrevSearchMatch() (tea.Model, tea.Cmd) { if len(m.searchMatches) == 0 { return m, nil } - + m.searchIndex = (m.searchIndex - 1 + len(m.searchMatches)) % len(m.searchMatches) match := m.searchMatches[m.searchIndex] prevRow := m.tbl.Cursor() @@ -572,7 +572,7 @@ func (m *Model) handleNextHelpSearchMatch() (tea.Model, tea.Cmd) { if len(m.helpSearchMatches) == 0 { return m, nil } - + m.helpSearchIndex = (m.helpSearchIndex + 1) % len(m.helpSearchMatches) // In the future, we could add visual indication of current match return m, nil @@ -582,7 +582,7 @@ func (m *Model) handlePrevHelpSearchMatch() (tea.Model, tea.Cmd) { if len(m.helpSearchMatches) == 0 { return m, nil } - + m.helpSearchIndex = (m.helpSearchIndex - 1 + len(m.helpSearchMatches)) % len(m.helpSearchMatches) // In the future, we could add visual indication of current match return m, nil @@ -593,7 +593,7 @@ func (m *Model) handleShowTaskDetail() (tea.Model, tea.Cmd) { if err != nil { return m, nil } - + // Find the task with this ID for i := range m.tasks { if m.tasks[i].ID == id { @@ -607,11 +607,11 @@ func (m *Model) handleShowTaskDetail() (tea.Model, tea.Cmd) { m.detailBlinkCount = 0 m.detailSearchInput = textinput.New() m.detailSearchInput.Placeholder = "Search..." - m.detailSearchInput.Width = 30 + m.detailSearchInput.SetWidth(30) break } } - + return m, nil } @@ -661,7 +661,7 @@ func (m *Model) handleEnterOrEdit() (tea.Model, tea.Cmd) { return m, nil } -func (m *Model) handleTableNavigation(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleTableNavigation(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { prevRow := m.tbl.Cursor() prevCol := m.tbl.ColumnCursor() var cmd tea.Cmd @@ -684,22 +684,22 @@ func (m *Model) handleJumpToRandomTask() (tea.Model, tea.Cmd) { m.statusMsg = "No tasks to jump to" return m, nil } - + // Pick a random index randomIndex := rand.Intn(len(m.tasks)) - + // Update cursor position prevRow := m.tbl.Cursor() prevCol := m.tbl.ColumnCursor() m.tbl.SetCursor(randomIndex) m.updateSelectionHighlight(prevRow, randomIndex, prevCol, m.tbl.ColumnCursor()) - + // Blink the task to indicate jump if randomIndex < len(m.tasks) { taskID := m.tasks[randomIndex].ID return m, m.startBlink(taskID, false) } - + return m, nil } @@ -712,27 +712,27 @@ func (m *Model) handleJumpToRandomTaskNoDue() (tea.Model, tea.Cmd) { noDueTasks = append(noDueTasks, i) } } - + if len(noDueTasks) == 0 { m.statusMsg = "No tasks without due date to jump to" return m, nil } - + // Pick a random task from the no-due list randomChoice := rand.Intn(len(noDueTasks)) randomIndex := noDueTasks[randomChoice] - + // Update cursor position prevRow := m.tbl.Cursor() prevCol := m.tbl.ColumnCursor() m.tbl.SetCursor(randomIndex) m.updateSelectionHighlight(prevRow, randomIndex, prevCol, m.tbl.ColumnCursor()) - + // Blink the task to indicate jump if randomIndex < len(m.tasks) { taskID := m.tasks[randomIndex].ID return m, m.startBlink(taskID, false) } - + return m, nil -}
\ No newline at end of file +} diff --git a/internal/ui/table.go b/internal/ui/table.go index 05c0548..1d593a0 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -11,10 +11,10 @@ import ( "github.com/charmbracelet/x/ansi" - "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "charm.land/bubbles/v2/textinput" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" "codeberg.org/snonux/tasksamurai/internal" atable "codeberg.org/snonux/tasksamurai/internal/atable" @@ -64,18 +64,18 @@ type searchState struct { // Blink fields here are separate from blinkState because they drive a // per-field highlight inside the detail view rather than a table row. type detailViewState struct { - showTaskDetail bool - currentTaskDetail *task.Task - detailSearching bool - detailSearchInput textinput.Model - detailSearchRegex *regexp.Regexp - detailFieldIndex int // currently selected field (-1 = none) - detailBlinkField int // field currently blinking (-1 = none) - detailBlinkOn bool // whether the blink is currently on - detailBlinkCount int // number of blink cycles completed so far + showTaskDetail bool |
