How to read the keyboard when stdin redirected
Adam D. Ruppe
destructionator at gmail.com
Thu May 14 12:36:12 UTC 2026
On Thursday, 14 May 2026 at 10:11:47 UTC, Darren Drapkin wrote:
> This seems to work, if I remember to press enter.
Yes, the kernel requires you to press enter before it sends info
to the program unless you switch the mode.
Switching the mode is not terribly hard on its own, but once you
are in raw mode, if you want to read things like arrow keys, it
gets pretty complicated. The terminals send all kinds of legacy
garbage down the file you have to parse out.
If you want to use a library for this, well, I kinda just wrote a
whole sample program using mine...
https://github.com/adamdruppe/arsd/
terminal.d and core.d from there all you need.
or it is `arsd-official:terminal` on dub if you use that.
anyway my sample program:
```
import arsd.terminal;
static import std.stdio; // do not want stdio writeln too easy to
access
void main() {
int terminalFd = 0;
if(!Terminal.stdoutIsTerminal) {
throw new Exception("no terminal to write to");
}
string fileContents;
string[] originalFileLines;
if(Terminal.stdinIsTerminal) {
// stdin is still a terminal, so nothing as piped to us
// the fileContents can be empty or read from arguments
// or whatever. just leaving it blank for this demo.
} else {
// read whatever was sent to us in stdin
foreach(chunk; std.stdio.stdin.byChunk(4096 * 8))
fileContents ~= cast(string) chunk;
// split the lines and replace some chars for easier drawing
later
import std.string;
originalFileLines = splitLines(fileContents.replace("\t", "
"));
// we can use stdout (file #1) as our handle to the terminal
// instead of the normal one, since we know from above it
// is a terminal or else we'd have thrown earlier
terminalFd = 1;
}
// create a terminal object using the possibly modified fd from
above
// cellular mode means using the alternate screen, so we can do
full screen
// writes to it
auto terminal = Terminal(ConsoleOutputMode.cellular, terminalFd);
// get real time input, raw with no echo
auto rtti = RealTimeConsoleInput(&terminal,
ConsoleInputFlags.raw);
int scrollPosition = 0;
mainLoop: while(true) {
// this redraw isn't very efficient but it is simple to code
// just clear and redo everything every time.
terminal.clear();
string[] lines;
lines.reserve(originalFileLines.length);
auto maxWidth = terminal.width - 1; // leave room for some
margin;
foreach(idx, line; originalFileLines) {
// wrap the long lines so they fit in the terminal.
// this wrap function sucks, it doesn't handle unicode chars
// or word breaks or anything
if(line.length > maxWidth) {
while(line.length > maxWidth) {
lines ~= line[0 .. maxWidth];
line = line[maxWidth .. $];
}
} else {
lines ~= line;
}
}
int lineCount = cast(int) lines.length;
foreach(y; 0 .. terminal.height - 1) { // leave room for info
at the end
auto lineToDraw = y + scrollPosition;
if(lineToDraw < 0)
continue;
if(lineToDraw >= lineCount)
break;
terminal.writeln(lines[lineToDraw]);
}
// write that info at the end
terminal.write(scrollPosition, "/", lineCount);
ignore:
auto c = rtti.getch();
switch(c) {
case 'q':
break mainLoop;
case KeyboardEvent.Key.Home:
scrollPosition = 0;
break;
case KeyboardEvent.Key.End:
scrollPosition = lineCount - terminal.height;
if(scrollPosition < 0)
scrollPosition = 0;
break;
case KeyboardEvent.Key.PageDown:
scrollPosition += terminal.height;
break;
case KeyboardEvent.Key.PageUp:
scrollPosition -= terminal.height;
if(scrollPosition < 0)
scrollPosition = 0;
break;
case KeyboardEvent.Key.DownArrow:
scrollPosition++;
break;
case KeyboardEvent.Key.UpArrow:
scrollPosition--;
break;
default:
goto ignore;
}
}
}
```
Checking terminal.width and height is an ioctl and signal handler
the lib does for you (the way this program is written, if you
resize the terminal, you need to press an arrow key to display
the update again, but you can handle all those in a loop too if
you wanted to), processing those KeyboardEvent.Keys is a mess of
ugly code it handles for you, etc.
Then the trick of passing the terminalFd lets it bypass the
redirected stdin and use your output terminal instead. You could
also open("/dev/tty") like i said in my first message if both in
and out are redirected to still get to it, passing the fd you get
from `open` to the Terminal object.
And like the comment says here this code isn't made to be
efficient lol but meh good enough.
Anyway going from a sample to a real program can be a lot of work
with or without the terminal library! Just terminal code is so
hideous using a lib for it does make some real sense.
More information about the Digitalmars-d-learn
mailing list