2

Pijul

 3 years ago
source link: https://pijul.org/posts/2020-12-09-minus/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:

  • It did not have a pager of it’s own, but rather required one of the existing pager such as less or more to be available
  • There was a bug, where if you reach the end of output, the pager is going to stop taking inputs. You needed to press Ctrl+C and then q to quit (this has been fixed in Pijul by exiting explicitly with std::env::exit(0)).

Two days later, I discovered bigger problems:

  • Since it requires an external pager, you have to package something like less or more along with the binary
  • Also, it used a lot of libc functions that are UNIX-specific, which means there was a big problem regarding portability

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:

  • The crate is the pager; it should not depend on any external program.
  • It should be asynchronous. It is the key factor for paging dynamic output. Also it should work with both tokio as well as async-std runtimes.
  • It should be cross-platform and distributable in a single binary.
  • No bloat. Application developers select what features they want; that’s it, nothing extra.

Dependencies

Some dependencies just immediately solve most problems:

  • Crossterm: Provides platform-abstracted functions for terminal access. Crossterm uses both libc as well as winapi for working in both Unix-like systems as well as Windows.
  • Tokio/async-std and Futures, for providing functions to write asynchronous code.

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

  • If the application uses tokio, enable tokio_lib feature and appropriate initializer function is tokio_updating
  • If the application uses async-std, enable async_std_lib feature and appropriate initializer function is async_std_updating
  • If the application is planning to output static information, enable static_output function and appropriate function is page_all

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.

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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK