🔥
git svg main

Picnic-TUI Where Go and Groceries Create a Command-Line Feast

Sep 12, 2023

During some downtime I decided I would really like to try to learn Go. It was a language I had some exposure to in past client projects, and my coworkers who used it gave it glowing reviews. Some of which were even using it for side projects, such as doing analytics on property prices in the area through the use of web scrapers. Something which is very useful in Amsterdam.

I didn’t initially have a real goal or use case for the language but that didn’t hold me back from giving it a shot.

My learning route

When learning any new programming language I like to approach it in the following steps:

  1. Learn the basics of the syntax and get a feel for the language.
  2. Try out some basic algorithms - solve some leet code problems.
  3. Make a basic to-do app.
  4. Enhance that app work to work with a database or HTTP endpoint etc.
  5. Build more silly apps.

Like most beginners, I started with the a tour of go and got a quick feel for the language. I feel all languages should copy a tour of go as its a delightful introduction to the language which builds confidence quickly.

I continued with my learning plan

Step 5 build more silly apps

In the words of Ben Davis: https://www.youtube.com/watch?v=OqdBixi_y1s

Just build stuff.

A web scraper

Inspired by my coworker’s web scraper project and Claud’s Article. I made a basic web scraper app to perform silly analytics on my company’s website. To answer questions such as:

Screenshot 2023-09-08 at 18.35.21.jpg

Credit to the package tgraph for giving me the ability to draw these cool graphs in the terminal with the results.

Pokemon CLI

It was at this point I was getting a lot of joy out of writing command line applications. I had also just learnt of the existence of spotify-tui and wanted to explore more I could build such applications. So building interfaces for APIs felt like a good way to try this out.

The PokeApi is a resource I like to use whilst getting familiar with making HTTP calls in a new language. So I threw together such a simple app to leverage all the various methods the API offered.

Time to do the groceries

In between learning Go I got curious if there was an API for the app I use for doing my groceries? I am an avid user of the Picnic, Picnic is an online supermarket who operate in Europe. Customers of Picnic do so exclusively via their mobile application.

However the thought crossed my mind, what if I could automate aspects of my weekly shop? Something like a script that pre-loads my basket in advance, that could be useful.

Fortunately for me I came across https://github.com/MRVDH/picnic-api/ an unofficial Node.js package for the picnic API. This was perfect, I made a few scripts using the node module mission accomplished.

Enter Picnic-TUI

Now that I got a taste of what I could do with the Picnic API the natural follow up was to make a wrapper for the API and terminal user interface in Go.

The goal in my mind at this point being, if at any point in the day, I want to add something to my order I could do so from the comfort of my terminal and not have to reach for my phone. This is not a bash at Picnic’s mobile app, I simply wanted to reduce the distraction which is my phone.

Picture it, you are writing code in your favourite terminal based text editor (lets say neovim) and you are configuring a new Spring bean. It is in this moment you remember you need beans. Simple you hit :term run picnic-tui add them straight to your basket and get back into the code.

The API

Using the documentation from https://github.com/MRVDH/picnic-api/ this allowed me to produce the base for a Go wrapper https://github.com/simonmartyr/picnic-api.

At this point I had the functionalities for:

Sniffing the rest

At this point I was really satisfied with everything I could do with the API. But I wanted to try and make a more complete experience. Would it be possible to finalise an order? How would the payment flow go?

As I mentioned I had expended all the knowledge I had acquired from the Node library so that left me with one option. Intercepting all the outgoing requires from my phone to Picnic to learn all of the APIs I had not captured.

To accomplish this I used the tool https://www.charlesproxy.com. Conveniently Charles Proxy also has an iOS application which allowed me intercept all outgoing request from my iPhone.

IMG_4787.PNG

This gave me all the information I needed to identify the endpoints and payloads I would need to succeed my goal.

A note on the authentication parts

I found it curious and would welcome any response if any Picnic developers happen to be reading. When a client authenticates with the API they send the user’s username and password as well as a client id. The detail I found interesting was the password needs to be MD5 hashed prior to being sent. I’m curious as to why, and curious if that MD5 hashed password is persisted as is or additionally encrypted.

Upon a successful authentication, a JWT is issued which can then be used as a header for endpoints which require authentication. A behaviour I did observer however is, the tokens specify an expiration time (of one day), but the server continues to accept expired tokens.

I believe the intention is that clients would persist the users credentials on device in a secure enclave. Likely storing the MD5 hashed password rather than in plain text. Each day or when it believes its token has expired the application calls the authentication endpoint to get a new one.

Ideally the tokens do not continue to live, as if a token did fall into the wrong hands, that person would continue to have access and the real user would be none the wiser.

Time to checkout

Prior to intercepting the http requests the checkout flow was unknown to me. From observing the endpoints I learnt that at the moment a user invokes a checkout request the API performs the following actions:

Due to the payment being simply an ideal link, I wanted to explore if I could render a QR code in the terminal to complete an order.

The TUI

With my API library complete, it was time to set my focus on producing a terminal UI. My inspiration was to replicate the feel of Spotify-TUI. The interface of Spotify-TUI was simple but effective in its implementation.

Screenshot 2023-09-12 at 11.14.46.jpg

The interface combined multiple intractable views in which the user can navigate the core functionality of Spotify. This application adjusted my expectations on what a terminal based ui could achieve.

