Overview
I recently wrote ncopds
, a TUI program for navigating Open Publication Distribution System (OPDS) feeds. I wrote it in Rust using the cursive crate, a library for building TUIs. Currently, only Linux distributions are supported.
This project was inspired by ncspot
and ranger
. ncspot
was one of the first TUI programs that I used that really opened my eyes to the utility of the terminal. I use ranger
all the time and I tried to imitate the ergonomics and feel of ranger
when building the directory view in ncopds
.
Features
ncopds
is multi-threaded, letting the user browse and download books from OPDS feeds without blocking the UI. Some other stand-out features:
- Book covers are rendered in the library screen using ASCII characters
- Manage multiple OPDS connections with ease, either through a config file or through the UI
- Navigate directories using a
ranger
-like interface - Search OPDS catalogs using the server’s search functionality
- Theme support
vim
-like key-binds
You can install it right now with Rust’s package manager. Just run cargo install ncopds
.
Implementation details
I structured ncopds
using the model-view-controller (MVC) architecture as suggested by this blog post. The UI
is owned by the Controller
and I use std::sync::mpsc
to pass messages back and forth.
Using the message-passing system made it really easy to go further and make the application asynchronous. In the program, HTTP requests get called in a separate thread that send a message with their results to the UI when they finish.
Rendering to the canvas was also simpler than expected. I borrowed the CanvasView
from kakikun
(source) and would simply download images from the server, convert them into the DynamicImage
type and render them in the side view.
Notification view
The notification view was a bit of a hack. I wanted to let users know when their download was finished without interrupting their browsing experience. I discovered that rendering a new layer takes the focus from the user, so I dug around the documentation for cursive
.
I found that there was a flag you could set on layers in the StackView
of cursive
that would not steal focus from the user. When you call set_modal(layer_position, false)
, the user can continue to send events to the layers underneath of it while the top layer is shown. Check out the relevant code here.
This solved the input issue, but I wanted the notifications to disappear after a few seconds. To do this, I needed to keep track of the current frame which I started to do in the main controller loop. This frame then gets passed down to the UI event loop.
In the main UI loop, I store a Vec<(u32, String)>
that represents the frame a notification was rendered and its name. When 5 seconds have passed (150 frames since cursive
is capped at 30 fps), the notification is selected and removed, providing the familiar experience of pop-up notifications.