10

I have the following nodejs program

import readline from 'readline'

readline.createInterface({input: process.stdin, output: process.stdout}).prompt()

process.stdin.on('keypress', (letter, key) => console.log({letter: letter, key: key}))

This works most of the time e.g. if I press SHIFT+X, it correctly outputs key.shift === True:

{
  letter: 'X',
  key: { sequence: 'X', name: 'x', ctrl: false, meta: false, shift: true }
}

But, if I press SHIFT+ENTER, the key.shift is never true:

{
  letter: '\r',
  key: {sequence: '\r', name: 'return', ctrl: false, meta: false, shift: false}
}

Why? How do I detect SHIFT+ENTER using readline in node.js?

What am I trying to do? I am building a terminal prompt app and thus the readline module is very handy since it comes with auto-completor, history, pagination, prompt prefix etc. But, I want to be able to enter multiline prompts too.

pathikrit
  • 32,469
  • 37
  • 142
  • 221
John Doe
  • 2,225
  • 6
  • 16
  • 44

5 Answers5

3

The why:

The reason you can't get the shift key state from stdin is because the shift key is not a byte that goes into stdin, which is just a stream of bytes and doesn't directly interact with the keyboard.

What's actually input:

For example if you were to run the following code and press Shift+Enter (then Ctrl+C):

process.stdin.on('data', d => {
    console.log([d]);
});

You would see something like the following echoed back:

$ node main.mjs

[ <Buffer 0a> ]
^C

As you can see, there is only a single 0x0A or \n character with no indication the Shift key was pressed.

You can repeat this process with other tools, to confirm it's not just a node thing.

$ cat > stdin.txt

^C
$ xxd stdin.txt
00000000: 0a                                       .

Here cat read a single newline character from stdin and wrote it to a file.

Why node stdin keypress events can have shift = true:

