diff options
| author | Paul Buetow <paul@buetow.org> | 2025-06-28 10:59:18 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-06-28 10:59:18 +0300 |
| commit | ab986c63c75a3114b409d171ab2aaf616c087554 (patch) | |
| tree | e4afe80fde04426d9605bfddf7e23d9f81f3f864 /internal | |
| parent | 43a0dcbd6617ccf28abfbb50b98bf9fbf8383835 (diff) | |
feat: add field navigation to task detail view
- Implement field-by-field navigation using arrow keys and vi-style keys (↑/k, ↓/j)
- Add highlighting for the currently selected field with theme's selected background
- Support Home/g and End/G keys to jump to first/last field
- Dynamically calculate field count based on optional fields (recurrence, annotations)
- Update instructions to show navigation help
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/ui/handlers.go | 17 | ||||
| -rw-r--r-- | internal/ui/keyhandlers.go | 1 | ||||
| -rw-r--r-- | internal/ui/table.go | 1 | ||||
| -rw-r--r-- | internal/ui/taskdetail.go | 105 |
4 files changed, 105 insertions, 19 deletions
diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index dce6bba..019e8d2 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -540,6 +540,23 @@ func (m *Model) handleTaskDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case "N": // Previous search match - not implemented yet but could be added return m, nil + case "up", "k": + if m.detailFieldIndex > 0 { + m.detailFieldIndex-- + } + return m, nil + case "down", "j": + maxFields := m.getDetailFieldCount() + if m.detailFieldIndex < maxFields-1 { + m.detailFieldIndex++ + } + return m, nil + case "g", "home": + m.detailFieldIndex = 0 + return m, nil + case "G", "end": + m.detailFieldIndex = m.getDetailFieldCount() - 1 + return m, nil } return m, nil diff --git a/internal/ui/keyhandlers.go b/internal/ui/keyhandlers.go index fe8c91e..e3bed58 100644 --- a/internal/ui/keyhandlers.go +++ b/internal/ui/keyhandlers.go @@ -458,6 +458,7 @@ func (m *Model) handleShowTaskDetail() (tea.Model, tea.Cmd) { m.currentTaskDetail = &m.tasks[i] m.detailSearching = false m.detailSearchRegex = nil + m.detailFieldIndex = 0 m.detailSearchInput = textinput.New() m.detailSearchInput.Placeholder = "Search..." m.detailSearchInput.Width = 30 diff --git a/internal/ui/table.go b/internal/ui/table.go index b863c30..281f3ba 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -127,6 +127,7 @@ type Model struct { detailSearching bool detailSearchInput textinput.Model detailSearchRegex *regexp.Regexp + detailFieldIndex int // Current selected field in detail view } // editDoneMsg is emitted when the external editor process finishes. diff --git a/internal/ui/taskdetail.go b/internal/ui/taskdetail.go index f332c8d..d98416c 100644 --- a/internal/ui/taskdetail.go +++ b/internal/ui/taskdetail.go @@ -9,6 +9,22 @@ import ( ) +// Define field indices for navigation +const ( + fieldID = iota + fieldUUID + fieldStatus + fieldPriority + fieldTags + fieldDue + fieldStart + fieldEntry + fieldRecur + fieldDescription + fieldAnnotations + fieldCount // Total number of fields +) + // renderTaskDetail renders the detailed view of a single task func (m *Model) renderTaskDetail() string { if m.currentTaskDetail == nil { @@ -44,9 +60,13 @@ func (m *Model) renderTaskDetail() string { lines = append(lines, "") // Task fields - lines = append(lines, m.renderTaskField("ID", fmt.Sprintf("%d", t.ID), labelStyle, valueStyle)) - lines = append(lines, m.renderTaskField("UUID", t.UUID, labelStyle, valueStyle)) - lines = append(lines, m.renderTaskField("Status", t.Status, labelStyle, valueStyle)) + currentField := 0 + lines = append(lines, m.renderTaskFieldWithIndex("ID", fmt.Sprintf("%d", t.ID), labelStyle, valueStyle, currentField)) + currentField++ + lines = append(lines, m.renderTaskFieldWithIndex("UUID", t.UUID, labelStyle, valueStyle, currentField)) + currentField++ + lines = append(lines, m.renderTaskFieldWithIndex("Status", t.Status, labelStyle, valueStyle, currentField)) + currentField++ // Priority with color priorityValue := t.Priority @@ -65,52 +85,69 @@ func (m *Model) renderTaskDetail() string { priorityStyle = priorityStyle.Background(lipgloss.Color(m.theme.PrioLowBG)) priorityValue = "L (Low)" } - lines = append(lines, m.renderTaskField("Priority", priorityValue, labelStyle, priorityStyle)) + lines = append(lines, m.renderTaskFieldWithIndex("Priority", priorityValue, labelStyle, priorityStyle, currentField)) + currentField++ // Tags tagStr := strings.Join(t.Tags, ", ") if tagStr == "" { tagStr = "-" } - lines = append(lines, m.renderTaskField("Tags", tagStr, labelStyle, valueStyle)) + lines = append(lines, m.renderTaskFieldWithIndex("Tags", tagStr, labelStyle, valueStyle, currentField)) + currentField++ // Dates - lines = append(lines, m.renderTaskField("Due", m.formatTaskDate(t.Due), labelStyle, valueStyle)) - lines = append(lines, m.renderTaskField("Start", m.formatTaskDate(t.Start), labelStyle, valueStyle)) - // End field doesn't exist in Task struct, removed - lines = append(lines, m.renderTaskField("Entry", m.formatTaskDate(t.Entry), labelStyle, valueStyle)) - // Modified field doesn't exist in Task struct, removed + lines = append(lines, m.renderTaskFieldWithIndex("Due", m.formatTaskDate(t.Due), labelStyle, valueStyle, currentField)) + currentField++ + lines = append(lines, m.renderTaskFieldWithIndex("Start", m.formatTaskDate(t.Start), labelStyle, valueStyle, currentField)) + currentField++ + lines = append(lines, m.renderTaskFieldWithIndex("Entry", m.formatTaskDate(t.Entry), labelStyle, valueStyle, currentField)) + currentField++ // Recurrence if t.Recur != "" { - lines = append(lines, m.renderTaskField("Recurrence", t.Recur, labelStyle, valueStyle)) + lines = append(lines, m.renderTaskFieldWithIndex("Recurrence", t.Recur, labelStyle, valueStyle, currentField)) + currentField++ } // Description - with full space lines = append(lines, "") - lines = append(lines, labelStyle.Render("Description:")) + descLabelStyle := labelStyle.Copy() + descValueStyle := descStyle.Copy() + if m.detailFieldIndex == currentField { + descLabelStyle = descLabelStyle.Background(lipgloss.Color(m.theme.SelectedBG)) + descValueStyle = descValueStyle.Background(lipgloss.Color(m.theme.SelectedBG)) + } + lines = append(lines, descLabelStyle.Render("Description:")) if t.Description != "" { // Highlight search matches if searching desc := t.Description if m.detailSearchRegex != nil && m.detailSearchRegex.MatchString(desc) { desc = m.highlightMatches(desc, m.detailSearchRegex) } - lines = append(lines, descStyle.Render(desc)) + lines = append(lines, descValueStyle.Render(desc)) } else { - lines = append(lines, descStyle.Render("-")) + lines = append(lines, descValueStyle.Render("-")) } + currentField++ // Annotations if len(t.Annotations) > 0 { lines = append(lines, "") - lines = append(lines, labelStyle.Render("Annotations:")) + annLabelStyle := labelStyle.Copy() + annValueStyle := descStyle.Copy() + if m.detailFieldIndex == currentField { + annLabelStyle = annLabelStyle.Background(lipgloss.Color(m.theme.SelectedBG)) + annValueStyle = annValueStyle.Background(lipgloss.Color(m.theme.SelectedBG)) + } + lines = append(lines, annLabelStyle.Render("Annotations:")) for _, ann := range t.Annotations { annText := fmt.Sprintf("[%s] %s", m.formatTaskDate(ann.Entry), ann.Description) // Highlight search matches if m.detailSearchRegex != nil && m.detailSearchRegex.MatchString(annText) { annText = m.highlightMatches(annText, m.detailSearchRegex) } - lines = append(lines, descStyle.Render(annText)) + lines = append(lines, annValueStyle.Render(annText)) } } @@ -121,10 +158,11 @@ func (m *Model) renderTaskDetail() string { Foreground(lipgloss.Color("245")). Italic(true) lines = append(lines, instructionStyle.Render("Press ESC or Q to return to table view")) + lines = append(lines, instructionStyle.Render("Use ↑/k and ↓/j to navigate fields")) if m.detailSearching { lines = append(lines, instructionStyle.Render("Type to search, Enter to confirm")) } else { - lines = append(lines, instructionStyle.Render("Press / to search, N/n to navigate matches")) + lines = append(lines, instructionStyle.Render("Press / to search")) } // Add search input if searching @@ -138,11 +176,18 @@ func (m *Model) renderTaskDetail() string { return strings.Join(lines, "\n") } -// renderTaskField renders a single field in the task detail view -func (m *Model) renderTaskField(label, value string, labelStyle, valueStyle lipgloss.Style) string { +// renderTaskFieldWithIndex renders a single field with highlighting based on index +func (m *Model) renderTaskFieldWithIndex(label, value string, labelStyle, valueStyle lipgloss.Style, fieldIndex int) string { if value == "" { value = "-" } + + // Apply selection highlighting if this field is selected + if m.detailFieldIndex == fieldIndex { + labelStyle = labelStyle.Background(lipgloss.Color(m.theme.SelectedBG)) + valueStyle = valueStyle.Background(lipgloss.Color(m.theme.SelectedBG)) + } + // Highlight search matches if m.detailSearchRegex != nil && m.detailSearchRegex.MatchString(value) { value = m.highlightMatches(value, m.detailSearchRegex) @@ -162,6 +207,28 @@ func (m *Model) formatTaskDate(dateStr string) string { return dateStr } +// getDetailFieldCount returns the actual number of navigable fields for the current task +func (m *Model) getDetailFieldCount() int { + if m.currentTaskDetail == nil { + return 0 + } + + // Basic fields that are always present: ID, UUID, Status, Priority, Tags, Due, Start, Entry, Description + count := 9 + + // Add recurrence if present + if m.currentTaskDetail.Recur != "" { + count++ + } + + // Add annotations if present + if len(m.currentTaskDetail.Annotations) > 0 { + count++ + } + + return count +} + // highlightMatches highlights regex matches in a string func (m *Model) highlightMatches(text string, re *regexp.Regexp) string { highlightStyle := lipgloss.NewStyle(). |
