Krolyxon
Commit 74a9b56

Branches

Tags

Refs →

remagesrccli.rs

214 lines · 6.3 KB · Rust

1//! Command-line interface.
2
3use std::path::PathBuf;
4
5use clap::{Parser, Subcommand};
6
7use crate::state::queue::SortKey;
8use crate::state::view::FitMode;
9
10#[derive(Parser, Debug)]
11#[command(
12 name = "remage",
13 version,
14 about = "Keyboard-centric image viewer",
15 long_about = "A keyboard-centric, GPU-rendered image viewer with vim-style keys, a thumbnail \
16 grid, user commands and an IPC socket. With no paths it opens the current \
17 directory; a single file opens its whole directory, starting on that file.",
18 args_conflicts_with_subcommands = true
19)]
20pub struct Cli {
21 #[command(subcommand)]
22 pub command: Option<Sub>,
23
24 /// Files or directories to open [default: .]
25 pub paths: Vec<PathBuf>,
26
27 /// Read file paths from stdin, one per line
28 #[arg(short = 'i', long)]
29 pub stdin: bool,
30
31 /// Paths on stdin and printed by -o are NUL-separated
32 #[arg(short = '0', long)]
33 pub null: bool,
34
35 /// Print the marked files on exit
36 #[arg(short = 'o', long)]
37 pub print_marked: bool,
38
39 /// Descend into directories
40 #[arg(short, long)]
41 pub recursive: bool,
42
43 /// Start in the thumbnail grid
44 #[arg(short = 't', long)]
45 pub grid: bool,
46
47 /// Read the shared thumbnail cache but never write to it
48 #[arg(short = 'p', long)]
49 pub private: bool,
50
51 /// Start at the Nth image (1-based)
52 #[arg(short = 'n', long, value_name = "N")]
53 pub start: Option<usize>,
54
55 /// Start in fullscreen
56 #[arg(short, long)]
57 pub fullscreen: bool,
58
59 /// Hide the status bar
60 #[arg(short = 'b', long)]
61 pub no_bar: bool,
62
63 /// Start a slideshow, optionally with this delay: -S=3 (the = keeps paths apart)
64 #[arg(
65 short = 'S',
66 long,
67 value_name = "SECS",
68 num_args = 0..=1,
69 require_equals = true
70 )]
71 pub slideshow: Option<Option<f32>>,
72
73 /// How images fit the window
74 #[arg(short = 's', long, value_enum, value_name = "MODE")]
75 pub fit: Option<FitMode>,
76
77 /// Fixed zoom level in percent (overrides --fit)
78 #[arg(short = 'z', long, value_name = "PCT")]
79 pub zoom: Option<f32>,
80
81 /// Initial window size in logical pixels
82 #[arg(short = 'g', long, value_name = "WxH", value_parser = parse_geometry)]
83 pub geometry: Option<(u32, u32)>,
84
85 /// Wayland app_id / X11 class [default: remage]
86 #[arg(long, value_name = "NAME")]
87 pub class: Option<String>,
88
89 /// Sort order for directory contents [default: natural]
90 #[arg(long, value_enum, value_name = "KEY")]
91 pub sort: Option<SortKey>,
92
93 /// Reverse the sort order
94 #[arg(long)]
95 pub reverse: bool,
96
97 /// Open only the named file, not the rest of its directory
98 #[arg(long)]
99 pub no_siblings: bool,
100
101 /// Config file [default: $XDG_CONFIG_HOME/remage/config.toml]
102 #[arg(short = 'c', long, value_name = "PATH")]
103 pub config: Option<PathBuf>,
104
105 /// Print the full commented default config and exit
106 #[arg(long)]
107 pub dump_config: bool,
108
109 /// Validate the config and exit non-zero on errors
110 #[arg(long)]
111 pub check_config: bool,
112}
113
114#[derive(Subcommand, Debug)]
115pub enum Sub {
116 /// Send an action or query to a running remage
117 Msg {
118 /// The instance to talk to [default: $REMAGE_SOCKET, else the newest]
119 #[arg(long, value_name = "PID")]
120 pid: Option<u32>,
121
122 /// Print current, marked, queue or state
123 #[arg(long, value_name = "Q", conflicts_with_all = ["subscribe", "action"])]
124 query: Option<String>,
125
126 /// Stream events (image, marks, mode) as JSON lines
127 #[arg(long, value_name = "EV", conflicts_with = "action")]
128 subscribe: Vec<String>,
129
130 /// The action, e.g. `goto 5`
131 #[arg(
132 value_name = "ACTION",
133 trailing_var_arg = true,
134 allow_hyphen_values = true
135 )]
136 action: Vec<String>,
137 },
138
139 /// Print a shell completion script
140 Completions {
141 #[arg(value_enum)]
142 shell: clap_complete::Shell,
143 },
144
145 /// Print the man page (roff)
146 #[command(hide = true)]
147 Man,
148}
149
150fn parse_geometry(s: &str) -> Result<(u32, u32), String> {
151 let (w, h) = s
152 .split_once(['x', 'X'])
153 .ok_or("expected WxH, e.g. 1280x800")?;
154 let parse = |v: &str| v.trim().parse::<u32>().ok().filter(|n| *n > 0);
155 match (parse(w), parse(h)) {
156 (Some(w), Some(h)) => Ok((w, h)),
157 _ => Err("expected positive WxH, e.g. 1280x800".into()),
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use clap::CommandFactory;
165
166 #[test]
167 fn cli_is_consistent() {
168 Cli::command().debug_assert();
169 }
170
171 #[test]
172 fn msg_subcommand() {
173 let c = Cli::try_parse_from(["remage", "msg", "goto", "5"]).unwrap();
174 match c.command {
175 Some(Sub::Msg { action, .. }) => assert_eq!(action, ["goto", "5"]),
176 other => panic!("{other:?}"),
177 }
178 let c = Cli::try_parse_from(["remage", "msg", "slideshow-delay", "-1"]).unwrap();
179 assert!(matches!(c.command, Some(Sub::Msg { action, .. }) if action[1] == "-1"));
180 assert!(Cli::try_parse_from(["remage", "msg", "--query", "x", "next"]).is_err());
181 let c = Cli::try_parse_from(["remage", "-io0", "a.png"]).unwrap();
182 assert!(c.stdin && c.print_marked && c.null && c.command.is_none());
183 }
184
185 #[test]
186 fn slideshow_seconds_need_an_equals() {
187 let c = Cli::try_parse_from(["remage", "-S", "pics"]).unwrap();
188 assert_eq!(c.slideshow, Some(None));
189 assert_eq!(c.paths, [PathBuf::from("pics")]);
190 let c = Cli::try_parse_from(["remage", "-S=2.5", "pics"]).unwrap();
191 assert_eq!(c.slideshow, Some(Some(2.5)));
192 let c = Cli::try_parse_from(["remage", "--slideshow=3"]).unwrap();
193 assert_eq!(c.slideshow, Some(Some(3.0)));
194 assert_eq!(Cli::try_parse_from(["remage"]).unwrap().slideshow, None);
195 }
196
197 #[test]
198 fn completions_subcommand() {
199 let c = Cli::try_parse_from(["remage", "completions", "zsh"]).unwrap();
200 assert!(matches!(
201 c.command,
202 Some(Sub::Completions {
203 shell: clap_complete::Shell::Zsh
204 })
205 ));
206 }
207
208 #[test]
209 fn geometry() {
210 assert_eq!(parse_geometry("800x600"), Ok((800, 600)));
211 assert!(parse_geometry("800").is_err());
212 assert!(parse_geometry("0x600").is_err());
213 }
214}