If we take a look at emitKeys in lib/internal/readline/utils.js we can see how the shift key is inferred based on certain input. For example, you can see that for alpha-numeric characters, the shift state is inferred based on if the character is upper-case or not. It doesn't actually know what the state of the shift key is, and if you were to paste in a capital A it would assume that shift is true, but not meta or ctrl.

    } else if (RegExpPrototypeExec(/^[0-9A-Za-z]$/, ch) !== null) {
      // Letter, number, shift+letter
      key.name = StringPrototypeToLowerCase(ch);
      key.shift = RegExpPrototypeExec(/^[A-Z]$/, ch) !== null;
      key.meta = escaped;

Essentially it's faking an event that resembles what you might see in the browser.

What you would have to do:

To know the actual state of the shift key you would have to query that information from the OS. You would likely need a native code library with implementations for every OS you wish to support, using whatever system API's are available in each OS to query this information. I don't know of any existing 3rd party modules and didn't find one in a search. Also keep in mind this would not work over ssh.

This is why multiline input into command prompts typically require escaping the newline character, or have some way of detecting the expression was incomplete.

As I understand it some terminal apps like iTerm apps can also detect the pasting of multiline input and buffer it together in a single write to stdin, but those apps have access to more than just stdin to achieve this.

Alexander O'Mara
  • 58,688
  • 18
  • 163
  • 171
0

OP mentioned, that he would need shift+enter key combination in order to enter multiline input. I would like to mention a different approach, which might or might not suite the OP.

I suggest that you should be only using the enter key for multiline input, and let the program logic decide whether to move to a new multiline, or, send the input for processing.

Below is the program (if I understood you correctly), I still don't know if this approach is practical or not. But I was able to write a hello world function using this approach.

const readline = require("readline")

let userInput = [];
let indentation = 0;

let rl = readline.createInterface({
   input: process.stdin,
   output: process.stdout
})

rl.on('line', (input) => {
   // if end of line is "(" or "{", indent next line
   if (input.trimEnd().slice(-1).match(/({|\()/)) {
       indentation = indentation + 15
   } else if (input.trimEnd().slice(-1).match(/(}|\))/)) {
       (indentation > 14) && (indentation = indentation - 15)

   readline.moveCursor(process.stdout, indentation)
   userInput.push(input + "\n")

   //send empty input to see your entered code snippet;
   if (input === '') {
      indentation = 0
      console.log("user input\n", userInput.join(''))
      console.log("no input")
   }
}
});

//This is code is just to prove the idea, please ignore any inconsistencies.

I understand, that this approach would bring more complexity to your code, as you have to parse user input at every step.

deepanshu
  • 625
  • 5
  • 15
-1

Have you tried mapping Shift + Enter to some other sequence and then check if it works.

I don't think what you're mentioning is possible just with the readline npm package, it might involve some tinkering with the specific terminal but not really useful for a generic solution.

By default Enter and Shift+Enter (as well as Ctrl+v and Ctrl+Shift+m) all generate the identical key sequence ^M (at least in most common terminal emulators). zsh (as well as other shells) do not act on key bindings but rather on key sequences received from the terminal. Converting key presses and combinations into key sequences is the responsibility of the terminal.

Some terminal emulators allow to configure the key sequences sent. For example iTerm2 allows you to set customized key bindings that send escape sequences (in Profile > Keys), you should be able to define a sequence for Shift+Enter there, e.g. [[SE and can then make the appropriate settings in zsh: bindkey '^[[[SE' 'accept-and-hold'.

  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/34291242) – Aaron Meese May 01 '23 at 14:35
  • Just take a look at all answers in this thread and the author's response that basically just says "no" to every provided answer. How do you expect new people to get reputation if such malafide actions are allowed on the platform? – pallasite99 May 02 '23 at 10:38
  • @pallasite99: I am the author. I upvoted the answer by Alexander (current top voted one). You should learn from that answer as its a clear answer of the mechanism and verification of why this won't work. Your answer of trying to "map SHIFT+ENTER" is not clear and is terminal specific. – pathikrit May 02 '23 at 12:37
  • I have mentioned the "why it won't work" part similar to the other answer. The `readline` module is not able to do that by itself. I don't know what else you expected as an answer here - but as long as you found your answer, that's the part which is important. I just wanted to make it clear that some research went into figuring out this from my side, but when I'm just greeted by a downvote after that then you can maybe understand. Nothing personal, no offense taken. Cheers – pallasite99 May 02 '23 at 12:54
-2

Don't rely on the keypress event of readline, it seems incomplete and only able to detect one character once, caused that recognize uppercase "F" as shift+f when you pressed in caps lock mode.

You can find the source code: emitKeys.

LitileXueZha
  • 512
  • 6
  • 11
  • Sure, but this does not answer the question. What should I do? – pathikrit Apr 25 '23 at 13:38
  • @pathikrit Write your own c++ native module, use OS-specific API to detect it. See [this question](https://stackoverflow.com/questions/41600981/how-do-i-check-if-a-key-is-pressed-on-c). – LitileXueZha Apr 25 '23 at 14:16
  • > it seems incomplete and only able to detect one character once But that's not true - it does detect `SHIFT+X` correctly (see `key.shift` in original post) > Write your own c++ native module, use OS-specific API to detect it This exists in various node.js libraries. See: https://github.com/enquirer/enquirer/commit/05af0863fc29d43f4273be1497bdcc7bbe260188 and https://github.com/tunnckoCore/prompt-promise/blob/fc9c881a5fabbe467722642ebdcf66a826c3e296/index.js#L45-L66 Not sure why I need to do C++ here – pathikrit Apr 25 '23 at 14:44
  • @pathikrit As I said, you will get `key.shift==false` when you had pressed capslock key. You got `key.shift==true` just because it is uppercase. – LitileXueZha Apr 25 '23 at 16:56
  • Sure, but it still does not answer how I can detect in SHIFT+ENTER in nodejs. Your suggestion to use C++ is not an acceptable Stackoverflow answer without a working prototype – pathikrit Apr 25 '23 at 17:07
-2

This is a tested script in node.js to detect the shift + enter in terminal.

const keySender = require("node-key-sender");

process.stdin.setRawMode(true);

process.stdin.on("data", function (data) {
    if (data[0] === 27 && data[1] === 91 && data[2] === 50 && data[3] === 52 && data[4] === 126 && data[5] === 99) {
        console.log("Shift + Enter detected!");
        keySender.sendKey("shift", "enter");
    }
});

console.log("Listening for input...");
Dhaval Italiya
  • 386
  • 2
  • 7