Let’s take a look at loiccoyle/thqm-rs.

Its a command line utility written in rust which generates and serves a menu web page on the local network.

Inspiration

thqm takes a lot of ideas from dynamic menu software like dmenu and davatorium/rofi. The basic idea is to read a list of menu entries from stdin, generate a menu, and forward a user’s selection to stdout.

For the interested, I’ve made my fair share of script which rely on rofi’s dmenu to interface with the user:

Where thqm comes in is in allowing the user to interact with the menu and script over the network. This let’s the user create simple scripts which can be used to control or interact with the host computer from any device on the local network.

The building blocks

thqm relies on two main components to function, the http server which serves the menu on the network and the templating engine which generates the menu.

Web server

thqm doesn’t need to a high performance or featureful web server. So I decided to use the small and flexible micro-framework tomaka/rouille.

Here is roughly how thqm’s’ the web server is configured:

use rouille::{match_assets, router, Response, Server};

// ...
let server = Server::new(address, move |request| {
    router!(request,
    (GET) (/) => {
        // show the generated menu
        Response::html(&rendered_template)
    },
    (GET) (/select/{entry: String}) => {
        // handle printing the user's selection to stdout
        handle_select(entry)
    },
    _ => {
        // provide static resources (css, js, images, etc)
        let response = match_assets(request, &style_base_path);
        if response.is_success() {
            response
        } else {
            Response::empty_404()
        }
    }
    )
})
// ...

For the full source code of the server, see the source.

Templating engine

thqm uses the keats/tera templating engine. It takes inspiration from the popular jinja2 templating engine.

To generate a web page containing the list of entries, the index.html contains something like:

<!-- ... -->
{%- for e in entries %}
<div>
  <button onclick="fetch('./select/' + '{{ e }}')">
    <pre>{{ e | safe }}</pre>
  </button>
</div>
{%- endfor %}
<!-- ... -->

thqm provides the template with the entries variable which is read from stdin:

use tera::{Context, Tera};

// ...
let template_contents = fs::read_to_string(template_path)?;
let mut context = Context::new();

context.insert("entries", &template_options.entries);
let result = Tera::one_off(&template_contents, &context, true)?;
// ...

thqm comes with a few menu template styles which live at loiccoyle/thqm-styles.

Example scripts

Basic

Most scripts will look something like this:

#!/bin/sh

# define the handler function, i.e. what each option should do.
handler() {
  while IFS= read -r event; do
    case "$event" in
    "Option 1")
      # handle Option 1
      ;;
    "Option 2")
      # handle Option 2
      ;;
    *)
      # pass through thqm's output
      echo "$event"
      ;;
    esac
  done
}

printf "Option 1\nOption 2" | thqm "$@" | handler
# ^                                 ^     ^ Pass user selections to the handler
# │                                 └ Forward script's options to thqm
# └ Provide the options to thqm through stdin

The menu entries are piped to thqm which then pipes the user’s selections to a handler function.

Media control

Controlling media playback and volume on the host is a great use case for thqm, in fact an example is included in the repo for exactly this. This script uses the fa-grid style which interprets the provided input as fontawesome icons and displays them in a grid optimized for mobile devices.

#!/usr/bin/env bash

# This script uses thqm to create a dashboard to control the playback and volume
# of media playing on the host, using fontawesome icons.
# Requires xdotool, pactl

media_control() {
    while IFS= read -r event; do
        case "$event" in
        "fas fa-volume-up")
            pactl set-sink-volume @DEFAULT_SINK@ +10%
            ;;
        "fas fa-volume-down")
            pactl set-sink-volume @DEFAULT_SINK@ -10%
            ;;
        "fas fa-volume-mute")
            pactl set-sink-mute @DEFAULT_SINK@ toggle
            ;;
        "fas fa-play")
            xdotool key --clearmodifiers XF86AudioPlay
            ;;
        "fas fa-step-backward")
            xdotool key --clearmodifiers XF86AudioPrev
            ;;
        "fas fa-step-forward")
            xdotool key --clearmodifiers XF86AudioNext
            ;;
        "fas fa-arrow-right")
            xdotool key --clearmodifiers Right
            ;;
        "fas fa-arrow-left")
            xdotool key --clearmodifiers Left
            ;;
        "fas fa-play-circle")
            xdotool key --clearmodifiers space
            ;;
        *)
            # pass through
            echo "$event"
            ;;
        esac
    done
}

printf "fas fa-volume-mute\nfas fa-volume-down\nfas fa-volume-up\nfas fa-step-backward\nfas fa-play\nfas fa-step-forward\nfas fa-arrow-left\nfas fa-play-circle\nfas fa-arrow-right" |
    thqm --title="media" --style fa-grid "$@" |
    media_control

This will generate the following web page which can be used to control media on the host.

Check out the examples folder for more scripts.