Files
task-master/internal/ticket/editfile.go
T
SkrueckenandCursor 24c1e146ec Add offline-first task-master TUI with tickets, board lists, and undo.
Charm-based CLI with JSON persistence, status/list filters, external editor editing, command-history undo/redo, and glamour markdown without AutoStyle OSC hangs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 10:21:48 +02:00

106 lines
2.7 KiB
Go

package ticket
import (
"fmt"
"os"
"os/exec"
"strings"
)
// FormatEditDocument is the markdown representation used for external editing.
// Format:
//
// # Title
//
// optional description body…
func FormatEditDocument(t Ticket) string {
title := strings.TrimSpace(t.Title)
title = strings.TrimLeft(title, "#")
title = strings.TrimSpace(title)
var b strings.Builder
b.WriteString("# ")
b.WriteString(title)
b.WriteString("\n")
if desc := strings.TrimSpace(t.Description); desc != "" {
b.WriteString("\n")
b.WriteString(desc)
if !strings.HasSuffix(desc, "\n") {
b.WriteString("\n")
}
}
return b.String()
}
// ParseEditDocument extracts title and description from an edited markdown file.
func ParseEditDocument(content string) (title, description string, err error) {
content = strings.ReplaceAll(content, "\r\n", "\n")
content = strings.TrimSpace(content)
if content == "" {
return "", "", fmt.Errorf("ticket content is empty")
}
lines := strings.Split(content, "\n")
first := strings.TrimSpace(lines[0])
if strings.HasPrefix(first, "#") {
title = strings.TrimSpace(strings.TrimLeft(first, "#"))
rest := strings.Join(lines[1:], "\n")
description = strings.TrimSpace(rest)
} else {
title = first
rest := strings.Join(lines[1:], "\n")
description = strings.TrimSpace(rest)
}
if title == "" {
return "", "", fmt.Errorf("ticket title is required")
}
return title, description, nil
}
// WriteEditFile writes the ticket to a temp .md file and returns its path.
func WriteEditFile(t Ticket) (string, error) {
f, err := os.CreateTemp("", "task-master-ticket-*.md")
if err != nil {
return "", fmt.Errorf("create temp file: %w", err)
}
path := f.Name()
if _, err := f.WriteString(FormatEditDocument(t)); err != nil {
_ = f.Close()
_ = os.Remove(path)
return "", fmt.Errorf("write temp file: %w", err)
}
if err := f.Close(); err != nil {
_ = os.Remove(path)
return "", fmt.Errorf("close temp file: %w", err)
}
return path, nil
}
// ResolveEditor returns the user's preferred editor command string.
// Order: $EDITOR → $VISUAL → nvim → vim → vi.
func ResolveEditor() string {
if ed := strings.TrimSpace(os.Getenv("EDITOR")); ed != "" {
return ed
}
if ed := strings.TrimSpace(os.Getenv("VISUAL")); ed != "" {
return ed
}
for _, cand := range []string{"nvim", "vim", "vi"} {
if _, err := exec.LookPath(cand); err == nil {
return cand
}
}
return "vi"
}
// EditorCmd builds a command that opens path in the default terminal editor.
// Supports multi-word EDITOR values like "code --wait".
func EditorCmd(path string) *exec.Cmd {
ed := ResolveEditor()
cmd := exec.Command("sh", "-c", ed+` "$1"`, "--", path)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}