diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 667b1fbbac..d8e226699c 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -815,6 +815,10 @@ declare namespace Deno { windowChange: () => SignalStream; }; + export type SetRawOptions = { + cbreak: boolean; + }; + /** **UNSTABLE**: new API, yet to be vetted * * Set TTY to be under raw mode or not. In raw mode, characters are read and @@ -823,11 +827,19 @@ declare namespace Deno { * Reading from a TTY device in raw mode is faster than reading from a TTY * device in canonical mode. * + * The `cbreak` option can be used to indicate that characters that correspond + * to a signal should still be generated. When disabling raw mode, this option + * is ignored. This functionality currently only works on Linux and Mac OS. + * * ```ts - * Deno.setRaw(myTTY.rid, true); + * Deno.setRaw(myTTY.rid, true, { cbreak: true }); * ``` */ - export function setRaw(rid: number, mode: boolean): void; + export function setRaw( + rid: number, + mode: boolean, + options?: SetRawOptions, + ): void; /** **UNSTABLE**: needs investigation into high precision time. * diff --git a/cli/ops/tty.rs b/cli/ops/tty.rs index 52c89ee11c..be1d7d3e4d 100644 --- a/cli/ops/tty.rs +++ b/cli/ops/tty.rs @@ -5,6 +5,7 @@ use super::io::StreamResource; use super::io::StreamResourceHolder; use deno_core::error::bad_resource_id; use deno_core::error::last_os_error; +use deno_core::error::not_supported; use deno_core::error::resource_unavailable; use deno_core::error::AnyError; use deno_core::serde_json; @@ -15,8 +16,6 @@ use deno_core::ZeroCopyBuf; use serde::Deserialize; use serde::Serialize; -#[cfg(unix)] -use deno_core::error::not_supported; #[cfg(unix)] use nix::sys::termios; @@ -53,10 +52,17 @@ pub fn init(rt: &mut deno_core::JsRuntime) { super::reg_json_sync(rt, "op_console_size", op_console_size); } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct SetRawOptions { + cbreak: bool, +} + #[derive(Deserialize)] struct SetRawArgs { rid: u32, mode: bool, + options: SetRawOptions, } fn op_set_raw( @@ -69,6 +75,7 @@ fn op_set_raw( let args: SetRawArgs = serde_json::from_value(args)?; let rid = args.rid; let is_raw = args.mode; + let cbreak = args.options.cbreak; // From https://github.com/kkawakam/rustyline/blob/master/src/tty/windows.rs // and https://github.com/kkawakam/rustyline/blob/master/src/tty/unix.rs @@ -86,6 +93,9 @@ fn op_set_raw( if resource_holder.is_none() { return Err(bad_resource_id()); } + if cbreak { + return Err(not_supported()); + } let resource_holder = resource_holder.unwrap(); // For now, only stdin. @@ -164,15 +174,13 @@ fn op_set_raw( } }; - if maybe_tty_mode.is_some() { - // Already raw. Skip. - return Ok(json!({})); + if maybe_tty_mode.is_none() { + // Save original mode. + let original_mode = termios::tcgetattr(raw_fd)?; + maybe_tty_mode.replace(original_mode); } - let original_mode = termios::tcgetattr(raw_fd)?; - let mut raw = original_mode.clone(); - // Save original mode. - maybe_tty_mode.replace(original_mode); + let mut raw = maybe_tty_mode.clone().unwrap(); raw.input_flags &= !(termios::InputFlags::BRKINT | termios::InputFlags::ICRNL @@ -184,8 +192,10 @@ fn op_set_raw( raw.local_flags &= !(termios::LocalFlags::ECHO | termios::LocalFlags::ICANON - | termios::LocalFlags::IEXTEN - | termios::LocalFlags::ISIG); + | 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)?; diff --git a/cli/rt/40_tty.js b/cli/rt/40_tty.js index b50b7668ce..598d332376 100644 --- a/cli/rt/40_tty.js +++ b/cli/rt/40_tty.js @@ -11,8 +11,13 @@ return core.jsonOpSync("op_isatty", { rid }); } - function setRaw(rid, mode) { - core.jsonOpSync("op_set_raw", { rid, mode }); + const DEFAULT_SET_RAW_OPTIONS = { + cbreak: false, + }; + + function setRaw(rid, mode, options = {}) { + const rOptions = { ...DEFAULT_SET_RAW_OPTIONS, ...options }; + core.jsonOpSync("op_set_raw", { rid, mode, options: rOptions }); } window.__bootstrap.tty = { diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 8ad4ea5ed8..1f40acbcb4 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -218,6 +218,39 @@ pub fn test_raw_tty() { } } +#[cfg(unix)] +#[test] +pub fn test_raw_tty_cbreak() { + use std::io::{Read, Write}; + use util::pty::fork::*; + let deno_exe = util::deno_exe_path(); + let root_path = util::root_path(); + let fork = Fork::from_ptmx().unwrap(); + + if let Ok(mut master) = fork.is_parent() { + let mut obytes: [u8; 100] = [0; 100]; + let mut nread = master.read(&mut obytes).unwrap(); + assert_eq!(String::from_utf8_lossy(&obytes[0..nread]), "S"); + master.write_all(&[3]).unwrap(); // send SIGINT + master.flush().unwrap(); + nread = master.read(&mut obytes).unwrap(); + assert_eq!(String::from_utf8_lossy(&obytes[0..nread]), "A"); + fork.wait().unwrap(); + } else { + // Keep echo enabled such that 'C^' would be printed in non-raw mode. + std::env::set_current_dir(root_path).unwrap(); + let err = exec::Command::new(deno_exe) + .arg("run") + .arg("--unstable") + .arg("--quiet") + .arg("--no-check") + .arg("cli/tests/raw_mode_cbreak.ts") + .exec(); + println!("err {}", err); + unreachable!() + } +} + #[test] fn test_pattern_match() { // foo, bar, baz, qux, quux, quuz, corge, grault, garply, waldo, fred, plugh, xyzzy diff --git a/cli/tests/raw_mode_cbreak.ts b/cli/tests/raw_mode_cbreak.ts new file mode 100644 index 0000000000..6506e89d7f --- /dev/null +++ b/cli/tests/raw_mode_cbreak.ts @@ -0,0 +1,15 @@ +Deno.setRaw(0, true); +Deno.setRaw(0, true, { cbreak: true }); // Can be called multiple times + +const signal = Deno.signals.interrupt(); + +Deno.stdout.writeSync(new TextEncoder().encode("S")); + +await signal; + +Deno.stdout.writeSync(new TextEncoder().encode("A")); + +signal.dispose(); + +Deno.setRaw(0, false); // restores old mode. +Deno.setRaw(0, false); // Can be safely called multiple times