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>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user