Bharat Kalluri     ·  About

Let's Rust! - Wallsplash

I wanted to learn rust for a while. After reading the manual, I decided to make something small and useful. Unsplash has always been my sole source of wallpapers. So this is an attempt to make a command line app to set wallpapers using unsplash API. Let’s Begin!

What is the end goal?

The end goal is to make a working command line application by which wallpaper can be changed. The source for the wallpapers will be a fantastic website called unsplash. These guys have a kickass API which we are going to use.They also have a rate limit of 50 requests per hour for a key, So we will just ask the user to create a .wallsplash file in their home directory with the API key. And use that key to make all requests. This is specifically made to work on GNOME-based environments(Although this can be extended to other environments easily).

Moving Parts

Initializing the project

Rust has an awesome package manager called cargo. We will name the command line tool as wallsplash (Wallpaper + Unsplash). So, to initialize the project.

cargo init wallspalsh --bin

The bin argument makes sure that we get an executable file. Inside the wallsplash directory, a config.toml file can be found. All the required packages must be added here. Change the cargo.toml as follows

[package]
name = "wallsplash"
version = "0.1.0"
authors = ["bharat"]

[dependencies]
clap = "*"
reqwest = "*"
serde_json = "1.0"
serde = "*"
serde_derive = "*"

Cargo follows semver(Semantic versioning). So when it sees a star. It grabs the latest version of the library. This is not usually advised as the library can change its API and that can break the project. But since this is a simple and small tool. Let’s just use the latest version.

Command line parser

For parsing the command line options, Let us use a library called clap. This has one of the most beautiful API I have ever seen in rust. Let’s start off by writing the main function.

extern crate clap;
extern crate reqwest;
extern crate serde;
extern crate serde_json;

#[macro_use]
extern crate serde_derive;

use clap::{App, Arg};
use std::io::Read;
use std::fs::File;
use std::process::Command;


fn main() {
    let matches = App::new("wallspash")
        .version("0.0.1")
        .author("Bharat Kalluri")
        .arg(
            Arg::with_name("query")
                .value_name("query")
                .short("q")
                .help("Search Query"),
        )
        .arg(
            Arg::with_name("random")
                .short("r")
                .help("set a random wallpaper"),
        )
        .get_matches();
}

The matches variable is a new clap object named wallsplash (Therefore this will be the name of the command line tool).The possible arguments are given using the arg argument. Wallsplash has 2 possible arguments(excluding the help argument). They are a query argument and a random argument. The query argument needs to have a value. The value is named query. Using this later, the value passed to the tool can be retrieved. The second argument is random. It does not need a value. This is a functional program and can be run now!

cargo run -- -h

You will probably see a bunch of warnings about unused imports. Ignore them for now. The output will be the help for your new shiny program!

Read key from a file

The .wallsplash file will contain the API key for the program. You can get your own API key from Wallsplash. Save your key in a file named .wallsplash in your home directory. Now before we write any more functions, let us create a simple structure which holds our API key. This structure will have functions tied up and these functions can access the key. Above the main function, make a struct called as Wallsplash.

pub struct Wallsplash {
    client_id: String,
    home_path: String
}

Above the main function, write an impl block.

impl Wallsplash {

	fn new() -> Wallsplash {
        let home_pathbuf = env::home_dir().unwrap(); 
        let home_path =  String::from(home_pathbuf.to_str().unwrap());
        return Wallsplash { client_id: String::new(),  home_path: home_path };
    }
    
    fn api_key(&mut self) {
        let mut file = match File::open(String::from(self.home_path.clone()) + "/.wallsplash") {
            Err(_) => panic!(
                "=> Could not find .wallsplash in home directory.Make a file named .wallsplash with your api key."
            ),
            Ok(file) => file,
        };
        let mut key = String::new();
        file.read_to_string(&mut key)
            .expect("config file not found!");
        self.client_id = key;
    }
}

Simply put, an Impl block is a set of functions tied up to the struct which can access the struct variables. There is a lot going on in this function. Let’s break it piece by piece. The first function is a static function which creates a new instance of the wallsplash struct, The new instance contains the client id and a home path. The second function takes a mutable struct object. Then we open the file using the rust native library. The File::open returns an option. The match it returns is matched with an Ok, which returns the file, else if an error occurs, the programs just quit printing - the file is not found. Now, the key is read from the file and written into the struct instance variable. This function can be called from the main function now. Right below the structure instance.

// Instantiate a struct object
let mut wallsplash = Wallsplash::new();

// read api key from .wallsplash
wallsplash.api_key();

The instance now has it’s variable assigned to the key and the home path!

Make an API call and extracting data from Json

Since this blog post is getting long, I will just show how to make a call to the API for a query. There is also an API call for getting a random wallpaper in the repo linked at the end of the post. Once the user gives a query with the -q parameter, It can be captured with

matches.value_of("query").unwrap_or("default.conf");

Using this value, we will make an API call.

Rust has a very popular requests library called hyper. But, The author wanted to go low level with hyper. So he made a wrapper around hyper that it is easier to use. This wrapper is called as reqwest. Now since we got the value of the query. Let us pass it to a function which gives back an URL, which can be later downloaded and used as the wallpaper. Let’s add a function inside the impl block for making the API call.

