Skip to content
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

Read one byte from Deno.stdin without hitting ENTER. #3614

Closed
fakoua opened this issue Jan 7, 2020 · 16 comments
Closed

Read one byte from Deno.stdin without hitting ENTER. #3614

fakoua opened this issue Jan 7, 2020 · 16 comments

Comments

@fakoua
Copy link

fakoua commented Jan 7, 2020

Hi,
Is there a way to read from Deno.stdin without hitting Enter? similar to process.stdin.on('EVENT' ... in node js?

For example, writing a program that print the uppercase of the char the user enters
a->A b->B .... just as an example.

Thanks

@lucacasonato
Copy link
Member

lucacasonato commented Jan 8, 2020

No, not right now. This is a good point though - it's not possible due to how stdin is implemented internally.


We use tokio::io::stdin() internally, which uses Rust's std::io::stdin(), which returns full lines rather than single characters. What we would have to use to make this possible would be to use std::io::stdin().bytes() instead. Because every read is a seperate op call this could cause a performance hit as we would have to allocate a Uint8Array, send an op, schedule an async callback, and receive the result for every single character.

For files we also read line by line rather than char by char, so at least this behaviour is consistent across readers. For reference, Go also does not have a way to read single characters from stdin in the standard library - you need external modules for that.

/cc @ry

@ry
Copy link
Member

ry commented Jan 8, 2020

I think we need something like Deno.stdin.set_raw() to disable the line buffering

@lucacasonato
Copy link
Member

I don't know if a method on stdin would be the best option as we would then have to replace the underlying StreamResource in the resource table, something that currently is not supported by the resource table atm. Maybe instead we should open a raw version of stdin on rid 4 on startup, and make it available as Deno.stdin.raw. An alternate approach would be to make raw an async method on Deno.stdin, and only open the raw reader once it is needed.

If any of that sounds good I can implement it.

@bartlomieju
Copy link
Member

Won't Deno.read() with 1-byte buffer solve the problem?

@lucacasonato
Copy link
Member

Won't Deno.read() with 1-byte buffer solve the problem?

No, the buffering happens at the lowest level, inside Rust's std::io::stdin(). If you read with a 1-byte buffer you only get one char, but only after the whole line has passed the buffer (i.e. when you press ENTER or linebreak).

Example here:

const decoder = new TextDecoder()
const file = Deno.stdin;
while (true) {
  const c = new Uint8Array(1)
  if (await file.read(c) == Deno.EOF) {
    break
  }
  const char = decoder.decode(c);
  console.log(char.toUpperCase())
}

@bartlomieju
Copy link
Member

Won't Deno.read() with 1-byte buffer solve the problem?

No, the buffering happens at the lowest level, inside Rust's std::io::stdin(). If you read with a 1-byte buffer you only get one char, but only after the whole line has passed the buffer (i.e. when you press ENTER or linebreak).

Example here:

const decoder = new TextDecoder()
const file = Deno.stdin;
while (true) {
  const c = new Uint8Array(1)
  if (await file.read(c) == Deno.EOF) {
    break
  }
  const char = decoder.decode(c);
  console.log(char.toUpperCase())
}

All clear, thanks for great example 👍

@hayd
Copy link
Contributor

hayd commented Jan 8, 2020

Related: #3416

@Naragod
Copy link

Naragod commented Apr 5, 2020

Won't Deno.read() with 1-byte buffer solve the problem?

No, the buffering happens at the lowest level, inside Rust's std::io::stdin(). If you read with a 1-byte buffer you only get one char, but only after the whole line has passed the buffer (i.e. when you press ENTER or linebreak).

Example here:

const decoder = new TextDecoder()
const file = Deno.stdin;
while (true) {
  const c = new Uint8Array(1)
  if (await file.read(c) == Deno.EOF) {
    break
  }
  const char = decoder.decode(c);
  console.log(char.toUpperCase())
}

As temporary solution, would it be possible to simulate ENTER on every character press?

@p8nut
Copy link

p8nut commented May 17, 2020

Hy,
I think the problem doesn't come from the read but from the way a terminal transmit data by default.

A terminal by default is set to work in canonical mode.
In this mode (as explain here), data are made available when:

  • a line delimiter is in the buffer
  • line editing is enable
  • the buffer is full

if you want / need to change this behavior; you need to update the setup of the terminal (and not forget to reset it at the end) by using termcap command.

But in our case we do not need to take care of that.

For instance with this exemples which read a buffer of 20 bytes we should be able to see data incoming in the buffer as soon as they are sent:

// main.ts
async function main() {
  const fd = Deno.stdin;
  const buffer = new Uint8Array(20);
  let readed: number | null = null;
  while (true) {
    readed = await fd.read(buffer);
    if (readed === null || readed === 0) {
      break;
    }
    console.log(buffer);
    buffer.fill(0);
  }
}

main();

in this following example you program will receive 'hello' then '\n' then 'world'.

>(echo -ne 'hello'; sleep 1; echo -ne '\n'; sleep 1; echo 'world') | deno run main.ts

in the following one; it will receive 'hello\nwor' then 'ld'

>(echo -ne 'hello\nwor'; sleep 1; echo 'ld') | deno run main.ts

this two example illustrate that the read is not in cause.

you could also simulate this behavior in a terminal (by hand) without a pipe by hitting ctrl-d;
the ctrl-d will force the terminal to send any data in it's internal buffer to your program.
if the buffer is empty, ctrl-d will close stdin.

so if you want to read one byte from stdin, you should set the terminal to no canonical and read a buffer of 1

if we want to be able to control the terminal behavior from Deno we might need to implement a terminal interface like in rust here or here
in nodejs some package aim to control the terminal through ncurses (which is a 'high' level library to control terminals) see blessed or some more direct termcap wrapper here

maybe some readings about terminals and termcaps:

Thanks for reading.
hope it help

@millette
Copy link

I'm just starting with deno and have no experience with rust, but it seems like #3958 is a step towards this, no? Although it might only be unstable for now #4925.

Coming from nodejs, I am looking for the equivalent of https://github.com/TooTallNate/keypress.

@bfaulk96
Copy link

With the current limitations, am I correct in assuming there's no way to get a user's password in the terminal? It appears that there isn't currently any capabilities available to mask or hide stdin, and you can't work around it by overwriting the current line on each keypress since you can only retrieve the bytes after hitting ENTER... This is a rather unfortunate limitation

@zored
Copy link

zored commented Jun 1, 2020

For now you may rely on unstable Deno.setRaw:

#!/usr/bin/env -S deno run --unstable
const buffer = new Uint8Array(1);
Deno.setRaw(0, true);
await Deno.stdin.read(buffer);
Deno.setRaw(0, false);
console.log(buffer);

@millette
Copy link

millette commented Jun 2, 2020

See https://github.com/caspervonb/deno-prompts and #6024

@millette
Copy link

Yay https://github.com/dmitriytat/keypress/tree/master thanks to @dmitriytat

@nycki93
Copy link

nycki93 commented Dec 26, 2023

For future readers: this is part of stable as of Deno 1.27, and it's moved to Deno.stdin.setRaw(true). #15796

Deno.stdin.setRaw(true);
let chunk = new Uint8Array(0);
const reader = Deno.stdin.readable.getReader();
while (true) {
    if (chunk.length === 0) {
        const readResult = await reader.read();
        if (readResult.done) break;
        chunk = readResult.value;
        continue;
    }
    const key = chunk[0];
    chunk = chunk.slice(1);
    console.log(key);
    if (key === 3) break; // ctrl-c
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests