As an exercise in understanding my computer better, and as a tool, I'm writing my own shell in C++. Stephen Brennan's article on writing a simple shell was very helpful.
However, what has me flummoxed is how to handle pressing up-arrow and down-arrow to scroll through my command history.
I tried ncurses
, but that replaces the entire screen, whereas the system-provided shell seems to just continue writing into the Terminal.
I tried using tcgetattr
to turn off canonical mode, but while that lets me get arrow key presses as they are typed, it also turns off all processing of left/right arrow keys for text navigation, and the backspace key, and Ctrl-C... While I could probably send a signal myself in response to Ctr-C, I have no idea how to get the Terminal to move the cursor back (apart from outputting a "return" and re-writing the start of the line). It also seems to give me different escape sequences for the keys, depending on whether I'm running in Xcode's "dumb" Terminal or in my Mac's Terminal.app.
I looked at the sources for fish
Shell and bash
, but there just seems to be so much going on that I can't find the relevant parts.
How do the standard shells handle receiving keypresses? How do they handle moving the cursor and doing backspace? How do they re-write parts of a line without having to take over the screen? Is there a standard somewhere that defines what a shell needs to do?
PS - I know how to record the previous commands. It's the actually getting the keypresses while they are being typed, as opposed to after someone presses return, that I can't get to work.
You have to turn off ICANON
and ECHO
and interpret the escape sequences from the arrow keys yourself.
You have to keep your own “actual” buffer of what's on the screen and where the cursor is. You also need a “desired” buffer of what you want on the screen and where you want the cursor. These buffers don't cover the whole screen, just the lines containing your prompt and the user's input (which you echoed manually because you turned off ECHO
). Since you printed everything on these lines, you know their contents.
Just before you wait for the next input byte, you update the screen to match the desired buffer. Back when you were on a 300 (or even 9600) baud connection you cared a lot about making this update as efficient as possible by looking for an optimal sequence of printable bytes and terminal-control sequences to transform the actual buffer into the desired buffer. These days it's a lot less important to be optimal.
These buffers will span lines if the input wraps, so you need to know and track the terminal width (using TIOCGWINSZ
and SIGWINCH
). You could stick to a single line with horizontal scrolling instead of line wrapping, but you'd still need to know the terminal width.
Theoretically you look up your terminal type (from $TERM
) in the termcap or terminfo database. That tells you what escape sequences to expect when the user presses special keys (arrows, home, end, etc.), and what escape sequences to send to move the cursor, clear parts of the screen, insert or delete characters or lines, etc.
These days it's pretty safe to assume everything's fairly xterm-compatible, especially for a hobby project.
For bash, this is all done in the GNU readline library. Updating the screen (called “redisplay”) is done in display.c
. Input escape decoding is done in input.c
.
However, if you want example code, you should probably take a look at linenoise, which is under 2000 lines. It assumes the terminal is VT100 (and therefore xterm) compatible.