fn api_query(&self, query: String) -> String {
    let mut buf = String::new();
    let url = String::from("https://api.unsplash.com/photos/random?client_id=")
        + &self.client_id + &String::from("&query=") + &query;
    let mut res = reqwest::get(&url).expect("Failed to send");
    res.read_to_string(&mut buf).expect("Failed");
    let v: Api = serde_json::from_str(&mut buf).unwrap();
    return String::from(v.urls.full.as_str());
}

reqwests is really simple to use. Before we proceed, let us make a buffer and declare the URL. Observe that we are borrowing the client id from the struct. In our case, the instance holds the key, which is inserted here. Once the URL is framed, we use reqwest::get to send the request and store the response. The response is read to the buffer. So, the buffer is filled with data. Since we got json, it needs to parse. For this, we use the famous serde library!

There are 2 ways in which JSON can be parsed. serde_json::value can be used to parse (As shown in the first example in readme of serde_json). This approach does not require any declaration of structures. But this approach is said to be slow. Since we understand what kind of API response we are expecting, let us create a struct to deserialize/parse JSON. declare these 2 structs just above the main function.

#[derive(Serialize, Deserialize)]
struct Urls {
    full: String,
}

#[derive(Serialize, Deserialize)]
struct Api {
    urls: Urls,
}

derive serialize and deserialize for serde to understand. To understand why this works. Have a look at the response the API returns using a simple query using something like postman. You can observe that it returns an object which has a urls object. It has a nested full string containing the URL to the image. The structs mimic the API structure. Now, Observe line 6 in the function api_query. We pass the buffer to the struct using serde. It then unwraps the data according to the structure. v holds all the needed data which can be easily got by just traversing the object v.

Save the wallpaper

For simplicity, let’s save the wallpaper as wallsplash.jpeg in the Pictures folder. reqwest can be used to download the file. Write the following function inside the impl block.

fn download_image(&self, url: String) -> Result<reqwest::StatusCode, reqwest::Error> {
    let mut file = File::create(String::from(self.home_path.clone()) + "/Pictures/wallsplash.jpeg").unwrap();
    let mut res = reqwest::get(&url)?;
    res.copy_to(&mut file)?;
    return Ok(res.status());
}

This is a bit more complicated. This function just takes in an URL and returns a Result. A result consists of a ok and err. In our case, if all goes fine, It will return a status code. If something fails, it returns the error. using File::create from rust library, we can create a file. send the request using the reqwests library. After the reqwest::get we have a ?. This is similar to unwrap. But it just passes the error to the parent. This is much more elegant. Since this just passes the error up the hierarchy, question mark operator cannot be used on the main function. Later we just copy the contents of the response to the file. Now we have an image in the /tmp folder.

Setting the wallpaper

Finally! Since the wallpaper is saved in the /tmp folder. Let’s write a function inside the impl block that set’s the wallpaper.

fn set_wallpaper(&mut self) {
    let path = String::from(String::from("file:///")+ &String::from(self.home_path.clone()) + &String::from("/Pictures/wallsplash.jpeg"));
    println!("Setting wallpaper!");
    Command::new("gsettings")
        .args(&[
            "set",
            "org.gnome.desktop.background",
            "picture-uri",
            &path
        ])
        .spawn()
        .expect("Failed to set wallpaper!");
}

This is much more simple. Just invoke a command using the Command.new and pass it arguments. For reference, to set a gnome wallpaper. The command is

gsetings set org.gnome.desktop.background picture-uri file://tmp/wallsplash.jpeg

We spawn the command and if an error occurs, it just says “Failed to set wallpaper!”.

Placing all pieces together

fn main() {
let matches = App::new("wallspash")
    .version("0.0.1")
    .author("Bharat Kalluri")
    .arg(
        Arg::with_name("query")
            .value_name("query")
            .short("q")
            .help("Search Query"),
    )
    .arg(
        Arg::with_name("random")
            .short("r")
            .help("set a random wallpaper"),
    )
    .get_matches();

// Instantiate a struct object
let mut wallsplash = Wallsplash::new();

// read api key from .wallsplash
wallsplash.api_key();

if matches.is_present("random") {
    // Pass this to the api which should return an url of a image
    let url = wallsplash.api_random();
    println!("The Image URL is - {}", url);
    // Pass the url of the image to a function to save in tmp folder
    wallsplash.download_image(url).unwrap();
    // Call the set wallpaper func to set wallpaper
    wallsplash.set_wallpaper();
} else if matches.is_present("query") {
    let query = matches.value_of("query").unwrap_or("default.conf");
    let url = wallsplash.api_query(String::from(query));
    println!("The Image URL is - {}", url);
    // Pass the url of the image to a function to save in tmp folder
    wallsplash.download_image(url).unwrap();
    // Call the set wallpaper func to set wallpaper
    wallsplash.set_wallpaper();
}

The main function now calls all the functions related to the struct in a sequence to execute the program. Notice that the methods which do not take a &self argument are static methods and are called directly(For example, Wallsplash::new()). But methods which have a &self parameter need to be called from their instance to which they are linked (For example, wallsplash.api_query(String::from(query))). If all goes well, we have a working rust command line application for changing wallpapers!

The complete source code is on github.

If you have any questions or suggestions, feel free to post them in the comments.

Cheers!

Written December 12, 2017.

← Hello world!  Managing dotfiles using GNU Stow →