ncopds: A TUI program for managing ebooks

Source code

Crates.io

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.

Last modified: September 24, 2024