Add term query, for querying information from terminals. (#14427)

## Related
- #10150
- https://github.com/nushell/nushell/pull/10150#issuecomment-1721238336
- #10387
- https://github.com/nushell/nushell/pull/10387#issuecomment-1722228185

# Description
`term query`: a command for querying information from the terminal.

Prints the `$query`, and immediately starts reading raw bytes from
stdin.

The standard input will be read until the `terminator` sequence is
encountered.
The `terminator` is not removed from the output.

It also stops on <kbd>Ctrl-C</kbd> with an error.

```
Usage:
  > term query {flags} <query> 

Flags:
  -h, --help: Display the help message for this command
  -t, --terminator (required parameter) <one_of(binary, string)>: stdin will be read until this sequence is encountered

Parameters:
  query <one_of(binary, string)>: The query that will be printed to stdout
```

This was previously possible with `input` until #10150.
`input` command's features such as cursor control, deleting input etc.
are useful, but interfere with this use case.

`term query` makes the following uses possible:

```nushell
# get the terminal size with ansi escape codes
def terminal-size [] {
    let response = term query (ansi size) --terminator 'R'
    # $response should look like this
    # Length: 9 (0x9) bytes | printable whitespace ascii_other non_ascii
    # 00000000:   1b 5b 33 38  3b 31 35 30  52             •[38;150R

    let sz = $response | bytes at 2..<-1 | decode
    # 38;150

    # $sz should look like 38;150
    let size = ($sz | split row ';' | each {into int})

    # output in record syntax
    {
        rows: $size.0
        columns: $size.1
    }
}
```

```nushell
# read clipboard content using OSC 52
term query $"(ansi --osc '52;c;?')(ansi st)" --terminator (ansi st)
| bytes at 7..<-2
| decode
| decode base64
| decode
```

# User-Facing Changes
- added `ansi query`

# Tests + Formatting
- Integration tests should be added if possible.
This commit is contained in:
Bahex 2024-11-26 00:13:11 +03:00 committed by GitHub
parent 4d3283e235
commit 32196cfe78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 182 additions and 2 deletions

View file

@ -248,7 +248,9 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
IsTerminal,
Kill,
Sleep,
Term,
TermSize,
TermQuery,
Whoami,
};

View file

@ -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;

View file

@ -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;

View file

@ -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<PipelineData, ShellError> {
Ok(Value::string(get_full_help(self, engine_state, stack), call.head).into_pipeline_data())
}
}

View file

@ -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<Example> {
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<PipelineData, ShellError> {
let query: Vec<u8> = call.req(engine_state, stack, 0)?;
let terminator: Option<Vec<u8>> = 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
}
}