bat: Keyboard arrow keys not working for scrolling from STDIN, rather shows keycode of the key pressed
What version of bat are you using?
bat 0.15.0
Describe the bug you encountered: Using keys to scroll through the long STDIN is emitting keycodes, rather than actually scrolling down. Some terminal details:-
terminal
--------
kitty 0.17.4 created by Kovid Goyal
exports(relevant ones)
---------------------
export BAT_PAGER="less -RF"
export EDITOR="nvim"
export TERMINAL="kitty"
Context
I was trying to pass the stdin from the node/npm package makemehapi to bat.
npx makemehapi print | bat
The content was a lengthy output. Thus i tried scrolling with arrow keys like i usually do. But currently the keycodes of the key pressed is displayed rather than actually scrolling down.
- Pressing Enter key somehow forwards down the line, but still some junk characters are added
cat longFile | batworks just fine. Arrow keys work there.
Content with paging and error - keycodes of arrow keys displayed at bottom

Content with paging and --show-all and error - keycodes of arrow keys displayed at bottom

Things I have tried
- commented the whole bashrc and error persists
- tried
bat --pager=neverstill no use - tried
bat --no-configstill no use
…
Describe what you expected to happen? … To be able to scroll through the long content using arrow keys or even Page Down/UP keys without having to press ENTER for each line.
How did you install bat?
yay -S bat
Info.sh
system
$ uname -srm Linux 5.6.13-arch1-1 x86_64
bat
$ bat --version bat 0.15.0
$ env BAT_PAGER=less -RF
bat_config
$ cat /home/ikigai/.config/bat/config
--pager="less -RF"
bat_wrapper
No wrapper script for ‘bat’.
bat_wrapper_function
No wrapper function for ‘bat’.
No wrapper function for ‘cat’.
tool
$ less --version less 551 (PCRE regular expressions)
Full STDOUT of npx makemehapi print
Create a server which responds to requests to /?name=Handling using a
template located at templates/index.html which outputs the following HTML:
<html>
<head><title>Hello Handling</title></head>
<body>
Hello Handling
</body>
</html>
─────────────────────────────────────────────────────────────────────────────
##HINTS
This exercise requires you to install the vision module, which is a hapi
plugin for rendering templates. You'll need to register the plugin in your
code in order to render your templates:
var Vision = require('vision');
await server.register(Vision);
The view key can be used to define the template to be used to generate the
response.
handler: {
view: "index.html"
}
server.views() is the server method that we use to configure the templates
used on our server. This method receives a configuration object in which
we can set different engines based on file extension. This object can also
set a directory path for your templates.
server.views({
engines: {
html: require('handlebars')
},
path: Path.join(__dirname, 'templates')
});
In this exercise, we'll be using Handlebars. To install handlebars:
npm install handlebars
With Handlebars templates, you can render a variable directly in HTML by
surrounding the variable with curly braces, e.g. {{foo}}.
The template receives some information from the request. For example, the
query parameters that were passed in via the URL are available in the
query object. These parameters can then be used in the template. Query
params get automatically parsed and aren't declared in the route path.
<div>{{query.paramName}}</div>
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 4
- Comments: 19 (7 by maintainers)
Commits related to this issue
- Fix #1006 by loading assets lazily to start pager quicker — committed to Enselic/bat by Enselic 4 years ago
Sorry for my slow pace, but I have a full-time job and a family, so spare time is a bit limited 😉
After quite a bit of research and debugging involving custom builds with custom debug prints in both
nodeandless, I now have a firm grasp of what’s going on. I can explain all the behavior we see. But first the main point:Summary =======
This issue should be fixable, or at least greatly mitigated, by making
batstart its pager much earlier.Details =====
Primer on input from terminals
Terminals can be in many modes, but the most common are commonly called “raw mode” and “canonical mode”. In “raw mode”, a
read()call on a terminal file descriptor will return what was typed as soon as a key is pressed, and typed characters will not be echoed back. In “canonical mode”, theread()call wait for a newline character to be typed before returning what was typed, and characters are echoed back to the terminal as they are typed.Note that the terminal mode is per-terminal. This means that if two processes use the same terminal, the terminal mode is shared, even if different file descriptors in different processes are used.
How less uses terminal modes
When
lessstarts, it will open a file descriptor to/dev/tty, i.e. the current terminal. Then it stores the current terminal mode in a global variable, and then change the terminal mode to “raw mode”. Then it starts waiting for key presses withread(). Changing to “raw mode” is what makes it possible to scroll inlesswith individual KeyDown presses. Without “raw mode”, it would be necessary to press KeyDown and then Return to scroll down one line. Just before thelessprocess quits, it will reset the terminal mode to what it was whenlesswas started. This is necessary since the terminal mode is per-terminal, and not per-process. It is simply a necessary restoration of global, shared state.How node uses terminal modes
When
nodestarts, it will also store the current terminal mode. It will do it for stdin and stderr, but since those are bound to the current terminal, it will be the same terminal aslessis using. Then, right beforenodeexists, it will reset the terminal mode to what it was when node started, since it might have changed the modes due to JS scripts. This is good practice and it should not stop doing that. As mentioned above,lessalso behaves like this.How the original problem occurs
The problem occurs like this. Note that
lessandnodeoperates independently, so this is all very racy.nodereads current terminal mode as “canonical mode” and stores that to restore it laterlessreads current terminal mode as “canonical mode” and stores that to restore it laterlesschanges current terminal mode to “raw mode”nodeexits and restore current terminal mode to “canonical mode”lesscallsread(), the method will block until a newline is typed.lessexits and restore current terminal mode to “canonical mode” (which it already is in)How making bat start less quicker will fix the problem
Assuming
batcan startlessas good as quick as pureless, this is what will happen:lessreads current terminal mode as “canonical mode” and stores that to restore it laterlesschanges current terminal mode to “raw mode”nodereads current terminal mode as “raw mode” and stores that to restore it laternodeexits and restore current terminal mode to “raw mode”lesscallsread(), it will return as soon as e.g. KeyDown is pressed, since terminal mode is still “raw mode”lessexits and restore current terminal mode to “canonical mode”Explanation of reproducibility examples
The reason
node -e 'for (var i = 0; i <= 10000; i++) { console.log(i); }' | batfails is the same reason as above.The reason
node -e 'for (var i = 0; i <= 1000; i++) { console.log(i); }' | bat’ is NOT buggy is becausenodestarts and exits fully BEFORElesseven starts. Sonodecan’t mess up the terminal mode forless, since they run in series and not in parallel.The reason
node -e 'for (var i = 0; i <= 100000; i++) { console.log(i); } | bat’ is NOT buggy is… false. It is actually buggy. It’s just that you have to scroll down enough lines (in my case 88000) to makenodeexit. If you don’t scroll down,nodewill wait for thestdoutbuffer to leave room for more data, and not quit. As soon asnodehas no more data to write, it will quit, and the bug will occur.The reason
node -e 'for (var i = 0; i <= 20000; i++) { console.log(i); }' | lessis not buggy is thatlessstarts quick enough to change the terminal mode beforenodereads it. Sincelessis quick enough to change the terminal modenodewill restore the terminal mode to “raw mode” when it exits, since that was what it was when it started. Sincelesswas so quick… This is the working scenario that I describe above.You will get the same bug with pure
lessif you just delay the start ofless. Try this and scroll down to the bottom, and you will see that purelessalso is “buggy”:node -e 'for (var i = 0; i <= 20000; i++) { console.log(i); }' | (sleep 1; less)(assuming your system doesn’t have big enough stdin buffer with room for all 20000 lines fromnode).The reason that
std::thread::sleep(std::time::Duration::from_millis(10));is needed is to makelessstart slow enough to givenodetime to read the current terminal mode as “canonical mode”. If you remove the sleep,lessis able to start and change terminal mode beforenodereads it the first time, so everything works. I.e. the working scenario I describe above.Closing words of this comment
Ok, that was a mouthful… Let me know if you have any questions, I will gladly elaborate on the details or answer any other questions you might have.
I think the next step here is a proof-of-concept patch that makes
batstartlessmuch quicker, which should solve the issue. Maybe I’ll take a stab at it. We’ll see 😃Sorry for somewhat spamming this bug report, but I’ve started to obsess about getting to the bottom of this bug.
Latest news is that this seems to be a regression in
node v12.5.0. I can’t reproduce withnode v12.4.0, but can easily reproduce innode v12.5.0.Looking at the changelog of
node v12.5.0I found this very suspicious bug fix: https://github.com/nodejs/node/pull/24260 src: restore stdio on program exitThey are messing around with file descriptors 0-2, i.e. stdin among other things. And since they ostensibly do it upon exiting node, it would explain the dependence upon specific timing (the exit of node) and thus the need to have sleeps in the repro code.
I’ll see if I can come up with a convincing bug report for the node project. To be continued…
I came up with the following when looking for a minimal node program. Seems to be something intrinsic to
node, and somehow timing/buffer-size specific, because I can reproduce the problem with this:but if I change the end condition to either 1000 or 100000, i.e. either higher or lower, I can not reproduce! However, taking
nodeout of the equation, all numbers work fine: 1000, 10000 and 100000:So, very strange. Do you see the same thing? Maybe the number used to reproduce depends on how fast your machine is or something weird like that. For the record, I use the following versions, on a MacBook Pro from 2018.
OS: Linux (Arch) | bat: 0.15.4
I have the same issue with the output of several Node.js (v14.9.0) programs, e.g.
Doesn’t happen with all Node.js programs, though. e.g. these are fine:
It’s not caused by a specific third-party dependency because, e.g., pidtree doesn’t have any.
Hi, I got the same issue today with the AWS
cdkcli when piping the output ofcdk synthto batI love it… keep investigating 😄 👍