| 146 |
146 |
|
/// `--theme ID` overrides the config file for one run, which is how you look
|
| 147 |
147 |
|
/// at a theme before committing to it.
|
| 148 |
148 |
|
theme: Option<String>,
|
|
149 |
+ |
/// `--app-id ID` sets the xdg-shell app_id this window reports.
|
|
150 |
+ |
///
|
|
151 |
+ |
/// What a compositor keys its window rules on. Alloy's sway config runs the
|
|
152 |
+ |
/// launcher as `shop --app-id=alloy-menu -e alloy-menu` so that
|
|
153 |
+ |
/// `for_window [app_id="alloy-menu"] floating enable, resize set 800 500`
|
|
154 |
+ |
/// matches it, and a terminal that cannot say who it is makes that rule
|
|
155 |
+ |
/// unwritable: every shop window would be the same window to sway.
|
|
156 |
+ |
app_id: Option<String>,
|
| 149 |
157 |
|
}
|
| 150 |
158 |
|
|
| 151 |
159 |
|
/// Every way argv can end the process before a window exists.
|
| 167 |
175 |
|
of the command line, so it goes last.
|
| 168 |
176 |
|
--exec CMD Run CMD through sh -c instead of the login shell.
|
| 169 |
177 |
|
--theme ID Use theme ID for this run, ignoring the config file.
|
|
178 |
+ |
--app-id ID Report ID as the window's app id, for window rules.
|
| 170 |
179 |
|
--record PATH Tee the terminal output to PATH as raw bytes.
|
| 171 |
180 |
|
-h, --help Print this help.
|
| 172 |
181 |
|
-V, --version Print the version.
|
| 173 |
182 |
|
|
|
183 |
+ |
Options take their value either way: --theme dark or --theme=dark.
|
|
184 |
+ |
|
| 174 |
185 |
|
Config is read from ~/.config/shop/config.toml.";
|
| 175 |
186 |
|
|
| 176 |
187 |
|
/// Read argv, or say why we are not opening a window.
|
| 195 |
206 |
|
cli.exec_argv = Some(argv).filter(|a| !a.is_empty());
|
| 196 |
207 |
|
return Ok(cli);
|
| 197 |
208 |
|
}
|
| 198 |
|
- |
let mut value = |flag: &str| {
|
| 199 |
|
- |
args.next()
|
| 200 |
|
- |
.ok_or_else(|| CliExit::Reject(format!("{flag} needs a value")))
|
|
209 |
+ |
// `--flag=value` as well as `--flag value`. Both spellings are ordinary
|
|
210 |
+ |
// and callers pick whichever reads better, so a parser that knows only
|
|
211 |
+ |
// one of them rejects correct command lines. Alloy's sway config writes
|
|
212 |
+ |
// the equals form, and understanding only the space form is exactly how
|
|
213 |
+ |
// the launcher broke.
|
|
214 |
+ |
let (name, inline) = match arg.split_once('=') {
|
|
215 |
+ |
Some((name, value)) if name.starts_with("--") => (name, Some(value.to_string())),
|
|
216 |
+ |
_ => (arg.as_str(), None),
|
| 201 |
217 |
|
};
|
| 202 |
|
- |
match arg.as_str() {
|
|
218 |
+ |
let mut value = |flag: &str| match &inline {
|
|
219 |
+ |
Some(value) => Ok(value.clone()),
|
|
220 |
+ |
None => args
|
|
221 |
+ |
.next()
|
|
222 |
+ |
.ok_or_else(|| CliExit::Reject(format!("{flag} needs a value"))),
|
|
223 |
+ |
};
|
|
224 |
+ |
match name {
|
|
225 |
+ |
"-h" | "--help" | "-V" | "--version" if inline.is_some() => {
|
|
226 |
+ |
return Err(CliExit::Reject(format!("{name} takes no value")));
|
|
227 |
+ |
}
|
| 203 |
228 |
|
"-h" | "--help" => return Err(CliExit::Answer(HELP.into())),
|
| 204 |
229 |
|
"-V" | "--version" => {
|
| 205 |
230 |
|
return Err(CliExit::Answer(format!(
|
| 210 |
235 |
|
"--exec" => cli.exec_cmd = Some(value("--exec")?),
|
| 211 |
236 |
|
"--theme" => cli.theme = Some(value("--theme")?),
|
| 212 |
237 |
|
"--record" => cli.record_path = Some(value("--record")?),
|
|
238 |
+ |
"--app-id" => cli.app_id = Some(value("--app-id")?),
|
| 213 |
239 |
|
other => {
|
| 214 |
240 |
|
return Err(CliExit::Reject(format!(
|
| 215 |
241 |
|
"unknown option {other}\nTry 'shop --help'."
|
| 248 |
274 |
|
exec_argv,
|
| 249 |
275 |
|
record_path,
|
| 250 |
276 |
|
theme: theme_arg,
|
|
277 |
+ |
app_id,
|
| 251 |
278 |
|
} = cli;
|
| 252 |
279 |
|
let config = Config::load().with_theme(theme_arg);
|
| 253 |
280 |
|
let scrollback_lines = config.scrollback_lines;
|
| 277 |
304 |
|
info!(spawn = %spawn_cmd, "spawned child");
|
| 278 |
305 |
|
|
| 279 |
306 |
|
let conn = Connection::connect_to_env()?;
|
|
307 |
+ |
// The default is the reverse-DNS name a desktop file would use. An
|
|
308 |
+ |
// override is for a window meant to be caught by a rule rather than to
|
|
309 |
+ |
// look like every other terminal: Alloy's launcher asks for `alloy-menu`
|
|
310 |
+ |
// so sway can float it at a fixed size.
|
| 280 |
311 |
|
let spec = WindowSpec {
|
| 281 |
312 |
|
title: "shop",
|
| 282 |
|
- |
app_id: "dev.makecreative.shop",
|
|
313 |
+ |
app_id: app_id.as_deref().unwrap_or("dev.makecreative.shop"),
|
| 283 |
314 |
|
min_size: (320, 240),
|
| 284 |
315 |
|
initial_size: INITIAL,
|
| 285 |
316 |
|
};
|
| 2503 |
2534 |
|
assert_eq!(cli.theme, None);
|
| 2504 |
2535 |
|
}
|
| 2505 |
2536 |
|
|
|
2537 |
+ |
// The exact line in Alloy's sway config, and the regression that put it
|
|
2538 |
+ |
// here: rejecting unknown options turned a flag shop silently ignored into
|
|
2539 |
+ |
// one that exited 2, so $mod+d stopped opening anything at all.
|
|
2540 |
+ |
#[test]
|
|
2541 |
+ |
fn the_launcher_command_line_parses() {
|
|
2542 |
+ |
let cli = parse_args(cmdline(&[
|
|
2543 |
+ |
"--app-id=alloy-menu",
|
|
2544 |
+ |
"-e",
|
|
2545 |
+ |
"/usr/bin/alloy-menu",
|
|
2546 |
+ |
]))
|
|
2547 |
+ |
.expect("the launcher's own command line must work");
|
|
2548 |
+ |
|
|
2549 |
+ |
assert_eq!(cli.app_id.as_deref(), Some("alloy-menu"));
|
|
2550 |
+ |
assert_eq!(cli.exec_argv, Some(argv(&["/usr/bin/alloy-menu"])));
|
|
2551 |
+ |
}
|
|
2552 |
+ |
|
|
2553 |
+ |
#[test]
|
|
2554 |
+ |
fn a_value_can_be_attached_with_equals_or_separated_by_a_space() {
|
|
2555 |
+ |
let attached = parse_args(cmdline(&["--theme=akari-night"])).expect("equals form");
|
|
2556 |
+ |
let separated = parse_args(cmdline(&["--theme", "akari-night"])).expect("space form");
|
|
2557 |
+ |
|
|
2558 |
+ |
assert_eq!(attached.theme.as_deref(), Some("akari-night"));
|
|
2559 |
+ |
assert_eq!(attached, separated);
|
|
2560 |
+ |
}
|
|
2561 |
+ |
|
|
2562 |
+ |
// An equals sign in a value is the value's business. Only the first one
|
|
2563 |
+ |
// separates, so a runner command or a path keeps its own.
|
|
2564 |
+ |
#[test]
|
|
2565 |
+ |
fn only_the_first_equals_separates() {
|
|
2566 |
+ |
let cli = parse_args(cmdline(&["--exec=echo a=b"])).expect("equals in a value");
|
|
2567 |
+ |
|
|
2568 |
+ |
assert_eq!(cli.exec_cmd.as_deref(), Some("echo a=b"));
|
|
2569 |
+ |
}
|
|
2570 |
+ |
|
|
2571 |
+ |
#[test]
|
|
2572 |
+ |
fn a_flag_that_takes_no_value_refuses_one() {
|
|
2573 |
+ |
let Err(CliExit::Reject(message)) = parse_args(cmdline(&["--help=please"])) else {
|
|
2574 |
+ |
panic!("--help does not take a value");
|
|
2575 |
+ |
};
|
|
2576 |
+ |
assert!(message.contains("takes no value"), "{message}");
|
|
2577 |
+ |
}
|
|
2578 |
+ |
|
| 2506 |
2579 |
|
#[test]
|
| 2507 |
2580 |
|
fn dash_e_with_nothing_after_it_is_a_shell_not_an_error() {
|
| 2508 |
2581 |
|
// A launcher that built a command line and found no command. Opening a
|