Skip to content

Commit

Permalink
fix(runtime): Make native modal keyboard interaction consistent with …
Browse files Browse the repository at this point in the history
…browsers (denoland#18453)

Fixes denoland#18223.
Fixes denoland#21477

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
Co-authored-by: David Sherret <dsherret@gmail.com>
  • Loading branch information
3 people authored Dec 13, 2023
1 parent 5a91a06 commit 346d812
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 100 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
179 changes: 147 additions & 32 deletions cli/tests/integration/run_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
});
}

Expand Down
21 changes: 0 additions & 21 deletions cli/tests/testdata/run/066_prompt.ts

This file was deleted.

1 change: 1 addition & 0 deletions runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
107 changes: 62 additions & 45 deletions runtime/js/41_prompt.js
Original file line number Diff line number Diff line change
@@ -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 };
Loading

0 comments on commit 346d812

Please sign in to comment.