Since I couldn’t easily find the answer to this problem, I decided to make this post. Keep in mind that I am still learning the language, so the code snippets might not be optimal
I was trying to make a system to move around the terminal when the user presses WASD. Reading input and updating position are easy tasks.
The problem: When reading input, the user needs to press RETURN in order to complete the operation. I would need to change this interaction so that every time a key is pressed it checks which character was used.
When searching for the solution, I stumbled upon this thing called termios(3). This three-part blog post introduces in a little bit more in depth, but basically, I would need to change how line buffering is handled by the terminal. Termios has a bunch of flags, but we are looking for a property called ICANON
.
ICANON
, found within c_lflag
, relates to what is called canonical mode. Basically:
- If canonical mode is on, inputs are processed line by line, and you can edit the contents.
- If noncanonical mode is on, inputs are read immediately. This is what we are looking for.
We have a starting point for our code
Imports
const reader = @import("std").io.getStdIn().reader();
const writer = @import("std").io.getStdOut().writer();
const linux = @import("std").os.linux;
const fs = @import("std").fs;
We are going to work with these imports.
- reader is responsible for reading the user’s input.
- writer is responsible for printing out texts on the terminal.
- linux will help us to get and set properties for
termios
. - fs will be used to get information about the current terminal.
Main code
pub fn main() !void {
const tty_file = try fs.openFileAbsolute("/dev/tty", .{});
defer tty_file.close();
const tty_fd = tty_file.handle;
var old_settings: linux.termios = undefined;
_ = linux.tcgetattr(tty_fd, &old_settings);
var new_settings: linux.termios = old_settings;
new_settings.lflag.ICANON = false;
new_settings.lflag.ECHO = false;
_ = linux.tcsetattr(tty_fd, linux.TCSA.NOW, &new_settings);
try writer.print("--- starting input --- \n", .{});
while (true) {
try writer.print("press now: \n", .{});
const c: u8 = reader.readByte() catch break;
if (c == '\n') break else try writer.print("pressed {c} \n", .{c});
}
_ = linux.tcsetattr(tty_fd, linux.TCSA.NOW, &old_settings);
try writer.print("--- ending --- \n", .{});
}
There is quite a bit to understand here. Let’s start from the start.
const tty_file = try fs.openFileAbsolute("/dev/tty", .{});
defer tty_file.close();
const tty_fd = tty_file.handle;
Here we are getting unique information about the terminal. For that, we access tty
and it will return a number.
var old_settings: linux.termios = undefined;
_ = linux.tcgetattr(tty_fd, &old_settings);
This is responsible to access current settings of termios
with tcgetattr()
. The value will be stored on old_settings
. tty_fd
will have the identification of current active terminal, so we avoid using a magic number as a parameter.
var new_settings: linux.termios = old_settings;
new_settings.lflag.ICANON = false;
new_settings.lflag.ECHO = false;
_ = linux.tcsetattr(tty_fd, linux.TCSA.NOW, &new_settings);
Now we are changing the configuration. Here, I’m working with two flags: ICANON
and ECHO
. The latter one tells the terminal to not print what was pressed. Finally, we save the new configurations with tcsetattr()
.
while (true) {
try writer.print("press now: \n", .{});
const c: u8 = reader.readByte() catch break;
if (c == '\n') break else try writer.print("pressed {c} \n", .{c});
}
Here we are waiting for each keystroke. Zig has this nifty error handling syntax, where if something happens while waiting for input, I can just break out of the loop. If RETURN is pressed, we also break out of the loop; otherwise we print to the screen.
_ = linux.tcsetattr(handle, linux.TCSA.NOW, &old_settings);
The last step, we reset the terminal settings to the original configurations.
Complete implementation
const reader = @import("std").io.getStdIn().reader();
const writer = @import("std").io.getStdOut().writer();
const linux = @import("std").os.linux;
const fs = @import("std").fs;
pub fn main() !void {
const tty_file = try fs.openFileAbsolute("/dev/tty", .{});
defer tty_file.close();
const tty_fd = tty_file.handle;
var old_settings: linux.termios = undefined;
_ = linux.tcgetattr(tty_fd, &old_settings);
var new_settings: linux.termios = old_settings;
new_settings.lflag.ICANON = false;
new_settings.lflag.ECHO = false;
_ = linux.tcsetattr(tty_fd, linux.TCSA.NOW, &new_settings);
try writer.print("--- starting input --- \n", .{});
while (true) {
try writer.print("press now: \n", .{});
const c: u8 = reader.readByte() catch break;
if (c == '\n') break else try writer.print("pressed {c} \n", .{c});
}
_ = linux.tcsetattr(tty_fd, linux.TCSA.NOW, &old_settings);
try writer.print("--- ending --- \n", .{});
}
Special thanks for the people on the Zig Programming Language Discord server that helped me to solve this problem.
Next time (maybe?) I will create a post about what I am doing to create a crappy Rouge knockoff.