diff options
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | cli/Cargo.toml | 2 | ||||
-rw-r--r-- | cli/tests/integration/run_tests.rs | 179 | ||||
-rw-r--r-- | cli/tests/testdata/run/066_prompt.ts | 21 | ||||
-rw-r--r-- | runtime/Cargo.toml | 1 | ||||
-rw-r--r-- | runtime/js/41_prompt.js | 107 | ||||
-rw-r--r-- | runtime/ops/tty.rs | 36 |
8 files changed, 249 insertions, 100 deletions
diff --git a/Cargo.lock b/Cargo.lock index 953b8bce5..e5c811f92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1618,6 +1618,7 @@ dependencies = [ "once_cell", "regex", "ring", + "rustyline", "serde", "signal-hook-registry", "termcolor", @@ -4843,6 +4844,7 @@ dependencies = [ "cfg-if", "clipboard-win", "fd-lock", + "home", "libc", "log", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 555fdf7cb..bc0bcf5b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ rustls = "0.21.8" rustls-pemfile = "1.0.0" rustls-tokio-stream = "=0.2.16" rustls-webpki = "0.101.4" +rustyline = "=13.0.0" webpki-roots = "0.25.2" scopeguard = "1.2.0" saffron = "=0.1.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3371cbebe..7ae5ae2bd 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -121,7 +121,7 @@ quick-junit = "^0.3.5" rand = { workspace = true, features = ["small_rng"] } regex.workspace = true ring.workspace = true -rustyline = { version = "=13.0.0", default-features = false, features = ["custom-bindings", "with-file-history"] } +rustyline.workspace = true rustyline-derive = "=0.7.0" serde.workspace = true serde_repr.workspace = true diff --git a/cli/tests/integration/run_tests.rs b/cli/tests/integration/run_tests.rs index 32df04483..6bba87ae3 100644 --- a/cli/tests/integration/run_tests.rs +++ b/cli/tests/integration/run_tests.rs @@ -2806,40 +2806,155 @@ mod permissions { fn _066_prompt() { TestContext::default() .new_command() - .args_vec(["run", "--quiet", "--unstable", "run/066_prompt.ts"]) + .args_vec(["repl"]) .with_pty(|mut console| { - console.expect("What is your name? [Jane Doe] "); - console.write_line_raw("John Doe"); - console.expect("Your name is John Doe."); - console.expect("What is your name? [Jane Doe] "); - console.write_line_raw(""); - console.expect("Your name is Jane Doe."); + // alert with no message displays default "Alert" + // alert displays "[Press any key to continue]" + // alert can be closed with Enter key + console.write_line_raw("alert()"); + console.expect("Alert [Press any key to continue]"); + console.write_raw("\r"); // Enter + console.expect("undefined"); + + // alert can be closed with Escape key + console.write_line_raw("alert()"); + console.expect("Alert [Press any key to continue]"); + console.write_raw("\x1b"); // Escape + console.expect("undefined"); + + // alert can display custom text + // alert can be closed with arbitrary keyboard key (x) + if !cfg!(windows) { + // it seems to work on windows, just not in the tests + console.write_line_raw("alert('foo')"); + console.expect("foo [Press any key to continue]"); + console.write_raw("x"); + console.expect("undefined"); + } + + // confirm with no message displays default "Confirm" + // confirm returns true by immediately pressing Enter + console.write_line_raw("confirm()"); + console.expect("Confirm [Y/n]"); + console.write_raw("\r"); // Enter + console.expect("true"); + + // tese seem to work on windows, just not in the tests + if !cfg!(windows) { + // confirm returns false by pressing Escape + console.write_line_raw("confirm()"); + console.expect("Confirm [Y/n]"); + console.write_raw("\x1b"); // Escape + console.expect("false"); + + // confirm can display custom text + // confirm returns true by pressing y + console.write_line_raw("confirm('continue?')"); + console.expect("continue? [Y/n]"); + console.write_raw("y"); + console.expect("true"); + + // confirm returns false by pressing n + console.write_line_raw("confirm('continue?')"); + console.expect("continue? [Y/n]"); + console.write_raw("n"); + console.expect("false"); + + // confirm can display custom text + // confirm returns true by pressing Y + console.write_line_raw("confirm('continue?')"); + console.expect("continue? [Y/n]"); + console.write_raw("Y"); + console.expect("true"); + + // confirm returns false by pressing N + console.write_line_raw("confirm('continue?')"); + console.expect("continue? [Y/n]"); + console.write_raw("N"); + console.expect("false"); + } + + // prompt with no message displays default "Prompt" + // prompt returns user-inserted text + console.write_line_raw("prompt()"); console.expect("Prompt "); - console.write_line_raw("foo"); - console.expect("Your input is foo."); - console.expect("Question 0 [y/N] "); - console.write_line_raw("Y"); - console.expect("Your answer is true"); - console.expect("Question 1 [y/N] "); - console.write_line_raw("N"); - console.expect("Your answer is false"); - console.expect("Question 2 [y/N] "); - console.write_line_raw("yes"); - console.expect("Your answer is false"); - console.expect("Confirm [y/N] "); - console.write_line(""); - console.expect("Your answer is false"); - console.expect("What is Windows EOL? "); - console.write_line("windows"); - console.expect("Your answer is \"windows\""); - console.expect("Hi [Enter] "); - console.write_line(""); - console.expect("Alert [Enter] "); - console.write_line(""); - console.expect("The end of test"); - console.expect("What is EOF? "); - console.write_line(""); - console.expect("Your answer is null"); + console.write_line_raw("abc"); + console.expect("\"abc\""); + + // prompt can display custom text + // prompt with no default value returns empty string when immediately pressing Enter + console.write_line_raw("prompt('foo')"); + console.expect("foo "); + console.write_raw("\r"); // Enter + console.expect("\"\""); + + // prompt with non-string default value converts it to string + console.write_line_raw("prompt('foo', 1)"); + console.expect("foo 1"); + console.write_raw("\r"); // Enter + console.expect("\"1\""); + + // prompt with non-string default value that can't be converted throws an error + console.write_line_raw("prompt('foo', Symbol())"); + console.expect( + "Uncaught TypeError: Cannot convert a Symbol value to a string", + ); + + // prompt with empty-string default value returns empty string when immediately pressing Enter + console.write_line_raw("prompt('foo', '')"); + console.expect("foo "); + console.write_raw("\r"); // Enter + console.expect("\"\""); + + // prompt with contentful default value returns default value when immediately pressing Enter + console.write_line_raw("prompt('foo', 'bar')"); + console.expect("foo bar"); + console.write_raw("\r"); // Enter + console.expect("\"bar\""); + + // prompt with contentful default value allows editing of default value + console.write_line_raw("prompt('foo', 'bar')"); + console.expect("foo bar"); + console.write_raw("\x1b[D"); // Left arrow + console.write_raw("\x1b[D"); // Left arrow + console.write_raw("\x7f"); // Backspace + console.write_raw("c"); + console.expect("foo car"); + console.write_raw("\r"); // Enter + console.expect("\"car\""); + + // prompt returns null by pressing Escape + console.write_line_raw("prompt()"); + console.expect("Prompt "); + console.write_raw("\x1b"); // Escape + console.expect("null"); + + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + { + // confirm returns false by pressing Ctrl+C + console.write_line_raw("confirm()"); + console.expect("Confirm [Y/n] "); + console.write_raw("\x03"); // Ctrl+C + console.expect("false"); + + // confirm returns false by pressing Ctrl+D + console.write_line_raw("confirm()"); + console.expect("Confirm [Y/n] "); + console.write_raw("\x04"); // Ctrl+D + console.expect("false"); + + // prompt returns null by pressing Ctrl+C + console.write_line_raw("prompt()"); + console.expect("Prompt "); + console.write_raw("\x03"); // Ctrl+C + console.expect("null"); + + // prompt returns null by pressing Ctrl+D + console.write_line_raw("prompt()"); + console.expect("Prompt "); + console.write_raw("\x04"); // Ctrl+D + console.expect("null"); + } }); } diff --git a/cli/tests/testdata/run/066_prompt.ts b/cli/tests/testdata/run/066_prompt.ts deleted file mode 100644 index e3daa7ac0..000000000 --- a/cli/tests/testdata/run/066_prompt.ts +++ /dev/null @@ -1,21 +0,0 @@ -const name0 = prompt("What is your name?", "Jane Doe"); // Answer John Doe -console.log(`Your name is ${name0}.`); -const name1 = prompt("What is your name?", "Jane Doe"); // Answer with default -console.log(`Your name is ${name1}.`); -const input = prompt(); // Answer foo -console.log(`Your input is ${input}.`); -const answer0 = confirm("Question 0"); // Answer y -console.log(`Your answer is ${answer0}`); -const answer1 = confirm("Question 1"); // Answer n -console.log(`Your answer is ${answer1}`); -const answer2 = confirm("Question 2"); // Answer with yes (returns false) -console.log(`Your answer is ${answer2}`); -const answer3 = confirm(); // Answer with default -console.log(`Your answer is ${answer3}`); -const windows = prompt("What is Windows EOL?"); -console.log(`Your answer is ${JSON.stringify(windows)}`); -alert("Hi"); -alert(); -console.log("The end of test"); -const eof = prompt("What is EOF?"); -console.log(`Your answer is ${JSON.stringify(eof)}`); diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 39d907a61..0ae26d811 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -109,6 +109,7 @@ notify.workspace = true once_cell.workspace = true regex.workspace = true ring.workspace = true +rustyline = { workspace = true, features = ["custom-bindings"] } serde.workspace = true signal-hook-registry = "1.4.0" termcolor = "1.1.3" diff --git a/runtime/js/41_prompt.js b/runtime/js/41_prompt.js index e665aae07..d73f9d26b 100644 --- a/runtime/js/41_prompt.js +++ b/runtime/js/41_prompt.js @@ -1,78 +1,95 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. import { core, primordials } from "ext:core/mod.js"; +const ops = core.ops; import { isatty } from "ext:runtime/40_tty.js"; import { stdin } from "ext:deno_io/12_io.js"; -const { ArrayPrototypePush, StringPrototypeCharCodeAt, Uint8Array } = - primordials; -const LF = StringPrototypeCharCodeAt("\n", 0); -const CR = StringPrototypeCharCodeAt("\r", 0); +import { getNoColor } from "ext:deno_console/01_console.js"; +const { Uint8Array, StringFromCodePoint } = primordials; + +const ESC = "\x1b"; +const CTRL_C = "\x03"; +const CTRL_D = "\x04"; + +const bold = ansi(1, 22); +const italic = ansi(3, 23); +const yellow = ansi(33, 0); +function ansi(start, end) { + return (str) => getNoColor() ? str : `\x1b[${start}m${str}\x1b[${end}m`; +} function alert(message = "Alert") { if (!isatty(stdin.rid)) { return; } - core.print(`${message} [Enter] `, false); + core.print( + `${yellow(bold(`${message}`))} [${italic("Press any key to continue")}] `, + ); + + try { + stdin.setRaw(true); + stdin.readSync(new Uint8Array(1024)); + } finally { + stdin.setRaw(false); + } - readLineFromStdinSync(); + core.print("\n"); } -function confirm(message = "Confirm") { +function prompt(message = "Prompt", defaultValue = "") { if (!isatty(stdin.rid)) { - return false; + return null; } - core.print(`${message} [y/N] `, false); - - const answer = readLineFromStdinSync(); - - return answer === "Y" || answer === "y"; + return ops.op_read_line_prompt( + `${message} `, + `${defaultValue}`, + ); } -function prompt(message = "Prompt", defaultValue) { - defaultValue ??= null; +const inputMap = new primordials.Map([ + ["Y", true], + ["y", true], + ["\r", true], + ["\n", true], + ["\r\n", true], + ["N", false], + ["n", false], + [ESC, false], + [CTRL_C, false], + [CTRL_D, false], +]); +function confirm(message = "Confirm") { if (!isatty(stdin.rid)) { - return null; + return false; } - if (defaultValue) { - message += ` [${defaultValue}]`; - } + core.print(`${yellow(bold(`${message}`))} [${italic("Y/n")}] `); - message += " "; + let val = false; + try { + stdin.setRaw(true); - // output in one shot to make the tests more reliable - core.print(message, false); + while (true) { + const b = new Uint8Array(1024); + stdin.readSync(b); + let byteString = ""; - return readLineFromStdinSync() || defaultValue; -} + let i = 0; + while (b[i]) byteString += StringFromCodePoint(b[i++]); -function readLineFromStdinSync() { - const c = new Uint8Array(1); - const buf = []; - - while (true) { - const n = stdin.readSync(c); - if (n === null || n === 0) { - break; - } - if (c[0] === CR) { - const n = stdin.readSync(c); - if (c[0] === LF) { - break; - } - ArrayPrototypePush(buf, CR); - if (n === null || n === 0) { + if (inputMap.has(byteString)) { + val = inputMap.get(byteString); break; } } - if (c[0] === LF) { - break; - } - ArrayPrototypePush(buf, c[0]); + } finally { + stdin.setRaw(false); } - return core.decode(new Uint8Array(buf)); + + core.print(`${val ? "y" : "n"}\n`); + return val; } export { alert, confirm, prompt }; diff --git a/runtime/ops/tty.rs b/runtime/ops/tty.rs index 477af9741..b0047eb85 100644 --- a/runtime/ops/tty.rs +++ b/runtime/ops/tty.rs @@ -5,6 +5,13 @@ use std::io::Error; use deno_core::error::AnyError; use deno_core::op2; use deno_core::OpState; +use rustyline::config::Configurer; +use rustyline::error::ReadlineError; +use rustyline::Cmd; +use rustyline::Editor; +use rustyline::KeyCode; +use rustyline::KeyEvent; +use rustyline::Modifiers; #[cfg(unix)] use deno_core::ResourceId; @@ -43,7 +50,12 @@ use winapi::um::wincon; deno_core::extension!( deno_tty, - ops = [op_stdin_set_raw, op_isatty, op_console_size], + ops = [ + op_stdin_set_raw, + op_isatty, + op_console_size, + op_read_line_prompt, + ], state = |state| { #[cfg(unix)] state.put(TtyModeStore::default()); @@ -320,3 +332,25 @@ mod tests { ); } } + +#[op2] +#[string] +pub fn op_read_line_prompt( + #[string] prompt_text: String, + #[string] default_value: String, +) -> Result<Option<String>, AnyError> { + 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 | ReadlineError::Eof) => Ok(None), + Err(err) => Err(err.into()), + } +} |