summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-28 11:32:30 +0300
committerPaul Buetow <paul@buetow.org>2025-06-28 11:32:30 +0300
commitd06b179332e82635f6a7c8366e51fb5b421a7c2c (patch)
treec571672a146d07de8d3969dc7e2c272331abef6a
parentb659b8cf87c86280f62cef0f499a60b999e6ce6b (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.go13
-rw-r--r--internal/ui/table.go100
-rw-r--r--internal/ui/taskdetail.go19
-rw-r--r--internal/version.go2
-rwxr-xr-xtasksamuraibin5963040 -> 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
index 0c7731e..d845745 100755
--- a/tasksamurai
+++ b/tasksamurai
Binary files differ