From 336a8f7eb20882b8328d8bcf28a40595364dc01d Mon Sep 17 00:00:00 2001 From: Albert Armea Date: Sun, 28 Dec 2025 00:35:33 -0500 Subject: [PATCH] Use common command/args syntax --- config.example.toml | 26 ++++++++++++++++++----- crates/shepherd-api/src/types.rs | 9 ++++++-- crates/shepherd-config/src/lib.rs | 4 ++-- crates/shepherd-config/src/policy.rs | 2 +- crates/shepherd-config/src/schema.rs | 10 ++++++--- crates/shepherd-config/src/validation.rs | 12 ++++++----- crates/shepherd-core/src/engine.rs | 9 +++++--- crates/shepherd-host-api/src/mock.rs | 6 ++++-- crates/shepherd-host-linux/src/adapter.rs | 12 +++++++---- crates/shepherdd/tests/integration.rs | 8 ++++--- 10 files changed, 68 insertions(+), 30 deletions(-) diff --git a/config.example.toml b/config.example.toml index 8d0cac6..32c53a8 100644 --- a/config.example.toml +++ b/config.example.toml @@ -41,7 +41,8 @@ icon = "scummvm" [entries.kind] type = "process" -argv = ["scummvm", "-f"] +command = "scummvm" +args = ["-f"] [entries.availability] [[entries.availability.windows]] @@ -95,7 +96,7 @@ seconds_before = 30 severity = "critical" message = "30 seconds! Save NOW!" -# Example: Educational game - unrestricted +# Example: Educational game [[entries]] id = "tuxmath" label = "Tux Math" @@ -103,7 +104,21 @@ icon = "tuxmath" [entries.kind] type = "process" -argv = ["tuxmath"] +command = "tuxmath" + +[entries.availability] +always = true + +# Example: Steam game +[[entries]] +id = "steam-human-resource-machine" +label = "Human Resource Machine" +icon = "steam" + +[entries.kind] +type = "snap" +snap_name = "steam" +args = ["steam://rungameid/375820"] # Steam App ID (passed to 'snap run steam') [entries.availability] always = true @@ -119,7 +134,8 @@ icon = "mpv" [entries.kind] type = "process" -argv = ["mpv", "https://www.youtube.com/watch?v=jfKfPfyJRdk"] +command = "mpv" +args = ["https://www.youtube.com/watch?v=jfKfPfyJRdk"] [entries.availability] always = true @@ -138,4 +154,4 @@ disabled_reason = "This game is being updated" [entries.kind] type = "process" -argv = ["/bin/false"] +command = "/bin/false" diff --git a/crates/shepherd-api/src/types.rs b/crates/shepherd-api/src/types.rs index 838bd0e..177ccdc 100644 --- a/crates/shepherd-api/src/types.rs +++ b/crates/shepherd-api/src/types.rs @@ -23,7 +23,11 @@ pub enum EntryKindTag { #[serde(tag = "type", rename_all = "snake_case")] pub enum EntryKind { Process { - argv: Vec, + /// Command to run (required) + command: String, + /// Additional command-line arguments + #[serde(default)] + args: Vec, #[serde(default)] env: HashMap, cwd: Option, @@ -252,7 +256,8 @@ mod tests { #[test] fn entry_kind_serialization() { let kind = EntryKind::Process { - argv: vec!["scummvm".into(), "-f".into()], + command: "scummvm".into(), + args: vec!["-f".into()], env: HashMap::new(), cwd: None, }; diff --git a/crates/shepherd-config/src/lib.rs b/crates/shepherd-config/src/lib.rs index 399afa2..77f8dc8 100644 --- a/crates/shepherd-config/src/lib.rs +++ b/crates/shepherd-config/src/lib.rs @@ -75,7 +75,7 @@ mod tests { [[entries]] id = "test-game" label = "Test Game" - kind = { type = "process", argv = ["/usr/bin/game"] } + kind = { type = "process", command = "/usr/bin/game" } "#; let policy = parse_config(config).unwrap(); @@ -91,7 +91,7 @@ mod tests { [[entries]] id = "test" label = "Test" - kind = { type = "process", argv = ["/bin/test"] } + kind = { type = "process", command = "/bin/test" } "#; let result = parse_config(config); diff --git a/crates/shepherd-config/src/policy.rs b/crates/shepherd-config/src/policy.rs index c6f8f41..993969b 100644 --- a/crates/shepherd-config/src/policy.rs +++ b/crates/shepherd-config/src/policy.rs @@ -194,7 +194,7 @@ pub struct LimitsPolicy { fn convert_entry_kind(raw: RawEntryKind) -> EntryKind { match raw { - RawEntryKind::Process { argv, env, cwd } => EntryKind::Process { argv, env, cwd }, + RawEntryKind::Process { command, args, env, cwd } => EntryKind::Process { command, args, env, cwd }, RawEntryKind::Snap { snap_name, command, args, env } => EntryKind::Snap { snap_name, command, args, env }, RawEntryKind::Vm { driver, args } => EntryKind::Vm { driver, args }, RawEntryKind::Media { library_id, args } => EntryKind::Media { library_id, args }, diff --git a/crates/shepherd-config/src/schema.rs b/crates/shepherd-config/src/schema.rs index e1f3a9d..468afca 100644 --- a/crates/shepherd-config/src/schema.rs +++ b/crates/shepherd-config/src/schema.rs @@ -78,7 +78,11 @@ pub struct RawEntry { #[serde(tag = "type", rename_all = "snake_case")] pub enum RawEntryKind { Process { - argv: Vec, + /// Command to run (required) + command: String, + /// Additional command-line arguments + #[serde(default)] + args: Vec, #[serde(default)] env: HashMap, cwd: Option, @@ -189,7 +193,7 @@ mod tests { [[entries]] id = "scummvm" label = "ScummVM" - kind = { type = "process", argv = ["scummvm", "-f"] } + kind = { type = "process", command = "scummvm", args = ["-f"] } [entries.limits] max_run_seconds = 3600 @@ -208,7 +212,7 @@ mod tests { [[entries]] id = "game" label = "Game" - kind = { type = "process", argv = ["/bin/game"] } + kind = { type = "process", command = "/bin/game" } [entries.availability] [[entries.availability.windows]] diff --git a/crates/shepherd-config/src/validation.rs b/crates/shepherd-config/src/validation.rs index fd347d4..9abbb32 100644 --- a/crates/shepherd-config/src/validation.rs +++ b/crates/shepherd-config/src/validation.rs @@ -55,11 +55,11 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec // Validate kind match &entry.kind { - RawEntryKind::Process { argv, .. } => { - if argv.is_empty() { + RawEntryKind::Process { command, .. } => { + if command.is_empty() { errors.push(ValidationError::EntryError { entry_id: entry.id.clone(), - message: "argv cannot be empty".into(), + message: "command cannot be empty".into(), }); } } @@ -252,7 +252,8 @@ mod tests { label: "Game 1".into(), icon: None, kind: RawEntryKind::Process { - argv: vec!["game1".into()], + command: "game1".into(), + args: vec![], env: Default::default(), cwd: None, }, @@ -267,7 +268,8 @@ mod tests { label: "Game 2".into(), icon: None, kind: RawEntryKind::Process { - argv: vec!["game2".into()], + command: "game2".into(), + args: vec![], env: Default::default(), cwd: None, }, diff --git a/crates/shepherd-core/src/engine.rs b/crates/shepherd-core/src/engine.rs index b99e2a2..fa42707 100644 --- a/crates/shepherd-core/src/engine.rs +++ b/crates/shepherd-core/src/engine.rs @@ -559,7 +559,8 @@ mod tests { label: "Test Game".into(), icon_ref: None, kind: EntryKind::Process { - argv: vec!["game".into()], + command: "game".into(), + args: vec![], env: HashMap::new(), cwd: None, }, @@ -635,7 +636,8 @@ mod tests { label: "Test".into(), icon_ref: None, kind: EntryKind::Process { - argv: vec!["test".into()], + command: "test".into(), + args: vec![], env: HashMap::new(), cwd: None, }, @@ -697,7 +699,8 @@ mod tests { label: "Test".into(), icon_ref: None, kind: EntryKind::Process { - argv: vec!["test".into()], + command: "test".into(), + args: vec![], env: HashMap::new(), cwd: None, }, diff --git a/crates/shepherd-host-api/src/mock.rs b/crates/shepherd-host-api/src/mock.rs index e83f968..8f92072 100644 --- a/crates/shepherd-host-api/src/mock.rs +++ b/crates/shepherd-host-api/src/mock.rs @@ -189,7 +189,8 @@ mod tests { let session_id = SessionId::new(); let entry = EntryKind::Process { - argv: vec!["test".into()], + command: "test".into(), + args: vec![], env: HashMap::new(), cwd: None, }; @@ -217,7 +218,8 @@ mod tests { let session_id = SessionId::new(); let entry = EntryKind::Process { - argv: vec!["test".into()], + command: "test".into(), + args: vec![], env: HashMap::new(), cwd: None, }; diff --git a/crates/shepherd-host-linux/src/adapter.rs b/crates/shepherd-host-linux/src/adapter.rs index 0b4eed5..412d97c 100644 --- a/crates/shepherd-host-linux/src/adapter.rs +++ b/crates/shepherd-host-linux/src/adapter.rs @@ -115,8 +115,10 @@ impl HostAdapter for LinuxHost { ) -> HostResult { // Extract argv, env, cwd, and snap_name based on entry kind let (argv, env, cwd, snap_name) = match entry_kind { - EntryKind::Process { argv, env, cwd } => { - (argv.clone(), env.clone(), cwd.clone(), None) + EntryKind::Process { command, args, env, cwd } => { + let mut argv = vec![command.clone()]; + argv.extend(args.clone()); + (argv, env.clone(), cwd.clone(), None) } EntryKind::Snap { snap_name, command, args, env } => { // For snap apps, we need to use 'snap run ' to launch them. @@ -318,7 +320,8 @@ mod tests { let session_id = SessionId::new(); let entry = EntryKind::Process { - argv: vec!["true".into()], + command: "true".into(), + args: vec![], env: HashMap::new(), cwd: None, }; @@ -348,7 +351,8 @@ mod tests { let session_id = SessionId::new(); let entry = EntryKind::Process { - argv: vec!["sleep".into(), "60".into()], + command: "sleep".into(), + args: vec!["60".into()], env: HashMap::new(), cwd: None, }; diff --git a/crates/shepherdd/tests/integration.rs b/crates/shepherdd/tests/integration.rs index 6b97584..e3b9076 100644 --- a/crates/shepherdd/tests/integration.rs +++ b/crates/shepherdd/tests/integration.rs @@ -22,7 +22,8 @@ fn make_test_policy() -> Policy { label: "Test Game".into(), icon_ref: None, kind: EntryKind::Process { - argv: vec!["sleep".into(), "999".into()], + command: "sleep".into(), + args: vec!["999".into()], env: HashMap::new(), cwd: None, }, @@ -227,7 +228,8 @@ async fn test_mock_host_integration() { let session_id = SessionId::new(); let entry = EntryKind::Process { - argv: vec!["test".into()], + command: "test".into(), + args: vec![], env: HashMap::new(), cwd: None, }; @@ -262,7 +264,7 @@ fn test_config_parsing() { [[entries]] id = "scummvm" label = "ScummVM" - kind = { type = "process", argv = ["scummvm", "-f"] } + kind = { type = "process", command = "scummvm", args = ["-f"] } [entries.availability] [[entries.availability.windows]]