1use clap::builder::{StyledStr, Styles};
2
3pub struct HelpDoc {
4 sections: Vec<HelpSection>,
5}
6
7pub struct HelpSection {
8 title: &'static str,
9 rows: Vec<HelpRow>,
10}
11
12pub enum HelpRow {
13 Item {
14 usage: &'static str,
15 description: &'static str,
16 },
17 Text(&'static str),
18}
19
20impl HelpDoc {
21 pub fn new() -> Self {
22 Self {
23 sections: Vec::new(),
24 }
25 }
26
27 pub fn section(mut self, section: HelpSection) -> Self {
28 self.sections.push(section);
29 self
30 }
31
32 pub fn render(&self, styles: &Styles) -> StyledStr {
33 let header = styles.get_header();
34 let literal = styles.get_literal();
35 let mut out = String::new();
36
37 for (section_index, section) in self.sections.iter().enumerate() {
38 if section_index > 0 {
39 out.push_str("\n\n");
40 }
41
42 out.push_str(&format!("{header}{}{header:#}\n", section.title));
43
44 let item_width = section
45 .rows
46 .iter()
47 .filter_map(|row| match row {
48 HelpRow::Item { usage, .. } => Some(usage.chars().count()),
49 HelpRow::Text(_) => None,
50 })
51 .max()
52 .unwrap_or(0);
53
54 for (row_index, row) in section.rows.iter().enumerate() {
55 if row_index > 0 {
56 out.push('\n');
57 }
58
59 match row {
60 HelpRow::Item { usage, description } => {
61 let padding =
62 " ".repeat(item_width.saturating_sub(usage.chars().count()) + 2);
63 out.push_str(&format!(
64 " {literal}{usage}{literal:#}{padding}{description}"
65 ));
66 }
67 HelpRow::Text(text) => {
68 for (line_index, line) in text.lines().enumerate() {
69 if line_index > 0 {
70 out.push('\n');
71 }
72 out.push_str(" ");
73 out.push_str(line);
74 }
75 }
76 }
77 }
78 }
79
80 StyledStr::from(out)
81 }
82}
83
84impl HelpSection {
85 pub fn new(title: &'static str) -> Self {
86 Self {
87 title,
88 rows: Vec::new(),
89 }
90 }
91
92 pub fn item(mut self, usage: &'static str, description: &'static str) -> Self {
93 self.rows.push(HelpRow::Item { usage, description });
94 self
95 }
96
97 pub fn text(mut self, text: &'static str) -> Self {
98 self.rows.push(HelpRow::Text(text));
99 self
100 }
101}
102
103pub fn root_after_help(styles: &Styles) -> StyledStr {
104 HelpDoc::new()
105 .section(
106 HelpSection::new("Task file:")
107 .text("By default, sq discovers the nearest existing .sift/issues.jsonl within the current git worktree and otherwise falls back to <cwd>/.sift/issues.jsonl.")
108 .text("Override with -q, --queue <PATH> or SQ_QUEUE_PATH=<PATH>."),
109 )
110 .section(
111 HelpSection::new("Workflow:")
112 .item("sq list --ready", "See the next actionable tasks")
113 .item("sq add --title <TITLE>", "Create a new task")
114 .item("sq show <id>", "Inspect one task in detail")
115 .item(
116 "sq edit <id> ...",
117 "Update fields, sources, metadata, or blockers",
118 ),
119 )
120 .section(
121 HelpSection::new("Command help:")
122 .item(
123 "sq <command> --help",
124 "See command-specific guidance and examples",
125 )
126 .item("sq prime", "Output workflow context for AI agents"),
127 )
128 .render(styles)
129}