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