Luminescent Dreams

Fitnesstrax 0.0.4 and GTK

January 03, 2020
_DSC3706.web

I finally return after a bunch of weeks. December was an incredibly hard month, and the mental health problems I referenced in my last post actually became significantly more pronounced. I found that I had to take a large amount of time away from the world in order to get things back together.

Last time I got this bad, back in 2013, I had the freedom to quit my job, sell my house, and go live in the woods for a year. Not so this time, as I still haven’t recovered from the financial impact of trying to control my re-entry into the workforce.

Nevertheless, I am probably better prepared, and better supported, that I was back in 2013.

FitnessTrax

I am pleased to finally do a new release of FitnessTrax!

This represents a significant technology shift. Instead of a web client/server application, this is now a dedicated GTK application. This should make it significantly easier for regular users, though right now I am only building and testing on Linux.

However, forward progress also means some backsliding. Here’s some differences from the previous web app:

  • You need a configuration file – fortunately, easy to set up
  • Most configuration is unsupported – you are stuck in English with the metric system, but timezones work
  • Weight, step counting, and time/distance workouts
  • No Set/rep workouts or duration-only workouts

I hope to move forward quickly on all of these things, especially now that I have largely discovered patterns that allow me to work with Rust and GTK together. Version 0.0.5 will have much better configuration support, including the ability to reconfigure and have the configuration options automatically saved.

So, to get the application, you can either get it from my Nix chcannel, or you can clone the repository and build it yourself.

FitnessTrax in Nix

This is certainly the easiest way to install FitnessTrax. Note, however, that it is most likely to work on various Linux distributions all running the Nix package manager. It may or may not work on MacOS.

nix-channel --add http://luminescent-dreams-apps.s3-website-us-west-2.amazonaws.com/nixexprs.tar.bz2 luminescent-dreams
nix-channel --update

nix-env -i -A luminescent-dreams.fitnesstrax_0_0_4

FitnessTrax from Source

If you want to build from source, start by cloning the repository:

git clone https://github.com/luminescent-dreams/fitnesstrax.git

You will need to install a variety of GTK development libraries, including Pango, Cairo, and possibly also Atk. Refer to your package manager for more information. You will also need Rust 1.39 (or newer, but I’ve built and tested on 1.39).

Once you have the libraries, you can build the GTK aplication:

cd fitnesstrax/gtk
cargo build --release

You may need to add version numbers to the two relevant Cargo files, as I removed the version numbers on the assumption that all of my distribution would be managed through Nix. I recommend version 0.0.4 for both, and I will fix that problem when I release 0.0.5.

Configuration file and starting the application

Once you have your executable, you’ll need to create a configuration file and then tell FitnessTrax where to find it. You can put it anywhere, but I rather like ~/.fitnesstrax.yaml and may make that the default location some time in the future.

---
series_path: /home/savanni/health/health.series
timezone: America/New_York
language: en

All three fields are required, even though not all three are supported.

Important note about timezone: it must be in the format specified in the Olsen Timezone Database. This will be more familiar than you expect as it is the same list of timezones available when you configure a Mac or an Ubuntu system.

Now, on the command line, this should start the application (replace the configuration file with the path to your configuration file):

CONFIG=~/.fitnesstrax.yaml fitnesstrax

The actual name of the configuration variable will change in version 0.0.5, as will how the configuration file gets handled.

GTK and Rust

Rust has a largely complete GTK binding. However, there are not many good examples of how to build a multi-threaded application with it. GTK is inherently single-threaded, and the Rust bindings enforce that by not providing a Send implementation for any of the widgets. This makes it impossible to move any of the widgets, or even references to those widgets, into secondary threads.

While the team is working on Async/Await support for the bindings, I have no idea what form those will take.

In order to do a multi-threaded application, they have provided a channel that receives messages on the main GTK loop. As normal, you receive both the transmit and receive channels. You must then attach a callback function which will be executed for every message that arrives on the receive channel:

    application.connect_activate(move |app| {
        let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);

        let ctx = Arc::new(RwLock::new(context::AppContext::new(tx).unwrap()));
        let gui = Arc::new(RwLock::new(components::MainWindow::new(ctx.clone(), app)));

        let gui_clone = gui.clone();
        rx.attach(None, move |msg| {
            gui_clone.write().unwrap().update_from(msg);
            glib::Continue(true)
        });

After this, I have modelled the components that I write pretty heavily on my experience with React. I create a lot of composite components, and as much as I can I try to make them stateless. For instance:

pub fn weight_record_c(record: &WeightRecord) -> gtk::Label {
    gtk::Label::new(Some(&format!("{} kg", &record.weight.value_unsafe)))
}

Or, for something more sophisticated, I may build a struct:

#[derive(Clone)]
pub struct ValidatedTextEntry<A: Clone> {
    pub widget: gtk::Entry,
    value: Arc<RwLock<Result<A, Error>>>,
}

impl<A: Clone> ValidatedTextEntry<A> {
    pub fn new(
        value: A,
        render: Box<dyn Fn(&A) -> String>,
        parse: Box<dyn Fn(&str) -> Result<A, Error>>,
        on_update: Box<dyn Fn(A)>,
    ) -> ValidatedTextEntry<A>
    where
        A: 'static + Clone,
    {
        ...
    }

In this case, the constructor is enough as all of the values get wired together inside the constructor.

This is not React, and there are no declarative automatic by-property updates. So I have to code those myself. This requires some clever thinking about coding. Most components can simply be updated in response to things that are happening internally. For instance, the ValidatedTextEntry above has multiple behaviors attached that respond to how the field changes.

The trickier work involves application-level changes, such as saving data to disk or changing the range of data that should be visible. Aside from the message handler above, some components have an update_from method so that they can update in response to global application events. For instance:

pub struct History {
    pub widget: gtk::Box,
    range_bar: RangeSelector,
    scrolling_history: gtk::ScrolledWindow,
    history_box: gtk::Box,
    ctx: Arc<RwLock<AppContext>>,
}

impl History {
    ...
    pub fn update_from(&self, range: DateRange, history: Vec<Record<TraxRecord>>) {
        ...
    }

}

History is the dominant view of my application, showing every day of information. In response to a ChangeRange{ DateRange, Vec<Record<TraxRecord>> } or a RecordsUpdated{ DateRange, Vec<Record<TraxRecord>> } message, it will delete all of the visible components and create new ones based on the data available. Remarkably, this happens with no visible flicker and is basically instantaneous on the data that most humans will want to look at.

This provides only an overview of GTK + Rust programming. There is a lot more for me to talk about, and I think in future weeks I will take a more detailed dive into the various component architectures that I create, as well as the overall application architecture. For now, consider this a simple teaser. However, this is an open source application, so feel free to take a look at more things in the application.

Looking to Fitnesstrax 0.0.5

For the next version, which I hope will only take a few weeks, I’m going to get configuration working. This means supporting the configuration UI, plus having the rest of the UI translate based on timezone, language, and measurement system.