nu_command/platform/term/
term_query.rs

1use std::{
2    io::{Read, Write},
3    time::Duration,
4};
5
6use nu_engine::command_prelude::*;
7use nu_protocol::shell_error::io::IoError;
8
9const CTRL_C: u8 = 3;
10
11#[derive(Clone)]
12pub struct TermQuery;
13
14impl Command for TermQuery {
15    fn name(&self) -> &str {
16        "term query"
17    }
18
19    fn description(&self) -> &str {
20        "Query the terminal for information."
21    }
22
23    fn extra_description(&self) -> &str {
24        "Print the given query, and read the immediate result from stdin.
25
26The standard input will be read right after `query` is printed, and consumed until the `terminator`
27sequence is encountered. The `terminator` is not included in the output.
28
29If `terminator` is not supplied, input will be read until Ctrl-C is pressed.
30
31If `prefix` is supplied, input's initial bytes will be validated against it.
32The `prefix` is not included in the output."
33    }
34
35    fn signature(&self) -> Signature {
36        Signature::build("term query")
37            .category(Category::Platform)
38            .input_output_types(vec![(Type::Nothing, Type::Binary)])
39            .allow_variants_without_examples(true)
40            .required(
41                "query",
42                SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::String]),
43                "The query that will be printed to stdout.",
44            )
45            .named(
46                "prefix",
47                SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::String]),
48                "Prefix sequence for the expected reply.",
49                Some('p'),
50            )
51            .named(
52                "terminator",
53                SyntaxShape::OneOf(vec![SyntaxShape::Binary, SyntaxShape::String]),
54                "Terminator sequence for the expected reply.",
55                Some('t'),
56            )
57            .switch(
58                "keep",
59                "Include prefix and terminator in the output.",
60                Some('k'),
61            )
62    }
63
64    fn examples(&self) -> Vec<Example> {
65        vec![
66            Example {
67                description: "Get cursor position.",
68                example: r#"term query (ansi cursor_position) --prefix (ansi csi) --terminator 'R'"#,
69                result: None,
70            },
71            Example {
72                description: "Get terminal background color.",
73                example: r#"term query $'(ansi osc)10;?(ansi st)' --prefix $'(ansi osc)10;' --terminator (ansi st)"#,
74                result: None,
75            },
76            Example {
77                description: "Get terminal background color. (some terminals prefer `char bel` rather than `ansi st` as string terminator)",
78                example: r#"term query $'(ansi osc)10;?(char bel)' --prefix $'(ansi osc)10;' --terminator (char bel)"#,
79                result: None,
80            },
81            Example {
82                description: "Read clipboard content on terminals supporting OSC-52.",
83                example: r#"term query $'(ansi osc)52;c;?(ansi st)' --prefix $'(ansi osc)52;c;' --terminator (ansi st)"#,
84                result: None,
85            },
86        ]
87    }
88
89    fn run(
90        &self,
91        engine_state: &EngineState,
92        stack: &mut Stack,
93        call: &Call,
94        _input: PipelineData,
95    ) -> Result<PipelineData, ShellError> {
96        let query: Vec<u8> = call.req(engine_state, stack, 0)?;
97        let keep = call.has_flag(engine_state, stack, "keep")?;
98        let prefix: Option<Vec<u8>> = call.get_flag(engine_state, stack, "prefix")?;
99        let prefix = prefix.unwrap_or_default();
100        let terminator: Option<Vec<u8>> = call.get_flag(engine_state, stack, "terminator")?;
101
102        crossterm::terminal::enable_raw_mode().map_err(|err| IoError::new(err, call.head, None))?;
103        scopeguard::defer! {
104            let _ = crossterm::terminal::disable_raw_mode();
105        }
106
107        // clear terminal events
108        while crossterm::event::poll(Duration::from_secs(0))
109            .map_err(|err| IoError::new(err, call.head, None))?
110        {
111            // If there's an event, read it to remove it from the queue
112            let _ = crossterm::event::read().map_err(|err| IoError::new(err, call.head, None))?;
113        }
114
115        let mut b = [0u8; 1];
116        let mut buf = vec![];
117        let mut stdin = std::io::stdin().lock();
118
119        {
120            let mut stdout = std::io::stdout().lock();
121            stdout
122                .write_all(&query)
123                .map_err(|err| IoError::new(err, call.head, None))?;
124            stdout
125                .flush()
126                .map_err(|err| IoError::new(err, call.head, None))?;
127        }
128
129        // Validate and skip prefix
130        for bc in prefix {
131            stdin
132                .read_exact(&mut b)
133                .map_err(|err| IoError::new(err, call.head, None))?;
134            if b[0] != bc {
135                return Err(ShellError::GenericError {
136                    error: "Input did not begin with expected sequence".into(),
137                    msg: "".into(),
138                    span: None,
139                    help: Some("Try running without `--prefix` and inspecting the output.".into()),
140                    inner: vec![],
141                });
142            }
143            if keep {
144                buf.push(b[0]);
145            }
146        }
147
148        if let Some(terminator) = terminator {
149            loop {
150                stdin
151                    .read_exact(&mut b)
152                    .map_err(|err| IoError::new(err, call.head, None))?;
153
154                if b[0] == CTRL_C {
155                    return Err(ShellError::InterruptedByUser {
156                        span: Some(call.head),
157                    });
158                }
159
160                buf.push(b[0]);
161
162                if buf.ends_with(&terminator) {
163                    if !keep {
164                        // Remove terminator
165                        buf.drain((buf.len() - terminator.len())..);
166                    }
167                    break;
168                }
169            }
170        } else {
171            loop {
172                stdin
173                    .read_exact(&mut b)
174                    .map_err(|err| IoError::new(err, call.head, None))?;
175
176                if b[0] == CTRL_C {
177                    break;
178                }
179
180                buf.push(b[0]);
181            }
182        };
183
184        Ok(Value::binary(buf, call.head).into_pipeline_data())
185    }
186}