Quote me on this

— OR —

“Building things out of spite”

The other day, I updated my computer (From Ubuntu 20.04 !!!), and I immediately found myself in a rabbit hole of personalization, After tweaking my Vim and ohmyZsh profiles (themes and plugin) for hours (It took me a looong time to get them just right), using my terminal now felt like using a beautiful, well lubricated, machine. Now that my terminal was looking good, I thought about adding something extra…

I always loved the concept of the “quote of the day”. When I was in high school, there was an option to display Dilbert comics on the homepage. I didn’t really like them, but having this little “advent calendar” every day was nice. So when I set up my zsh, I used a quote plugin to display a new quote at each terminal opening ! And it worked, and it was great.

I then noticed that most quotes were… very American. Pardon my French point of view, but the editorial line was very “self help” oriented. Very individualistic. Very “God” centered. Which is obviously fine ! Just not to my tastes. Most of all, there were some people I didn’t like. So when, sipping my morning coffee, I landed on a quote by Bill Cosby… I snapped. I had to save my little quote machine. Here I will present this little project of mine, nothing really technical, just the path I took, but hey, I said I was gonna blog.

Building an API

Should i learn Letters first? Or choose the path of Numbers? A queston every baby must ask it self.

— @dril

First I had to build a small API to get my quotes. Why not fetch the data locally ? Well, I would like to be able to use my dotfiles on servers via ssh. Then the question is how the data has to be stored. Here’s my specifications, I need to have a list of element that have:

  • A list of author/dialogue line pairs, so that I can display dialogues
  • A tag to define the category of the quote (basic/movie/lyric…)

I wanted the API to run on the existent flask infrastructure of my website. I saw two options, a database with two table (quote and quote handler) or a simple file. Having a database just for this small project and to hold very few quotes seemed a bit much. Here’s the mighty JSON structure I chose, just enough to store a few lines:

data/quotes.json
[
    {
        "lines": [
            {
                "author": "Donald Knuth",
                "quote": "To program is to write to another programmer about our solutions to a problem"
            },
            {
                "author": "Dlanod Knuth",
                "quote": "To program is to write to another programmer about anything but a solution"
            }
        ],
        "tag": "dev"
    },
    ...
]

As that was settled, I had to fill the “database”. That took me quite some time, but I enjoyed scraping quotes from my favorite authors, movies, musics… After collecting a few quotes I liked, I built a simple API with Flask.

routes_api.py
@bp.route("/get_quote", methods=["GET"])
@bp.route("/get_quote/<tag>", methods=["GET"])
def quote_giver(tag: str=None):
    _json = pick_quote(tag=tag)
    if _json is None:
        return "Wrong tag", 404
    response = jsonify(_json)
    return response

Using a JSON file meant that I couldn’t select according to the tag easily, so I made a script that made another one split by tags:

data/quotes_sorted.json
{
    "tag0" : [ {"lines" : [...]}, {"lines" : [...]}, ... ],
    "tag1" : [ {"lines" : [...]}, {"lines" : [...]}, ... ],
    "tag3" : [ {"lines" : [...]}, {"lines" : [...]}, ... ],
    ...
}

Now I could make function to pick the quote accordingly.

routes_api.py
def pick_quote(tag=None):
    if tag is None:
        path = Path(current_app.static_folder)/'data/quotes.json' 
        with open(path, 'r') as file:
            return random.choice(json.loads(file.read()))
    else:
        path = Path(current_app.static_folder)/'data/quotes_sorted.json'  
        with open(path, 'r') as file:
            quotes = json.loads(file.read())
            try:
                return random.choice(quotes[tag])
            except KeyError as e:  # incorrect tag
                return None

And it worked great ! Task done, ready to go back to the terminal !

Losing track and building a web page

Screenshot of my page to display quotes

I couldn’t help myself. I was there, fetching quotes in my browser, and I thought: hey, what if I built a web page to display them ? So, having no self-control, I did. I built a small webpage that fetched quotes dynamically using a touch of HTMX. Again, I didn’t want to add too much stuff in my flask files, so I did most of the JSON object -> HTML conversion in the webpage (instead of rendering a the quote as a template, as would have been the cleaner and recommended option).

