DEV Community

loading...

A Gemini Client in Rust - 07 Caching Pages

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

Hello! In this chapter we're not going to do anything major, we're going to add some quality of life things. Currently we need to visit each page directly, if we want to see a page we went to before, we need to reconnect to that gemini server and request the page again. Instead we should save that page, this way we can show the user from our cache instead of going across the internet. This means that we can also save a history of all the pages we traverse!

Let's get started!

...
struct Page {
    url: Url,
    content: String
}

impl Page {
    fn new(url: Url, content: String) -> Self {
        History { url, content }
    }
}
...
Enter fullscreen mode Exit fullscreen mode

We're going to start by creating a Page struct that will store the content and the URL.

Next we need to update our main function.

...
fn main() {
    let prompt = ">";
    let mut cache :Vec<Page> = vec![];
...
            "ls" => {   
                if cache.len() > 0 {
                    println!("{}", cache.last().unwrap().content);
                } else {
                    println!("Nothing cached.");
                }
            }
            "v" | "visit" => {
                if tokens.len() < 2 {
                    println!("Nowhere to visit.");
                    return;
                }

                let url = Url::new(tokens[1]);
                let content = visit(&url);

                println!("{}", content);
                cache.push(Page::new(url, content));
            },
...
Enter fullscreen mode Exit fullscreen mode

The first thing to note is that we note have a variable called cache which is a vector of Page objects. This way as we visit more and more Gemini pages we will continually add them to our cache variable.

The next thing to look at is our updated match on visit. I added "v" as an option because I wanted a short hand and you can see I added a whole slew to exit out of our client as well. (I had typed each one at some point and didn't get kicked out so I added them in!:))

Inside our visit match, we now create a Page object from the url and content. We are passing the ownership of the url to the cache variable so we need to update the way our visit function works. Before it worked by getting the url transferred to it, now that we want our cache to own it, we need to give our visit function a borrow.

Our visit function now uses the & symbol to show that it is only borrowing url.

...
fn visit(url: &Url) -> String {
...
'1' => {
...
visit(&Url::new(&dest))
},
...
'3' => visit(&Url::new(&response.status.meta)),
...
Enter fullscreen mode Exit fullscreen mode

Anywhere we call our visit function will also need to use borrows instead of transferring ownership.

The next thing we need to look at is our ls option. I like the idea of Gopher being a file system and so I'm using some verbiage from file systems for Gemini as well. This may not make sense so feel free to use anything that does. A good word might be view or even last.

Inside our ls match we make sure we have something in our cache and if we do we simply show the last item in the cache.

Now we can use ls to view the last page we accessed!

We have a vector of Pages for a reason, because we have a vector, we can now display the history and access any page we visited by going through the cache!

...
            "h" | "history" => {
                for (index, page) in cache.iter().enumerate() {
                    println!("{}. {}", index, page.url.request().trim());
                }
            },
...
Enter fullscreen mode Exit fullscreen mode

This is a very simple loop where if we type h or history, we will print out what's currently in the cache.

Now that we can see our cache we need to access it.

...
            "h" | "history" => {
                for (index, page) in cache.iter().enumerate() {
                    println!("{}. {}", index, page.url.request().trim());
                }
            },
            _ if tokens[0].starts_with("h") => {
                let option = tokens[0][1..].to_string().parse::<i32>().unwrap_or(-1);
                if option < 0 || option >= cache.len() as i32 {
                    println!("Invalid history option.");
                } else {
                    println!("{}", cache[option as usize].content);
                }
            },
...
Enter fullscreen mode Exit fullscreen mode

Now we add another match statement, we have a match that will go through if the token starts with h. This is because we want to access our history by doing h1, which will mean to access the cache vector at element 1.

The first thing we need to do is remove the first character from our token, we then parse it into a variable. If the user had entered something else after the first h that couldn't be parsed into a number, we will return -1 instead. This is why we parse it into an i32.

Next we check to see if the option the user entered is with in the range of our cache vector. The cache.len() returns a usize so we need to read it as a i32. If the input is in the range, then we can print what we currently have in our cache!

A good thing to implement that would be very similar to the history option would be to use the cache to list our previous pages, but instead of reading from the cache we go to the Gemini server. This could be a function like r3 meaning reload the 3rd element in the cache vector.

For now however we are done! We have our history and our ls command working to show Gemini pages now. Now let's move to the next chapter where we will clean up the displaying of Gemini pages and allow for links in a Gemini page to be traversable.

See you soon!

Discussion (0)