#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_possible_truncation)]
use std::io::Error;
use std::sync::Arc;
use deno_core::op2;
use deno_core::parking_lot::Mutex;
use deno_core::OpState;
use deno_error::builtin_classes::GENERIC_ERROR;
use deno_error::JsErrorBox;
use deno_error::JsErrorClass;
use deno_io::WinTtyState;
use rustyline::config::Configurer;
use rustyline::error::ReadlineError;
use rustyline::Cmd;
use rustyline::Editor;
use rustyline::KeyCode;
use rustyline::KeyEvent;
use rustyline::Modifiers;
use winapi::shared::minwindef::FALSE;
use winapi::um::consoleapi;
use winapi::shared::minwindef::DWORD;
use winapi::um::wincon;
deno_core::extension!(
deno_tty,
ops = [op_set_raw, op_console_size, op_read_line_prompt],
);
#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum TtyError {
#[class(inherit)]
#[error(transparent)]
Resource(
#[from]
#[inherit]
deno_core::error::ResourceError,
),
#[class(inherit)]
#[error("{0}")]
Io(
#[from]
#[inherit]
Error,
),
#[class(inherit)]
#[error(transparent)]
Other(#[inherit] JsErrorBox),
}
const COOKED_MODE: DWORD =
wincon::ENABLE_LINE_INPUT
| wincon::ENABLE_ECHO_INPUT
| wincon::ENABLE_PROCESSED_INPUT;
fn mode_raw_input_on(original_mode: DWORD) -> DWORD {
original_mode & !COOKED_MODE | wincon::ENABLE_VIRTUAL_TERMINAL_INPUT
}
fn mode_raw_input_off(original_mode: DWORD) -> DWORD {
original_mode & !wincon::ENABLE_VIRTUAL_TERMINAL_INPUT | COOKED_MODE
}
#[op2(fast)]
fn op_set_raw(state: &mut OpState, rid: u32, is_raw: bool, cbreak: bool) -> Result<(), TtyError> {
let handle_or_fd = state.resource_table.get_fd(rid)?;
let handle = handle_or_fd;
if cbreak {
return Err(TtyError::Other(JsErrorBox::not_supported()));
}
let mut original_mode: DWORD = 0;
if unsafe { consoleapi::GetConsoleMode(handle, &raw mut original_mode) } == FALSE {
return Err(TtyError::Io(Error::last_os_error()));
}
let new_mode = if is_raw {
mode_raw_input_on(original_mode)
} else {
mode_raw_input_off(original_mode)
};
let stdin_state = state.borrow::<Arc<Mutex<WinTtyState>>>();
let mut stdin_state = stdin_state.lock();
if stdin_state.reading {
let cvar = stdin_state.cvar.clone();
if original_mode & COOKED_MODE != 0 && !stdin_state.cancelled {
let record = unsafe {
let mut record: wincon::INPUT_RECORD = std::mem::zeroed();
record.EventType = wincon::KEY_EVENT;
record.Event.KeyEvent_mut().wVirtualKeyCode = winapi::um::winuser::VK_RETURN as u16;
record.Event.KeyEvent_mut().bKeyDown = 1;
record.Event.KeyEvent_mut().wRepeatCount = 1;
*record.Event.KeyEvent_mut().uChar.UnicodeChar_mut() = '\r' as u16;
record.Event.KeyEvent_mut().dwControlKeyState = 0;
record.Event.KeyEvent_mut().wVirtualScanCode = winapi::um::winuser::MapVirtualKeyW(
winapi::um::winuser::VK_RETURN as u32,
winapi::um::winuser::MAPVK_VK_TO_VSC,
) as u16;
record
};
stdin_state.cancelled = true;
let active_screen_buffer = unsafe {
let handle = winapi::um::fileapi::CreateFileW(
"conout$"
.encode_utf16()
.chain(Some(0))
.collect::<Vec<_>>()
.as_ptr(),
winapi::um::winnt::GENERIC_READ | winapi::um::winnt::GENERIC_WRITE,
winapi::um::winnt::FILE_SHARE_READ | winapi::um::winnt::FILE_SHARE_WRITE,
std::ptr::null_mut(),
winapi::um::fileapi::OPEN_EXISTING,
0,
std::ptr::null_mut(),
);
let mut active_screen_buffer = std::mem::zeroed();
winapi::um::wincon::GetConsoleScreenBufferInfo(
handle,
&raw mut active_screen_buffer,
);
winapi::um::handleapi::CloseHandle(handle);
active_screen_buffer
};
stdin_state.screen_buffer_info = Some(active_screen_buffer);
if unsafe {
winapi::um::wincon::WriteConsoleInputW(handle, &raw const record, 1, &mut 0)
} == FALSE
{
return Err(TtyError::Io(Error::last_os_error()));
}
cvar.wait_while(&mut stdin_state, |state: &mut WinTtyState| state.cancelled);
}
}
if unsafe { consoleapi::SetConsoleMode(handle, new_mode) } == FALSE {
return Err(TtyError::Io(Error::last_os_error()));
}
Ok(())
}
#[op2(fast)]
fn op_console_size(state: &mut OpState, #[buffer] result: &mut [u32]) -> Result<(), TtyError> {
fn check_console_size(
state: &mut OpState,
result: &mut [u32],
rid: u32,
) -> Result<(), TtyError> {
let fd = state.resource_table.get_fd(rid)?;
let size = console_size_from_fd(fd)?;
result[0] = size.cols;
result[1] = size.rows;
Ok(())
}
let mut last_result = Ok(());
for rid in [0, 1, 2] {
last_result = check_console_size(state, result, rid);
if last_result.is_ok() {
return last_result;
}
}
last_result
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct ConsoleSize {
pub cols: u32,
pub rows: u32,
}
fn console_size_from_fd(
handle: std::os::windows::io::RawHandle,
) -> Result<ConsoleSize, std::io::Error> {
unsafe {
let mut bufinfo: winapi::um::wincon::CONSOLE_SCREEN_BUFFER_INFO = std::mem::zeroed();
if winapi::um::wincon::GetConsoleScreenBufferInfo(handle, &raw mut bufinfo) == 0 {
return Err(Error::last_os_error());
}
let cols = std::cmp::max(
i32::from(bufinfo.srWindow.Right) - i32::from(bufinfo.srWindow.Left) + 1,
0,
) as u32;
let rows = std::cmp::max(
i32::from(bufinfo.srWindow.Bottom) - i32::from(bufinfo.srWindow.Top) + 1,
0,
) as u32;
Ok(ConsoleSize { cols, rows })
}
}
deno_error::js_error_wrapper!(ReadlineError, JsReadlineError, |err| {
match err {
ReadlineError::Io(e) => e.get_class(),
ReadlineError::Eof
| ReadlineError::Interrupted
| ReadlineError::WindowResized
| ReadlineError::Decode(_)
| ReadlineError::SystemError(_)
| _ => GENERIC_ERROR.into(),
}
});
#[op2]
#[string]
pub fn op_read_line_prompt(
#[string] prompt_text: &str,
#[string] default_value: &str,
) -> Result<Option<String>, JsReadlineError> {
let mut editor =
Editor::<(), rustyline::history::DefaultHistory>::new().expect("Failed to create editor.");
editor.set_keyseq_timeout(1);
editor.bind_sequence(KeyEvent(KeyCode::Esc, Modifiers::empty()), Cmd::Interrupt);
let read_result = editor.readline_with_initial(prompt_text, (default_value, ""));
match read_result {
Ok(line) => Ok(Some(line)),
Err(ReadlineError::Interrupted) => {
unsafe {
libc::raise(libc::SIGINT);
}
Ok(None)
}
Err(ReadlineError::Eof) => Ok(None),
Err(err) => Err(JsReadlineError(err)),
}
}