During the last few weeks, I have been working on the TUI of
wutil
, a WiFi utility
for FreeBSD. wutil
, the CLI supports most station mode operations like
scanning, connecting, disconnecting WiFi Access Points,
managing known networks, automatic wpa_supplicant config update etc. The TUI,
which was built as a proof-of-concept for my GSoC proposal along the lines of
The Basics of “Uncooked” Terminal IO,
though pretty could only display scan results and
network interfaces info. So it was finally time to make all wutil
functionalities available in the TUI, wutui
(It sounds better when pronounced
What-You-I IMHO („ᵕᴗᵕ„)),
Getting the Terminal Raw
So normally a terminal is in a cooked state and does all the nice output and
input processing on every read and write on the tty,
like CTRL-C
and CTRL-Z
keybindings send a SIGINT
and a SIGTSTP
respectively,
carefully waiting for an ENTER key to read a line etc. But when building a TUI,
they must be disabled, we rather want a raw fresh uncooked tty,
which we can cook ourselves (no pun intended). We can do this by fetching and
modifying the termios
attributes of the tty. There’s
cfmakeraw(...)
in termios.h
but it sets VMIN
control character to 1 which blocks till a single byte is
available to for the read syscall. We must set it to 0, so that reads are
non-blocking and can poll the tty with other file descriptors.
We also draw the UI in an alternate buffer so that the user’s previous shell screen isn’t lost.
Some Warm-Up
I wasn’t very comfortable with event handling and rendering complex UIs with
just control sequences so I tried implementing
antirez’s 1000 LOC TUI editor,
kilo
. It uses uncooked terminal IO and
control sequence spells. I learned a bunch of cool tricks like how to implement
scrolling, proper input handling and buffering the output to prevent flickering.
The Kernel as an Alarm Clock for our File Descriptors
We have to listen to a bunch of event sources in our TUI, majorly the tty for
reading input when available, wpa_supplicant’s ctrl interface socket for
the supplicant’s events, SIGWINCH when there’s a change in the terminal window’s
dimensions, and also some sort of periodic timers to remind us to start a
background scan etc. One way to do it is to have a blocking queue for the events
in the main thread and listen to each event source and push the events to the
queue in listener threads. libvaxis
,
the Zig TUI library does this. But it would be way less tedious if our program
could just sleep when there’s no event and wake up only when necessary,
saving ourselves some CPU cycles. There’s poll(2)
,
but we gotta pass the whole list of file descriptors every call. But fret not,
on FreeBSD we have kqueue(2)
, which lets us register events with the kernel
and let it keep track of them. It also lets us listen to signals and
create custom timers. Perfect for our event loop.
The wutui
Recipe
So the TUI architecture is quite simple,
have an initial state
|> render the UI with that state
|> listen for events in an event loop
|> cause state change on events
|> render the UI with that state
|> ... *goes all over again* ...
The wutui
struct holds all the resources
(tty, wpa_supplicant, kq file descriptors etc)
and keeps track of the UI states (current section, scan results,
known networks, currently selected network, terminal window dimensions etc),
it’s initialized at program startup in the main function and deinitialized
atexit(3)
.
The event loop is a kqueue wait loop which renders the current UI and waits for an event to occur. The event identifiers and their respective handlers are stored in the state struct too, this lets us swap out the handlers on an identifier for a specific scenario. Like for example when reading password for a WPA-PSK network in the input dialog box, we want the status and scan results to still be updating live in the background. So, we can launch a sub event loop with a modified handler for handling input on the tty file descriptor avoiding the TUI keybindings till the user completes entering the password.
The wpa_supplicant ctrl interface handler updates the scan results and known networks on appropriate wpa_supplicant messages and also pushes the message as a notification which gets rendered as a toast notification in the UI. SIGWINCH is also handled by the kqueue event loop, it’s event handler sets updates the window dimension values allowing us to resize on the next render. We also have timers for performing a periodic background scan and periodically cleaning up the older toast notifications. The older toast notification text is word wrapped.
We also do all the writes in a
sbuf(9)
buffer and do a single
write on the tty to avoid screen flickering. sbuf
is a dynamic string buffer
library in FreeBSD base.
The TUI
You can get a quick cheat sheet of the available keybindings by pressing ‘h’ in the TUI. All the CLI functionality is available in the TUI and you get live feedback on many events. You can also do a HOME, END, PAGE_UP and PAGE_DOWN.
Initially scan results and known-networks were internally represented as
TAILQ
‘s but it was somewhat messy
to jump to a specific entry (as we always have to loop to that entry)
or do scrolling so we switched to a dynamic array.
Changes to the CLI
The CLI design has been fully revamped,
$ ./wutil help
Usage: wutil {subcommand [args...]}
wutil help
wutil interfaces
wutil interface <interface>
wutil [-c <wpa-ctrl-path>] known-networks
wutil [-c <wpa-ctrl-path>] {known-network | forget} <ssid>
wutil [-c <wpa-ctrl-path>] set
[-p <priority>] [--autoconnect {y | n}] <ssid>
wutil [-c <wpa-ctrl-path>] {scan | networks | status | disconnect}
wutil [-c <wpa-ctrl-path>] connect
[-i <eap-id>] [-p <password>] [-h] <ssid> [password]
The ability to set an interface’s UP/DOWN state, IP, netmask and gateway has been
removed as it’s already been done by
ifconfig(8)
and
route(8)
. Also all lib80211
ioctl
code, which was still being used by older wutui
,
has been removed as that information can be
fetched from libwpa_client
‘s wpa_ctrl
.
What’s next?
Currently when the dimension of the terminal window is less than 80x36
(80 cols and 36 rows), a “Terminal is too small” text is displayed. Scaling
even below that should be supported. The wutil(1)
manpage also needs updating.
Once libwpa_client
is available in net/wpa_supplicant
port, we can submit
the wutil
port.
In the coming weeks I’ll also
implement libifconfig
helpers to set UP/DOWN state, IP/netmask and MAC on
network interfaces.