Krolyxon
Commit a44bdf7

Branches

Tags

Refs →

remagesrccli.rs

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