skim/
input.rs

1///! Input will listens to user input, modify the query string, send special
2///! keystrokes(such as Enter, Ctrl-p, Ctrl-n, etc) to the controller.
3use crate::event::{Event, parse_event};
4use regex::Regex;
5use std::collections::HashMap;
6use std::sync::LazyLock;
7use tuikit::event::Event as TermEvent;
8use tuikit::key::{Key, from_keyname};
9
10pub type ActionChain = Vec<Event>;
11
12pub struct Input {
13    keymap: HashMap<Key, ActionChain>,
14}
15
16impl Input {
17    pub fn new() -> Self {
18        Input {
19            keymap: get_default_key_map(),
20        }
21    }
22
23    pub fn translate_event(&self, event: TermEvent) -> (Key, ActionChain) {
24        match event {
25            // search event from keymap
26            TermEvent::Key(key) => (
27                key,
28                self.keymap.get(&key).cloned().unwrap_or_else(|| {
29                    if let Key::Char(ch) = key {
30                        vec![Event::EvActAddChar(ch)]
31                    } else {
32                        vec![Event::EvInputKey(key)]
33                    }
34                }),
35            ),
36            TermEvent::Resize { .. } => (Key::Null, vec![Event::EvActRedraw]),
37            _ => (Key::Null, vec![Event::EvInputInvalid]),
38        }
39    }
40
41    pub fn bind(&mut self, key: &str, action_chain: ActionChain) {
42        let key = from_keyname(key);
43        if key.is_none() || action_chain.is_empty() {
44            return;
45        }
46
47        let key = key.unwrap();
48
49        // remove the key for existing keymap;
50        let _ = self.keymap.remove(&key);
51        self.keymap.entry(key).or_insert(action_chain);
52    }
53
54    pub fn parse_keymaps(&mut self, maps: &[&str]) {
55        for &map in maps {
56            self.parse_keymap(map);
57        }
58    }
59
60    // key_action is comma separated: 'ctrl-j:accept,ctrl-k:kill-line'
61    pub fn parse_keymap(&mut self, key_action: &str) {
62        debug!("got key_action: {:?}", key_action);
63        for (key, action_chain) in parse_key_action(key_action).into_iter() {
64            debug!("parsed key_action: {:?}: {:?}", key, action_chain);
65            let action_chain = action_chain
66                .into_iter()
67                .filter_map(|(action, arg)| parse_event(action, arg))
68                .collect();
69            self.bind(key, action_chain);
70        }
71    }
72
73    pub fn parse_expect_keys(&mut self, keys: Option<&str>) {
74        if let Some(keys) = keys {
75            for key in keys.split(',') {
76                self.bind(key, vec![Event::EvActAccept(Some(key.to_string()))]);
77            }
78        }
79    }
80}
81
82type KeyActions<'a> = (&'a str, Vec<(&'a str, Option<String>)>);
83
84/// parse key action string to `(key, action, argument)` tuple
85/// key_action is comma separated: 'ctrl-j:accept,ctrl-k:kill-line'
86pub fn parse_key_action(key_action: &str) -> Vec<KeyActions<'_>> {
87    // match `key:action` or `key:action:arg` or `key:action(arg)` etc.
88    static RE: LazyLock<Regex> = LazyLock::new(|| {
89        Regex::new(
90            r#"(?si)([^:]+?):((?:\+?[a-z-]+?(?:"[^"]*?"|'[^']*?'|\([^\)]*?\)|\[[^\]]*?\]|:[^:]*?)?\s*)+)(?:,|$)"#,
91        )
92        .unwrap()
93    });
94    // grab key, action and arg out.
95    static RE_BIND: LazyLock<Regex> = LazyLock::new(|| {
96        Regex::new(r#"(?si)([a-z-]+)("[^"]+?"|'[^']+?'|\([^\)]+?\)|\[[^\]]+?\]|:[^:]+?)?(?:\+|$)"#).unwrap()
97    });
98
99    RE.captures_iter(key_action)
100        .map(|caps| {
101            debug!("RE: caps: {:?}", caps);
102            let key = caps.get(1).unwrap().as_str();
103            let actions = RE_BIND
104                .captures_iter(caps.get(2).unwrap().as_str())
105                .map(|caps| {
106                    debug!("RE_BIND: caps: {:?}", caps);
107                    (
108                        caps.get(1).unwrap().as_str(),
109                        caps.get(2).map(|s| {
110                            // (arg) => arg, :end_arg => arg
111                            let action = s.as_str();
112                            if let Some(stripped) = action.strip_prefix(':') {
113                                stripped.to_owned()
114                            } else {
115                                action[1..action.len() - 1].to_string()
116                            }
117                        }),
118                    )
119                })
120                .collect();
121            (key, actions)
122        })
123        .collect()
124}
125
126/// e.g. execute(...) => Some(Event::EvActExecute, Box::new(Option("...")))
127pub fn parse_action_arg(action_arg: &str) -> Option<Event> {
128    // construct a fake key_action: `fake_key:action(arg)`
129    let fake_key_action = format!("fake_key:{}", action_arg);
130    // get keys: [(key, [(action, arg), (action, arg)]), ...]
131    let keys = parse_key_action(&fake_key_action);
132    // only get the first key(since it is faked), and get the first action
133    if keys.is_empty() || keys[0].1.is_empty() {
134        None
135    } else {
136        // first action pair of key(keys[0].1) and first action (keys[0].1[0])
137        let (action, new_arg) = keys[0].1[0].clone();
138        parse_event(action, new_arg)
139    }
140}
141
142#[rustfmt::skip]
143fn get_default_key_map() -> HashMap<Key, ActionChain> {
144    let mut ret = HashMap::new();
145    ret.insert(Key::ESC,          vec![Event::EvActAbort]);
146    ret.insert(Key::Ctrl('c'),    vec![Event::EvActAbort]);
147    ret.insert(Key::Ctrl('g'),    vec![Event::EvActAbort]);
148    ret.insert(Key::Enter,        vec![Event::EvActAccept(None)]);
149    ret.insert(Key::Left,         vec![Event::EvActBackwardChar]);
150    ret.insert(Key::Ctrl('b'),    vec![Event::EvActBackwardChar]);
151    ret.insert(Key::Ctrl('h'),    vec![Event::EvActBackwardDeleteChar]);
152    ret.insert(Key::Backspace,    vec![Event::EvActBackwardDeleteChar]);
153    ret.insert(Key::AltBackspace, vec![Event::EvActBackwardKillWord]);
154    ret.insert(Key::Alt('b'),     vec![Event::EvActBackwardWord]);
155    ret.insert(Key::ShiftLeft,    vec![Event::EvActBackwardWord]);
156    ret.insert(Key::CtrlLeft,     vec![Event::EvActBackwardWord]);
157    ret.insert(Key::Ctrl('a'),    vec![Event::EvActBeginningOfLine]);
158    ret.insert(Key::Home,         vec![Event::EvActBeginningOfLine]);
159    ret.insert(Key::Ctrl('l'),    vec![Event::EvActClearScreen]);
160    ret.insert(Key::Delete,       vec![Event::EvActDeleteChar]);
161    ret.insert(Key::Ctrl('d'),    vec![Event::EvActDeleteCharEOF]);
162    ret.insert(Key::Ctrl('j'),    vec![Event::EvActDown(1)]);
163    ret.insert(Key::Ctrl('n'),    vec![Event::EvActDown(1)]);
164    ret.insert(Key::Down,         vec![Event::EvActDown(1)]);
165    ret.insert(Key::Ctrl('e'),    vec![Event::EvActEndOfLine]);
166    ret.insert(Key::End,          vec![Event::EvActEndOfLine]);
167    ret.insert(Key::Ctrl('f'),    vec![Event::EvActForwardChar]);
168    ret.insert(Key::Right,        vec![Event::EvActForwardChar]);
169    ret.insert(Key::Alt('f'),     vec![Event::EvActForwardWord]);
170    ret.insert(Key::CtrlRight,    vec![Event::EvActForwardWord]);
171    ret.insert(Key::ShiftRight,   vec![Event::EvActForwardWord]);
172    ret.insert(Key::Alt('d'),     vec![Event::EvActKillWord]);
173    ret.insert(Key::ShiftUp,      vec![Event::EvActPreviewPageUp(1)]);
174    ret.insert(Key::ShiftDown,    vec![Event::EvActPreviewPageDown(1)]);
175    ret.insert(Key::PageDown,     vec![Event::EvActPageDown(1)]);
176    ret.insert(Key::PageUp,       vec![Event::EvActPageUp(1)]);
177    ret.insert(Key::Ctrl('r'),    vec![Event::EvActRotateMode]);
178    ret.insert(Key::Alt('h'),     vec![Event::EvActScrollLeft(1)]);
179    ret.insert(Key::Alt('l'),     vec![Event::EvActScrollRight(1)]);
180    ret.insert(Key::Tab,          vec![Event::EvActToggle, Event::EvActDown(1)]);
181    ret.insert(Key::Ctrl('q'),    vec![Event::EvActToggleInteractive]);
182    ret.insert(Key::BackTab,      vec![Event::EvActToggle, Event::EvActUp(1)]);
183    ret.insert(Key::Ctrl('u'),    vec![Event::EvActUnixLineDiscard]);
184    ret.insert(Key::Ctrl('w'),    vec![Event::EvActUnixWordRubout]);
185    ret.insert(Key::Ctrl('p'),    vec![Event::EvActUp(1)]);
186    ret.insert(Key::Ctrl('k'),    vec![Event::EvActUp(1)]);
187    ret.insert(Key::Up,           vec![Event::EvActUp(1)]);
188    ret.insert(Key::Ctrl('y'),    vec![Event::EvActYank]);
189    ret.insert(Key::Null,         vec![Event::EvActAbort]);
190    ret
191}
192
193#[cfg(test)]
194mod test {
195    use super::*;
196
197    #[test]
198    fn execute_should_be_parsed_correctly() {
199        // example from https://github.com/lotabout/skim/issues/73
200        let cmd = "
201        (grep -o '[a-f0-9]\\{7\\}' | head -1 |
202        xargs -I % sh -c 'git show --color=always % | less -R') << 'FZF-EOF'
203        {}
204        FZF-EOF";
205
206        let key_action_str = format!("ctrl-s:toggle-sort,ctrl-m:execute:{},ctrl-t:toggle", cmd);
207
208        let key_action = parse_key_action(&key_action_str);
209        assert_eq!(("ctrl-s", vec![("toggle-sort", None)]), key_action[0]);
210        assert_eq!(("ctrl-m", vec![("execute", Some(cmd.to_string()))]), key_action[1]);
211        assert_eq!(("ctrl-t", vec![("toggle", None)]), key_action[2]);
212
213        let key_action_str = "f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)";
214        let key_action = parse_key_action(key_action_str);
215        assert_eq!(("f1", vec![("execute", Some("less -f {}".to_string()))]), key_action[0]);
216        assert_eq!(
217            ("ctrl-y", vec![("execute-silent", Some("echo {} | pbcopy".to_string()))]),
218            key_action[1]
219        );
220
221        // #196
222        let key_action_str = "enter:execute($EDITOR +{2} {1})";
223        let key_action = parse_key_action(key_action_str);
224        assert_eq!(
225            ("enter", vec![("execute", Some("$EDITOR +{2} {1}".to_string()))]),
226            key_action[0]
227        );
228    }
229
230    #[test]
231    fn action_chain_should_be_parsed() {
232        let key_action = parse_key_action("ctrl-t:toggle+up");
233        assert_eq!(("ctrl-t", vec![("toggle", None), ("up", None)]), key_action[0]);
234
235        let key_action_str = "f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort";
236        let key_action = parse_key_action(key_action_str);
237        assert_eq!(("f1", vec![("execute", Some("less -f {}".to_string()))]), key_action[0]);
238        assert_eq!(
239            (
240                "ctrl-y",
241                vec![
242                    ("execute-silent", Some("echo {} | pbcopy".to_string())),
243                    ("abort", None)
244                ]
245            ),
246            key_action[1]
247        );
248    }
249}