Skip to content

fix(runtime): Make native modal keyboard interaction consistent with browsers #18453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d279a07
Rebase and fix tests
lionel-rowe Apr 3, 2023
8b237e9
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
bartlomieju Apr 6, 2023
d31fe0e
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
lionel-rowe Apr 11, 2023
780797d
breaking - throw instead of returning early if !isatty
lionel-rowe Apr 11, 2023
8861563
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
lionel-rowe Sep 2, 2023
d679281
Implement changes from code review
lionel-rowe Sep 2, 2023
dc369c5
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
lionel-rowe Sep 4, 2023
06ebf48
Implement changes from code review
lionel-rowe Sep 3, 2023
88317d4
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
bartlomieju Sep 4, 2023
b79f443
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
bartlomieju Sep 5, 2023
67a5e25
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
bartlomieju Sep 6, 2023
d3b7253
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
lionel-rowe Sep 12, 2023
94e192f
Exclude ctrl+c/ctrl+d tests from macos
lionel-rowe Sep 12, 2023
27d449a
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
lionel-rowe Sep 12, 2023
9c2c740
Fix test on Windows
dsherret Sep 18, 2023
f8f13de
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
dsherret Sep 18, 2023
1332048
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
bartlomieju Dec 6, 2023
6778a12
update rustyline
bartlomieju Dec 6, 2023
ebb3f63
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
bartlomieju Dec 6, 2023
2634dc6
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
bartlomieju Dec 8, 2023
445c895
Merge branch 'main' into alert-prompt-confirm-keyboard-interaction
bartlomieju Dec 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
Comment on lines +30 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're supposed to read only a single key here, why do we need a buffer with size 1024? Could you also explain why you're setting the terminal to raw mode here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're supposed to read only a single key here, why do we need a buffer with size 1024?

A single logical keypress (most notably, combos such as shift+insert/ctrl+v) can send an arbitrarily large amount of input to stdin. 1024 is just an arbitrary size that should typically be large enough to avoid problems but small enough to avoid undue memory usage.

For example, if we used a smaller buffer size like 10:

const { stdin } = Deno

const b = new Uint8Array(10)
stdin.setRaw(true)
stdin.readSync(b)

That can be broken (Deno exits with Error: Io(Kind(InvalidData))) by copying 字字字字 to the clipboard and pasting it with the next keypress, because the 3 UTF-8 bytes of the last lie across the boundary.

Could you also explain why you're setting the terminal to raw mode here?

Raw mode is necessary to capture ESC keypresses and to progress immediately after a key is pressed.

} 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