It worked fine, except that when I reloaded twice, nothing happened. Well in dev mode there was no issue, everything worked fine. Without dev mode, nothing happened. I was puzzled for quite some time, why does the brrowser works differently in dev mode ? Isn’t dev mode just a way to inspect the DOM and networks and stuff ? And it happened on multiple browsers too. Then I figured that the request was indeed sent, but the browser cached the answer the first time… Makes sense, in dev mode they disable the cache. So I added some lines to my Flask API:

routes_api.py
@bp.route("/get_quote", methods=["GET"])
@bp.route("/get_quote/<tag>", methods=["GET"])
def quote_giver(tag: str=None):
    _json = pick_quote(tag=tag)
    if _json is None:
        return "Wrong tag", 404
    response = jsonify(_json)
    response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Expires'] = '0'
    return response

Now that was perfect. You can see it live here. Great stuff.

I have nothing to offer anyone except my own confusion.

— Jack Kerouak

Now I could finally get back to my termin…

Still lost, building two web pages

Screenshot of my page to display mass effect quotes

Ok. Please don’t yell at me. I was collecting some more quote and noticed that I had a LOT of quote from Mass Effect. Yes, it’s my favorite game, I would die for Jack and Cortez, and all the crew, sue me. So I also built a webpage for those special quotes, with some random event and everything… You can see it here.

Removing evil isn’t the same as creating good.

— Thane

Time to get back to the terminal (The Broker Terminal, am I right ? Ahah, ah..).

Back on track: the mighty terminal

There. So I just had to figure a way to get from the API to the terminal. It’s been a long time since I did anything “new” in shell. I knew I would use curl to get the JSON object, but after that…

I discovered jq which is really well documented, and I ended with this (more or less) one-liner:

fetch_kwote () {
    if [[ $# < 1 ]]; then
        echo "Usage fetch_kwote API_url [timeout]"
        exit 1
    fi
    if [[ $# -eq 2 ]]; then
        timeout=$2
    else
        timeout=5
    fi
    curl --connect-timeout $timeout --silent $1 | jq '[.lines.[] | .author, .quote]' | display_kwote
}

Here I get the JSON from the address given as the first parameter, in silent mode (the goal is to display just the quote, not curl infos !), and with a defined timeout (I need to connect quickly).

The result is piped to jq. Jq get the lines key, iterate over the array, piping each object to extract author and quote. The result is a series of string alternating between author and quote.

Strings which I then feed to display_kwote my display function. I won’t put it here as it is useless, it collects author and quote alternatively, and display them with different colors. The Github is here

And that’s it! With the magic of curl, jq, and a touch of flask, my terminal now gives me the quote I want, the quotes I NEED.

After nourishment, shelter and companionship, stories are the thing we need most in the world.

— Philip Pullman

Screenshot of my terminal displaying a quote from Tolkien

Birthdays and Dr. Manhattan

One thing I realized is that I wanted to display some quotes on special days. For example, on my birthday, I want my terminal to sing. Or on the 10th of November, I want it to tells us that Dr. Manhattan is starting to come back to life… That means I had to add three things:

  1. A way to define the correct date range for a quote in the JSON file
  2. A way to fetching quote with a higher probability for quotes corresponding to the date given
  3. A way to avoid displaying those quote when they’re outside their defined interval.

For 1. I added a “date” field to some object of the form weekday day/month/year, with an * when the element is not defined. Which for my birthday would give * 18/01/* (remember it, it might be important later 1!). I also set their tag to date but that might change, I’ll think about it !

For 2. I added a probability to display a date object, and otherwise display any object (not dated)

For 3. a simple avoid get list parameter.

Salut everybody tout le monde ! Ca y est c’est mardi, c’est bientôt le week-end !

— Fatal Bazooka

I can now make sure I never miss those two national holidays.

Conclusion

What I love with those kinds of easy projects is that they keep you happy for a long time. You feel gratification at first, but then, as you keep using the tool, building around it, as it settles into dust, you forget how it was made. It’s there, a bit rusted, but still working, like an old phone full of old stuff. It’s so present, that you forget about its existence and then eventually you rediscover it, and it brings you joy again. And there’s no surprise that it took a little spite to fuel a project I’d actually use every day.

‘I wish it need not have happened in my time,’ said Frodo. ‘lmao’ said Gandalf, ‘well it has.’

— @joshcarlosjosh

Take care, XOXO.

Footnotes

  1. In fact,you are now contractually obliged to wish me happy birthday until death do us part↩︎