diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index cbbee717d4..9903eed774 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -248,7 +248,9 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { IsTerminal, Kill, Sleep, + Term, TermSize, + TermQuery, Whoami, }; diff --git a/crates/nu-command/src/platform/mod.rs b/crates/nu-command/src/platform/mod.rs index 16a245f87b..25df4e0c59 100644 --- a/crates/nu-command/src/platform/mod.rs +++ b/crates/nu-command/src/platform/mod.rs @@ -5,7 +5,7 @@ mod input; mod is_terminal; mod kill; mod sleep; -mod term_size; +mod term; #[cfg(unix)] mod ulimit; mod whoami; @@ -19,7 +19,7 @@ pub use input::InputListen; pub use is_terminal::IsTerminal; pub use kill::Kill; pub use sleep::Sleep; -pub use term_size::TermSize; +pub use term::{Term, TermQuery, TermSize}; #[cfg(unix)] pub use ulimit::ULimit; pub use whoami::Whoami; diff --git a/crates/nu-command/src/platform/term/mod.rs b/crates/nu-command/src/platform/term/mod.rs new file mode 100644 index 0000000000..cd37a0583b --- /dev/null +++ b/crates/nu-command/src/platform/term/mod.rs @@ -0,0 +1,7 @@ +mod term_; +mod term_query; +mod term_size; + +pub use term_::Term; +pub use term_query::TermQuery; +pub use term_size::TermSize; diff --git a/crates/nu-command/src/platform/term/term_.rs b/crates/nu-command/src/platform/term/term_.rs new file mode 100644 index 0000000000..b28f226263 --- /dev/null +++ b/crates/nu-command/src/platform/term/term_.rs @@ -0,0 +1,34 @@ +use nu_engine::{command_prelude::*, get_full_help}; + +#[derive(Clone)] +pub struct Term; + +impl Command for Term { + fn name(&self) -> &str { + "term" + } + + fn signature(&self) -> Signature { + Signature::build("term") + .category(Category::Platform) + .input_output_types(vec![(Type::Nothing, Type::String)]) + } + + fn description(&self) -> &str { + "Commands for querying information about the terminal." + } + + fn extra_description(&self) -> &str { + "You must use one of the following subcommands. Using this command as-is will only produce this help message." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/platform/term/term_query.rs b/crates/nu-command/src/platform/term/term_query.rs new file mode 100644 index 0000000000..a4966edde0 --- /dev/null +++ b/crates/nu-command/src/platform/term/term_query.rs @@ -0,0 +1,137 @@ +use std::{ + io::{Read, Write}, + time::Duration, +}; + +use nu_engine::command_prelude::*; + +const CTRL_C: u8 = 3; + +#[derive(Clone)] +pub struct TermQuery; + +impl Command for TermQuery { + fn name(&self) -> &str { + "term query" + } + + fn description(&self) -> &str { + "Query the terminal for information." + } + + fn extra_description(&self) -> &str { + "Print the given query, and read the immediate result from stdin. + +The standard input will be read right after `query` is printed, and consumed until the `terminator` +sequence is encountered. The `terminator` is not removed from the output. + +If `terminator` is not supplied, input will be read until Ctrl-C is pressed." + } + + fn signature(&self) -> Signature { + Signature::build("term query") + .category(Category::Platform) + .input_output_types(vec![(Type::Nothing, Type::Binary)]) + .allow_variants_without_examples(true) + .required( + "query", + SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::String]), + "The query that will be printed to stdout.", + ) + .named( + "terminator", + SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::String]), + "Terminator sequence for the expected reply.", + Some('t'), + ) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get cursor position.", + example: r#"term query (ansi cursor_position) --terminator 'R'"#, + result: None, + }, + Example { + description: "Get terminal background color.", + example: r#"term query $'(ansi osc)10;?(ansi st)' --terminator (ansi st)"#, + result: None, + }, + Example { + description: "Read clipboard content on terminals supporting OSC-52.", + example: r#"term query $'(ansi osc)52;c;?(ansi st)' --terminator (ansi st)"#, + result: None, + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let query: Vec = call.req(engine_state, stack, 0)?; + let terminator: Option> = call.get_flag(engine_state, stack, "terminator")?; + + crossterm::terminal::enable_raw_mode()?; + + // clear terminal events + while crossterm::event::poll(Duration::from_secs(0))? { + // If there's an event, read it to remove it from the queue + let _ = crossterm::event::read()?; + } + + let mut b = [0u8; 1]; + let mut buf = vec![]; + let mut stdin = std::io::stdin().lock(); + + { + let mut stdout = std::io::stdout().lock(); + stdout.write_all(&query)?; + stdout.flush()?; + } + + let out = if let Some(terminator) = terminator { + loop { + if let Err(err) = stdin.read_exact(&mut b) { + break Err(ShellError::from(err)); + } + + if b[0] == CTRL_C { + break Err(ShellError::Interrupted { span: call.head }); + } + + buf.push(b[0]); + + if buf.ends_with(&terminator) { + break Ok(Value::Binary { + val: buf, + internal_span: call.head, + } + .into_pipeline_data()); + } + } + } else { + loop { + if let Err(err) = stdin.read_exact(&mut b) { + break Err(ShellError::from(err)); + } + + if b[0] == CTRL_C { + break Ok(Value::Binary { + val: buf, + internal_span: call.head, + } + .into_pipeline_data()); + } + + buf.push(b[0]); + } + }; + crossterm::terminal::disable_raw_mode()?; + out + } +} diff --git a/crates/nu-command/src/platform/term_size.rs b/crates/nu-command/src/platform/term/term_size.rs similarity index 100% rename from crates/nu-command/src/platform/term_size.rs rename to crates/nu-command/src/platform/term/term_size.rs