DEV Community

loading...

A Gemini Client in Rust - 09 Pagination

Nivethan
Originally published at nivethan.dev Updated on ・4 min read

Hello! Now that we can traverse Gemini space, we should deal with very long Gemini pages. Currently we print everything to the screen which works fine but it'd be a little nicer if we only displayed what would fit on our screen and then hit enter for the next page.

Let's get started!

Page Height

The first thing we need to do is get out term height. Unfortunately there doesn't seem to be a way to do this purely in rust so we need to use libc. Please let me know below if there is a way get the height through rust!

Many of the solutions to getting the terminal's height involved getting a crate but I didn't want to add a dependency. So I went into the source code and luckily it was simple enough to steal!

https://docs.rs/crate/term_size/0.3.2/source/src/platform/unix.rs

This is crate I stole the term size logic from. As I am building very specifically for myself, we can get away with just using the parts applicable to us. I have no dreams of using this in windows so I didn't steal that part.

We still need to add a dependency to libc so let's go ahead and do that. (Relying on libc, I feel much more comfortable with than a full crate that in turn relies on libc)

./Cargo.toml

[dependencies]
rustls = { version="0.18", features=["dangerous_configuration"] }
webpki = "0.21"
libc = "0.2"
Enter fullscreen mode Exit fullscreen mode

We now have 3 dependencies!

Now for our rust code.

./src/main.rs

...
use webpki::{DNSNameRef};
use libc::{STDOUT_FILENO, c_int, c_ulong, winsize};
use std::mem::zeroed;
static TIOCGWINSZ: c_ulong = 0x5413;

extern "C" {
    fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
}

unsafe fn get_dimensions_out() -> (usize, usize) {
    let mut window: winsize = zeroed();
    ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut window);
    (window.ws_col as usize, window.ws_row as usize)
}

fn term_dimensions() -> (usize, usize) {
    unsafe { get_dimensions_out() }
}
...
Enter fullscreen mode Exit fullscreen mode

We include a few modules from libc and we also set a magic variable. I did steal this code so I don't really understand this code. I know what it does and for now that is enough!

We now have a function term_dimesions() that will return a tuple of our window's width and window's height.

Now let's get to the important part where we use this function!

Pagination

In our display function, we are now going the height of the window and display only what can fit. This means that we'll need to have another input loop here so that a user can hit enter to get the next page.

...
fn display(page: &Page) {
    let (_, window_height) = term_dimensions();

    let mut link_counter = 0;
    let mut current_pos = 0;
    let mut done = false;

    while !done {
        let max = current_pos + window_height - 1;

        for i in current_pos..max {
            if i >= page.lines.len()  {
                done = true;
                break;
            }
            let line = &page.lines[i];
            match line {
                _ if line.starts_with("=>") => {
                    println!("{link_counter} => {text}", 
                        link_counter=link_counter,
                        text=page.links[link_counter].text
                    );
                    link_counter = link_counter + 1;
                },
                _ => println!("{}", line)
            }

            if i == max - 1 {
                current_pos = i;
                if line.starts_with("=>") {
                    link_counter = link_counter - 1;
                }
            }
        }

        if !done {
            print!("\x1b[92m[Press Enter for Next page]\x1b[0m");
            io::stdout().flush().unwrap();

            let mut command = String::new();
            io::stdin().read_line(&mut command).unwrap();
            if command.trim() == "q" {
                done = true;
            }
        }
    }
}
...
Enter fullscreen mode Exit fullscreen mode

Our display function has now become a little bit more complex but the core idea is simple. We have some number of lines to show on the screen, if our page has more lines than that then we need to chunk our page so it fits on the screen.

We now have an outer loop that keeps track of if were doing paging through our Gemini page.

We set the max based on how many lines we can show. So if have a 50 line terminal, then we can only display 50 lines.

We keep track of the current line we're on by updating our current_pos variable. We only need to update this variable when we get to the end of the page.

So we loop through from our current location to our max which would be our current_pos + max. We subtract one from our max so that we can leave space for our prompt.

Next we check to see if we've gone past the length of our page.lines. If we have we are done and can break out of our loop.

If we aren't done we then do our logic to display the line. If it is a link we need to show the user the link text and update the link_counter.

If the line anything else we just need to display the line.

The next thing we do is check if we're at the bottom of the page. If we are, we will update our current_pos to this, that way the next time the loop starts, the top of the page will be the end of the previous one.

We also need to check if are breaking on a link line. If are breaking on a link line then on the next iteration of the loop we will display the link again. This means we need to decrement our link_counter so our link numbering still works.

Now the next section will handle our prompt. If we haven't reached the end of our Gemini document, then we display a prompt. The funny escape codes are colour codes. I like green but you can find all sorts of terminal colour codes online.

We need to set the colour of the text first and then at the end reset it. That is why we have 2 fancy colour codes. We then read some input in from the user. Currently pressing anything will cause us to go to the next page.

If we press q we will immediately leave the page.

Voila! We have pagination working now!

...
            "ls" | "more" => {   
                if cache.len() > 0 {
                    display(&cache.last().unwrap());
                } else {
                    println!("Nothing cached.");
                }
            },
...
Enter fullscreen mode Exit fullscreen mode

I updated our ls command option to allow more as a keyword as well. more in my head makes more sense.:)

Now we have a much easier time reading Gemini pages! In the next chapter let's implement bookmarks so we don't have to keep typing out full gemini paths!

See you soon!

PS. Just to be fancy, let's update our regular prompt to be green as well.

...
fn main() {
    let prompt = "\x1b[92m>\x1b[0m";
    let mut cache :Vec<Page> = vec![];
...
Enter fullscreen mode Exit fullscreen mode

The colour codes are longer than the text itself!

Discussion (0)