| 1 | //! The man page: clap's sections plus keys and actions generated from the compiled-in defaults. |
| 2 | |
| 3 | use std::io::Write; |
| 4 | |
| 5 | use clap::CommandFactory; |
| 6 | use clap_mangen::Man; |
| 7 | use clap_mangen::roff::{Roff, bold, italic, roman}; |
| 8 | |
| 9 | use crate::action::CATALOG; |
| 10 | use crate::cli::Cli; |
| 11 | use crate::config; |
| 12 | use crate::input::keymap::Bindings; |
| 13 | |
| 14 | pub 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 | |
| 33 | fn section(r: &mut Roff, title: &str) { |
| 34 | r.control("SH", [title]); |
| 35 | } |
| 36 | |
| 37 | fn 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 | |
| 43 | fn 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. |
| 49 | fn 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. |
| 104 | fn 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 | |
| 121 | fn 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)] |
| 216 | mod 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 | } |