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/ui | |
| parent | 33064821d637aeef94fe6ee96edbac7a503c0692 (diff) | |
Migrate UI stack to Bubble Tea v2
Diffstat (limited to 'internal/ui')
| -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 |
5 files changed, 290 insertions, 264 deletions
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 + 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 // detailDescEditing lives here (not in editState) because it drives an // external-editor launch from the detail overlay, not inline text input. - detailDescEditing bool // whether the description editor is open + detailDescEditing bool // whether the description editor is open } // editState holds inline field-editing state for the task table. @@ -153,9 +153,9 @@ type Model struct { inProgress int due int - filters []string - tasks []task.Task - undoStack []string + filters []string + tasks []task.Task + undoStack []string browserCmd string theme Theme @@ -171,8 +171,8 @@ type Model struct { type editDoneMsg struct{ err error } // descEditDoneMsg is emitted when the external editor for description finishes. -type descEditDoneMsg struct{ - err error +type descEditDoneMsg struct { + err error tempFile string } @@ -202,7 +202,7 @@ func editDescriptionCmd(description string) tea.Cmd { return descEditDoneMsg{err: err, tempFile: ""} } tmpPath := tmpFile.Name() - + // Write current description to temp file _, err = tmpFile.WriteString(description) _ = tmpFile.Close() @@ -210,19 +210,19 @@ func editDescriptionCmd(description string) tea.Cmd { _ = os.Remove(tmpPath) return descEditDoneMsg{err: err, tempFile: ""} } - + // Get editor from environment editor := os.Getenv("EDITOR") if editor == "" { editor = "vi" // fallback to vi } - + // Create the command c := exec.Command(editor, tmpPath) c.Stdin = os.Stdin c.Stdout = os.Stdout c.Stderr = os.Stderr - + // Use ExecProcess to properly handle the external TUI editor return tea.ExecProcess(c, func(err error) tea.Msg { return descEditDoneMsg{err: err, tempFile: tmpPath} @@ -384,7 +384,7 @@ func (m *Model) reload() error { m.total = task.TotalTasks(tasks) m.inProgress = task.InProgressTasks(tasks) m.due = task.DueTasks(tasks, time.Now()) - + // Refresh current task detail if in detail view if m.showTaskDetail { m.refreshCurrentTaskDetail() @@ -447,12 +447,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case struct{ clearStatus bool }: m.statusMsg = "" return m, nil - case tea.KeyMsg: + case tea.KeyPressMsg: // Handle blinking state first if m.blinkID != 0 { return m.handleBlinkingState(msg) } - + // Check if we're in detail view if m.showTaskDetail { // If we're editing in detail view, let editing modes handle it @@ -464,16 +464,16 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Otherwise handle detail view navigation return m.handleTaskDetailMode(msg) } - + // Check if we're in any editing mode if handled, model, cmd := m.handleEditingModes(msg); handled { return model, cmd } - + // Otherwise handle normal mode return m.handleNormalMode(msg) } - + // Default case - pass through to appropriate component if m.showHelp { // Update help viewport for mouse wheel and other events @@ -481,7 +481,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.helpViewport, cmd = m.helpViewport.Update(msg) return m, cmd } - + var cmd tea.Cmd m.tbl, cmd = m.tbl.Update(msg) return m, cmd @@ -493,17 +493,17 @@ func (m *Model) handleWindowResize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) { m.windowHeight = msg.Height m.computeColumnWidths() m.updateTableHeight() - + // Update help viewport if active - if m.showHelp && m.helpViewport.Width > 0 { + if m.showHelp && m.helpViewport.Width() > 0 { width := msg.Width - 4 height := msg.Height - 6 if width > 0 && height > 0 { - m.helpViewport.Width = width - m.helpViewport.Height = height + m.helpViewport.SetWidth(width) + m.helpViewport.SetHeight(height) } } - + return m, nil } @@ -522,7 +522,7 @@ func (m *Model) handleEditDone(msg editDoneMsg) (tea.Model, tea.Cmd) { func (m *Model) handleDescEditDone(msg descEditDoneMsg) (tea.Model, tea.Cmd) { m.detailDescEditing = false _ = os.Remove(msg.tempFile) // Clean up temp file - + if msg.err != nil { m.statusMsg = fmt.Sprintf("Edit error: %v", msg.err) cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { @@ -530,7 +530,7 @@ func (m *Model) handleDescEditDone(msg descEditDoneMsg) (tea.Model, tea.Cmd) { }) return m, cmd } - + // Read the edited content content, err := os.ReadFile(msg.tempFile) if err != nil { @@ -540,7 +540,7 @@ func (m *Model) handleDescEditDone(msg descEditDoneMsg) (tea.Model, tea.Cmd) { }) return m, cmd } - + // Update the description newDesc := strings.TrimSpace(string(content)) if m.currentTaskDetail != nil { @@ -552,12 +552,12 @@ func (m *Model) handleDescEditDone(msg descEditDoneMsg) (tea.Model, tea.Cmd) { }) return m, cmd } - + // Reload and start blinking m.reload() return m, m.startDetailBlink(m.detailDescriptionFieldIndex()) } - + return m, nil } @@ -567,7 +567,7 @@ func (m *Model) handleBlinkMsg() (tea.Model, tea.Cmd) { if m.showTaskDetail && m.detailBlinkField != -1 { m.detailBlinkOn = !m.detailBlinkOn m.detailBlinkCount++ - + if m.detailBlinkCount >= blinkCycles { m.detailBlinkField = -1 m.detailBlinkOn = false @@ -577,15 +577,15 @@ func (m *Model) handleBlinkMsg() (tea.Model, tea.Cmd) { } return m, nil } - + if m.blinkID == 0 { return m, nil } - + m.blinkOn = !m.blinkO |
