Krolyxon
Commit e3295f9

Branches

Tags

Refs →

remagesrccli.rs

168 lines · 4.7 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 /// How images fit the window
61 #[arg(short = 's', long, value_enum, value_name = "MODE")]
62 pub fit: Option<FitMode>,
63
64 /// Fixed zoom level in percent (overrides --fit)
65 #[arg(short = 'z', long, value_name = "PCT")]
66 pub zoom: Option<f32>,
67
68 /// Initial window size in logical pixels
69 #[arg(short = 'g', long, value_name = "WxH", value_parser = parse_geometry)]
70 pub geometry: Option<(u32, u32)>,
71
72 /// Wayland app_id / X11 class [default: remage]
73 #[arg(long, value_name = "NAME")]
74 pub class: Option<String>,
75
76 /// Sort order for directory contents [default: natural]
77 #[arg(long, value_enum, value_name = "KEY")]
78 pub sort: Option<SortKey>,
79
80 /// Reverse the sort order
81 #[arg(long)]
82 pub reverse: bool,
83
84 /// Open only the named file, not the rest of its directory
85 #[arg(long)]
86 pub no_siblings: bool,
87
88 /// Config file [default: $XDG_CONFIG_HOME/remage/config.toml]
89 #[arg(short = 'c', long, value_name = "PATH")]
90 pub config: Option<PathBuf>,
91
92 /// Print the full commented default config and exit
93 #[arg(long)]
94 pub dump_config: bool,
95
96 /// Validate the config and exit non-zero on errors
97 #[arg(long)]
98 pub check_config: bool,
99}
100
101#[derive(Subcommand, Debug)]
102pub enum Sub {
103 /// Send an action or query to a running remage
104 Msg {
105 /// The instance to talk to [default: $REMAGE_SOCKET, else the newest]
106 #[arg(long, value_name = "PID")]
107 pid: Option<u32>,
108
109 /// Print current, marked, queue or state
110 #[arg(long, value_name = "Q", conflicts_with_all = ["subscribe", "action"])]
111 query: Option<String>,
112
113 /// Stream events (image, marks, mode) as JSON lines
114 #[arg(long, value_name = "EV", conflicts_with = "action")]
115 subscribe: Vec<String>,
116
117 /// The action, e.g. `goto 5`
118 #[arg(
119 value_name = "ACTION",
120 trailing_var_arg = true,
121 allow_hyphen_values = true
122 )]
123 action: Vec<String>,
124 },
125}
126
127fn parse_geometry(s: &str) -> Result<(u32, u32), String> {
128 let (w, h) = s
129 .split_once(['x', 'X'])
130 .ok_or("expected WxH, e.g. 1280x800")?;
131 let parse = |v: &str| v.trim().parse::<u32>().ok().filter(|n| *n > 0);
132 match (parse(w), parse(h)) {
133 (Some(w), Some(h)) => Ok((w, h)),
134 _ => Err("expected positive WxH, e.g. 1280x800".into()),
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use clap::CommandFactory;
142
143 #[test]
144 fn cli_is_consistent() {
145 Cli::command().debug_assert();
146 }
147
148 #[test]
149 fn msg_subcommand() {
150 let c = Cli::try_parse_from(["remage", "msg", "goto", "5"]).unwrap();
151 match c.command {
152 Some(Sub::Msg { action, .. }) => assert_eq!(action, ["goto", "5"]),
153 other => panic!("{other:?}"),
154 }
155 let c = Cli::try_parse_from(["remage", "msg", "slideshow-delay", "-1"]).unwrap();
156 assert!(matches!(c.command, Some(Sub::Msg { action, .. }) if action[1] == "-1"));
157 assert!(Cli::try_parse_from(["remage", "msg", "--query", "x", "next"]).is_err());
158 let c = Cli::try_parse_from(["remage", "-io0", "a.png"]).unwrap();
159 assert!(c.stdin && c.print_marked && c.null && c.command.is_none());
160 }
161
162 #[test]
163 fn geometry() {
164 assert_eq!(parse_geometry("800x600"), Ok((800, 600)));
165 assert!(parse_geometry("800").is_err());
166 assert!(parse_geometry("0x600").is_err());
167 }
168}