Skip to content

Commit

Permalink
feat: add cmd variant that outputs keys (#304)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtroo committed Feb 18, 2023
1 parent 0bd8183 commit bc05596
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 52 deletions.
15 changes: 15 additions & 0 deletions docs/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
<<macro, macro action>>. 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
<<table-of-contents,Back to ToC>>

Expand Down
45 changes: 29 additions & 16 deletions src/cfg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
}))
Expand Down Expand Up @@ -1219,9 +1221,9 @@ fn parse_mods_held_for_submacro(held_mods: &SExpr) -> Result<Vec<KeyCode>> {
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<KeyCode>, &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<KeyCode>, &str)> {
let mut key_stack = Vec::new();
let mut rem = mods;
loop {
Expand Down Expand Up @@ -1286,24 +1288,35 @@ 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");
}
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> {
Expand Down
1 change: 1 addition & 0 deletions src/custom_action.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CustomAction {
Cmd(Vec<String>),
CmdOutputKeys(Vec<String>),
Unicode(char),
Mouse(Btn),
MouseTap(Btn),
Expand Down
192 changes: 192 additions & 0 deletions src/kanata/cmd.rs
Original file line number Diff line number Diff line change
@@ -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<String>) -> 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<Item> {
vec![].into_iter()
}

fn from_sexpr(sexpr: Vec<SExpr>) -> std::vec::IntoIter<Item> {
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<Item>) -> &'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<Item>) -> &'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<Item>) {
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<Item>,
) -> &'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<Item = Item> {
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()
}
}
}
Loading

0 comments on commit bc05596

Please sign in to comment.