1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
use crate::platform::RawModeGuard;
use crossterm::{
cursor,
event::{Event, KeyCode, KeyEventKind, KeyModifiers},
execute,
style::Print,
terminal::{self, ClearType},
};
use itertools::Itertools;
use nu_engine::command_prelude::*;
use nu_protocol::shell_error::{self, io::IoError};
use std::{io::Write, time::Duration};
pub trait LegacyInput {
fn legacy_input(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let prompt: Option<String> = call.opt(engine_state, stack, 0)?;
let bytes_until: Option<String> = call.get_flag(engine_state, stack, "bytes-until-any")?;
let suppress_output = call.has_flag(engine_state, stack, "suppress-output")?;
let numchar: Option<Spanned<i64>> = call.get_flag(engine_state, stack, "numchar")?;
let numchar: Spanned<i64> = numchar.unwrap_or(Spanned {
item: i64::MAX,
span: call.head,
});
let from_io_error = IoError::factory(call.head, None);
if numchar.item < 1 {
return Err(ShellError::UnsupportedInput {
msg: "Number of characters to read has to be positive".to_string(),
input: "value originated from here".to_string(),
msg_span: call.head,
input_span: numchar.span,
});
}
let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
// Acquire the guard (and its `require_stdin` check) before writing anything, so a
// detached stack (completion worker or MCP) errors out before the prompt is printed.
let raw_mode = RawModeGuard::acquire(stack, call.head)?;
if let Some(prompt) = &prompt {
match &default_val {
None => print!("{prompt}"),
Some(val) => print!("{prompt} (default: {val})"),
}
let _ = std::io::stdout().flush();
}
let mut buf = String::new();
// clear terminal events
while crossterm::event::poll(Duration::from_secs(0)).map_err(&from_io_error)? {
// If there's an event, read it to remove it from the queue
let _ = crossterm::event::read().map_err(&from_io_error)?;
}
loop {
if i64::try_from(buf.len()).unwrap_or(0) >= numchar.item {
break;
}
match crossterm::event::read() {
Ok(Event::Key(k)) => match k.kind {
KeyEventKind::Press | KeyEventKind::Repeat => {
match k.code {
// TODO: maintain keycode parity with existing command
KeyCode::Char(c) => {
if k.modifiers == KeyModifiers::ALT
|| k.modifiers == KeyModifiers::CONTROL
{
if k.modifiers == KeyModifiers::CONTROL && c == 'c' {
return Err(IoError::new(
shell_error::io::ErrorKind::from_std(
std::io::ErrorKind::Interrupted,
),
call.head,
None,
)
.into());
}
continue;
}
if let Some(bytes_until) = bytes_until.as_ref()
&& bytes_until.bytes().contains(&(c as u8))
{
break;
}
buf.push(c);
}
KeyCode::Backspace => {
let _ = buf.pop();
}
KeyCode::Enter => {
break;
}
_ => continue,
}
}
_ => continue,
},
Ok(_) => continue,
Err(event_error) => {
return Err(from_io_error(event_error).into());
}
}
if !suppress_output {
// clear the current line and print the current buffer
execute!(
std::io::stdout(),
terminal::Clear(ClearType::CurrentLine),
cursor::MoveToColumn(0),
)
.map_err(|err| IoError::new(err, call.head, None))?;
if let Some(prompt) = &prompt {
execute!(std::io::stdout(), Print(prompt.to_string()))
.map_err(&from_io_error)?;
}
execute!(std::io::stdout(), Print(buf.to_string())).map_err(&from_io_error)?;
}
}
// Leave raw mode before the trailing newline so it gets the usual carriage return.
drop(raw_mode);
std::io::stdout().write_all(b"\n").map_err(&from_io_error)?;
match default_val {
Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
_ => Ok(Value::string(buf, call.head).into_pipeline_data()),
}
}
}