summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-08 22:07:25 +0300
committerPaul Buetow <paul@buetow.org>2026-04-08 22:07:25 +0300
commit42226b12e7a172810bf785cef4cbb4fb41e2da3d (patch)
tree96ad6868d4609192ba522a3b10481bb0ab1933c3 /internal
parent595075473a51b2709f1554f055df43a69369cddd (diff)
task a: fix temp-file cleanup on desc write failure
Diffstat (limited to 'internal')
-rw-r--r--internal/ui/table.go42
-rw-r--r--internal/ui/table_test.go37
2 files changed, 68 insertions, 11 deletions
diff --git a/internal/ui/table.go b/internal/ui/table.go
index 1314eb1..ae32070 100644
--- a/internal/ui/table.go
+++ b/internal/ui/table.go
@@ -204,6 +204,12 @@ type descEditDoneMsg struct {
type blinkMsg struct{}
+type descriptionTempFile interface {
+ Name() string
+ WriteString(string) (int, error)
+ Close() error
+}
+
// blinkInterval controls how quickly the row flashes when a task changes.
// A shorter interval results in a faster blink.
const blinkInterval = 150 * time.Millisecond
@@ -212,6 +218,28 @@ const blinkInterval = 150 * time.Millisecond
// The total blink duration is blinkInterval * blinkCycles.
const blinkCycles = 8
+func prepareDescriptionTempFile(description string, newTempFile func() (descriptionTempFile, error)) (string, error) {
+ tmpFile, err := newTempFile()
+ if err != nil {
+ return "", err
+ }
+
+ tmpPath := tmpFile.Name()
+
+ _, writeErr := tmpFile.WriteString(description)
+ closeErr := tmpFile.Close()
+ if writeErr != nil {
+ _ = os.Remove(tmpPath)
+ return "", writeErr
+ }
+ if closeErr != nil {
+ _ = os.Remove(tmpPath)
+ return "", closeErr
+ }
+
+ return tmpPath, nil
+}
+
// editCmd returns a command that edits the task and sends an
// editDoneMsg once the process is complete.
func editCmd(id int) tea.Cmd {
@@ -222,18 +250,10 @@ func editCmd(id int) tea.Cmd {
// 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()
+ tmpPath, err := prepareDescriptionTempFile(description, func() (descriptionTempFile, error) {
+ return os.CreateTemp("", "tasksamurai-desc-*.txt")
+ })
if err != nil {
- _ = os.Remove(tmpPath)
return descEditDoneMsg{err: err, tempFile: ""}
}
diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go
index dccec31..d8c9db4 100644
--- a/internal/ui/table_test.go
+++ b/internal/ui/table_test.go
@@ -200,6 +200,27 @@ func TestHandleDescEditDoneUpdatesDescriptionAndRemovesTempFile(t *testing.T) {
}
}
+func TestPrepareDescriptionTempFileRemovesTempFileOnWriteError(t *testing.T) {
+ tmp := t.TempDir()
+ tempFile := filepath.Join(tmp, "desc.txt")
+ if err := os.WriteFile(tempFile, []byte("stale"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ path, err := prepareDescriptionTempFile("updated description", func() (descriptionTempFile, error) {
+ return &failingDescriptionTempFile{path: tempFile}, nil
+ })
+ if err == nil {
+ t.Fatalf("prepareDescriptionTempFile succeeded unexpectedly: %q", path)
+ }
+ if path != "" {
+ t.Fatalf("expected empty path on error, got %q", path)
+ }
+ if _, statErr := os.Stat(tempFile); !os.IsNotExist(statErr) {
+ t.Fatalf("temp file still exists after write error: %v", statErr)
+ }
+}
+
func TestHandleDescEditDoneRemovesTempFileOnEditorError(t *testing.T) {
tmp := t.TempDir()
tempFile := filepath.Join(tmp, "desc.txt")
@@ -228,6 +249,22 @@ func TestHandleDescEditDoneRemovesTempFileOnEditorError(t *testing.T) {
}
}
+type failingDescriptionTempFile struct {
+ path string
+}
+
+func (f *failingDescriptionTempFile) Name() string {
+ return f.path
+}
+
+func (f *failingDescriptionTempFile) WriteString(string) (int, error) {
+ return 0, fmt.Errorf("write failed")
+}
+
+func (f *failingDescriptionTempFile) Close() error {
+ return nil
+}
+
func TestHandleFilterModeReportsReloadError(t *testing.T) {
tmp := t.TempDir()
taskPath := filepath.Join(tmp, "task")