From 325f4ea71b8e32e8cb6876b17a5b33bf3044ade0 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 19 Sep 2025 23:05:43 +0200 Subject: [PATCH] toml: add exec.shell --- toml-config/src/config/parsers/exec.rs | 43 ++++++++++++++++++++++---- toml-spec/spec/spec.generated.json | 14 +++++---- toml-spec/spec/spec.generated.md | 29 +++++++++++++++-- toml-spec/spec/spec.yaml | 31 +++++++++++++++++-- 4 files changed, 100 insertions(+), 17 deletions(-) diff --git a/toml-config/src/config/parsers/exec.rs b/toml-config/src/config/parsers/exec.rs index 1d6829e5..bc0eaa61 100644 --- a/toml-config/src/config/parsers/exec.rs +++ b/toml-config/src/config/parsers/exec.rs @@ -16,6 +16,7 @@ use { }, }, indexmap::IndexMap, + std::sync::LazyLock, thiserror::Error, }; @@ -31,6 +32,12 @@ pub enum ExecParserError { Env(#[from] EnvParserError), #[error("Array cannot be empty")] Empty, + #[error("Exactly one of the `prog` or `shell` fields must be specified")] + ProgXorShell, + #[error("Could not read $SHELL")] + ShellNotDefined, + #[error("The `args` field cannot be used for shell commands")] + ArgsForShell, } pub struct ExecParser<'a>(pub &'a Context<'a>); @@ -72,16 +79,33 @@ impl Parser for ExecParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.0, span, table); - let (prog, args_val, envs_val, privileged) = ext.extract(( - str("prog"), + let (prog_opt, shell_opt, args_val, envs_val, privileged) = ext.extract(( + opt(str("prog")), + opt(str("shell")), opt(arr("args")), opt(val("env")), recover(opt(bol("privileged"))), ))?; + let prog; let mut args = vec![]; - if let Some(args_val) = args_val { - for arg in args_val.value { - args.push(arg.parse_map(&mut StringParser)?); + match (prog_opt, shell_opt) { + (None, None) | (Some(_), Some(_)) => { + return Err(ExecParserError::ProgXorShell.spanned(span)); + } + (Some(v), _) => { + prog = v.value.to_string(); + if let Some(args_val) = args_val { + for arg in args_val.value { + args.push(arg.parse_map(&mut StringParser)?); + } + } + } + (_, Some(v)) => { + prog = shell(v.span)?; + args = vec!["-c".to_string(), v.value.to_string()]; + if let Some(v) = args_val { + return Err(ExecParserError::ArgsForShell.spanned(v.span)); + } } } let envs = match envs_val { @@ -89,10 +113,17 @@ impl Parser for ExecParser<'_> { Some(e) => e.parse_map(&mut EnvParser)?, }; Ok(Exec { - prog: prog.value.to_string(), + prog, args, envs, privileged: privileged.despan().unwrap_or(false), }) } } + +fn shell(span: Span) -> Result> { + static SHELL: LazyLock> = LazyLock::new(|| std::env::var("SHELL").ok()); + SHELL + .clone() + .ok_or(ExecParserError::ShellNotDefined.spanned(span)) +} diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 4716895b..d6ac7e73 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1200,7 +1200,7 @@ ] }, "Exec": { - "description": "Describes how to execute a program.\n\n- Example 1:\n\n ```toml\n [shortcuts]\n ctrl-a = { type = \"exec\", exec = \"alacritty\" }\n ```\n\n- Example 2:\n\n ```toml\n [shortcuts]\n ctrl-a = { type = \"exec\", exec = [\"notify-send\", \"hello world\"] }\n ```\n\n- Example 3:\n\n ```toml\n [shortcuts]\n ctrl-a = { type = \"exec\", exec = { prog = \"notify-send\", args = [\"hello world\"], env.WAYLAND_DISPLAY = \"2\" } }\n ```\n", + "description": "Describes how to execute a program.\n\n- Example 1:\n\n ```toml\n [shortcuts]\n ctrl-a = { type = \"exec\", exec = \"alacritty\" }\n ```\n\n- Example 2:\n\n ```toml\n [shortcuts]\n ctrl-a = { type = \"exec\", exec = [\"notify-send\", \"hello world\"] }\n ```\n\n- Example 3:\n\n ```toml\n [shortcuts]\n ctrl-a = { type = \"exec\", exec = { prog = \"notify-send\", args = [\"hello world\"], env.WAYLAND_DISPLAY = \"2\" } }\n ```\n\n- Example 4:\n\n ```toml\n [shortcuts]\n ctrl-a = { type = \"exec\", exec = { shell = \"grim - | wl-copy\", privileged = true } }\n ```\n", "anyOf": [ { "type": "string", @@ -1215,16 +1215,20 @@ } }, { - "description": "The name, arguments, and environment variables of the executable to execute.\n\n- Example:\n\n ```toml\n [shortcuts]\n ctrl-a = { type = \"exec\", exec = { prog = \"notify-send\", args = [\"hello world\"], env.WAYLAND_DISPLAY = \"2\" } }\n ```\n", + "description": "The name, arguments, and environment variables of the executable to execute.\n\nExactly one of the `prog` or `shell` fields must be specified.\n\n- Example 1:\n\n ```toml\n [shortcuts]\n ctrl-a = { type = \"exec\", exec = { prog = \"notify-send\", args = [\"hello world\"], env.WAYLAND_DISPLAY = \"2\" } }\n ```\n\n- Example 2:\n \n ```toml\n [shortcuts]\n ctrl-a = { type = \"exec\", exec = { shell = \"grim - | wl-copy\", privileged = true } }\n ```\n", "type": "object", "properties": { "prog": { "type": "string", "description": "The name of the executable." }, + "shell": { + "type": "string", + "description": "The name of a shell command to execute. The command will be executed as\n`$SHELL -c \"command\"`.\n" + }, "args": { "type": "array", - "description": "The arguments to pass to the executable.", + "description": "The arguments to pass to the executable.\n\nThis field must not be specified if a shell command is used.\n", "items": { "type": "string", "description": "" @@ -1243,9 +1247,7 @@ "description": "If `true`, the executable gets access to privileged wayland protocols.\n\nThe default is `false`.\n" } }, - "required": [ - "prog" - ] + "required": [] } ] }, diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 023341ac..dd15622f 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -2483,6 +2483,13 @@ Describes how to execute a program. ctrl-a = { type = "exec", exec = { prog = "notify-send", args = ["hello world"], env.WAYLAND_DISPLAY = "2" } } ``` +- Example 4: + + ```toml + [shortcuts] + ctrl-a = { type = "exec", exec = { shell = "grim - | wl-copy", privileged = true } } + ``` + Values of this type should have one of the following forms: #### A string @@ -2513,24 +2520,42 @@ Each element of this array should be a string. The name, arguments, and environment variables of the executable to execute. -- Example: +Exactly one of the `prog` or `shell` fields must be specified. + +- Example 1: ```toml [shortcuts] ctrl-a = { type = "exec", exec = { prog = "notify-send", args = ["hello world"], env.WAYLAND_DISPLAY = "2" } } ``` +- Example 2: + + ```toml + [shortcuts] + ctrl-a = { type = "exec", exec = { shell = "grim - | wl-copy", privileged = true } } + ``` + The table has the following fields: -- `prog` (required): +- `prog` (optional): The name of the executable. The value of this field should be a string. +- `shell` (optional): + + The name of a shell command to execute. The command will be executed as + `$SHELL -c "command"`. + + The value of this field should be a string. + - `args` (optional): The arguments to pass to the executable. + + This field must not be specified if a shell command is used. The value of this field should be an array of strings. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index cbba0148..28608a00 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -733,6 +733,13 @@ Exec: [shortcuts] ctrl-a = { type = "exec", exec = { prog = "notify-send", args = ["hello world"], env.WAYLAND_DISPLAY = "2" } } ``` + + - Example 4: + + ```toml + [shortcuts] + ctrl-a = { type = "exec", exec = { shell = "grim - | wl-copy", privileged = true } } + ``` kind: variable variants: - kind: string @@ -760,24 +767,42 @@ Exec: - kind: table description: | The name, arguments, and environment variables of the executable to execute. + + Exactly one of the `prog` or `shell` fields must be specified. - - Example: + - Example 1: ```toml [shortcuts] ctrl-a = { type = "exec", exec = { prog = "notify-send", args = ["hello world"], env.WAYLAND_DISPLAY = "2" } } ``` + + - Example 2: + + ```toml + [shortcuts] + ctrl-a = { type = "exec", exec = { shell = "grim - | wl-copy", privileged = true } } + ``` fields: prog: kind: string - required: true + required: false description: The name of the executable. + shell: + kind: string + required: false + description: | + The name of a shell command to execute. The command will be executed as + `$SHELL -c "command"`. args: kind: array required: false items: kind: string - description: The arguments to pass to the executable. + description: | + The arguments to pass to the executable. + + This field must not be specified if a shell command is used. env: kind: map required: false