diff options
| author | Paul Buetow <paul@buetow.org> | 2025-06-28 11:32:30 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-06-28 11:32:30 +0300 |
| commit | d06b179332e82635f6a7c8366e51fb5b421a7c2c (patch) | |
| tree | c571672a146d07de8d3969dc7e2c272331abef6a | |
| parent | b659b8cf87c86280f62cef0f499a60b999e6ce6b (diff) | |
feat: add external editor support for description editing in detail viewv0.9.1
- Enable editing task description using external editor ($EDITOR)
- Create temporary file for editing and handle TUI editor properly
- Show editing indicator while external editor is active
- Add blinking feedback after successful description update
- Increment version from 0.9.0 to 0.9.1
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
| -rw-r--r-- | internal/ui/handlers.go | 13 | ||||
| -rw-r--r-- | internal/ui/table.go | 100 | ||||
| -rw-r--r-- | internal/ui/taskdetail.go | 19 | ||||
| -rw-r--r-- | internal/version.go | 2 | ||||
| -rwxr-xr-x | tasksamurai | bin | 5963040 -> 5998672 bytes |
5 files changed, 124 insertions, 10 deletions
diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index 3be357a..576519c 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -686,6 +686,17 @@ func (m *Model) handleDetailFieldEdit() (tea.Model, tea.Cmd) { fieldPos++ } - // Description and Annotations are not in the editable list + // Description (9) + if m.detailFieldIndex == fieldPos { + // Launch external editor for description + m.detailDescEditing = true + desc := "" + if m.currentTaskDetail != nil { + desc = m.currentTaskDetail.Description + } + return m, editDescriptionCmd(desc) + } + + // Annotations are not editable in detail view return m, nil }
\ No newline at end of file diff --git a/internal/ui/table.go b/internal/ui/table.go index cb9d13d..74d5440 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -3,6 +3,8 @@ package ui import ( "fmt" "math/rand" + "os" + "os/exec" "regexp" "strconv" "strings" @@ -127,15 +129,23 @@ type Model struct { detailSearching bool 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 + 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 + detailDescEditing bool // Whether we're editing description in detail view + detailDescTempFile string // Temp file path for description editing } // editDoneMsg is emitted when the external editor process finishes. type editDoneMsg struct{ err error } +// descEditDoneMsg is emitted when the external editor for description finishes. +type descEditDoneMsg struct{ + err error + tempFile string +} + type blinkMsg struct{} // blinkInterval controls how quickly the row flashes when a task changes. @@ -153,6 +163,43 @@ func editCmd(id int) tea.Cmd { return tea.ExecProcess(c, func(err error) tea.Msg { return editDoneMsg{err: err} }) } +// editDescriptionCmd returns a command that opens the description in external editor +func editDescriptionCmd(description string) tea.Cmd { + return func() tea.Msg { + // Create temp file + tmpFile, err := os.CreateTemp("", "tasksamurai-desc-*.txt") + if err != nil { + return descEditDoneMsg{err: err, tempFile: ""} + } + tmpPath := tmpFile.Name() + + // Write current description to temp file + _, err = tmpFile.WriteString(description) + tmpFile.Close() + if err != nil { + 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} + })() + } +} + func blinkCmd() tea.Cmd { return tea.Tick(blinkInterval, func(time.Time) tea.Msg { return blinkMsg{} }) } @@ -335,6 +382,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m.handleWindowResize(msg) case editDoneMsg: return m.handleEditDone(msg) + case descEditDoneMsg: + return m.handleDescEditDone(msg) case blinkMsg: return m.handleBlinkMsg() case struct{ clearStatus bool }: @@ -397,6 +446,49 @@ func (m *Model) handleEditDone(msg editDoneMsg) (tea.Model, tea.Cmd) { return m, cmd } +// handleDescEditDone handles the completion of description editing +func (m *Model) handleDescEditDone(msg descEditDoneMsg) (tea.Model, tea.Cmd) { + m.detailDescEditing = false + defer 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 { + return struct{ clearStatus bool }{true} + }) + return m, cmd + } + + // Read the edited content + content, err := os.ReadFile(msg.tempFile) + if err != nil { + m.statusMsg = fmt.Sprintf("Error reading file: %v", err) + cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { + return struct{ clearStatus bool }{true} + }) + return m, cmd + } + + // Update the description + newDesc := strings.TrimSpace(string(content)) + if m.currentTaskDetail != nil { + err = task.SetDescription(m.currentTaskDetail.ID, newDesc) + if err != nil { + m.statusMsg = fmt.Sprintf("Error updating description: %v", err) + cmd := tea.Tick(2*time.Second, func(time.Time) tea.Msg { + return struct{ clearStatus bool }{true} + }) + return m, cmd + } + + // Reload and start blinking + m.reload() + return m, m.startDetailBlink(9) // Description field index + } + + return m, nil +} + // handleBlinkMsg handles the blinking animation timer func (m *Model) handleBlinkMsg() (tea.Model, tea.Cmd) { // Handle detail view blinking diff --git a/internal/ui/taskdetail.go b/internal/ui/taskdetail.go index 9d68e4f..70c06a0 100644 --- a/internal/ui/taskdetail.go +++ b/internal/ui/taskdetail.go @@ -141,12 +141,23 @@ func (m *Model) renderTaskDetail() string { lines = append(lines, "") descLabelStyle := labelStyle.Copy() descValueStyle := descStyle.Copy() - if m.detailFieldIndex == currentField { + // Apply blinking if this field is blinking + if m.detailBlinkField == currentField && m.detailBlinkOn { + blinkBG := lipgloss.Color("226") // Bright yellow + descLabelStyle = descLabelStyle.Background(blinkBG).Foreground(lipgloss.Color("0")) + descValueStyle = descValueStyle.Background(blinkBG).Foreground(lipgloss.Color("0")) + } else 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 != "" { + if m.detailDescEditing { + // Show editing indicator + editingStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("226")). + Italic(true) + lines = append(lines, editingStyle.Render(" [Editing in external editor...]")) + } else if t.Description != "" { // Highlight search matches if searching desc := t.Description if m.detailSearchRegex != nil && m.detailSearchRegex.MatchString(desc) { @@ -185,12 +196,12 @@ func (m *Model) renderTaskDetail() string { Foreground(lipgloss.Color("245")). Italic(true) // Check if we're in any editing mode - if m.prioritySelecting || m.tagsEditing || m.dueEditing || m.recurEditing { + if m.prioritySelecting || m.tagsEditing || m.dueEditing || m.recurEditing || m.detailDescEditing { lines = append(lines, instructionStyle.Render("Editing mode - Follow on-screen prompts")) } else { 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)")) + lines = append(lines, instructionStyle.Render("Press i or Enter to edit (Priority, Tags, Due, Recurrence, Description)")) if m.detailSearching { lines = append(lines, instructionStyle.Render("Type to search, Enter to confirm")) } else { diff --git a/internal/version.go b/internal/version.go index f10ab7b..0e7e01e 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,4 +1,4 @@ package internal // Version is the current version of Task Samurai. -const Version = "0.9.0" +const Version = "0.9.1" diff --git a/tasksamurai b/tasksamurai Binary files differindex 0c7731e..d845745 100755 --- a/tasksamurai +++ b/tasksamurai |
