Announcing Minus, a cross-platform pager

Wednesday, December 9, 2020

This is a guest post by Arijit Dey, introducing a cross-platform paging crate he wrote, which will allow Pijul to work equally on all platforms.

Hello everyone. Before I continue on with this blog post, I really want to thank Pierre-√Čtienne Meunier for giving me this opportunity to write a blog post on their website

In this blog post, I want to talk about Minus, which is a cross-platform, asynchronous terminal paging library written in Rust. You can check it out here

Why I wrote minus

I started writing minus because of my work on Pijul. It all started with a discussion where I pointed out that we should use a pager for easier viewing of pijul logs/diffs/credits. After getting encouragement from a few people, I started working on a implementation, unfortunately all of the existing Rust paging crates were kind a unsatisfactory to me. The first option that I looked into was pager. It was a popular library and had good documentation. I had to write only a few lines to start using it. And this is when I got my first dissatisfaction:

Two days later, I discovered bigger problems:

Between these two days, I had submitted another implementation using the moins crate. It did solve the first two issues, but introduced another issue: indeed, there can be a long hang-up when running log/diff/credit on a really large repository, and due to the nature of the moins crate, the data must be supplied entirely before anything is rendered, which means we cannot atomically update the result.

Although the first implementation using the pager crate was merged into the main Pijul codebase, I wasn’t really happy with the implementation. So I wrote a small post on the Pijul Discourse about writing a new paging library for Pijul.

The Planning

The first thing before starting any project is to understand what your project’s primary goals are. What are the most important things that it must solve? For minus, these are the primary goals:

Dependencies

Some dependencies just immediately solve most problems:

Implementation

Without going into all the hit and trials that I had to do, I am going straight to the current implementation

For different applications, different features and functions are available

The application has to initialize an Arc<Mutex<String>> and give a clone of it to the respective dynamic initializer function. If you are paging static output, page_all takes a String rather than an Arc.

Minus then initializes itself, switching to alternate screen of the terminal, enabling raw mode of the terminal and hiding the cursor. Then it checks whether there are more lines in the terminal, then the number of lines in the output. In case of dynamic paging, it keeps continuously updating with new information. In static paging, minus prints the entire output and quits

If there are more lines of output than number of rows, it only displays a range of output, between upper_mark and lower_mark. upper_mark is what we keep a track of, it is simply a pointer of scrolling: whenever the user presses the down arrow key, we increment upper_mark by 1 and when they press the arrow up key, we decrement upper_mark. lower_mark is a calculated value, using this formula

upper_mark + number of lines in terminal = lower_mark

While doing all this, we also check for events, up arrow, down arrow, q or Ctrl+C resets all changes to terminal and quits. Resize events sent by Crossterm update the number of lines in terminal.

So that’s a quick rundown of the inner workings of minus and how it works with other top-level programs.

Q&A

There have been some questions about what others can expect from minus. Let me clear them here:

Is there any plan to make minus replace less?

As far as I am concerned, I currently don’t have any plans to do so. But I really encourage the community to make something out of it. There are some things which are application specific, like piping stdout of another program into stdin of the application.

Are there plans to integrate it with other Rust CLI tools like ripgrep.bat?

I am ready to work with these guys to maybe integrate minus into something like bat/ripgrep to provide better output. This would bring lots of improvements to minus as well.

Why the name ‘minus’?

Minus is the Latin translation of less

Does minus support formatted output like bold/colors?

Absolutely, just make sure that every line starts and resets the formatting. Basically your line should be a concatenation of regular text and sequences like:

(Whatever formatting ANSI sequence)(regular text)(Reset sequence)

If you’re using ansi_term, use something like this

// Assuming formatted_line is the final input to the pager
let mut formatted_line = String::new();
let line = String::from("Hello World");
formatted_line.push_str(&ansi_term::Colour::Blue.paint(line).to_string());

minus::page_all(formatted_line);

So, that’s some quick introduction and workings of minus. Pijul developers are currently working to integrate it in Pijul. I am also currently to make minus more efficient and bug free. I hope this blog post helps.