1use std::io::Error;
4
5use deno_core::op2;
6use deno_core::OpState;
7use rustyline::config::Configurer;
8use rustyline::error::ReadlineError;
9use rustyline::Cmd;
10use rustyline::Editor;
11use rustyline::KeyCode;
12use rustyline::KeyEvent;
13use rustyline::Modifiers;
14
15#[cfg(windows)]
16use deno_core::parking_lot::Mutex;
17#[cfg(windows)]
18use deno_io::WinTtyState;
19#[cfg(windows)]
20use std::sync::Arc;
21
22#[cfg(unix)]
23use deno_core::ResourceId;
24#[cfg(unix)]
25use nix::sys::termios;
26#[cfg(unix)]
27use std::cell::RefCell;
28#[cfg(unix)]
29use std::collections::HashMap;
30
31#[cfg(unix)]
32#[derive(Default, Clone)]
33struct TtyModeStore(
34 std::rc::Rc<RefCell<HashMap<ResourceId, termios::Termios>>>,
35);
36
37#[cfg(unix)]
38impl TtyModeStore {
39 pub fn get(&self, id: ResourceId) -> Option<termios::Termios> {
40 self.0.borrow().get(&id).map(ToOwned::to_owned)
41 }
42
43 pub fn take(&self, id: ResourceId) -> Option<termios::Termios> {
44 self.0.borrow_mut().remove(&id)
45 }
46
47 pub fn set(&self, id: ResourceId, mode: termios::Termios) {
48 self.0.borrow_mut().insert(id, mode);
49 }
50}
51
52#[cfg(windows)]
53use winapi::shared::minwindef::DWORD;
54#[cfg(windows)]
55use winapi::um::wincon;
56
57deno_core::extension!(
58 deno_tty,
59 ops = [op_set_raw, op_console_size, op_read_line_prompt],
60 state = |state| {
61 #[cfg(unix)]
62 state.put(TtyModeStore::default());
63 },
64);
65
66#[derive(Debug, thiserror::Error)]
67pub enum TtyError {
68 #[error(transparent)]
69 Resource(deno_core::error::AnyError),
70 #[error("{0}")]
71 Io(#[from] std::io::Error),
72 #[cfg(unix)]
73 #[error(transparent)]
74 Nix(nix::Error),
75 #[error(transparent)]
76 Other(deno_core::error::AnyError),
77}
78
79#[cfg(windows)]
81const COOKED_MODE: DWORD =
82 wincon::ENABLE_LINE_INPUT
84 | wincon::ENABLE_ECHO_INPUT
86 | wincon::ENABLE_PROCESSED_INPUT;
88
89#[cfg(windows)]
90fn mode_raw_input_on(original_mode: DWORD) -> DWORD {
91 original_mode & !COOKED_MODE | wincon::ENABLE_VIRTUAL_TERMINAL_INPUT
92}
93
94#[cfg(windows)]
95fn mode_raw_input_off(original_mode: DWORD) -> DWORD {
96 original_mode & !wincon::ENABLE_VIRTUAL_TERMINAL_INPUT | COOKED_MODE
97}
98
99#[op2(fast)]
100fn op_set_raw(
101 state: &mut OpState,
102 rid: u32,
103 is_raw: bool,
104 cbreak: bool,
105) -> Result<(), TtyError> {
106 let handle_or_fd = state
107 .resource_table
108 .get_fd(rid)
109 .map_err(TtyError::Resource)?;
110
111 #[cfg(windows)]
117 {
118 use winapi::shared::minwindef::FALSE;
119
120 use winapi::um::consoleapi;
121
122 let handle = handle_or_fd;
123
124 if cbreak {
125 return Err(TtyError::Other(deno_core::error::not_supported()));
126 }
127
128 let mut original_mode: DWORD = 0;
129 if unsafe { consoleapi::GetConsoleMode(handle, &mut original_mode) }
131 == FALSE
132 {
133 return Err(TtyError::Io(Error::last_os_error()));
134 }
135
136 let new_mode = if is_raw {
137 mode_raw_input_on(original_mode)
138 } else {
139 mode_raw_input_off(original_mode)
140 };
141
142 let stdin_state = state.borrow::<Arc<Mutex<WinTtyState>>>();
143 let mut stdin_state = stdin_state.lock();
144
145 if stdin_state.reading {
146 let cvar = stdin_state.cvar.clone();
147
148 if original_mode & COOKED_MODE != 0 && !stdin_state.cancelled {
151 let record = unsafe {
153 let mut record: wincon::INPUT_RECORD = std::mem::zeroed();
154 record.EventType = wincon::KEY_EVENT;
155 record.Event.KeyEvent_mut().wVirtualKeyCode =
156 winapi::um::winuser::VK_RETURN as u16;
157 record.Event.KeyEvent_mut().bKeyDown = 1;
158 record.Event.KeyEvent_mut().wRepeatCount = 1;
159 *record.Event.KeyEvent_mut().uChar.UnicodeChar_mut() = '\r' as u16;
160 record.Event.KeyEvent_mut().dwControlKeyState = 0;
161 record.Event.KeyEvent_mut().wVirtualScanCode =
162 winapi::um::winuser::MapVirtualKeyW(
163 winapi::um::winuser::VK_RETURN as u32,
164 winapi::um::winuser::MAPVK_VK_TO_VSC,
165 ) as u16;
166 record
167 };
168 stdin_state.cancelled = true;
169
170 let active_screen_buffer = unsafe {
172 let handle = winapi::um::fileapi::CreateFileW(
174 "conout$"
175 .encode_utf16()
176 .chain(Some(0))
177 .collect::<Vec<_>>()
178 .as_ptr(),
179 winapi::um::winnt::GENERIC_READ | winapi::um::winnt::GENERIC_WRITE,
180 winapi::um::winnt::FILE_SHARE_READ
181 | winapi::um::winnt::FILE_SHARE_WRITE,
182 std::ptr::null_mut(),
183 winapi::um::fileapi::OPEN_EXISTING,
184 0,
185 std::ptr::null_mut(),
186 );
187
188 let mut active_screen_buffer = std::mem::zeroed();
189 winapi::um::wincon::GetConsoleScreenBufferInfo(
190 handle,
191 &mut active_screen_buffer,
192 );
193 winapi::um::handleapi::CloseHandle(handle);
194 active_screen_buffer
195 };
196 stdin_state.screen_buffer_info = Some(active_screen_buffer);
197
198 if unsafe {
200 winapi::um::wincon::WriteConsoleInputW(handle, &record, 1, &mut 0)
201 } == FALSE
202 {
203 return Err(TtyError::Io(Error::last_os_error()));
204 }
205
206 cvar.wait_while(&mut stdin_state, |state: &mut WinTtyState| {
210 state.cancelled
211 });
212 }
213 }
214
215 if unsafe { consoleapi::SetConsoleMode(handle, new_mode) } == FALSE {
217 return Err(TtyError::Io(Error::last_os_error()));
218 }
219
220 Ok(())
221 }
222 #[cfg(unix)]
223 {
224 fn prepare_stdio() {
225 unsafe {
227 use libc::atexit;
228 use libc::tcgetattr;
229 use libc::tcsetattr;
230 use libc::termios;
231 use once_cell::sync::OnceCell;
232
233 static ORIG_TERMIOS: OnceCell<Option<termios>> = OnceCell::new();
235 ORIG_TERMIOS.get_or_init(|| {
236 let mut termios = std::mem::zeroed::<termios>();
237 if tcgetattr(libc::STDIN_FILENO, &mut termios) == 0 {
238 extern "C" fn reset_stdio() {
239 unsafe {
241 tcsetattr(
242 libc::STDIN_FILENO,
243 0,
244 &ORIG_TERMIOS.get().unwrap().unwrap(),
245 )
246 };
247 }
248
249 atexit(reset_stdio);
250 return Some(termios);
251 }
252
253 None
254 });
255 }
256 }
257
258 prepare_stdio();
259 let tty_mode_store = state.borrow::<TtyModeStore>().clone();
260 let previous_mode = tty_mode_store.get(rid);
261
262 let raw_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(handle_or_fd) };
264
265 if is_raw {
266 let mut raw = match previous_mode {
267 Some(mode) => mode,
268 None => {
269 let original_mode =
271 termios::tcgetattr(raw_fd).map_err(TtyError::Nix)?;
272 tty_mode_store.set(rid, original_mode.clone());
273 original_mode
274 }
275 };
276
277 raw.input_flags &= !(termios::InputFlags::BRKINT
278 | termios::InputFlags::ICRNL
279 | termios::InputFlags::INPCK
280 | termios::InputFlags::ISTRIP
281 | termios::InputFlags::IXON);
282
283 raw.control_flags |= termios::ControlFlags::CS8;
284
285 raw.local_flags &= !(termios::LocalFlags::ECHO
286 | termios::LocalFlags::ICANON
287 | termios::LocalFlags::IEXTEN);
288 if !cbreak {
289 raw.local_flags &= !(termios::LocalFlags::ISIG);
290 }
291 raw.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1;
292 raw.control_chars[termios::SpecialCharacterIndices::VTIME as usize] = 0;
293 termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &raw)
294 .map_err(TtyError::Nix)?;
295 } else {
296 if let Some(mode) = tty_mode_store.take(rid) {
298 termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &mode)
299 .map_err(TtyError::Nix)?;
300 }
301 }
302
303 Ok(())
304 }
305}
306
307#[op2(fast)]
308fn op_console_size(
309 state: &mut OpState,
310 #[buffer] result: &mut [u32],
311) -> Result<(), TtyError> {
312 fn check_console_size(
313 state: &mut OpState,
314 result: &mut [u32],
315 rid: u32,
316 ) -> Result<(), TtyError> {
317 let fd = state
318 .resource_table
319 .get_fd(rid)
320 .map_err(TtyError::Resource)?;
321 let size = console_size_from_fd(fd)?;
322 result[0] = size.cols;
323 result[1] = size.rows;
324 Ok(())
325 }
326
327 let mut last_result = Ok(());
328 for rid in [0, 1, 2] {
331 last_result = check_console_size(state, result, rid);
332 if last_result.is_ok() {
333 return last_result;
334 }
335 }
336
337 last_result
338}
339
340#[derive(Debug, PartialEq, Eq, Clone, Copy)]
341pub struct ConsoleSize {
342 pub cols: u32,
343 pub rows: u32,
344}
345
346pub fn console_size(
347 std_file: &std::fs::File,
348) -> Result<ConsoleSize, std::io::Error> {
349 #[cfg(windows)]
350 {
351 use std::os::windows::io::AsRawHandle;
352 let handle = std_file.as_raw_handle();
353 console_size_from_fd(handle)
354 }
355 #[cfg(unix)]
356 {
357 use std::os::unix::io::AsRawFd;
358 let fd = std_file.as_raw_fd();
359 console_size_from_fd(fd)
360 }
361}
362
363#[cfg(windows)]
364fn console_size_from_fd(
365 handle: std::os::windows::io::RawHandle,
366) -> Result<ConsoleSize, std::io::Error> {
367 unsafe {
369 let mut bufinfo: winapi::um::wincon::CONSOLE_SCREEN_BUFFER_INFO =
370 std::mem::zeroed();
371
372 if winapi::um::wincon::GetConsoleScreenBufferInfo(handle, &mut bufinfo) == 0
373 {
374 return Err(Error::last_os_error());
375 }
376
377 let cols = std::cmp::max(
381 bufinfo.srWindow.Right as i32 - bufinfo.srWindow.Left as i32 + 1,
382 0,
383 ) as u32;
384 let rows = std::cmp::max(
385 bufinfo.srWindow.Bottom as i32 - bufinfo.srWindow.Top as i32 + 1,
386 0,
387 ) as u32;
388
389 Ok(ConsoleSize { cols, rows })
390 }
391}
392
393#[cfg(not(windows))]
394fn console_size_from_fd(
395 fd: std::os::unix::prelude::RawFd,
396) -> Result<ConsoleSize, std::io::Error> {
397 unsafe {
399 let mut size: libc::winsize = std::mem::zeroed();
400 if libc::ioctl(fd, libc::TIOCGWINSZ, &mut size as *mut _) != 0 {
401 return Err(Error::last_os_error());
402 }
403 Ok(ConsoleSize {
404 cols: size.ws_col as u32,
405 rows: size.ws_row as u32,
406 })
407 }
408}
409
410#[cfg(all(test, windows))]
411mod tests {
412 #[test]
413 fn test_winos_raw_mode_transitions() {
414 use crate::ops::tty::mode_raw_input_off;
415 use crate::ops::tty::mode_raw_input_on;
416
417 let known_off_modes =
418 [0xf7 , 0x1f7 ];
419 let known_on_modes =
420 [0x2f0 , 0x3f0 ];
421
422 assert_eq!(known_on_modes[0], mode_raw_input_on(known_off_modes[0]));
424 assert_eq!(known_on_modes[1], mode_raw_input_on(known_off_modes[1]));
425
426 assert_eq!(
428 known_off_modes[0],
429 mode_raw_input_off(mode_raw_input_on(known_off_modes[0]))
430 );
431 assert_eq!(
432 known_off_modes[1],
433 mode_raw_input_off(mode_raw_input_on(known_off_modes[1]))
434 );
435 }
436}
437
438#[op2]
439#[string]
440pub fn op_read_line_prompt(
441 #[string] prompt_text: &str,
442 #[string] default_value: &str,
443) -> Result<Option<String>, ReadlineError> {
444 let mut editor = Editor::<(), rustyline::history::DefaultHistory>::new()
445 .expect("Failed to create editor.");
446
447 editor.set_keyseq_timeout(1);
448 editor
449 .bind_sequence(KeyEvent(KeyCode::Esc, Modifiers::empty()), Cmd::Interrupt);
450
451 let read_result =
452 editor.readline_with_initial(prompt_text, (default_value, ""));
453 match read_result {
454 Ok(line) => Ok(Some(line)),
455 Err(ReadlineError::Interrupted) => {
456 unsafe {
458 libc::raise(libc::SIGINT);
459 }
460 Ok(None)
461 }
462 Err(ReadlineError::Eof) => Ok(None),
463 Err(err) => Err(err),
464 }
465}