Enabling raw mode
The original booklet I mentioned in the introduction goes into great detail in explaining what all the flags mean. I have no intention to do that, if you are curious about them you can consult the original.
First we make a copy of the original configuration, so that we can modify it.
// stuff here
// make a copy
var termios = orig_termios;
We then set a number of flags in this copy. We disable echoing of the characters we type:
termios.lflag.ECHO = false; // don't echo input characters
We disable canonical mode, so that the terminal doesn't wait for Enter to be pressed when reading characters:
termios.lflag.ICANON = false; // read input byte-by-byte instead of line-by-line
We disable some key combinations that usually have a special behavior in terminals, so that are available for us to use them in our program:
termios.lflag.ISIG = false; // disable Ctrl-C and Ctrl-Z signals
termios.iflag.IXON = false; // disable Ctrl-S and Ctrl-Q signals
termios.lflag.IEXTEN = false; // disable Ctrl-V
termios.iflag.ICRNL = false; // CTRL-M being read as CTRL-J
For reference:
key | default behavior |
---|---|
Ctrl-C | sends a SIGINT signal that causes the program to terminate |
Ctrl-Z | sends a SIGSTOP signal which causes the suspension of the program (which you can then resume with fg in the terminal command line) |
Ctrl-S | produces XOFF control character, halts data transmission |
Ctrl-Q | produces XON control character, resumes data transmission |
Ctrl-V | next character will be inserted literally |
Ctrl-M | read as ASCII 10 Ctrl-J instead of 13 Enter |
Let's disable output processing, to prevent the terminal to issue a carriage
return (\r
) in addition to each new line (\n
) when Enter is
pressed:
termios.oflag.OPOST = false; // disable output processing
You can see that the termios flags are placed into structs that start either
with i
(input, as in iflags
) or o
(output, as in oflags
).
Let's disable more flags, which are even more obscure than the previous ones and that I won't even try to explain (sorry):
termios.iflag.BRKINT = false; // break conditions cause SIGINT signal
termios.iflag.INPCK = false; // disable parity checking (obsolete?)
termios.iflag.ISTRIP = false; // disable stripping of 8th bit
termios.cflag.CSIZE = .CS8; // set character size to 8 bits
From the original booklet
From the original booklet
This step probably won’t have any observable effect for you, because these flags are either already turned off, or they don’t really apply to modern terminal emulators. But at one time or another, switching them off was considered (by someone) to be part of enabling “raw mode”, so we carry on the tradition (of whoever that someone was) in our program.
As far as I can tell:
- When BRKINT is turned on, a break condition will cause a SIGINT signal to be sent to the program, like pressing Ctrl-C.
- INPCK enables parity checking, which doesn’t seem to apply to modern terminal emulators.
- ISTRIP causes the 8th bit of each input byte to be stripped, meaning it will set it to 0. This is probably already turned off.
- CS8 is not a flag, it is a bit mask with multiple bits, which we set using the bitwise-OR (|) operator unlike all the flags we are turning off. It sets the character size (CS) to 8 bits per byte. On my system, it’s already set that way.
A timeout for read()
Finally, we want to set a timeout for read()
, so that our editor will be able
to discern an Esc from an escape sequence. In fact, all terminal
escape sequences that codify for many keys begin with an Esc (that's
why they are called escape sequences), and we want to be able to handle them
accordingly.
Here we use some constants that are defined in std.os.linux
. Since they are
in an enum
, we'll have to use the builtin function @intFromEnum()
so that
we can use them for array indexing (which expects an usize
type).
// Set read timeouts
termios.cc[@intFromEnum(linux.V.MIN)] = 0; // Return immediately when any bytes are available
termios.cc[@intFromEnum(linux.V.TIME)] = 1; // Wait up to 0.1 seconds for input
This took me hours to figure out. The original kilo editor uses constants that
come from the libc termios.h
header, but initially I simply used the values
from the C version, thinking they would apply also for the Zig version. They
didn't work, that is, there was no read timeout. I initially asked the AI, and
it didn't help. I then looked for other Zig implementations of this same editor
on the internet, but all of them repeated this mistake, until I found one
implementation that did the right thing, that is, to use the constants that are
provided by the Zig standard library (what is being done in the snippet of code
above).
The lesson was: don't try to reinvent a system-defined constant, use the system-defined constant, even if it means that you must look for it in the standard library.
We're done, we can apply the new terminal configuration and return the original one:
// update config
try posix.tcsetattr(STDIN_FILENO, .FLUSH, termios);
return orig_termios;