diff --git a/docs/config.adoc b/docs/config.adoc index 681f366fb..a490530bb 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -1163,6 +1163,21 @@ Example: ) ---- +There is a variant of `cmd`: `cmd-output-keys`. This variant reads the output +of the executed program and reads it as an S-expression, similarly to the +<>. However — unlike macro — only keys, chords, and +chorded lists are supported. Delays and other actions are not supported. + +---- +(defalias + ;; bash: type date-time as YYYY-MM-DD HH:MM + pdb (cmd-output-keys bash -c "date +'%F %R' | sed 's/./& /g' | sed 's/:/S-;/g' | sed 's/\(.\{20\}\)\(.*\)/\(\1 spc \2\)/'") + + ;; powershell: type date-time as YYYY-MM-DD HH:MM + pdp (cmd-output-keys powershell.exe "echo '(' (((Get-Date -Format 'yyyy-MM-dd HH:mm').toCharArray() -join ' ').insert(20, ' spc ') -replace ':','S-;') ')'") +) +---- + === arbitrary-code <> diff --git a/src/cfg/mod.rs b/src/cfg/mod.rs index 906fd1b1c..629411a4f 100644 --- a/src/cfg/mod.rs +++ b/src/cfg/mod.rs @@ -37,7 +37,7 @@ //! the behaviour in kmonad. //! //! The specific values in example above applies to Linux, but the same logic applies to Windows. -mod sexpr; +pub mod sexpr; mod alloc; use alloc::*; @@ -936,7 +936,8 @@ fn parse_action_list(ac: &[SExpr], s: &ParsedState) -> Result<&'static KanataAct "dynamic-macro-record" => parse_dynamic_macro_record(&ac[1..], s), "dynamic-macro-play" => parse_dynamic_macro_play(&ac[1..], s), "arbitrary-code" => parse_arbitrary_code(&ac[1..], s), - "cmd" => parse_cmd(&ac[1..], s), + "cmd" => parse_cmd(&ac[1..], s, CmdType::Standard), + "cmd-output-keys" => parse_cmd(&ac[1..], s, CmdType::OutputKeys), _ => bail!( "Unknown action type: {}. Valid types:\n\tlayer-switch\n\tlayer-toggle | layer-while-held\n\ttap-hold | tap-hold-press | tap-hold-release\n\tmulti\n\tmacro\n\tunicode\n\tone-shot\n\ttap-dance\n\trelease-key | release-layer\n\tmwheel-up | mwheel-down | mwheel-left | mwheel-right\n\ton-press-fakekey | on-release-fakekey\n\ton-press-fakekey-delay | on-release-fakekey-delay\n\tcmd", ac_type @@ -1121,6 +1122,7 @@ fn parse_macro(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAc (events, params_remainder) = parse_macro_item(params_remainder, s)?; all_events.append(&mut events); } + all_events.shrink_to_fit(); Ok(s.a.sref(Action::Sequence { events: s.a.sref_vec(all_events), })) @@ -1219,9 +1221,9 @@ fn parse_mods_held_for_submacro(held_mods: &SExpr) -> Result> { Ok(mod_keys) } -/// Parses mod keys like `C-S-`. There must be no remaining text after the prefixes. Returns the -/// `KeyCode`s for the modifiers parsed and the unparsed text after any parsed modifier prefixes. -fn parse_mod_prefix(mods: &str) -> Result<(Vec, &str)> { +/// Parses mod keys like `C-S-`. Returns the `KeyCode`s for the modifiers parsed and the unparsed +/// text after any parsed modifier prefixes. +pub fn parse_mod_prefix(mods: &str) -> Result<(Vec, &str)> { let mut key_stack = Vec::new(); let mut rem = mods; loop { @@ -1286,7 +1288,16 @@ fn parse_unicode(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static Kanata } } -fn parse_cmd(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { +enum CmdType { + Standard, + OutputKeys, +} + +fn parse_cmd( + ac_params: &[SExpr], + s: &ParsedState, + cmd_type: CmdType, +) -> Result<&'static KanataAction> { const ERR_STR: &str = "cmd expects one or more strings"; if !s.is_cmd_enabled { bail!("cmd is not enabled but cmd action is specified somewhere"); @@ -1294,16 +1305,18 @@ fn parse_cmd(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataActi if ac_params.is_empty() { bail!(ERR_STR); } - Ok(s.a.sref(Action::Custom(s.a.sref_slice(CustomAction::Cmd( - ac_params.iter().try_fold(vec![], |mut v, p| { - if let SExpr::Atom(a) = p { - v.push(a.t.trim_matches('"').to_owned()); - Ok(v) - } else { - bail!("{}, found a list", ERR_STR); - } - })?, - ))))) + let cmd = ac_params.iter().try_fold(vec![], |mut v, p| { + if let SExpr::Atom(a) = p { + v.push(a.t.trim_matches('"').to_owned()); + Ok(v) + } else { + bail!("{}, found a list", ERR_STR); + } + })?; + Ok(s.a.sref(Action::Custom(s.a.sref_slice(match cmd_type { + CmdType::Standard => CustomAction::Cmd(cmd), + CmdType::OutputKeys => CustomAction::CmdOutputKeys(cmd), + })))) } fn parse_one_shot(ac_params: &[SExpr], s: &ParsedState) -> Result<&'static KanataAction> { diff --git a/src/custom_action.rs b/src/custom_action.rs index c0eeee19d..e2f24f58b 100644 --- a/src/custom_action.rs +++ b/src/custom_action.rs @@ -1,6 +1,7 @@ #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CustomAction { Cmd(Vec), + CmdOutputKeys(Vec), Unicode(char), Mouse(Btn), MouseTap(Btn), diff --git a/src/kanata/cmd.rs b/src/kanata/cmd.rs new file mode 100644 index 000000000..60d212ea5 --- /dev/null +++ b/src/kanata/cmd.rs @@ -0,0 +1,192 @@ +use crate::cfg::parse_mod_prefix; +use crate::cfg::sexpr::*; +use crate::keys::*; + +// local log prefix +const LP: &str = "cmd-out:"; + +pub(super) fn run_cmd_in_thread(cmd_and_args: Vec) -> std::thread::JoinHandle<()> { + std::thread::spawn(move || { + let mut args = cmd_and_args.iter(); + let mut cmd = std::process::Command::new( + args.next() + .expect("parsing should have forbidden empty cmd"), + ); + for arg in args { + cmd.arg(arg); + } + match cmd.output() { + Ok(output) => { + log::info!( + "Successfully ran cmd {}\nstdout:\n{}\nstderr:\n{}", + { + let mut printable_cmd = Vec::new(); + printable_cmd.push(format!("{:?}", cmd.get_program())); + + let printable_cmd = cmd.get_args().fold(printable_cmd, |mut cmd, arg| { + cmd.push(format!("{arg:?}")); + cmd + }); + printable_cmd.join(" ") + }, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + Err(e) => log::error!("Failed to execute cmd: {}", e), + }; + }) +} + +pub(super) type Item = (KeyAction, OsCode); + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(super) enum KeyAction { + Press, + Release, +} +use kanata_keyberon::key_code::KeyCode; +use KeyAction::*; + +fn empty() -> std::vec::IntoIter { + vec![].into_iter() +} + +fn from_sexpr(sexpr: Vec) -> std::vec::IntoIter { + let mut items = vec![]; + let mut remainder = sexpr.as_slice(); + while !remainder.is_empty() { + remainder = parse_items(remainder, &mut items); + } + items.into_iter() +} + +fn parse_items<'a>(exprs: &'a [SExpr], items: &mut Vec) -> &'a [SExpr] { + match &exprs[0] { + SExpr::Atom(osc) => match str_to_oscode(&osc.t) { + Some(osc) => { + items.push((Press, osc)); + items.push((Release, osc)); + &exprs[1..] + } + None => try_parse_chord(&osc.t, exprs, items), + }, + SExpr::List(sexprs) => { + let mut remainder = sexprs.t.as_slice(); + while !remainder.is_empty() { + remainder = parse_items(remainder, items); + } + &exprs[1..] + } + } +} + +fn try_parse_chord<'a>(chord: &str, exprs: &'a [SExpr], items: &mut Vec) -> &'a [SExpr] { + match parse_mod_prefix(chord) { + Ok((mods, osc)) => match osc.is_empty() { + true => try_parse_chorded_list(&mods, chord, &exprs[1..], items), + false => { + try_parse_chorded_key(&mods, osc, chord, items); + &exprs[1..] + } + }, + Err(e) => { + log::warn!("{LP} found invalid chord {chord}: {e}"); + &exprs[1..] + } + } +} + +fn try_parse_chorded_key(mods: &[KeyCode], osc: &str, chord: &str, items: &mut Vec) { + if mods.is_empty() { + log::warn!("{LP} found invalid key: {osc}"); + return; + } + match str_to_oscode(osc) { + Some(osc) => { + for mod_kc in mods.iter().copied() { + items.push((Press, mod_kc.into())); + } + items.push((Press, osc)); + for mod_kc in mods.iter().copied() { + items.push((Release, mod_kc.into())); + } + items.push((Release, osc)); + } + None => { + log::warn!("{LP} found chord {chord} with invalid key: {osc}"); + } + }; +} + +fn try_parse_chorded_list<'a>( + mods: &[KeyCode], + chord: &str, + exprs: &'a [SExpr], + items: &mut Vec, +) -> &'a [SExpr] { + if exprs.is_empty() { + log::warn!( + "{LP} found chord modifiers with no attached key or list - ignoring it: {chord}" + ); + return exprs; + } + match &exprs[0] { + SExpr::Atom(osc) => { + log::warn!("{LP} expected list after {chord}, got string {}", &osc.t); + exprs + } + SExpr::List(subexprs) => { + for mod_kc in mods.iter().copied() { + items.push((Press, mod_kc.into())); + } + let mut remainder = subexprs.t.as_slice(); + while !remainder.is_empty() { + remainder = parse_items(remainder, items); + } + for mod_kc in mods.iter().copied() { + items.push((Release, mod_kc.into())); + } + &exprs[1..] + } + } +} + +pub(super) fn keys_for_cmd_output(cmd_and_args: &[String]) -> impl Iterator { + let mut args = cmd_and_args.iter(); + let mut cmd = std::process::Command::new( + args.next() + .expect("parsing should have forbidden empty cmd"), + ); + for arg in args { + cmd.arg(arg); + } + let output = match cmd.output() { + Ok(o) => o, + Err(e) => { + log::error!("Failed to execute cmd: {e}"); + return empty(); + } + }; + log::debug!("{LP} stderr: {}", String::from_utf8_lossy(&output.stderr)); + let stdout = String::from_utf8_lossy(&output.stdout); + match parse(&stdout) { + Ok(lists) => match lists.len() { + 0 => { + log::warn!("{LP} got zero top-level S-expression from cmd, expected 1:\n{stdout}"); + empty() + } + 1 => from_sexpr(lists.into_iter().next().unwrap().t), + _ => { + log::warn!( + "{LP} got multiple top-level S-expression from cmd, expected 1:\n{stdout}" + ); + empty() + } + }, + Err(e) => { + log::warn!("{LP} could not parse an S-expression from cmd:\n{stdout}\n{e}"); + empty() + } + } +} diff --git a/src/kanata/mod.rs b/src/kanata/mod.rs index 15a54a3cd..183e139f0 100644 --- a/src/kanata/mod.rs +++ b/src/kanata/mod.rs @@ -23,6 +23,11 @@ use crate::oskbd::*; use crate::tcp_server::ServerMessage; use crate::{cfg, ValidatedArgs}; +#[cfg(feature = "cmd")] +mod cmd; +#[cfg(feature = "cmd")] +use cmd::*; + #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] @@ -748,10 +753,20 @@ impl Kanata { } } } - CustomAction::Cmd(cmd) => { cmds.push(cmd.clone()); } + CustomAction::CmdOutputKeys(_cmd) => { + #[cfg(feature = "cmd")] + { + for (key_action, osc) in keys_for_cmd_output(_cmd) { + match key_action { + KeyAction::Press => self.kbd_out.press_key(osc)?, + KeyAction::Release => self.kbd_out.release_key(osc)?, + } + } + } + } CustomAction::FakeKey { coord, action } => { let (x, y) = (coord.x, coord.y); log::debug!( @@ -891,6 +906,7 @@ impl Kanata { } run_multi_cmd(cmds); } + CustomEvent::Release(custacts) => { // Unclick only the last mouse button if let Some(Err(e)) = custacts @@ -962,7 +978,6 @@ impl Kanata { } pbtn } - CustomAction::Delay(delay) => { log::debug!("on-press: sleeping for {delay} ms"); std::thread::sleep(std::time::Duration::from_millis((*delay).into())); @@ -1277,44 +1292,11 @@ fn set_altgr_behaviour(_cfg: &cfg::Cfg) -> Result<()> { Ok(()) } -#[cfg(feature = "cmd")] -fn run_cmd(cmd_and_args: Vec) -> std::thread::JoinHandle<()> { - std::thread::spawn(move || { - let mut args = cmd_and_args.iter().cloned(); - let mut cmd = std::process::Command::new( - args.next() - .expect("parsing should have forbidden empty cmd"), - ); - for arg in args { - cmd.arg(arg); - } - match cmd.output() { - Ok(output) => { - log::info!( - "Successfully ran cmd {}\nstdout:\n{}\nstderr:\n{}", - { - let mut printable_cmd = Vec::new(); - printable_cmd.push(format!("{:?}", cmd.get_program())); - let printable_cmd = cmd.get_args().fold(printable_cmd, |mut cmd, arg| { - cmd.push(format!("{arg:?}")); - cmd - }); - printable_cmd.join(" ") - }, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - Err(e) => log::error!("Failed to execute cmd: {}", e), - }; - }) -} - #[cfg(feature = "cmd")] fn run_multi_cmd(cmds: Vec>) { std::thread::spawn(move || { for cmd in cmds { - if let Err(e) = run_cmd(cmd).join() { + if let Err(e) = run_cmd_in_thread(cmd).join() { log::error!("problem joining thread {:?}", e); } }