Krolyxon
Commit 4654f72

Branches

Tags

Refs →

remagesrcman.rs

226 lines · 7.2 KB · Rust

1//! The man page: clap's sections plus keys and actions generated from the compiled-in defaults.
2
3use std::io::Write;
4
5use clap::CommandFactory;
6use clap_mangen::Man;
7use clap_mangen::roff::{Roff, bold, italic, roman};
8
9use crate::action::CATALOG;
10use crate::cli::Cli;
11use crate::config;
12use crate::input::keymap::Bindings;
13
14pub fn render(w: &mut dyn Write) -> std::io::Result<()> {
15 let man = Man::new(Cli::command()).manual("remage manual");
16 man.render_title(w)?;
17 man.render_name_section(w)?;
18 man.render_synopsis_section(w)?;
19 man.render_description_section(w)?;
20 man.render_options_section(w)?;
21 man.render_subcommands_section(w)?;
22
23 let mut r = Roff::new();
24 keys(&mut r);
25 actions(&mut r);
26 prose(&mut r);
27 r.to_writer(w)?;
28
29 man.render_version_section(w)?;
30 Ok(())
31}
32
33fn section(r: &mut Roff, title: &str) {
34 r.control("SH", [title]);
35}
36
37fn item(r: &mut Roff, term: Vec<clap_mangen::roff::Inline>, body: &str) {
38 r.control("TP", []);
39 r.text(term);
40 r.text([roman(body)]);
41}
42
43fn para(r: &mut Roff, text: &str) {
44 r.control("PP", []);
45 r.text([roman(text)]);
46}
47
48/// The default bindings per mode table, keys for the same action merged as in `?` help.
49fn keys(r: &mut Roff) {
50 section(r, "KEYS");
51 para(
52 r,
53 "The default bindings. Mode tables override global ones, and visual mode also inherits \
54 the grid table. Notation follows vim. A count typed first applies to the action \
55 (5n, 12G, 3+). Press ? in remage for the live keymap, including your own bindings.",
56 );
57 let Some(tables) = config::defaults().keys.into_iter().next() else {
58 return;
59 };
60 let modes: [(&str, &Bindings); 4] = [
61 ("Global", &tables.global),
62 ("Image mode", &tables.image),
63 ("Grid mode", &tables.grid),
64 ("Visual mode", &tables.visual),
65 ];
66 for (title, table) in modes {
67 r.control("SS", [title]);
68 // (catalog rank, action, keys, description)
69 let mut rows: Vec<(usize, String, String, String)> = Vec::new();
70 for (seq, actions) in table {
71 let Some(actions) = actions else { continue };
72 let Some(first) = actions.first() else {
73 continue;
74 };
75 let desc = actions
76 .iter()
77 .map(|a| a.description())
78 .collect::<Vec<_>>()
79 .join(", then ");
80 let k = crate::ui::pretty_keys(&seq.to_string());
81 match rows.iter_mut().find(|row| row.3 == desc) {
82 Some(row) => {
83 row.2.push_str(", ");
84 row.2.push_str(&k);
85 }
86 None => {
87 let rank = CATALOG
88 .iter()
89 .position(|e| e.name == first.name())
90 .unwrap_or(usize::MAX);
91 rows.push((rank, first.to_string(), k, desc));
92 }
93 }
94 }
95 // Catalog order, then by action (`next` before `next 10`), as in `?` help.
96 rows.sort_by(|a, b| (a.0, &a.1).cmp(&(b.0, &b.1)));
97 for (_, _, k, desc) in rows {
98 item(r, vec![bold(k)], &desc);
99 }
100 }
101}
102
103/// Every action in the grammar shared by keys, the : palette, IPC and command output.
104fn actions(r: &mut Roff) {
105 section(r, "ACTIONS");
106 para(
107 r,
108 "Keys, the : palette, remage msg and commands with output = \"actions\" all speak the \
109 same grammar: a name and its arguments.",
110 );
111 for e in CATALOG {
112 let mut term = vec![bold(e.name)];
113 if !e.args.is_empty() {
114 term.push(roman(" "));
115 term.push(italic(e.args));
116 }
117 item(r, term, e.desc);
118 }
119}
120
121fn prose(r: &mut Roff) {
122 section(r, "CONFIGURATION");
123 para(
124 r,
125 "remage reads $XDG_CONFIG_HOME/remage/config.toml (or ~/.config/remage/config.toml, or \
126 the file given with -c). The file is merged over the compiled-in defaults, so a partial \
127 file is fine; remage --dump-config prints the complete commented default and \
128 remage --check-config validates a file. Changes apply live.",
129 );
130 para(
131 r,
132 "User commands live in [commands.NAME] tables and are bound as \"run NAME\". Theme \
133 colours can come from matugen through include = [...] or from pywal with \
134 [theme] pywal = \"~/.cache/wal/colors.json\".",
135 );
136
137 section(r, "IPC");
138 para(
139 r,
140 "Each instance listens on $XDG_RUNTIME_DIR/remage/PID.sock for newline-delimited JSON: \
141 {\"action\":\"goto 5\"}, {\"query\":\"current\"} (or marked, queue, state) and \
142 {\"subscribe\":[\"image\",\"marks\",\"mode\"]}. remage msg is a client for it.",
143 );
144
145 section(r, "ENVIRONMENT");
146 for (name, desc) in [
147 (
148 "REMAGE_SOCKET",
149 "The socket remage msg talks to. Set for user commands.",
150 ),
151 (
152 "REMAGE_FILE, REMAGE_INDEX, REMAGE_COUNT, REMAGE_MODE, REMAGE_PID",
153 "Set for user commands: the current file, its 1-based index, the number of targets, \
154 the mode and the viewer's pid.",
155 ),
156 (
157 "XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_RUNTIME_DIR",
158 "Where the config, the shared thumbnail cache and the IPC sockets live.",
159 ),
160 (
161 "WGPU_BACKEND, WGPU_POWER_PREF",
162 "Override the GPU backend (vulkan, gl) and power preference (low, high).",
163 ),
164 (
165 "RUST_LOG",
166 "Logging, e.g. remage=debug for startup and first-frame timings.",
167 ),
168 ] {
169 item(r, vec![bold(name)], desc);
170 }
171
172 section(r, "SIGNALS");
173 item(r, vec![bold("SIGUSR1")], "Reload the config.");
174
175 section(r, "FILES");
176 for (name, desc) in [
177 ("~/.config/remage/config.toml", "The config file."),
178 (
179 "~/.cache/thumbnails/",
180 "The freedesktop thumbnail cache, shared with file managers.",
181 ),
182 (
183 "/usr/share/doc/remage/config.example.toml",
184 "An example config with user commands.",
185 ),
186 ] {
187 item(r, vec![bold(name)], desc);
188 }
189
190 section(r, "EXAMPLES");
191 for (cmd, desc) in [
192 ("remage", "Open the current directory."),
193 (
194 "remage photo.jpg",
195 "Open photo.jpg with the rest of its directory.",
196 ),
197 ("remage -rt ~/Pictures", "Browse a tree in the grid."),
198 (
199 "find . -name '*.png' | remage -i -o",
200 "Pick images from a list; print the marked ones on exit.",
201 ),
202 ("remage msg next", "Advance the newest instance."),
203 (
204 "remage msg --subscribe image",
205 "Print a JSON line whenever the image changes.",
206 ),
207 ] {
208 item(r, vec![bold(cmd)], desc);
209 }
210
211 section(r, "SEE ALSO");
212 r.text([roman("nsxiv(1), matugen(1), wal(1)")]);
213}
214
215#[cfg(test)]
216mod tests {
217 #[test]
218 fn renders_keys_and_actions() {
219 let mut out = Vec::new();
220 super::render(&mut out).unwrap();
221 let page = String::from_utf8(out).unwrap();
222 for needle in [".SH KEYS", ".SH ACTIONS", "slideshow\\-delay", "Next image"] {
223 assert!(page.contains(needle), "missing {needle}");
224 }
225 }
226}