From cce69d040559b6986fa42c0966d7e82ecfa2cbf3 Mon Sep 17 00:00:00 2001 From: Jon Gallant <2163001+jongio@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:48:56 -0700 Subject: [PATCH] feat(cli): add Markdown output to dispatch views Add a --markdown flag to dispatch views (and views list) that renders the active view and a table of saved views with their filter settings. --markdown cannot be combined with --json or --csv. Update man page and README. Closes #428 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 1 + cmd/dispatch/man.go | 2 +- cmd/dispatch/views.go | 34 +++++++++++++++++++++--- cmd/dispatch/views_test.go | 54 +++++++++++++++++++++++++++++++++----- 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4700a4ed..5b9acea0 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,7 @@ List named views and switch the active view from scripts: dispatch views dispatch views --json dispatch views --csv +dispatch views --markdown dispatch views use Work dispatch views use default ``` diff --git a/cmd/dispatch/man.go b/cmd/dispatch/man.go index 608698cd..7cb79ac0 100644 --- a/cmd/dispatch/man.go +++ b/cmd/dispatch/man.go @@ -108,7 +108,7 @@ var manCommands = []manEntry{ {"aliases [--json]", "List session aliases with orphan detection."}, {"alias list [--json]", "List session aliases."}, {"alias ", "Set, reassign, clear (--clear), or remove (--remove) a session alias."}, - {"views [list] [--json|--csv]", "List configured named views."}, + {"views [list] [--json|--csv|--markdown]", "List configured named views."}, {"views use ", "Set or clear the active named view."}, {"notes [command]", "List, get, set, or clear session notes."}, {"config ", "Read, change, validate, document, or transfer preferences."}, diff --git a/cmd/dispatch/views.go b/cmd/dispatch/views.go index 80389252..830a1c5a 100644 --- a/cmd/dispatch/views.go +++ b/cmd/dispatch/views.go @@ -24,7 +24,7 @@ func runViews(w io.Writer, args []string) error { if len(rest) > 0 { rest = rest[1:] } - if len(rest) == 0 || rest[0] == "list" || rest[0] == "--json" || rest[0] == "--csv" { + if len(rest) == 0 || rest[0] == "list" || rest[0] == "--json" || rest[0] == "--csv" || rest[0] == "--markdown" { return runViewsList(w, rest) } @@ -39,6 +39,7 @@ func runViews(w io.Writer, args []string) error { func runViewsList(w io.Writer, args []string) error { jsonOut := false csvOut := false + markdownOut := false if len(args) > 0 && args[0] == "list" { args = args[1:] } @@ -48,12 +49,14 @@ func runViewsList(w io.Writer, args []string) error { jsonOut = true case "--csv": csvOut = true + case "--markdown": + markdownOut = true default: return fmt.Errorf("views list does not take arguments, got %q", arg) } } - if jsonOut && csvOut { - return fmt.Errorf("--json and --csv cannot be combined") + if (jsonOut && csvOut) || (jsonOut && markdownOut) || (csvOut && markdownOut) { + return fmt.Errorf("--json, --csv, and --markdown cannot be combined") } cfg, err := configLoadFn() @@ -69,6 +72,10 @@ func runViewsList(w io.Writer, args []string) error { if csvOut { return writeViewsCSV(w, report) } + if markdownOut { + writeViewsMarkdown(w, report) + return nil + } writeViewsText(w, report) return nil } @@ -157,6 +164,27 @@ func writeViewsCSV(w io.Writer, report viewsReport) error { return cw.Error() } +func writeViewsMarkdown(w io.Writer, report viewsReport) { + fmt.Fprintln(w, "# Dispatch views") + fmt.Fprintln(w) + fmt.Fprintf(w, "Active: %s\n", markdownCell(report.ActiveView)) + if len(report.Views) == 0 { + fmt.Fprintln(w) + fmt.Fprintln(w, "No named views found.") + return + } + fmt.Fprintln(w) + fmt.Fprintln(w, "| Active | Name | Settings |") + fmt.Fprintln(w, "|---|---|---|") + for _, v := range report.Views { + active := "" + if v.Name == report.ActiveView { + active = "*" + } + fmt.Fprintf(w, "| %s | %s | %s |\n", active, markdownCell(v.Name), markdownCell(describeView(v))) + } +} + func describeView(v config.NamedView) string { parts := make([]string, 0, 8) if v.Search != "" { diff --git a/cmd/dispatch/views_test.go b/cmd/dispatch/views_test.go index b3193b83..dbc4a174 100644 --- a/cmd/dispatch/views_test.go +++ b/cmd/dispatch/views_test.go @@ -101,14 +101,56 @@ func TestRunViewsListCSVEmpty(t *testing.T) { } } -func TestRunViewsListCSVAndJSONConflict(t *testing.T) { +func TestRunViewsListMarkdown(t *testing.T) { withConfigSeams(t, viewsConfig()) - err := runViews(&bytes.Buffer{}, []string{"views", "list", "--json", "--csv"}) - if err == nil { - t.Fatal("expected error for --csv + --json conflict") + var buf bytes.Buffer + if err := runViews(&buf, []string{"views", "--markdown"}); err != nil { + t.Fatalf("runViews markdown: %v", err) + } + out := buf.String() + for _, want := range []string{ + "# Dispatch views", + "Active: Work", + "| Active | Name | Settings |", + "| * | Work |", + "repo:jongio/dispatch", + "| Personal |", + "show_hidden", + } { + if !strings.Contains(out, want) { + t.Errorf("markdown output missing %q:\n%s", want, out) + } + } + if strings.Contains(out, "Broken") { + t.Fatalf("invalid view should not appear:\n%s", out) + } +} + +func TestRunViewsListMarkdownEmpty(t *testing.T) { + withConfigSeams(t, config.Default()) + var buf bytes.Buffer + if err := runViews(&buf, []string{"views", "list", "--markdown"}); err != nil { + t.Fatalf("runViews list markdown: %v", err) } - if !strings.Contains(err.Error(), "--json and --csv cannot be combined") { - t.Errorf("wrong error: %v", err) + if !strings.Contains(buf.String(), "No named views found.") { + t.Fatalf("expected empty note, got:\n%s", buf.String()) + } +} + +func TestRunViewsListFormatConflicts(t *testing.T) { + withConfigSeams(t, viewsConfig()) + for _, args := range [][]string{ + {"views", "list", "--json", "--csv"}, + {"views", "list", "--json", "--markdown"}, + {"views", "list", "--csv", "--markdown"}, + } { + err := runViews(&bytes.Buffer{}, args) + if err == nil { + t.Fatalf("expected error for args %v", args) + } + if !strings.Contains(err.Error(), "cannot be combined") { + t.Errorf("wrong error for %v: %v", args, err) + } } }