use crossterm::event::{
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
EnableMouseCapture, KeyCode, KeyEventKind, KeyModifiers, MouseEvent, MouseEventKind,
};
use crossterm::terminal;
use nu_engine::CallExt;
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{
record, Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Span,
SyntaxShape, Type, Value,
};
use num_traits::AsPrimitive;
use std::io::stdout;
#[derive(Clone)]
pub struct InputListen;
impl Command for InputListen {
fn name(&self) -> &str {
"input listen"
}
fn search_terms(&self) -> Vec<&str> {
vec!["prompt", "interactive", "keycode"]
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.category(Category::Platform)
.named(
"types",
SyntaxShape::List(Box::new(SyntaxShape::String)),
"Listen for event of specified types only (can be one of: focus, key, mouse, paste, resize)",
Some('t'),
)
.switch(
"raw",
"Add raw_code field with numeric value of keycode and raw_flags with bit mask flags",
Some('r'),
)
.input_output_types(vec![(
Type::Nothing,
Type::Record(vec![
("keycode".to_string(), Type::String),
("modifiers".to_string(), Type::List(Box::new(Type::String))),
]),
)])
}
fn usage(&self) -> &str {
"Listen for user interface event."
}
fn extra_usage(&self) -> &str {
r#"There are 5 different type of events: focus, key, mouse, paste, resize. Each will produce a
corresponding record, distinguished by type field:
```
{ type: focus event: (gained|lost) }
{ type: key key_type: <key_type> code: <string> modifiers: [ <modifier> ... ] }
{ type: mouse col: <int> row: <int> kind: <string> modifiers: [ <modifier> ... ] }
{ type: paste content: <string> }
{ type: resize col: <int> row: <int> }
```
There are 6 `modifier` variants: shift, control, alt, super, hyper, meta.
There are 4 `key_type` variants:
f - f1, f2, f3 ... keys
char - alphanumeric and special symbols (a, A, 1, $ ...)
media - dedicated media keys (play, pause, tracknext ...)
other - keys not falling under previous categories (up, down, backspace, enter ...)"#
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Listen for a keyboard shortcut and find out how nu receives it",
example: "input listen --types [key]",
result: None,
}]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let event_type_filter = get_event_type_filter(engine_state, stack, call, head)?;
let add_raw = call.has_flag("raw");
terminal::enable_raw_mode()?;
let console_state = event_type_filter.enable_events()?;
loop {
let event = crossterm::event::read().map_err(|_| ShellError::GenericError {
error: "Error with user input".into(),
msg: "".into(),
span: Some(head),
help: None,
inner: vec![],
})?;
let event = parse_event(head, &event, &event_type_filter, add_raw);
if let Some(event) = event {
terminal::disable_raw_mode()?;
console_state.restore();
return Ok(event.into_pipeline_data());
}
}
}
}
fn get_event_type_filter(
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
head: Span,
) -> Result<EventTypeFilter, ShellError> {
let event_type_filter = call.get_flag::<Value>(engine_state, stack, "types")?;
let event_type_filter = event_type_filter
.map(|list| EventTypeFilter::from_value(list, head))
.transpose()?
.unwrap_or_else(EventTypeFilter::all);
Ok(event_type_filter)
}
#[derive(Clone)]
struct EventTypeFilter {
listen_focus: bool,
listen_key: bool,
listen_mouse: bool,
listen_paste: bool,
listen_resize: bool,
}
impl EventTypeFilter {
fn none() -> EventTypeFilter {
EventTypeFilter {
listen_focus: false,
listen_key: false,
listen_mouse: false,
listen_paste: false,
listen_resize: false,
}
}
fn all() -> EventTypeFilter {
EventTypeFilter {
listen_focus: true,
listen_key: true,
listen_mouse: true,
listen_paste: true,
listen_resize: true,
}
}
fn from_value(value: Value, head: Span) -> Result<EventTypeFilter, ShellError> {
if let Value::List { vals, .. } = value {
let mut filter = Self::none();
for event_type in vals {
let span = event_type.span();
if let Value::String { val, .. } = event_type {
match val.as_str() {
"focus" => filter.listen_focus = true,
"key" => filter.listen_key = true,
"mouse" => filter.listen_mouse = true,
"paste" => filter.listen_paste = true,
"resize" => filter.listen_resize = true,
_ => return Err(Self::wrong_type_error(head, val.as_str(), span)),
}
} else {
return Err(Self::bad_list_error(head, &event_type));
}
}
Ok(filter)
} else {
Err(Self::bad_list_error(head, &value))
}
}
fn wrong_type_error(head: Span, val: &str, val_span: Span) -> ShellError {
ShellError::UnsupportedInput {
msg: format!("{} is not a valid event type", val),
input: "value originates from here".into(),
msg_span: head,
input_span: val_span,
}
}
fn bad_list_error(head: Span, value: &Value) -> ShellError {
ShellError::UnsupportedInput {
msg: "--types expects a list of strings".to_string(),
input: "value originates from here".into(),
msg_span: head,
input_span: value.span(),
}
}
fn enable_events(&self) -> Result<DeferredConsoleRestore, ShellError> {
if self.listen_mouse {
crossterm::execute!(stdout(), EnableMouseCapture)?;
}
if self.listen_paste {
crossterm::execute!(stdout(), EnableBracketedPaste)?;
}
if self.listen_focus {
crossterm::execute!(stdout(), crossterm::event::EnableFocusChange)?;
}
Ok(DeferredConsoleRestore {
setup_event_types: self.clone(),
})
}
}
struct DeferredConsoleRestore {
setup_event_types: EventTypeFilter,
}
impl DeferredConsoleRestore {
fn restore(self) {
if self.setup_event_types.listen_mouse {
let _ = crossterm::execute!(stdout(), DisableMouseCapture);
}
if self.setup_event_types.listen_paste {
let _ = crossterm::execute!(stdout(), DisableBracketedPaste);
}
if self.setup_event_types.listen_focus {
let _ = crossterm::execute!(stdout(), DisableFocusChange);
}
}
}
fn parse_event(
head: Span,
event: &crossterm::event::Event,
filter: &EventTypeFilter,
add_raw: bool,
) -> Option<Value> {
match event {
crossterm::event::Event::FocusGained => {
create_focus_event(head, filter, FocusEventType::Gained)
}
crossterm::event::Event::FocusLost => {
create_focus_event(head, filter, FocusEventType::Lost)
}
crossterm::event::Event::Key(event) => create_key_event(head, filter, event, add_raw),
crossterm::event::Event::Mouse(event) => create_mouse_event(head, filter, event, add_raw),
crossterm::event::Event::Paste(content) => create_paste_event(head, filter, content),
crossterm::event::Event::Resize(cols, rows) => {
create_resize_event(head, filter, *cols, *rows)
}
}
}
enum FocusEventType {
Gained,
Lost,
}
impl FocusEventType {
fn string(self) -> String {
match self {
FocusEventType::Gained => "gained".to_string(),
FocusEventType::Lost => "lost".to_string(),
}
}
}
fn create_focus_event(
head: Span,
filter: &EventTypeFilter,
event_type: FocusEventType,
) -> Option<Value> {
if filter.listen_focus {
Some(Value::record(
record! {
"type" => Value::string("focus", head),
"event" => Value::string(event_type.string(), head)
},
head,
))
} else {
None
}
}
fn create_key_event(
head: Span,
filter: &EventTypeFilter,
event: &crossterm::event::KeyEvent,
add_raw: bool,
) -> Option<Value> {
if filter.listen_key {
let crossterm::event::KeyEvent {
code: raw_code,
modifiers: raw_modifiers,
kind,
..
} = event;
if let KeyEventKind::Release | KeyEventKind::Repeat = kind {
return None;
}
let (key, code) = get_keycode_name(head, raw_code);
let mut record = record! {
"type" => Value::string("key", head),
"key_type" => key,
"code" => code,
"modifiers" => parse_modifiers(head, raw_modifiers),
};
if add_raw {
if let KeyCode::Char(c) = raw_code {
record.push("raw_code", Value::int(c.as_(), head));
}
record.push(
"raw_modifiers",
Value::int(raw_modifiers.bits() as i64, head),
);
}
Some(Value::record(record, head))
} else {
None
}
}
fn get_keycode_name(head: Span, code: &KeyCode) -> (Value, Value) {
let (typ, code) = match code {
KeyCode::F(n) => ("f", n.to_string()),
KeyCode::Char(c) => ("char", c.to_string()),
KeyCode::Media(m) => ("media", format!("{m:?}").to_ascii_lowercase()),
KeyCode::Modifier(m) => ("modifier", format!("{m:?}").to_ascii_lowercase()),
_ => ("other", format!("{code:?}").to_ascii_lowercase()),
};
(Value::string(typ, head), Value::string(code, head))
}
fn parse_modifiers(head: Span, modifiers: &KeyModifiers) -> Value {
const ALL_MODIFIERS: [KeyModifiers; 6] = [
KeyModifiers::SHIFT,
KeyModifiers::CONTROL,
KeyModifiers::ALT,
KeyModifiers::SUPER,
KeyModifiers::HYPER,
KeyModifiers::META,
];
let parsed_modifiers = ALL_MODIFIERS
.iter()
.filter(|m| modifiers.contains(**m))
.map(|m| format!("{m:?}").to_ascii_lowercase())
.map(|string| Value::string(string, head))
.collect();
Value::list(parsed_modifiers, head)
}
fn create_mouse_event(
head: Span,
filter: &EventTypeFilter,
event: &MouseEvent,
add_raw: bool,
) -> Option<Value> {
if filter.listen_mouse {
let kind = match event.kind {
MouseEventKind::Down(btn) => format!("{btn:?}_down"),
MouseEventKind::Up(btn) => format!("{btn:?}_up"),
MouseEventKind::Drag(btn) => format!("{btn:?}_drag"),
MouseEventKind::Moved => "moved".to_string(),
MouseEventKind::ScrollDown => "scroll_down".to_string(),
MouseEventKind::ScrollUp => "scroll_up".to_string(),
MouseEventKind::ScrollLeft => "scroll_left".to_string(),
MouseEventKind::ScrollRight => "scroll_right".to_string(),
};
let mut record = record! {
"type" => Value::string("mouse", head),
"col" => Value::int(event.column as i64, head),
"row" => Value::int(event.row as i64, head),
"kind" => Value::string(kind, head),
"modifiers" => parse_modifiers(head, &event.modifiers),
};
if add_raw {
record.push(
"raw_modifiers",
Value::int(event.modifiers.bits() as i64, head),
);
}
Some(Value::record(record, head))
} else {
None
}
}
fn create_paste_event(head: Span, filter: &EventTypeFilter, content: &str) -> Option<Value> {
if filter.listen_paste {
let record = record! {
"type" => Value::string("paste", head),
"content" => Value::string(content, head),
};
Some(Value::record(record, head))
} else {
None
}
}
fn create_resize_event(
head: Span,
filter: &EventTypeFilter,
columns: u16,
rows: u16,
) -> Option<Value> {
if filter.listen_resize {
let record = record! {
"type" => Value::string("resize", head),
"col" => Value::int(columns as i64, head),
"row" => Value::int(rows as i64, head),
};
Some(Value::record(record, head))
} else {
None
}
}