use deno_core::OpState;
use deno_core::ResourceId;
use deno_core::op2;
use deno_error::JsErrorBox;
use deno_error::JsErrorClass;
use deno_error::builtin_classes::GENERIC_ERROR;
use nix::sys::termios;
use rustc_hash::FxHashMap;
use rustyline::Cmd;
use rustyline::Editor;
use rustyline::KeyCode;
use rustyline::KeyEvent;
use rustyline::Modifiers;
use rustyline::config::Configurer;
use rustyline::error::ReadlineError;
use std::cell::RefCell;
use std::io::Error;
use std::rc::Rc;
deno_core::extension!(deno_tty, ops = [op_set_raw, op_console_size, op_read_line_prompt],);
#[derive(Default, Clone)]
pub struct TtyModeStore(Rc<RefCell<FxHashMap<ResourceId, termios::Termios>>>);
impl TtyModeStore {
pub fn get(&self, id: ResourceId) -> Option<termios::Termios> {
self.0.borrow().get(&id).map(ToOwned::to_owned)
}
pub fn take(&self, id: ResourceId) -> Option<termios::Termios> {
self.0.borrow_mut().remove(&id)
}
pub fn set(&self, id: ResourceId, mode: termios::Termios) {
self.0.borrow_mut().insert(id, mode);
}
}
use deno_process::JsNixError;
#[derive(Debug, deno_error::JsError)]
pub enum TtyError {
#[class(inherit)]
Resource(deno_core::error::ResourceError),
#[class(inherit)]
Io(Error),
#[class(inherit)]
Nix(JsNixError),
#[class(inherit)]
#[allow(dead_code)]
Other(JsErrorBox),
}
impl std::fmt::Display for TtyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TtyError::Resource(err) => write!(f, "{err}"),
TtyError::Io(err) => write!(f, "{err}"),
TtyError::Nix(err) => write!(f, "{err}"),
TtyError::Other(err) => write!(f, "{err}"),
}
}
}
impl std::error::Error for TtyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
TtyError::Resource(err) => Some(err),
TtyError::Io(err) => Some(err),
TtyError::Nix(err) => Some(err),
TtyError::Other(err) => Some(err),
}
}
}
impl From<deno_core::error::ResourceError> for TtyError {
fn from(err: deno_core::error::ResourceError) -> Self {
TtyError::Resource(err)
}
}
impl From<Error> for TtyError {
fn from(err: Error) -> Self {
TtyError::Io(err)
}
}
#[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)?;
fn prepare_stdio() {
unsafe {
use libc::atexit;
use libc::tcgetattr;
use libc::tcsetattr;
use libc::termios;
use std::sync::OnceLock;
static ORIG_TERMIOS: OnceLock<Option<termios>> = OnceLock::new();
ORIG_TERMIOS.get_or_init(|| {
let mut termios = std::mem::zeroed::<termios>();
if tcgetattr(libc::STDIN_FILENO, &mut termios) == 0 {
extern "C" fn reset_stdio() {
unsafe {
if let Some(Some(termios)) = ORIG_TERMIOS.get() {
tcsetattr(libc::STDIN_FILENO, 0, termios)
} else {
-1
}
};
}
atexit(reset_stdio);
return Some(termios);
}
None
});
}
}
prepare_stdio();
let tty_mode_store = state.borrow::<TtyModeStore>().clone();
let previous_mode = tty_mode_store.get(rid);
let raw_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(handle_or_fd) };
if is_raw {
let mut raw = match previous_mode {
Some(mode) => mode,
None => {
let original_mode =
termios::tcgetattr(raw_fd).map_err(|e| TtyError::Nix(JsNixError(e)))?;
tty_mode_store.set(rid, original_mode.clone());
original_mode
}
};
raw.input_flags &= !(termios::InputFlags::BRKINT
| termios::InputFlags::ICRNL
| termios::InputFlags::INPCK
| termios::InputFlags::ISTRIP
| termios::InputFlags::IXON);
raw.control_flags |= termios::ControlFlags::CS8;
raw.local_flags &= !(termios::LocalFlags::ECHO
| termios::LocalFlags::ICANON
| termios::LocalFlags::IEXTEN);
if !cbreak {
raw.local_flags &= !(termios::LocalFlags::ISIG);
}
raw.control_chars[termios::SpecialCharacterIndices::VMIN as usize] = 1;
raw.control_chars[termios::SpecialCharacterIndices::VTIME as usize] = 0;
termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &raw)
.map_err(|e| TtyError::Nix(JsNixError(e)))?;
} else {
if let Some(mode) = tty_mode_store.take(rid) {
termios::tcsetattr(raw_fd, termios::SetArg::TCSADRAIN, &mode)
.map_err(|e| TtyError::Nix(JsNixError(e)))?;
}
}
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,
}
#[allow(dead_code)]
pub fn console_size(std_file: &std::fs::File) -> Result<ConsoleSize, std::io::Error> {
use std::os::unix::io::AsRawFd;
let fd = std_file.as_raw_fd();
console_size_from_fd(fd)
}
fn console_size_from_fd(fd: std::os::unix::prelude::RawFd) -> Result<ConsoleSize, std::io::Error> {
unsafe {
let mut size: libc::winsize = std::mem::zeroed();
if libc::ioctl(fd, libc::TIOCGWINSZ, &mut size as *mut _) != 0 {
return Err(Error::last_os_error());
}
Ok(ConsoleSize { cols: size.ws_col as u32, rows: size.ws_row as u32 })
}
}
deno_error::js_error_wrapper!(ReadlineError, JsReadlineError, |err| {
match err {
ReadlineError::Io(e) => e.get_class(),
ReadlineError::Eof => GENERIC_ERROR.into(),
ReadlineError::Interrupted => GENERIC_ERROR.into(),
ReadlineError::Errno(e) => JsNixError(*e).get_class(),
_ => 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(Some(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)),
}
}