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 }