summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-28 11:22:55 +0300
committerPaul Buetow <paul@buetow.org>2025-06-28 11:22:55 +0300
commitb659b8cf87c86280f62cef0f499a60b999e6ce6b (patch)
tree5c0dc0606a8bda997a3b25f4c8a51479c34bb51b
parentab986c63c75a3114b409d171ab2aaf616c087554 (diff)
feat: add editing and visual feedback to task detail view
- Enable in-place editing of Priority, Tags, Due, and Recurrence fields - Add blinking effect (bright yellow) after field changes - Change all status message timeouts from 3 to 2 seconds - Support disco mode in detail view (random theme on edit) - Add showLabel parameter to view functions to hide redundant labels - Temporarily clear input prompts in detail view for cleaner UI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--internal/ui/handlers.go148
-rw-r--r--internal/ui/keyhandlers.go3
-rw-r--r--internal/ui/table.go68
-rw-r--r--internal/ui/taskdetail.go186
-rwxr-xr-xtasksamuraibin5902743 -> 5963040 bytes
5 files changed, 330 insertions, 75 deletions
diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go
index 019e8d2..3be357a 100644
--- a/internal/ui/handlers.go
+++ b/internal/ui/handlers.go
@@ -20,7 +20,7 @@ func (m *Model) handleTextInput(msg tea.KeyMsg, input *textinput.Model, onEnter
value := input.Value()
if err := onEnter(value); err != nil {
m.statusMsg = fmt.Sprintf("Error: %v", err)
- cmd := tea.Tick(3*time.Second, func(time.Time) tea.Msg {
+ cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg {
return struct{ clearStatus bool }{true}
})
return m, cmd
@@ -145,6 +145,10 @@ func (m *Model) handleTagsMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
model, cmd := m.handleTextInput(msg, &m.tagsInput, onEnter, onExit)
if msg.Type == tea.KeyEnter {
+ if m.showTaskDetail {
+ // In detail view, blink the tags field
+ return model, m.startDetailBlink(4) // Tags is field index 4
+ }
return model, m.startBlink(m.tagsID, false)
}
return model, cmd
@@ -156,14 +160,20 @@ func (m *Model) handleDueEditMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case tea.KeyEnter:
if err := task.SetDueDate(m.dueID, m.dueDate.Format("2006-01-02")); err != nil {
m.statusMsg = fmt.Sprintf("Error: %v", err)
- cmd := tea.Tick(3*time.Second, func(time.Time) tea.Msg {
+ cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg {
return struct{ clearStatus bool }{true}
})
return m, cmd
}
m.dueEditing = false
m.reload()
- cmd := m.startBlink(m.dueID, false)
+ var cmd tea.Cmd
+ if m.showTaskDetail {
+ // In detail view, blink the due field
+ cmd = m.startDetailBlink(5) // Due is field index 5
+ } else {
+ cmd = m.startBlink(m.dueID, false)
+ }
m.updateTableHeight()
return m, cmd
case tea.KeyEsc:
@@ -204,6 +214,14 @@ func (m *Model) handleRecurrenceMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
model, cmd := m.handleTextInput(msg, &m.recurInput, onEnter, onExit)
if msg.Type == tea.KeyEnter {
+ if m.showTaskDetail {
+ // In detail view, blink the recurrence field (dynamic index)
+ // Need to calculate the index based on whether recurrence field exists
+ fieldIndex := 8 // Base index for recurrence
+ if m.currentTaskDetail != nil && m.currentTaskDetail.Recur != "" {
+ return model, m.startDetailBlink(fieldIndex)
+ }
+ }
return model, m.startBlink(m.recurID, false)
}
return model, cmd
@@ -216,21 +234,27 @@ func (m *Model) handlePriorityMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
priority := priorityOptions[m.priorityIndex]
if err := validatePriority(priority); err != nil {
m.statusMsg = fmt.Sprintf("Error: %v", err)
- cmd := tea.Tick(3*time.Second, func(time.Time) tea.Msg {
+ cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg {
return struct{ clearStatus bool }{true}
})
return m, cmd
}
if err := task.SetPriority(m.priorityID, priority); err != nil {
m.statusMsg = fmt.Sprintf("Error: %v", err)
- cmd := tea.Tick(3*time.Second, func(time.Time) tea.Msg {
+ cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg {
return struct{ clearStatus bool }{true}
})
return m, cmd
}
m.prioritySelecting = false
m.reload()
- cmd := m.startBlink(m.priorityID, false)
+ var cmd tea.Cmd
+ if m.showTaskDetail {
+ // In detail view, blink the priority field
+ cmd = m.startDetailBlink(3) // Priority is field index 3
+ } else {
+ cmd = m.startBlink(m.priorityID, false)
+ }
m.updateTableHeight()
return m, cmd
case tea.KeyEsc:
@@ -274,7 +298,7 @@ func (m *Model) handleAddTaskMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if err := task.AddLine(m.addInput.Value()); err != nil {
m.statusMsg = fmt.Sprintf("Error: %v", err)
- cmd := tea.Tick(3*time.Second, func(time.Time) tea.Msg {
+ cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg {
return struct{ clearStatus bool }{true}
})
return m, cmd
@@ -438,9 +462,6 @@ 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) {
switch {
- case m.showTaskDetail:
- model, cmd = m.handleTaskDetailMode(msg)
- return true, model, cmd
case m.annotating:
model, cmd = m.handleAnnotationMode(msg)
return true, model, cmd
@@ -557,7 +578,114 @@ func (m *Model) handleTaskDetailMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "G", "end":
m.detailFieldIndex = m.getDetailFieldCount() - 1
return m, nil
+ case "i", "enter":
+ // Check if current field is editable
+ return m.handleDetailFieldEdit()
+ }
+
+ return m, nil
+}
+
+// handleDetailFieldEdit starts editing for the current field in detail view
+func (m *Model) handleDetailFieldEdit() (tea.Model, tea.Cmd) {
+ if m.currentTaskDetail == nil {
+ return m, nil
+ }
+
+ id := m.currentTaskDetail.ID
+
+ // Map detail field index to editable fields
+ // fieldPriority = 3, fieldTags = 4, fieldDue = 5, fieldStart = 6, fieldRecur = 8 or 9 (depending on if fields exist)
+
+ // Count fields up to current position to handle dynamic fields
+ fieldPos := 0
+
+ // ID, UUID, Status (0-2)
+ if m.detailFieldIndex <= 2 {
+ return m, nil // Not editable
+ }
+ fieldPos = 3
+
+ // Priority (3)
+ if m.detailFieldIndex == fieldPos {
+ m.clearEditingModes()
+ m.priorityID = id
+ m.prioritySelecting = true
+
+ // Set current priority index
+ switch m.currentTaskDetail.Priority {
+ case "H":
+ m.priorityIndex = 0
+ case "M":
+ m.priorityIndex = 1
+ case "L":
+ m.priorityIndex = 2
+ default:
+ m.priorityIndex = 3
+ }
+ m.updateTableHeight()
+ return m, nil
+ }
+ fieldPos++
+
+ // Tags (4)
+ if m.detailFieldIndex == fieldPos {
+ m.clearEditingModes()
+ m.tagsID = id
+ m.tagsEditing = true
+ m.tagsInput.SetValue("")
+ m.tagsInput.Focus()
+ m.updateTableHeight()
+ return m, nil
+ }
+ fieldPos++
+
+ // Due (5)
+ if m.detailFieldIndex == fieldPos {
+ m.dueID = id
+ if m.currentTaskDetail.Due != "" {
+ if ts, err := parseTaskDate(m.currentTaskDetail.Due); err == nil {
+ m.dueDate = ts
+ } else {
+ m.dueDate = time.Now()
+ }
+ } else {
+ m.dueDate = time.Now()
+ }
+ m.clearEditingModes()
+ m.dueEditing = true
+ m.updateTableHeight()
+ return m, nil
+ }
+ fieldPos++
+
+ // Start (6)
+ if m.detailFieldIndex == fieldPos {
+ // Start date is not editable in the original code, only toggled via 's' key
+ return m, nil
+ }
+ fieldPos++
+
+ // Entry (7)
+ if m.detailFieldIndex == fieldPos {
+ return m, nil // Not editable
+ }
+ fieldPos++
+
+ // Recurrence (8) - only if it exists
+ if m.currentTaskDetail.Recur != "" {
+ if m.detailFieldIndex == fieldPos {
+ m.clearEditingModes()
+ m.recurID = id
+ m.recurEditing = true
+ m.recurInput.SetValue(m.currentTaskDetail.Recur)
+ m.recurInput.Focus()
+ m.updateTableHeight()
+ return m, nil
+ }
+ fieldPos++
}
+ // Description and Annotations are not in the editable list
return m, nil
} \ No newline at end of file
diff --git a/internal/ui/keyhandlers.go b/internal/ui/keyhandlers.go
index e3bed58..2ff0ed7 100644
--- a/internal/ui/keyhandlers.go
+++ b/internal/ui/keyhandlers.go
@@ -459,6 +459,9 @@ func (m *Model) handleShowTaskDetail() (tea.Model, tea.Cmd) {
m.detailSearching = false
m.detailSearchRegex = nil
m.detailFieldIndex = 0
+ m.detailBlinkField = -1
+ m.detailBlinkOn = false
+ m.detailBlinkCount = 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 281f3ba..cb9d13d 100644
--- a/internal/ui/table.go
+++ b/internal/ui/table.go
@@ -128,6 +128,9 @@ type Model struct {
detailSearchInput textinput.Model
detailSearchRegex *regexp.Regexp
detailFieldIndex int // Current selected field in detail view
+ detailBlinkField int // Field that is currently blinking (-1 for none)
+ detailBlinkOn bool // Whether the blink is currently on
+ detailBlinkCount int // Number of blinks remaining
}
// editDoneMsg is emitted when the external editor process finishes.
@@ -167,6 +170,21 @@ func (m *Model) clearEditingModes() {
m.prioritySelecting = false
}
+// startDetailBlink starts blinking a field in the detail view
+func (m *Model) startDetailBlink(fieldIndex int) tea.Cmd {
+ if !m.showTaskDetail {
+ return nil
+ }
+ if m.disco {
+ m.theme = RandomTheme()
+ m.applyTheme()
+ }
+ m.detailBlinkField = fieldIndex
+ m.detailBlinkOn = true
+ m.detailBlinkCount = blinkCycles
+ return blinkCmd()
+}
+
func (m *Model) startBlink(id int, markDone bool) tea.Cmd {
m.blinkID = id
m.blinkMarkDone = markDone
@@ -264,6 +282,11 @@ 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()
+ }
m.computeColumnWidths()
@@ -323,6 +346,18 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
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
+ if m.prioritySelecting || m.tagsEditing || m.dueEditing || m.recurEditing {
+ if handled, model, cmd := m.handleEditingModes(msg); handled {
+ return model, 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
@@ -364,6 +399,21 @@ func (m *Model) handleEditDone(msg editDoneMsg) (tea.Model, tea.Cmd) {
// handleBlinkMsg handles the blinking animation timer
func (m *Model) handleBlinkMsg() (tea.Model, tea.Cmd) {
+ // Handle detail view blinking
+ if m.showTaskDetail && m.detailBlinkField != -1 {
+ m.detailBlinkOn = !m.detailBlinkOn
+ m.detailBlinkCount++
+
+ if m.detailBlinkCount >= blinkCycles {
+ m.detailBlinkField = -1
+ m.detailBlinkOn = false
+ m.detailBlinkCount = 0
+ } else {
+ return m, blinkCmd()
+ }
+ return m, nil
+ }
+
if m.blinkID == 0 {
return m, nil
}
@@ -427,13 +477,13 @@ func (m Model) View() string {
if m.dueEditing {
view = lipgloss.JoinVertical(lipgloss.Left,
view,
- m.dueView(),
+ m.dueView(true),
)
}
if m.prioritySelecting {
view = lipgloss.JoinVertical(lipgloss.Left,
view,
- m.priorityView(),
+ m.priorityView(true),
)
}
if m.descEditing {
@@ -667,11 +717,14 @@ func (m Model) formatUrgency(u string, width int) string {
return u
}
-func (m Model) dueView() string {
- return fmt.Sprintf("due: %s", m.dueDate.Format("2006-01-02"))
+func (m Model) dueView(showLabel bool) string {
+ if showLabel {
+ return fmt.Sprintf("due: %s", m.dueDate.Format("2006-01-02"))
+ }
+ return m.dueDate.Format("2006-01-02")
}
-func (m Model) priorityView() string {
+func (m Model) priorityView(showLabel bool) string {
var parts []string
for i, p := range priorityOptions {
label := p
@@ -684,7 +737,10 @@ func (m Model) priorityView() string {
}
parts = append(parts, style.Render(label))
}
- return "priority: " + strings.Join(parts, " ")
+ if showLabel {
+ return "priority: " + strings.Join(parts, " ")
+ }
+ return strings.Join(parts, " ")
}
func (m Model) highlightCell(base lipgloss.Style, re *regexp.Regexp, raw string) string {
diff --git a/internal/ui/taskdetail.go b/internal/ui/taskdetail.go
index d98416c..9d68e4f 100644
--- a/internal/ui/taskdetail.go
+++ b/internal/ui/taskdetail.go
@@ -6,7 +6,6 @@ import (
"strings"
"github.com/charmbracelet/lipgloss"
-
)
// Define field indices for navigation
@@ -32,33 +31,33 @@ func (m *Model) renderTaskDetail() string {
}
t := m.currentTaskDetail
-
+
// Create styles based on theme
titleStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(m.theme.SelectedFG)).
Background(lipgloss.Color(m.theme.SelectedBG)).
Padding(0, 1)
-
+
labelStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color(m.theme.HeaderFG))
-
+
valueStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("252"))
-
+
descStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("250")).
PaddingLeft(2)
-
+
// Build the detail view
var lines []string
-
+
// Title bar
title := fmt.Sprintf("Task %d Details", t.ID)
lines = append(lines, titleStyle.Render(title))
lines = append(lines, "")
-
+
// Task fields
currentField := 0
lines = append(lines, m.renderTaskFieldWithIndex("ID", fmt.Sprintf("%d", t.ID), labelStyle, valueStyle, currentField))
@@ -67,49 +66,77 @@ func (m *Model) renderTaskDetail() string {
currentField++
lines = append(lines, m.renderTaskFieldWithIndex("Status", t.Status, labelStyle, valueStyle, currentField))
currentField++
-
+
// Priority with color
- priorityValue := t.Priority
- if priorityValue == "" {
- priorityValue = "-"
- }
- priorityStyle := valueStyle.Copy()
- switch t.Priority {
- case "H":
- priorityStyle = priorityStyle.Background(lipgloss.Color(m.theme.PrioHighBG))
- priorityValue = "H (High)"
- case "M":
- priorityStyle = priorityStyle.Background(lipgloss.Color(m.theme.PrioMedBG))
- priorityValue = "M (Medium)"
- case "L":
- priorityStyle = priorityStyle.Background(lipgloss.Color(m.theme.PrioLowBG))
- priorityValue = "L (Low)"
+ if m.prioritySelecting && m.priorityID == t.ID {
+ // Show priority selection UI
+ lines = append(lines, m.renderEditingField("Priority", m.priorityView(false), labelStyle, currentField))
+ } else {
+ priorityValue := t.Priority
+ if priorityValue == "" {
+ priorityValue = "-"
+ }
+ priorityStyle := valueStyle.Copy()
+ switch t.Priority {
+ case "H":
+ priorityStyle = priorityStyle.Background(lipgloss.Color(m.theme.PrioHighBG))
+ priorityValue = "H (High)"
+ case "M":
+ priorityStyle = priorityStyle.Background(lipgloss.Color(m.theme.PrioMedBG))
+ priorityValue = "M (Medium)"
+ case "L":
+ priorityStyle = priorityStyle.Background(lipgloss.Color(m.theme.PrioLowBG))
+ priorityValue = "L (Low)"
+ }
+ lines = append(lines, m.renderTaskFieldWithIndex("Priority", priorityValue, labelStyle, priorityStyle, currentField))
}
- lines = append(lines, m.renderTaskFieldWithIndex("Priority", priorityValue, labelStyle, priorityStyle, currentField))
currentField++
-
+
// Tags
- tagStr := strings.Join(t.Tags, ", ")
- if tagStr == "" {
- tagStr = "-"
+ if m.tagsEditing && m.tagsID == t.ID {
+ // Show tags editing UI without prompt
+ originalPrompt := m.tagsInput.Prompt
+ m.tagsInput.Prompt = ""
+ tagsView := m.tagsInput.View()
+ m.tagsInput.Prompt = originalPrompt
+ lines = append(lines, m.renderEditingField("Tags", tagsView, labelStyle, currentField))
+ } else {
+ tagStr := strings.Join(t.Tags, ", ")
+ if tagStr == "" {
+ tagStr = "-"
+ }
+ lines = append(lines, m.renderTaskFieldWithIndex("Tags", tagStr, labelStyle, valueStyle, currentField))
}
- lines = append(lines, m.renderTaskFieldWithIndex("Tags", tagStr, labelStyle, valueStyle, currentField))
currentField++
-
+
// Dates
- lines = append(lines, m.renderTaskFieldWithIndex("Due", m.formatTaskDate(t.Due), labelStyle, valueStyle, currentField))
+ if m.dueEditing && m.dueID == t.ID {
+ // Show due date editing UI
+ lines = append(lines, m.renderEditingField("Due", m.dueView(false), labelStyle, currentField))
+ } else {
+ 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.renderTaskFieldWithIndex("Recurrence", t.Recur, labelStyle, valueStyle, currentField))
+ if m.recurEditing && m.recurID == t.ID {
+ // Show recurrence editing UI without prompt
+ originalPrompt := m.recurInput.Prompt
+ m.recurInput.Prompt = ""
+ recurView := m.recurInput.View()
+ m.recurInput.Prompt = originalPrompt
+ lines = append(lines, m.renderEditingField("Recurrence", recurView, labelStyle, currentField))
+ } else {
+ lines = append(lines, m.renderTaskFieldWithIndex("Recurrence", t.Recur, labelStyle, valueStyle, currentField))
+ }
currentField++
}
-
+
// Description - with full space
lines = append(lines, "")
descLabelStyle := labelStyle.Copy()
@@ -130,7 +157,7 @@ func (m *Model) renderTaskDetail() string {
lines = append(lines, descValueStyle.Render("-"))
}
currentField++
-
+
// Annotations
if len(t.Annotations) > 0 {
lines = append(lines, "")
@@ -150,44 +177,66 @@ func (m *Model) renderTaskDetail() string {
lines = append(lines, annValueStyle.Render(annText))
}
}
-
+
// Instructions at bottom
lines = append(lines, "")
lines = append(lines, "")
instructionStyle := lipgloss.NewStyle().
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"))
+ // Check if we're in any editing mode
+ if m.prioritySelecting || m.tagsEditing || m.dueEditing || m.recurEditing {
+ lines = append(lines, instructionStyle.Render("Editing mode - Follow on-screen prompts"))
} else {
- lines = append(lines, instructionStyle.Render("Press / to search"))
+ 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"))
+ lines = append(lines, instructionStyle.Render("Press i or Enter to edit (Priority, Tags, Due, Recurrence)"))
+ if m.detailSearching {
+ lines = append(lines, instructionStyle.Render("Type to search, Enter to confirm"))
+ } else {
+ lines = append(lines, instructionStyle.Render("Press / to search"))
+ }
}
-
+
// Add search input if searching
if m.detailSearching {
searchStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("248")).
PaddingTop(1)
- lines = append(lines, searchStyle.Render("Search: " + m.detailSearchInput.View()))
+ lines = append(lines, searchStyle.Render("Search: "+m.detailSearchInput.View()))
}
-
+
return strings.Join(lines, "\n")
}
+// renderEditingField renders a field that is currently being edited
+func (m *Model) renderEditingField(label, editView string, labelStyle lipgloss.Style, fieldIndex int) string {
+ // Apply selection highlighting if this field is selected
+ if m.detailFieldIndex == fieldIndex {
+ labelStyle = labelStyle.Background(lipgloss.Color(m.theme.SelectedBG))
+ }
+
+ return fmt.Sprintf("%s %s", labelStyle.Render(label+":"), editView)
+}
+
// 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 {
+
+ // Apply blinking if this field is blinking
+ if m.detailBlinkField == fieldIndex && m.detailBlinkOn {
+ // Use a bright background for blinking
+ blinkBG := lipgloss.Color("226") // Bright yellow
+ labelStyle = labelStyle.Background(blinkBG).Foreground(lipgloss.Color("0"))
+ valueStyle = valueStyle.Background(blinkBG).Foreground(lipgloss.Color("0"))
+ } else if m.detailFieldIndex == fieldIndex {
+ // Apply selection highlighting if this field is selected
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)
@@ -207,25 +256,44 @@ func (m *Model) formatTaskDate(dateStr string) string {
return dateStr
}
+// refreshCurrentTaskDetail updates the current task detail pointer after a reload
+func (m *Model) refreshCurrentTaskDetail() {
+ if m.currentTaskDetail == nil {
+ return
+ }
+
+ id := m.currentTaskDetail.ID
+ for i := range m.tasks {
+ if m.tasks[i].ID == id {
+ m.currentTaskDetail = &m.tasks[i]
+ return
+ }
+ }
+
+ // Task no longer exists, clear detail view
+ m.showTaskDetail = false
+ m.currentTaskDetail = nil
+}
+
// 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
}
@@ -234,15 +302,15 @@ func (m *Model) highlightMatches(text string, re *regexp.Regexp) string {
highlightStyle := lipgloss.NewStyle().
Background(lipgloss.Color(m.theme.SearchBG)).
Foreground(lipgloss.Color(m.theme.SearchFG))
-
+
matches := re.FindAllStringIndex(text, -1)
if len(matches) == 0 {
return text
}
-
+
var result strings.Builder
lastEnd := 0
-
+
for _, match := range matches {
start, end := match[0], match[1]
// Add text before match
@@ -253,11 +321,11 @@ func (m *Model) highlightMatches(text string, re *regexp.Regexp) string {
result.WriteString(highlightStyle.Render(text[start:end]))
lastEnd = end
}
-
+
// Add remaining text
if lastEnd < len(text) {
result.WriteString(text[lastEnd:])
}
-
+
return result.String()
-} \ No newline at end of file
+}
diff --git a/tasksamurai b/tasksamurai
index bee372e..0c7731e 100755
--- a/tasksamurai
+++ b/tasksamurai
Binary files differ