diff --git a/src/actions/shell.rs b/src/actions/shell.rs index 12df479b..9fbe156a 100644 --- a/src/actions/shell.rs +++ b/src/actions/shell.rs @@ -1,25 +1,18 @@ -pub fn run(cmd: &str) -> anyhow::Result<()> { +pub fn run(cmd: &str, keep_open: bool) -> anyhow::Result<()> { let mut command = { let mut c = std::process::Command::new("cmd"); - c.arg("/C").arg(cmd); + c.arg(if keep_open { "/K" } else { "/C" }).arg(cmd); c }; command.spawn().map(|_| ()).map_err(|e| e.into()) } pub fn add(name: &str, args: &str) -> anyhow::Result<()> { - crate::plugins::shell::append_shell_cmd( - crate::plugins::shell::SHELL_CMDS_FILE, - name, - args, - )?; + crate::plugins::shell::append_shell_cmd(crate::plugins::shell::SHELL_CMDS_FILE, name, args)?; Ok(()) } pub fn remove(name: &str) -> anyhow::Result<()> { - crate::plugins::shell::remove_shell_cmd( - crate::plugins::shell::SHELL_CMDS_FILE, - name, - )?; + crate::plugins::shell::remove_shell_cmd(crate::plugins::shell::SHELL_CMDS_FILE, name)?; Ok(()) } diff --git a/src/gui/shell_cmd_dialog.rs b/src/gui/shell_cmd_dialog.rs index 72eff79f..b8fd3819 100644 --- a/src/gui/shell_cmd_dialog.rs +++ b/src/gui/shell_cmd_dialog.rs @@ -64,6 +64,7 @@ impl ShellCmdDialog { name: self.name.clone(), args: self.args.clone(), autocomplete: true, + keep_open: false, }); } else if let Some(e) = self.entries.get_mut(idx) { e.name = self.name.clone(); diff --git a/src/launcher.rs b/src/launcher.rs index a82af76e..0895b40b 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -258,7 +258,10 @@ pub(crate) fn system_command(action: &str) -> Option { #[derive(Debug, Clone, PartialEq)] enum ActionKind<'a> { - Shell(&'a str), + Shell { + cmd: &'a str, + keep_open: bool, + }, ShellAdd { name: &'a str, args: &'a str, @@ -267,7 +270,10 @@ enum ActionKind<'a> { ClipboardClear, ClipboardCopy(usize), ClipboardText(&'a str), - Calc { result: &'a str, expr: Option<&'a str> }, + Calc { + result: &'a str, + expr: Option<&'a str>, + }, CalcHistory(usize), BookmarkAdd(&'a str), BookmarkRemove(&'a str), @@ -375,8 +381,17 @@ fn parse_action_kind(action: &Action) -> ActionKind<'_> { if let Some(name) = s.strip_prefix("shell:remove:") { return ActionKind::ShellRemove(name); } + if let Some(cmd) = s.strip_prefix("shell_keep:") { + return ActionKind::Shell { + cmd, + keep_open: true, + }; + } if let Some(cmd) = s.strip_prefix("shell:") { - return ActionKind::Shell(cmd); + return ActionKind::Shell { + cmd, + keep_open: false, + }; } if let Some(rest) = s.strip_prefix("clipboard:") { if rest == "clear" { @@ -703,7 +718,7 @@ fn parse_action_kind(action: &Action) -> ActionKind<'_> { pub fn launch_action(action: &Action) -> anyhow::Result<()> { use crate::actions::*; match parse_action_kind(action) { - ActionKind::Shell(cmd) => shell::run(cmd), + ActionKind::Shell { cmd, keep_open } => shell::run(cmd, keep_open), ActionKind::ShellAdd { name, args } => shell::add(name, args), ActionKind::ShellRemove(name) => shell::remove(name), ActionKind::ClipboardClear => clipboard::clear_history(), diff --git a/src/plugins/shell.rs b/src/plugins/shell.rs index e52a8178..83b4bdc4 100644 --- a/src/plugins/shell.rs +++ b/src/plugins/shell.rs @@ -13,6 +13,8 @@ pub struct ShellCmdEntry { /// When false this command will not be suggested when typing `sh `. #[serde(default = "default_autocomplete")] pub autocomplete: bool, + #[serde(default)] + pub keep_open: bool, } fn default_autocomplete() -> bool { @@ -44,6 +46,7 @@ pub fn append_shell_cmd(path: &str, name: &str, args: &str) -> anyhow::Result<() name: name.to_string(), args: args.to_string(), autocomplete: true, + keep_open: false, }); save_shell_cmds(path, &list)?; } @@ -67,12 +70,12 @@ impl Plugin for ShellPlugin { let trimmed = query.trim(); if let Some(rest) = crate::common::strip_prefix_ci(trimmed, "sh") { if rest.is_empty() { - return vec![Action { - label: "sh: edit saved commands".into(), - desc: "Shell".into(), - action: "shell:dialog".into(), - args: None, - }]; + return vec![Action { + label: "sh: edit saved commands".into(), + desc: "Shell".into(), + action: "shell:dialog".into(), + args: None, + }]; } } @@ -124,11 +127,14 @@ impl Plugin for ShellPlugin { matcher.fuzzy_match(&c.name, filter).is_some() || matcher.fuzzy_match(&c.args, filter).is_some() }) - .map(|c| Action { - label: c.name, - desc: "Shell".into(), - action: format!("shell:{}", c.args), - args: None, + .map(|c| { + let prefix = if c.keep_open { "shell_keep:" } else { "shell:" }; + Action { + label: c.name, + desc: "Shell".into(), + action: format!("{}{}", prefix, c.args), + args: None, + } }) .collect(); } @@ -151,10 +157,15 @@ impl Plugin for ShellPlugin { } } if let Some((entry, _)) = best { + let prefix = if entry.keep_open { + "shell_keep:" + } else { + "shell:" + }; return vec![Action { label: format!("Run {}", entry.name), desc: "Shell".into(), - action: format!("shell:{}", entry.args), + action: format!("{}{}", prefix, entry.args), args: None, }]; } @@ -183,10 +194,30 @@ impl Plugin for ShellPlugin { fn commands(&self) -> Vec { vec![ - Action { label: "sh".into(), desc: "Shell".into(), action: "query:sh".into(), args: None }, - Action { label: "sh add".into(), desc: "Shell".into(), action: "query:sh add ".into(), args: None }, - Action { label: "sh rm".into(), desc: "Shell".into(), action: "query:sh rm ".into(), args: None }, - Action { label: "sh list".into(), desc: "Shell".into(), action: "query:sh list".into(), args: None }, + Action { + label: "sh".into(), + desc: "Shell".into(), + action: "query:sh".into(), + args: None, + }, + Action { + label: "sh add".into(), + desc: "Shell".into(), + action: "query:sh add ".into(), + args: None, + }, + Action { + label: "sh rm".into(), + desc: "Shell".into(), + action: "query:sh rm ".into(), + args: None, + }, + Action { + label: "sh list".into(), + desc: "Shell".into(), + action: "query:sh list".into(), + args: None, + }, ] } } diff --git a/tests/shell_plugin.rs b/tests/shell_plugin.rs index 49eb4db6..35866bb7 100644 --- a/tests/shell_plugin.rs +++ b/tests/shell_plugin.rs @@ -19,6 +19,7 @@ fn load_shell_cmds_roundtrip() { name: "test".into(), args: "echo hi".into(), autocomplete: true, + keep_open: false, }]; save_shell_cmds(SHELL_CMDS_FILE, &entries).unwrap(); let loaded = load_shell_cmds(SHELL_CMDS_FILE).unwrap(); @@ -26,6 +27,7 @@ fn load_shell_cmds_roundtrip() { assert_eq!(loaded[0].name, "test"); assert_eq!(loaded[0].args, "echo hi"); assert!(loaded[0].autocomplete); + assert!(!loaded[0].keep_open); } #[test] @@ -38,6 +40,7 @@ fn search_named_command_returns_action() { name: "demo".into(), args: "dir".into(), autocomplete: true, + keep_open: false, }]; save_shell_cmds(SHELL_CMDS_FILE, &entries).unwrap(); @@ -57,6 +60,7 @@ fn search_respects_autocomplete_flag() { name: "demo".into(), args: "dir".into(), autocomplete: false, + keep_open: false, }]; save_shell_cmds(SHELL_CMDS_FILE, &entries).unwrap(); @@ -66,6 +70,26 @@ fn search_respects_autocomplete_flag() { assert_eq!(results[0].action, "shell:demo"); } +#[test] +fn search_keep_open_uses_shell_keep_prefix() { + let _lock = TEST_MUTEX.lock().unwrap(); + let dir = tempdir().unwrap(); + std::env::set_current_dir(dir.path()).unwrap(); + + let entries = vec![ShellCmdEntry { + name: "demo".into(), + args: "dir".into(), + autocomplete: true, + keep_open: true, + }]; + save_shell_cmds(SHELL_CMDS_FILE, &entries).unwrap(); + + let plugin = ShellPlugin; + let results = plugin.search("sh demo"); + assert_eq!(results.len(), 1); + assert_eq!(results[0].action, "shell_keep:dir"); +} + #[test] fn search_plain_sh_opens_dialog() { let _lock = TEST_MUTEX.lock().unwrap(); @@ -98,11 +122,13 @@ fn rm_lists_matching_commands() { name: "a".into(), args: "cmd_a".into(), autocomplete: true, + keep_open: false, }, ShellCmdEntry { name: "b".into(), args: "cmd_b".into(), autocomplete: true, + keep_open: false, }, ]; save_shell_cmds(SHELL_CMDS_FILE, &entries).unwrap(); @@ -123,6 +149,7 @@ fn list_returns_saved_commands() { name: "x".into(), args: "dir".into(), autocomplete: true, + keep_open: false, }]; save_shell_cmds(SHELL_CMDS_FILE, &entries).unwrap();