Additionally I had an appreciation for the detail to include vim motions in many aspects of their interface for example ‘/’ to search, the option to use ‘h’, ‘j’, ‘k’, ‘l’ in place of arrow keys. From top to bottom the developers demonstrate a lot of passion and care.

The Picnic Application

app

The images above show screenshots from the Picnic mobile application. Typically a user would search the application for different articles of food they wish to have delivered and add them to their basket. When they are satisfied with their selection, the user schedules a delivery date and pays for the order. When the time comes around for the oder to be delivered, the application can even show the route the driver is taking, so they are aware as to what time their delivery should arrive.

Within my application, I wanted to only support the functionality to create an order. Rendering the route information of the ongoing delivery would be very cool, but for my needs didn’t add value.

Evolution of the screens

Spotify-TUI was developed in Rust, therefore I couldn’t simply use the same UI framework. Within Go a popular choice is tview https://github.com/rivo/tview which provides many similar UI widgets which covered all my needs.

Screenshot 2023-08-30 at 17.25.47.jpg

I initially started with the main screen. I wanted to explore how much I could do with the product searching and manipulating the basket.

It was surprising to me how simple it was to get the barebones going. One widget I relied on heavily in tview was flex views. They work similar to flex boxs in the web world, where you can add a collection of different views into a flex view and those widgets would be arranged and sized correctly.

func newMainPage() *MainPage {
	//reduced for simplicity	
leftFlex := tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(mainPage.Search, 3, 1, false).
		AddItem(mainPage.ArticleImage, 0, 4, false).
		AddItem(mainPage.ArticleInfo, 0, 4, false)

	flex := tview.NewFlex().
		AddItem(leftFlex, 0, 1, false).
		AddItem(mainPage.Articles, 0, 2, false).
		AddItem(mainPage.Basket, 0, 1, false)

	mainPage.Flex = tview.NewFlex().SetDirection(tview.FlexRow).
		AddItem(flex, 0, 5, false)

	return mainPage
}

When you add an item to flex view, you can instruct to tview if that item has a fixed size or the proportions of the view and if that view should be focused (meaning the screen highlights that view making it listen to interactions).

Main Page

One idea that I had with the main page that I was curious to pull off was, how would I see the products in the terminal?

tview included an image widget that required a reference to an image. So within the API I wrote a method to query the image url and pass the response body through a png decoder and see what happens.

func (c *Client) GetArticleImage(articleImageId string, size ImageSize) (*image.Image, error) {
	url, urlErr := c.GetArticleImageUrl(articleImageId, size)
	if urlErr != nil {
		return nil, urlErr
	}
	res, resErr := c.http.Get(url)
	if resErr != nil {
		return nil, resErr
	}
	if res.StatusCode != http.StatusOK {
		return nil, c.parseError(res)
	}
	articleImage, imageErr := png.Decode(res.Body)
	if imageErr != nil {
		return nil, imageErr
	}
	return &articleImage, nil
}

Success, I could see what peanut butter looks like in the terminal.

Screenshot 2023-09-11 at 17.26.05.jpg

Delivery Page

I attempted initially to have the delivery slots as a dynamic element that appeared on the main page. However this felt clunky and overall a bad end user experience.

Screenshot 2023-09-01 at 16.55.21.jpg

My first iteration of the delivery view was to create a list of all the slot with an item highlighted when the day changed. It felt natural at this point to move this content into its own page by leveraging the tview pages widget.

This widget allows you to set different views as a page within your application and dynamically switch to them when needed.

For the delivery page I experimented with the idea to generate content into a flex view. The idea was simple, each time there was a new day, create a new list of options and add that to a growing flex view.

The API only returned the possible delivery slots for the next seven days, so I knew it shouldn’t grow out of control.

Screenshot 2023-09-04 at 10.52.03.jpg

With the experiment a success, I cleaned up the view by not having all lists look like they were being interacted with at the same time. Additionally I highlighted which slot was currently selected and which were the more environmental options.

Screenshot 2023-09-04 at 12.39.13 1.jpg

Checkout Page

The checkout page was fun, because as mentioned above there is a sequence of events in order to complete the order.

The biggest part of the puzzle was, how would I render the payment QR code?

Luckily the library https://github.com/skip2/go-qrcode allowed for a QR code to be generated as a string allowing that string to be presented inside a text view.

func (c *CheckoutPage) renderPaymentLink(url string) {
	q, _ := qrcode.New(url, qrcode.Highest)
	c.PaymentLink.SetText(q.ToSmallString(true))
	c.Instructions.SetText("Scan QR code to complete or ESC to cancel")
}

Screenshot 2023-09-12 at 13.33.19.jpg

The other challenge inside the checkout page was to present different checks and errors based on the content of the cart.

Within the API, I decided that errors produced by the checkout should be wrapped in their own error object allowing for the page to dynamically handle them.

For example, if the cart contains alcohol, the user is instructed to confirm they are 18+ or if they haven’t met the minimum order price modals are presented to the user with directions of what to do.

Screenshot 2023-09-12 at 13.39.00.jpg

If all actions are successfully resolved the screen presents the QR code allowing for the completion of the order.

Final words

I reached my goal and successfully made a complete order and had my perfect cli tool to manage my groceries.

More importantly it gave me a great platform to experiment and learn more about Go. This experience really reenforced to me if you can find something that gives you joy to build your learning will go faster and you will get more out of it.

If you wish to checkout the source code of the API or TUI check them out here: