Fitnesstrax 0.0.4 and GTK

Savanni D'Gerinel 3 Jan, 2020

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.


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 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

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| {

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:

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>
        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.

Some Ramblings for November 24th, 2019

Savanni D'Gerinel 24 Nov, 2019

Welcome back to another week of my ramblings.

Autumn here is quite thoroughly over, with Boston having gotten a couple of light freezes and a lot of dreary, watery weather. This picture is from more than a month ago, right around the time I was starting to get sick. Chances are, going on a hike and “taking the fresh mountain air” did not actually help my health at all, seeing as I then spent two weeks sick and several additional weeks with the dregs of illness.

Mental Health

This has been a struggle of a week. My mental health has cratered severly, and I have gotten into the range of not leaving the house because of the risk that I might have to interact with strangers.

Almost all of my trouble is work-related. I despair of ever getting to do work that I like, while I’m simultaneously having to maintain professionalism while dealing with complete disasters, and undisciplined men who are not actually interested in my experience (where by experience, I mean “hey, I’ve done that before and everything I touched exploded into flame”).

It is hard for me to take a mental health day primarily because I know that I will have to return to work to face whatever problems I was unable to solve before. This would not be a huge problem if I actually worked on interesting problems, but instead the problems I face are things like “the state model of this application is uncontrolled and getting worse” or “javascript is flagging errors in places where there cannot possibly errors”.

Let’s get something really clear: My career advanced as far as it did because I was viewed as a man until I reached the 32 years of age. My career has been backsliding ever since, with my options gradually shrinking, even though I have relentlessly expanded my technical skills and my project management skills.

Mental health, and the attendant suicidality that I manage, all revolve around hope. I basically don’t have any.

Obviously, this is not a healthy way to live. Where my co-workers and my clients see how I bring intense passion and skill to my work, the reality is that I actually just cannot disconnect from my work. I have never learned the trick of shutting work out of my mind when I am not working, and so problems at work always spill out into the rest of my life.

Rust, GUIs, and notifications

On my mental health day, I took the time to start getting familiar with building applicatinos in GTK again. I follow a development philosophy in which I have an “application” which is basically a headless object on which all operations can be performed, and the visible GUI is just a thin skin over that.

It turns out, React + Redux applications follow this model almost precisely. We all know that we can store our application state in Redux, using our actions and reducers to update the state, and using React to display the state.

So, on Thursday I took up the challenge of figuring out some version of that with GTK in Rust, and with some help I succeeded.


GTK inherently follows a “push” model of state updates, where React + Redux is made to feel like a pull update. React components exist and will pull any new updates whenever a state update occurs. With GTK, though, I have to build some of that infrastructure myself. I opted to use a listener model, in which I will register an update function for every component that I want to update in response to a state change. That update function receives the updated information and manually updates the component. The state manager (AppContext in the link above) calls all listeners (which really means just executing the registered functions) whenever a state update occurs. Functions that update the state are responsible for ensuring that this dispatch actually happens.

While this is conceptually straightforward, Rust’s very strict memory checking made this rather challenging to figure out.

Pay close attention to the closures on line 73 and 77. connect_clicked requires an Fn, not an FnOnce, implying that the closures will be executed more than once. Less obvious, though, the closures must outlast the closure passed to connect_activate on line 57, because the connect_activate call is specifically for setting up widgets. These widgets will inherently outlast the function call. So, dec_ctx and inc_ctx must both be cloned off of ctx, and ownership of them moved into the closures that get registered as callback functions.

It is not clear to me, however, why dec_ctx and inc_ctx, once moved, outlast an individual invocation of the enclosed functions. I can only assume that both variables (and, technically, ctx in the larger function) and the code fo the closure are bound together into a context that persists much longer than any particular invocation.

In writing this, I also look ta line 67 and wonder about the lifetime of the label, the label clone, and the update listener. In particular, I wonder what happens if the box containing the original counter_label gets destroyed. I suspect that while counter_label itself gets dropped, label_clone and the listener function continue to work as expected, but with no visual effects because no widget actually contians the label_clone.

For my next challenge, I will be converting the AppContext into something that keeps the handle to the underlying trax database, and which observes changes to the currently selecting range of data to show.

Immediate plans include full editing and record creation, plus better formatting. But I still want to get timezone, display units, and translations into the application as quickly as I can.


In the midst of the depression, I have done a lot of self-distraction. First, I bought and played through Sayonara Wild Hearts on the Switch. This is more of a music game, racing through lots of neon scenes at breakneck speeds, and putting together an impression of a story of a broken heart attached to astral highways. But, the game is intensely positive, the protagonist is hella trans, and there are furries, androgynes, and lesbians scattered around as shattered fragments that the protagonist has to reclaim.

Second, today I got Steam working correctly on my NixOS machine and then fired up “Torment: Tides of Numenera”. I bought the game while I was sick last December, but I was unable to play it because so many of the scenes broke on my Macbook’s graphics hardware. I find it horribly ironic that a Linux machine runs games better than a Macbook pro, but that is the world we live in.

As a sidenote, this is what it takes to get Steam working on a NixOS machine. Install these packages:


Next, enable this in your system’s configuration.nix:

  hardware.opengl.driSupport32Bit = true;

That’s it for this week.

The upcoming week will involve me dedicating most of my spare time to baking until Thanksgiving arrives, and then I will re-approach fitnesstrax late Thursday and for most of the day on Friday.

With any luck, things at work will go well enough that I can stop stressing.

Weekly Ramblings for November 17th, 2019

Savanni D'Gerinel 17 Nov, 2019
Barnaby Castle, 2019-09

This is Barnaby Castle in Providence, Rhode Island. Some of my friends live here, and it is currently undergoing incredible rennovations. The castle was apparently neglected for many years, and fell into disrepair, but it is still a very recognizable landmark that apparently everybody in Providence knows about. Currently it consists of something like five separate apartments, with signs of room for several more as the owner progressively continues with improvements.

I rather like my visits there, but I could really not live in the middle of a construction site.


Those who know me know that photography has been a significant part of my life for eight years now. Three cameras, many thousands of photos, and a bit shy of 1000 photos fully developed.

Photography on Linux, especially HDR photography, is definitely a trial. Five years ago I tried to build an HDR processor for Linux, but I never actually succeeded. I always ended up with a result that didn’t look like it could even be tone mapped correctly.

Sadly, the state of photography on Linux really hasn’t changed in that time. Digikam is still the best tool for the organization job, but it’s incredibly clunky with a 2000’s user interface when we’re about to enter the 2020’s. HDR photography exists in Luminance HDR, which is still very user unfriendly in that it presents the tone mappers to the user with just the names of the people who created them, and doesn’t allow the user to apply multiple different tone mapping operations to the same file.

Since I want to be doing my photography now, I’ve updated and rebuilt some old applications that chain together Hugin’s align_image_stack tool, RawTherapee, and pfstools into a semi-coherent whole. But since pfs-tools is batch mode only, there is no interactivity for me to run some tone mappers and view the results.

This is a hard problem. There is no way that I would ever say that it is not. It saddens me that the Linux tools have not advanced since 2011, though it saddens me more that I failed to actually build a tool myself.

At this point, these tools do the job, but it appears that Photomatix now exists for Linux, and I’ll need to see how to rebundle it for NixOS.

Though I have not given up the dream for building an entire excellent toolkit for doing photo editing on Linux.

Fitnesstrax and GTK

I spent a few hours reworking Fitnesstrax to be a GTK application, and had some interesting results. By interesting, I mean that I can get data to appear on the screen, but not in good format and certainly not in an interactive way.

Terrifyingly, I have learned that GTK 3 uses CSS for actually styling components. While this may make some things easier for me, I am terrified of doing CSS-style programming without all of the DOM-tree examination that is available in the browser. I will learn more about that in the upcoming week and will write more next time.

I will also need to learn how to handle state and its interactions with components. I have gotten very accustomed to React, Redux, changing the properties of a component to have it automatically update, making the Redux store look like properties. If I understand things correctly, React and Redux basically amount to Functional Reactive Programming. Rust has Carboxyl, but not really anything else comes up in Google. On the other hand, when I did Haskell programming I never fully understood functional reactive programming, so I may end up building something terrible, or I may hand-roll something more naive. In the very distant past, I set up a Mediator pattern where different parts of the application would listen to application events. I could possibly do something similar here, where state updates trigger mediator events.

But, likely, I should start by looking more deeply at Carboxyl, because FRP is generally a good tool.

And, that’s it for this week.

As always, I keep my nixos configuration and my nix shell on Github, so if you see anything Nix-related that interests you, that is where to see how I accomplished my tasks.

Weekly Ramblings for November 3rd, 2019

Savanni D'Gerinel 3 Nov, 2019
Along the Tracks, 2019-05

Hello, everyone, and welcome back to another round of my weekly ramblings!

This week I was still a bit under the weather, but I did a lot of work and started setting up my new machine.


For me, setting up a machine is a complicated process. I typically run NixOS where I can, and I run MacOS with a Nix overlay when I cannot. For running MacOS, where the OS comes pre-installed and ready on the pre-configured hardware, system setup can take less than half an hour. I have to install nix, clone my nix-shell repository, then run nix-env -i all, and suddenly I have a full system with all of my configuration options.

But, with NixOS, I do not have any configuration options any more. I am having to build from the base system up. My last NixOS machine has been gone for several years, the configuration options on that machine have been lost, and wouldn’t really work on the new machine, anyway.

I went with AMD fo this build, and there were a couple of different quirks that I had to resolve.

  • Thinkpad x395
  • Ryzen 7 processor

First of all, and since this is my first time really dealing with the UEFI boot, be sure to turn off Secure Boot! NixOS cannot be booted in secure mode at this time, so it is very necessary to turn this off. Primary symptom was that I would reach the UEFI boot menu, select a NixOS USB key, and would immediately get back to the UEFI boot menu. This is particularly annoying because there is no error message and no log to check.

Second one is that the stable versions of NixOS ship with a 4.x Linux kernel, and it turns out that recent AMD processors really require a 5.x kernel to resolve quite a few different bugs, especially with ACPI. That meant switching to the latest linux kernel by setting boot.kernelPackages = pkgs.linuxPackages_latest;.

I have done quite a bit in the way of configuring my new system. I have shared my entir NixOS configuration on Github. Relevant options for a Thinkpad probably come down to just these, with everything else being specific more to me.

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
  boot.kernelPackages = pkgs.linuxPackages_latest;
  boot.kernelModules = [ "kvm-amd" ];
  boot.extraModulePackages = with config.boot.kernelPackages; [ ]; = ["" "${pkgs.coreutils}/bin/true"];

  boot.kernelParams = [ "acpi_backlight=none" ];
  programs.light.enable = true;
  services.actkbd = {
    enable = true;
    bindings = [
      { keys = [ 225 ]; events = [ "key" ]; command = "/run/current-system/sw/bin/light -s sysfs/backlight/amdgpu_bl0 -A 10"; }
      { keys = [ 224 ]; events = [ "key" ]; command = "/run/current-system/sw/bin/light -s sysfs/backlight/amdgpu_bl0 -U 10"; }

Still got more to do here, as I don’t have the screen locking when I suspend the machine, and sometimes the machine hard locks on suspend instead of actually suspending. But I’ve made progress!


It turns out that very few of my MacOS derivations work on NixOS. It appears that on NixOS, the derivation builders generally don’t have internet access, and so derivations that download as part of the build don’t work.

As an example, my derivation for floki depends on running npm install in a normal build script, and that npm install doesn’t have access to the internet.

For some of these, the only thing I shoul need to do is to learn how to use node2nix. But for Floki it gets more complicated because Floki is not a Javascript app, but a Clojure app that compiles to Javacript.

  • all of my development derivations
  • Fitnesstrax
  • Photography tools
  • Steam

I have a lot more to do in order to be fully running on my new machine. Much more than I expected when I started on this, but it is all educational.

Furthermore, I think of it is training, and it certainly gives me more hands-on experience with NixOS that I can share here.

But, I’m running late on this post, so that’s it for this week! I’ll let you know about additional progress over the weekend.

Weekly Ramblings for October 26th, 2019

Savanni D'Gerinel 26 Oct, 2019

Every week I try to post a set of ramblings about things I find interesting or things that I have done in tech for the most recent week.

However, I’ve spent weeks sick. Longest illness of my life, to the point that I felt better while on bed rest eight days post-op than I did eight days into this illness.

In the midst of this, though, I was able to celebrate my 41st birthday, complete with wine, cheese, friends, and me threatening a cake with a knife. My pastry creations don’t fare well, but they are very, very tasty.


A few weeks ago attended a Go tournament for the first time. This was an official handicap tournament under the rules of the American Go Association. In theory, the results have been reported to the AGA and I will get an official rating, though that hasn’t happened yet. I’m sure it’s because AGA is not a technical organization and everything has to be manually entered.

On OGS, I currently have a rating of 16 kyu. I entered the tournament at 15 kyu. I then won three of my four games. In some cases, I received six and eight stone handicaps, and in both cases I was victorious by a significant margin. While some of the problem is that my opponents have never had to fight against such a handicap, which really does change the nature of the game significantly, I do feel that I should have entered as a 14 or even a 13 kyu.

Either way, that tournament, and playing Go Quest (all blitz games), I have certainly gotten a lot more momentum in my playing, especially because it has been easy to play at least one 13x13 game on the train ride from Davis Square to South Station. This inspiration has made it relatively easy for me to play one or two full size games in an evening after all of my chores are done, or while I am unwinding from work.

In case you are interested, this is my account on OGS.

Por miaj esperantistaj sekvantoj, mi trovis afiŝon Baza strategio de goo. Mi bonvenas defion el alia Esperantisto.

New Machine

I have a new machine coming my way. I switched off of Linux onto MacOS about two years ago when my office pressured me to switch operating systems, and I also made the decision to continue having only one computer, which means that I have effectively not owned my own machine for a couple of years.

In some ways, this has been really nice. I’ve had access to a library of games that did not used to be available for Linux. But in other ways, it has been hard. As much as I have tried to continue using Nix to keep my environments isolated and to be able to consistently have a known good baseline state for every development environment, in practice some of the tools that we use as a matter of course actually resist such isolation. Yes, I’m glaring at you, Ruby. See about my nixpkgs bug report here. That bug has been resolved, but that, plus the next contract I joined, hammered home that at Cloud City we really do not use tools that lend themselves to isolation, and for work purposes I cannot wait for fixes to Nixpkgs.

I would actually rather just set up a separate VM for each client, but MacOS is very unfriendly towards virtual machines and I was never able to get an installation to actually work.

Time Machine has already had three catastrophic failures this year. While I have not lost data, that is only sheer dumb luck. In the case of this failure, Time Machine’s recovery strategy is to delete the original backup and then start on a new backup, leaving me with multiple days of no backups while the new one is being built. Apple should bloody well be ashamed of themselves for that. Further, when I was using rsnapshot on Linux, the only failure I ever had over the course of years was actually a physical hard drive failure. That is the degree of reliability that I expect.

So, I’m really looking forward to switching back to NixOS.

One of the benefits that I’d forgetten about is that the GTK bindings for Rust are much more complete than the Cocoa bindings, so I can just start on porting Fitnesstrax to a GUI application immediately instead of also taking the time to rewire a widget set. Now that i am well again, I can actually start thinking about resuming development on the project.

The only drawback is that I’ll likely lose access to Aurora HDR, which I have been using for my photography for a few years. This isn’t necessarily terrible, as I did a lot of very good photography before switching to MacOS, but the tools available on Linux are really substandard. I tried to build an HDR application for Linux many years ago, but unfortunately the math was too hard. It is always possible that I will return to that project, as it is as interesting to me as Fitnesstrax, but that is not in my schedule for today. Note that there are HDR processors for Linux, but they are horribly user-unfriendly. The one and only GUI tool exposes a bunch of different tone mappers, but it does so with excessively technical names such as “Debevec”, “Mantiuk”, and “Fattal”. None of the tone mappers can be used together, and the settings have undescriptive names such as “Phi” and “Key Value”. Hence why I wanted to develop my own tool, but it turns out that the core math problem, developing an HDR map from the original sources, was beyond my ability.

Hollow Knight

As I mentioned, I was sick for almost two weeks. Not hospital-level sick, but coughing, low energy, my entire face hurting kind of sick. With the lack of energy, I haven’t studied, written or coded outside of work.

What did I do?

I played a lot of Hollow Knight.

I actually started back at the beginning of the game. This is easily the best game released in the last two years, but it was so hard that I got completely stumped in my last play-through. This time, though, I actually am doing much better. Partly, I am looking at the wiki to make sure that I find important items. Mostly, though, I’ve practiced some of the more… ahem acrobatic techniques, and I’ve learned to spam spells, and that has made some of the bosses, like the second encounter with Hornet, much easier.

Now, I said that the game was be best released in the last few years, and there’s a few reasons why.

The most obvious one is that the action is clear and crisp. Other games, like Salt and Sanctuary, are plenty good, but they are more about tanking than about developing any real skill. Hollow Knight obviously has upgrades that make the game easier, but generally no battle can be won just by tanking, only by developing skill, combining the right techniques and having some good luck. This is very satisfying.

But my real love for the game comes from the very consistent mood through the entire arena. You are exploring a fallen kingdom, one which tried many times to survive, but simply couldn’t. And while the wild areas around it seem to be doing fine, the kingdom itself; through the architecture, color schemes, and music; all speak to a place of sadness, loss, and resignation. I have never before encountered a game that was so consistent in maintaining a mood and theme, and this was done masterfully, with beauty mixed so carefully with nostalgia.

Hence… why I decided to spend my sick time re-playing a game that caused me to nearly throw my controller across the room back in March.

And that’s it for this week. Now that I am well again I should be able to resume posting regularly. I hope you all enjoy reading my ramblings.

Dreamer, Shaper, Seeker, Maker