on master help sign in
help: ? opens a help overlay in the TUI (also via the palette); fold help prints the same text on the CLI
commit766f659e562991d7f5ca0c5debb7e0cbc3b1784d authoriko <ilyakooo0@gmail.com> dateSat 12 Sep 2026, 11:35 (1 month ago) parentbfda665
6 files changed+146−2
.vix/access_stats.db .vix/history.txt+6 crates/fold-cli/src/main.rs+9−1 crates/fold-cli/tests/cli.rs+11 crates/fold-tui/src/app.rs+99−1 crates/fold-tui/tests/tui.rs+21
.vix/access_stats.db

Binary file not shown.

.vix/history.txt+6
@@ -86,3 +86,9 @@
8686
8787# 2026-09-12 08:02:08.035115456 +0000 UTC
8888+rename the application to "fold"
89+
90+# 2026-09-12 11:32:45.555652593 +0000 UTC
91++Are you done?
92+
93+# 2026-09-12 12:51:59.768963915 +0000 UTC
94++Add a brief "help" command describing how to use it. ? should also bring it up
crates/fold-cli/src/main.rs+9−1
@@ -6,7 +6,7 @@ use std::io::Read;
66use std::path::PathBuf;
77
88#[derive(Parser)]
9−#[command(name = "fold", version, about = "tree-shaped plain-text notes and tasks")]
9+#[command(name = "fold", version, about = "tree-shaped plain-text notes and tasks", disable_help_subcommand = true)]
1010struct Cli {
1111 /// Vault directory (default: $FOLD_VAULT, else nearest ancestor of $PWD
1212 /// containing root.md, else ~/fold).
@@ -45,6 +45,8 @@ enum Command {
4545 #[command(subcommand)]
4646 action: TrashAction,
4747 },
48+ /// How to use fold: keys, concepts, workflow.
49+ Help,
4850}
4951
5052#[derive(Subcommand)]
@@ -179,6 +181,12 @@ fn main() -> anyhow::Result<()> {
179181 }
180182 }
181183 },
184+ Some(Command::Help) => {
185+ for line in fold_tui::app::help_text() {
186+ let text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
187+ println!("{}", text);
188+ }
189+ }
182190 }
183191 Ok(())
184192}
crates/fold-cli/tests/cli.rs+11
@@ -120,3 +120,14 @@ fn trash_list_and_restore() {
120120 .to_string_lossy()
121121 .contains("doomed")));
122122}
123+
124+#[test]
125+fn help_prints_usage() {
126+ let dir = tempfile::tempdir().unwrap();
127+ notes(dir.path())
128+ .args(["help"])
129+ .assert()
130+ .success()
131+ .stdout(predicate::str::contains("command palette"))
132+ .stdout(predicate::str::contains("make block"));
133+}
crates/fold-tui/src/app.rs+99−1
@@ -29,6 +29,7 @@ pub enum Mode {
2929 Picker,
3030 Conflict,
3131 Props,
32+ Help,
3233}
3334
3435#[derive(PartialEq, Clone, Copy, Debug)]
@@ -175,6 +176,7 @@ impl App {
175176 Mode::Picker => "picker",
176177 Mode::Conflict => "conflict",
177178 Mode::Props => "props",
179+ Mode::Help => "help",
178180 }
179181 }
180182 pub fn title_of(&self, r: NRef) -> String { self.vault.tree.node(r).title.clone() }
@@ -1150,10 +1152,13 @@ impl App {
11501152 self.filter.clear();
11511153 self.filter_rows.clear();
11521154 }
1153− KeyCode::Char(':') | KeyCode::Char('?') => {
1155+ KeyCode::Char(':') => {
11541156 self.mode = Mode::Picker;
11551157 self.palette.clear();
11561158 }
1159+ KeyCode::Char('?') => {
1160+ self.mode = Mode::Help;
1161+ }
11571162 KeyCode::Char('u') => self.act_undo(),
11581163 KeyCode::Char('U') => self.act_redo(),
11591164 KeyCode::Char('e') => self.act_edit(),
@@ -1585,6 +1590,7 @@ impl App {
15851590 self.zoom_root = None;
15861591 self.cursor = 0;
15871592 }
1593+ "help" => self.mode = Mode::Help,
15881594 "quit" => self.quit = true,
15891595 _ => self.say(format!("{}: no handler", name)),
15901596 }
@@ -1625,6 +1631,7 @@ impl App {
16251631 Mode::Filter => self.draw_filter(f, size),
16261632 Mode::Picker => self.draw_palette(f, size),
16271633 Mode::Props => self.draw_props(f, size),
1634+ Mode::Help => self.draw_help(f, size),
16281635 _ => {}
16291636 }
16301637 if let Some(p) = self.prompt.as_ref() {
@@ -1927,6 +1934,7 @@ impl App {
19271934 Mode::Picker => " :",
19281935 Mode::Conflict => " CONFLICT",
19291936 Mode::Props => " PROPS",
1937+ Mode::Help => " HELP",
19301938 };
19311939 let conflicts = fold_core::merge::conflict_pairs(&self.vault).len();
19321940 let cpart = if conflicts > 0 {
@@ -1967,6 +1975,23 @@ impl App {
19671975 f.render_widget(Paragraph::new(lines).block(block), rect);
19681976 }
19691977
1978+ fn draw_help(&mut self, f: &mut ratatui::Frame, area: Rect) {
1979+ let w = area.width.saturating_sub(8).min(76);
1980+ let h = area.height.saturating_sub(4);
1981+ let rect = Rect {
1982+ x: (area.width.saturating_sub(w)) / 2,
1983+ y: (area.height.saturating_sub(h)) / 2,
1984+ width: w,
1985+ height: h,
1986+ };
1987+ f.render_widget(Clear, rect);
1988+ let block = WBlock::default()
1989+ .borders(Borders::ALL)
1990+ .title(" fold — help (?/Esc closes) ")
1991+ .border_style(Style::default().fg(Color::Cyan));
1992+ f.render_widget(Paragraph::new(help_text()).block(block), rect);
1993+ }
1994+
19701995 fn draw_palette(&mut self, f: &mut ratatui::Frame, area: Rect) {
19711996 let actions = palette_actions();
19721997 let q = self.palette.to_lowercase();
@@ -2054,6 +2079,73 @@ fn style_markdown_line(l: &str, vault: &Vault, _ctx: NRef) -> Line<'static> {
20542079 Line::from(TSpan::raw(l.to_string()))
20552080}
20562081
2082+/// The help text, shared by `?` in the TUI and `fold help` (§10.9).
2083+pub fn help_text() -> Vec<Line<'static>> {
2084+ let entries: &[(&str, &str)] = &[
2085+ ("", "fold — a tree of notes and tasks in plain Markdown."),
2086+ ("", "One outline; zoom is the unit of reading and editing."),
2087+ ("", ""),
2088+ ("MOVING", ""),
2089+ ("j/k ←/→", "move · h/l fold & unfold · gg/G first/last"),
2090+ ("Enter", "zoom into the node (reading pane takes focus)"),
2091+ ("Backspace", "zoom out · - parent · {/} prev/next sibling"),
2092+ ("Tab", "switch outline ↔ reading pane · Ctrl-d/u scroll"),
2093+ ("", ""),
2094+ ("NODES", ""),
2095+ ("n / N", "new sibling / new child (opens the editor)"),
2096+ ("e", "edit the subtree's Markdown (saves as you type)"),
2097+ ("a", "properties form — the first property makes a block"),
2098+ ("x / t", "toggle done / toggle task-ness"),
2099+ ("s", "make block: give the node its own file + id"),
2100+ ("~", "spelling: heading ↔ bullet · >/< demote/promote"),
2101+ ("J / K", "move among siblings · r refile (id, path or title)"),
2102+ ("y / d", "yank / trash subtree · p/P paste after/before"),
2103+ ("u / U", "undo / redo"),
2104+ ("", ""),
2105+ ("FINDING", ""),
2106+ ("/", "filter box: fuzzy titles + full text, Enter zooms"),
2107+ (":", "command palette — every action by name"),
2108+ ("?", "this help"),
2109+ ("", ""),
2110+ ("TASKS & CAPTURE", ""),
2111+ ("c / C", "capture a note / a task into today's inbox day"),
2112+ ("za", "archive subtree under # Archive · zd hide done"),
2113+ ("zr", "raw mode: exact source, frontmatter included"),
2114+ (":clear done", "trash done items under the zoom root"),
2115+ ("", ""),
2116+ ("READING PANE", ""),
2117+ ("Enter", "toggle task / zoom heading / follow embed"),
2118+ ("x e a o", "toggle · edit · properties · open link"),
2119+ ("[[ / ]]", "previous / next heading · / search, n/N next"),
2120+ ("", ""),
2121+ ("CONFLICTS & QUIT", ""),
2122+ (":merge", "fold sync-conflict files in, then resolve:"),
2123+ ("o t b", "keep ours / theirs / both · n/N pairs · Enter done"),
2124+ ("q", "quit — everything is always saved"),
2125+ ];
2126+ entries
2127+ .iter()
2128+ .map(|(k, v)| {
2129+ if v.is_empty() && !k.is_empty() {
2130+ Line::from(TSpan::styled(
2131+ k.to_string(),
2132+ Style::default()
2133+ .fg(Color::Cyan)
2134+ .add_modifier(Modifier::BOLD),
2135+ ))
2136+ } else {
2137+ Line::from(vec![
2138+ TSpan::styled(
2139+ format!("{:<11}", k),
2140+ Style::default().add_modifier(Modifier::BOLD),
2141+ ),
2142+ TSpan::raw(v.to_string()),
2143+ ])
2144+ }
2145+ })
2146+ .collect()
2147+}
2148+
20572149fn fuzzy_match(needle: &str, hay: &str) -> bool {
20582150 // simple subsequence match (nucleo would weight; this is the picker path)
20592151 let mut n = needle.chars().peekable();
@@ -2115,6 +2207,7 @@ fn palette_actions() -> Vec<PaletteAction> {
21152207 PaletteAction { name: "go to", key: None, desc: "jump to a node by id, path or title" },
21162208 PaletteAction { name: "refile", key: Some("r"), desc: "move the subtree under a new parent" },
21172209 PaletteAction { name: "zoom out", key: Some("Backspace"), desc: "up one zoom level" },
2210+ PaletteAction { name: "help", key: Some("?"), desc: "how to use fold" },
21182211 PaletteAction { name: "quit", key: Some("q"), desc: "save and exit" },
21192212 ]
21202213}
@@ -2234,6 +2327,11 @@ fn run_loop(
22342327 app.key_edit(key)
22352328 }
22362329 Mode::Props => app.key_props(key),
2330+ Mode::Help => {
2331+ if matches!(key.code, KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') | KeyCode::Char('?')) {
2332+ app.mode = Mode::Normal;
2333+ }
2334+ }
22372335 Mode::Conflict => app.key_conflict(key),
22382336 }
22392337 }
crates/fold-tui/tests/tui.rs+21
@@ -149,3 +149,24 @@ fn undo_restores_delete() {
149149fn app_vault_path(app: &App) -> std::path::PathBuf {
150150 app.vault_dir()
151151}
152+
153+#[test]
154+fn question_mark_opens_help() {
155+ let (_d, mut app) = app_with("# A\n");
156+ app.key_normal(key(KeyCode::Char('?')));
157+ assert_eq!(app.mode_pub(), "help");
158+ let s = screen(&mut app, 100, 40);
159+ assert!(s.contains("fold — help"), "{}", s);
160+ assert!(s.contains("command palette"), "{}", s);
161+ assert!(s.contains("make block"), "{}", s);
162+ // Esc closes
163+ app.key_normal(key(KeyCode::Esc));
164+ // Esc in help mode is handled by the dispatcher, not key_normal; simulate:
165+ // (the real loop routes Help mode keys) — here we just re-open via palette
166+ app.key_normal(key(KeyCode::Char(':')));
167+ for c in "help".chars() {
168+ app.key_palette(key(KeyCode::Char(c)));
169+ }
170+ app.key_palette(key(KeyCode::Enter));
171+ assert_eq!(app.mode_pub(), "help");
172+}