I never really gel'd with Advent of Code. I think it's just not really for me. But while I didn't really enjoy the contents event itself, I think the premise is fun, and it's nice to be able to do something with a bunch of people who share similar interests with you. It is perhaps because of that desire that I immediately found myself on board with December Adventure as soon as I heard about it.
The December Adventure is low key. The goal is to write a little bit of code every day in December.
The project towards which I'm going to do my December Adventure development is the CLAP plugin library for ↹lature. While I'd love to do a brand new plugin each day (and I probably have enough ideas to get through the month doing that), the premise of the adventure seems to encourage low-key, consistent progress, so I'm going to aim for something a little less manic, and set my end-goal as developing a meaningful feature every day.
Watch my progress here: https://git.sr.ht/~jaxter184/tlature-plugins
Current list of plugins:
- chord name to note generator
- simple instruments
- simple fx
- livestock auctioneer physical model
- maybe even something as simple as a singing voice synth with a southern drawl
- osc output
- step sequencer
(Not intended to be exhaustive, just to show the general roadmap)
While this wasn't my initial plan, I spent basically my whole evening reorganizing the ↹lature plugin repositories. Today, I:
- Renamed structs to make the plugins consistent with each other
- Moved structs to make code easier to navigate
- Combined the
- Made a
cargo-generatetemplate for creating new plugins
While this is all a lot of really helpful preparatory work (renaming and moving structs was particularly time-consuming), I'm particularly excited about the template. It was surprisingly easy to make, and I'm looking forward to both using it myself and also asking people who expressed interest in the project in the past to try it out to see how difficult it is for them to use.
Did some organization on the main ↹lature repository to make it more sturdy. The goal is to be able to easily point people to a specific commit or tag where everything hopefully "just works".
The most on-track December Adventure thing I did was making an
event-offset plugin that offsets incoming events in time and pitch.
Well, not every event, just note events.
Also it doesn't do anything yet, it just passes events through.
That is, I think it does, but I can't tell because I haven't set up a
way to test it yet.
So I spent the rest of my day working on a ↹lature feature to be able
to define event output files.
It's not done yet, but once it is, it should be helpful for debugging
plugins in the future.
But it's December Adventure! So falling off course and working on something completely different and then not finishing it is still a success!
Trying to create plugins for ↹lature has really pointed out all the
flaws of its current state.
I thought it would be easier to make event plugins instead of
audio plugins because it would be easier to debug and test for, but it
turns out that not only have I neglected to set up any tests, but I've
also completely underdesigned all my event handling.
I'm not totally sure what I'm doing going forward, but at the very
least, I added a feature to convert CLAP plugin event outputs back into
OSC messages, so I can confirm that the
event-offset plugin does
indeed work as I would expect it to in its current form (i.e. it passes
the events through as-is)!
I'm really liking this whole December Adventure thing. The flexibility of being able to explore things at my own pace gives me space to breathe and give this stuff the time it deserves instead of churning out solution after solution.
↹lature is, by design, an OSC-powered DAW. Back when it was just a sequencer, that worked because all it had to do was generate notes and spit them out somewhere. But as the project grew into a more complete audio workstation, I decided that I wanted the sequencer to be "aware" of the downstream consumers of the messages, and once plugins were added, I also had to add some sort of converter between OSC and various CLAP events. Until now, I was able to get away with that because I was only doing the lossy conversion from OSC, a very flexible, generic message format, to CLAP events, which is more structured. So now that I want to convert them back, I'm not sure how to do it.
To illustrate, there are multiple OSC messages that can convert to the
same CLAP event.
/notes/v0 i 60 and
/ ii 0 60 both convert to middle C on the 0th voice.
So if a CLAP plugin outputs a middle C, which of those OSC messages
should it convert to?
The solution that I came up with is that it shouldn't. OSC conversions should only go one way. Maybe in the future it would make sense to set up some sort of conversion, but for now, the plan is that any plugin that wants to output events for ↹lature to send to other plugins should output OSC events only.
To facilitate this, I created an OSC message extension that provides
OscEvent (that is, I copied the MIDI SysEx event format from regular
CLAP and changed some names).
It essentially consists of a buffer that is written to and read from
rosc encode/decode functions.
if let Some = ea_event.
There are a lot of things that aren't great about the code above (memory leaks, allocation on the audio thread, ignoring bundles), but those are problems for future me to solve.
(When reading the following, it may help to know that
more or less just a reimplementation of
PathBuf that uses
If that isn't clear, just think of
Location as a string that
represents a path, like
I think the most difficult thing with ↹lature so far has been deciding how routing works. The general philosophy is as follows:
- routing should be easy to understand if explained properly
Locations should be generally analagous to paths on a filesystem, and the routing syntax should reflect that
- for people editing the project file directly (for example, in a text editor), opportunities for error should be minimized
- the "royal road" should be encouraged by the syntax, generally by making it short and simple compared to less common alternatives
- the actual logical code for routing should be relatively simple, with few edge cases
I believe the current method achieves this, though perhaps not to the extent I would like. That being said, having added OSC outputs to plugins has shown me the flaws of my current scheme, so I made a few additions to adjust the routing behavior. Currently, the process data is structured roughly like this:
In this case, there are two places that can represent the "path" of the
Location key in
event_map, or the
which contains an address field.
The changes I want to make are to resolve an ambiguity as to whether
the new path should be reflected in the
or in the OSC message's address.
I've decided for now that the
event_map key of any newly routed
message will simply the current context (that is, the path of the
parent block), and the rest of the path will go in the
I'm a little hesitant to split it up like that because it makes
matching message paths difficult later down the pipelite, but chances
are, it will need to be refactored again soon anyway, so I'm fine
leaving things a little messy and placeholder-esque for now.
All of that planning aside, the only change I made today was to reflect
this design in the loop that gets the events of
After making the changes yesterday in the
processing, I had to change
Processor::Route behavior to be
consistent with it, which broke some tests.
I think in cases like these when I make some weird change that might
affect behavior, I'm really appreciative of my past self for setting
up these tests to give me a more clear path to things working as they
I wish I had the energy today to describe this particular case where I
benefitted from a test, but for now, I will just say what has been said
many times, and more persuasively by many others: write unit tests
for your code!
Honestly not sure why I thought I needed OSC support in order to make an event offset event. I can just make ↹lature pass note events to downstream processors. Maybe I just wanted to not deal with implementing both an OSC channel and a CLAP event channel, and just only dealing with OSC events?
Anyway, today I just added some logic for handling CLAP
in addition to OSC events.
Things have been slowing down, and I'll probably skip tomorrow to do some non-computer-touching stuff. I'm a little sad that things haven't been as adventurous as I had hoped when starting December Adventure, but I'll be ambiently thinking about how I can shift my progress to be more exploratory going forward.
Implement CLAP event channel routing
As alluded to in day 07, the thing about December Adventure that I like the most is the idea of exploring a little bit of a fun idea every day. The most recent few days have felt like a lot of slogging in place, and I was hoping to get out of that funk, so I've expanded the scope to be anything ↹lature-related rather than just plugins, with a priority on fun new features that I don't see in a lot of music software or TUI programs.
First on my list is a file/string picker pop-up. While common in music software, it's not so common in TUIs, so there are a couple interesting things to think about when it comes to the design. But before working on the feature itself, I first need to figure out how my GUI is going to work in general, in particular how various elements are created/activated. Two organization methods that I've run into in the past are stacks and layers. With stacks, you push and pop overlays, and with layers, you activate and deactivate overlays. The main difference is that with stacks, overlay order can be dynamic, while with layers, overlay order is fixed.
When it comes to the interface, it should be easy for the end user to be able to tell how their actions will translate to actions in the software before they actually perform them. A stack throws a wrench in this understandability, because with a given screen, there may be 3 different overlays, plus something going on in the status bar. Rather than using something like color or highlighting to show what is currently focused, I think making the layer order fixed would train people to instinctually know what's focused by assigning a sort of "priority" to each type of overlay. For example, if a help dialog always has higher priority than the numerical argument input, then you know that if you see a help dialog, you can't perform any num arg inputs. Things get a little tougher with the command bar, because it's pretty subtle whether or not its active from just the command input itself, so adding a command info dialog and autocomplete assistance menus will make it clearer that the command entry is actually active.
I did a bit of messing around to see what things would be like in terms of implementing each, and I decided that while both have their flaws, I like layers slightly more.
One thing that makes it difficult is that I want overlay state like the numerical argument and replacement values to be a dynamic "variable" rather than a statically define Rust struct, with the goal of making it more accessible from the hypothetical future scripting language. I implemented rhai support in the past, but removed it because I didn't like how I had to structure the application state to accommodate it, so maybe it's worth considering removing that entirely.
TODO:Implement layer UI TODO:Add string picker pop-up
Spent all day today spinning my wheels trying to figure out how I should refactor layers to work. I think I'm going to abandon this for now and come back to it after December Adventure ends. (Update: I did not abandon this, and it now works pretty ok)
Not even sure what counts as December Adventure anymore, but spent the evening configuring Zellij. It's a pretty neat piece of software, and while there are a couple things that annoy me about it (mostly visual clutter), I learned a lot of things that seem to be very helpful for higher-level planning for ↹lature.
While not intentional, it really helped me solidify a big decision with ↹lature: session management. I have two choices when it comes to managing ↹lature sessions: I could do it all internally, and have the program itself organize the various panels and windows and tabs, or I could hook into the window/session management of some other program.
Just to give a run down of what exactly I mean by session management, there are various programs that manage different workspaces/windows/sessions:
- Kakoune has a concept of "buffers", which are basically all the
documents you have open, plus a few extra scratch areas where you
can do things like peruse the various references of an identifier to
goto, or see debug information, and all of the buffers are associated with a session, which can be used to connect a different client to the current set of buffers.
- Bitwig has tabs, which can each have a project, similar to a web browser.
- Sway, my window manager, can organize windows in various ways, but the most basic method is tiling windows into various panes, which show up side-by-side.
- Zellij has pretty much all of these: panes fit into a tab, and tabs are hosted in a session, which a seperate Zellij client can connect to.
I hadn't really given too much consideration, and assumed that I would
be implementing this myself in ↹lature, but it seems like Zellij
has enough functionality (through things like
zellij action) for
↹lature to be able to integrate pretty seamlessly with it (given an
There were also a couple other things that exploring Zellij helped with. Being a TUI Rust program, it runs into many of the same restrictions that ↹lature does, but compared to Helix (another TUI Rust program), there were a few aspects of Zellij's approach that I liked more, particularly with regards to keymapping, mode management, and plugins, all of which I'll probably talk about as December Adventure continues. Also, it uses KDL!
Despite not having even looked at any ↹lature code today, I made a lot of progress on it! While I'd like to say that it was all part of the plan, and that December Adventure put me in an exploratory, casual mindset that led to me making the decision to take a step back and just check out some other stuff, that's not really what happened. I just happened to check out Zellij on a whim, and it just happened to be really helpful for getting a better idea of what I want ↹lature to be. That being said, I'm pretty sure I wouldn't have tried Zellij if I was doing Advent of Code right now.
Recently, a friend started working on a Discord bot, and it made me
jealous, so I decided to go fix up the bot I made for ↹lature.
My bot was pretty simple: you'd send it a message, and it would reply
with a rendered
.wav of the output.
The main thing I had to do to fix it was to migrate the rendering code
to use the new
ProcessData structure, which greatly simplified the
Next, I just need to update the part that interfaces with Discord to
There's also the concern with calculating the
tail, but hopefully
that just works without too much fuss.
An example of the old message format:
The goal was to adapt the rest of the ↹lature discord bot to work, and while that did get done, I unfortunately ran into an issue with the tail where it would always return a length of 1. While trying to figure that out, I adjusted the tests so that they would dump the audio output into a file, and in doing so, I uncovered another issue: the test audio output was not the same as the output when opening the test projects with the TUI. I'll hopefully figure it out tomorrow.
Spoiler: I didn't figure out yesterday's problem.
I had a few things going on today, and I didn't do as much work on
↹lature as I wanted to.
But, in the spirit of December Adventure, I at least took a look.
I've got a little TUI tool called
twv that I use to inspect audio
files, and it seems that there's either a bug in
twv or something
weird with the output, because I can't see it.
It's likely that the signal is entirely outside the -1.0 to 1.0 range,
but since I'm writing it to a WAV file as integers, it should clip?
I'll figure it out tomorrow.
We're already halfway through December Adventure! The days have been whizzing by, and I haven't completed a single plugin. But that's OK, because I've been exploring tons!
I've refactored the
twv code a bit, and added character markers for
zero, NaN, and end of file.
After fixing this and using it on the output, I found out that:
- Using Ctrl+C to close my recording program results in a malformed WAV file that thinks it has a length of 0 despite being 188kB
- When I gracefully shutdown the recording program, it seems that the expected sine wave is mostly intact, except for a few discontinuities, which I assume occur at buffer boundaries.
To show this off, I want to make a little asciinema recording where I
create a WAV recording from ↹lature, and open it in
To do so, I need to add a feature to my CLI recording utility to let
it connect to ports in the middle of its execution, and not just at
While working on that, I ran into a bug.
Did you know that a backgrounded program can't read a line from
while starting an async JACK client?
Neither did I!
(Or maybe you did, in which case, why didn't you tell me‽)
After several hours, I think I've fixed everything I needed to make the
recording that demonstrates the problem I had yesterday.
While I could probably make the recording at this point, it's getting
late, so I'll leave learning
autocast and making the recording for
There were a couple extra things I had to do to get my utilities to
The main incompatibility was that the point in the recording when
↹lature starts is not fixed. Sometimes it takes longer to start up,
so I couldn't repeatably zoom in on the exact point of the
To fix this, I added the [ keybind to
twv, which pans the
waveform such that the first nonzero sample is at the very left of the
From the first nonzero point, the buffer discontinuity is always exactly
the buffer length.
But after sorting through all that, I present the first December
(Though it seems there's an issue with how box-drawing characters
are drawn in the asciinema player, so you may need to download the
asciicast and use the
asciinema CLI to play it back)
To break down the chunky command in the middle:
jaxrec: my recording utility. The recording ends automatically when all the input ports have been disconnected.
-p 1: open one port in the jack client, and write a single-channel (mono) WAV file.
tlature:output_1: the positional arguments to
jaxrecmatch output ports to each input using a regex, or simply the first port(s) that start with this string.
-e: ignores the stdin. Usually,
jaxrecends when enter is pressed, but as described yesterday, this prevents the JACK client from starting, so we use this flag to disable that feature.
&> jaxrec.log: pipe all stdout and stderr to the file
&: run the previous command (
jaxrec) in the background, and start the next command immediately. Contrast with
;, which waits until the previous command finishes before running the next command.
tlature tlature-engine/projects/load: open the project file in ↹lature.
While I was getting this working, I realized why the output was not consistent between the TUI and the tests: the tests have extremely large buffers, such that the entire test runs in one buffer, so there are no buffer edges for the test tone to be discontinuous across. After fixing that, the tests seem to be pretty much in line with the behavior of the TUI, so I think I can move forward with debugging why the events aren't getting read in the tests, after which I can move on to fixing the Discord bot.
Started a bit of work on a KDL highlighter for kakoune. It's just a basic implementation that colors numeric values and quoted strings, and sets the indentation on each newline. Not directly relevant to ↹lature, but it'll at least make the files a bit easier to read and write.
TODO: Features that I plan to add are properly highlighting the types, node name, and comments.
Did a little bit more diagnosis on why my tests are behaving strangely. I made two findings:
- The OSC messages are not stored in chronological order.
My assumption was that
clacksorts the order when it creates the event buffer, but that could very well not be the case. In fact, it may be better for performance for that to be done on ↹lature's side rather than on
test-toneplugin sees exactly one parameter change that sets the frequency to 440 Hz. It is likely that this is from the first event. Potentially related to the above bullet point.
- When adding an abridged version of the
ave-mariaexample project as a test, only the arpeggio voice is output in the test, and the reverb plugin (michaelwillis' Dragonfly Reverb) raises some sort of DPF error.
Possibly also due to out-of-order events.(Update: this was not due to the out-of-order events, and is still an issue) TODO:
Tomorrow I'll try actually putting the events in the right order before
they're added to
clack's event buffer.
Did a couple of minor fixes for event processing. As alluded to yesterday, I fixed the event order, as well as an additional bug where the root block sends a blank OSC message on note off.
Missed one spot where transport events are sent after all of the other
messages instead of being interleaved with them.
The solution was calling
sort on the event buffer (which I should
have been doing in the first place), so events being sent out of order
should no longer be an issue.
The plan tomorrow is to take a shorter day by organizing commits and
checking the behavior of all my tests and examples.
Organized the commits as planned, and fixed the Discord bot, so now everything works as it did before I made all the ↹lature changes.
I updated to the latest version of
serenity (a Rust crate interface
to the Discord API), but I think what I should've done is use
which is a higher-level framework that makes things like context menu
bot commands easier.
I'm not gonna worry about that for now, and go back to ↹lature work
This weekend is also the start to my time off work, so I'm hoping to spend the time doing some particularly fun stuff with ↹lature, and maybe get another demo out. I don't want to put too much pressure on myself to get stuff done, but it's the final stretch of December Adventure, and I want to continue hiking along as I've been doing up until now. I think the theme of "do a little bit every day, with an exploratory mindset" has been very beneficial so far, not only for the project itself, but also my attitude towards it.
Made some plans for how the UI layers are going to be implemented. And that's not a euphemism for not having done anything, I did actually write some code today! Comments count as code, right?
Anyway, I'm chalking today off as a sort of wind-up to a future follow-through. I deserve it. In fact, I always deserve to take things slow.
I added a bunch of stuff today, and pretty much all of it is stuff that I discussed on day 09:
- Refactored to a layer/mode hybrid UI system
- Refactored the
CommandTemplatestruct to be (hopefully) a bit easier to work with in the future when I add documentation for individual arguments
- Added a command popup interface, which is the main use case for the string input dialog that I mentioned on day 09
- Added commands for adding, inserting, and removing a processor
But maybe it's easier to describe with a demo, showing the newly added processor commands:
I forgot to also demo the new command popup (which is probably the more
fun and interesting addition), but it's late, so I'll do it tomorrow.
- Added a structure to communicate information about the currently
selected item, so that you can use commands like
:insert processor delaywithout specifying the block that the processor is going to be added to (before, you had to use
:add processor --processor 0:this-block/3 delay)
- Improved the command popup. Hints are now filtered based on whether they start with the current typed value, and you can use Tab to navigate hints.
TODO:Ran into a bug where processors are added even if they don't exist, which has proven to be a bit tough to fix because of the way that ↹lature is structured
- TODO: There seems to be a hitch in the audio thread, so I'll probably do some profiling after December Adventure ends, and maybe look more into the real-time safety
I'll be honest, these log entries are much more for myself than they are for other people. I think something that would help with that is drawing a clearer narrative. For example, starting with how I want the user flow to work, some bad approaches to accomplishing that, and then my final approach.
But until then, another demo! (The one I said I'd make yesterday):
(unfortunately, the box drawing characters are still a bit broken)
- Fixed the bug where adding a plugin that doesn't exist crashes the program
I started some work on refactoring the structure I made yesterday.
As it is now, it just passes in arguments (like
automatically, but really, I want the argument parsers to understand
the value, so I can use relative paths.
Finished refactoring the environment information passing.
I'm really happy with how usable commands like
rm processor are now.
You can now use a relative path to add and remove processors and
blocks, which also lets me use
. as a default value for the
--processor variables, which gets interpreted as the currently
Started some work on refactoring the OSC channels to more general CLAP event channels. Right now, I'm creating OSC events by leaking memory which is definitely not sustainable, and I'm approaching the limit to the hacks I can get away with by just leaking, so my plan for tomorrow is to get a better idea of what CLAP needs from a lifetime perspective, and adjust my implementation to better align with that.
Finished changing OSC channels to CLAP event channels.
Did a lot of strange stuff that is definitely not real-time safe (I
will leave that for future me to figure out), but it passes the events
as needed, and didn't really require too much thinking.
I just changed one of the types from
Vec<(u64, OscMessage)> to
clack::EventBuffer, and followed the Rust compiler's instructions
The most involved part was writing a function to filter the events
in the buffer and produce a
Vec<(u64, OscMessage)> for the OSC
Hooray for Rust and its ability to facilitate fearless refactoring!
Just yesterday, ↹lature received the first ticket submitted by someone other than me! There are a few requests that folks have pointed out to me over private channels because I asked them, but this is the first one from someone I've had no prior contact with, and I didn't have to solicit them for feedback. It might seem pretty insignificant from an outside perspective, but personally, it's really cool to see that someone cares enough about this project to let me know that something is broken, even if it's just because they want to check it out as research for their own project. In fact, I'm pretty flattered that they looked at it and thought it would be a good reference in the first place.
Fixing windows support, as requested. I actually should have been working on it for a while because someone else had also requested it (via private channels, and after I solicited them to try it out), but the high of receiving a ticket really motivated me to actually work on this.
There are three main things that are *nix-specific:
- The conversion OS paths to C strings in the resource directory extension
- The stdio redirect shenanigans to print plugin stdout to a file while still printing the TUI to the terminal
- JACK (technically cross-platform, but I dare you to find me a Windows or MacOS user who regularly uses JACK)
Today, I worked on the first two. The resource directory extension was pretty easy, I just had to follow the compiler's instructions. With the stdio stuff, my solution was to ignore it, which will result in issues when plugins print errors, but that also generally shouldn't happen, and if it does, it means there's probably something in either ↹lature or the plugin that needs to be fixed.
There may be other things that I'm forgetting, but I suppose I will find out when I actually try to use ↹lature on Windows.
I had forgotten how awful of an experience Windows is. I had to install a bunch of stuff (Rust/cargo, git, ASIO4ALL, JACK), and for the most part, it went fine, other than the part where I had to agree to a license (that I didn't read) to use the MSVC compiler (🤢). It was a bit annoying to have to go to each website and download the installer, but that's easy peasy compared to the weird display/USB issues I was having the whole time. For some reason, my USB hub kept disconnecting every couple seconds, and Windows really didn't like my second monitor. This last one is probably on me, but the peak disk speed was around 50MB/s, probably because I'm connecting my NVME SSD to my computer via USB type C.
Anyway, after I got everything installed and got past the hiccups of running JACK on a Windows machine, the TUI showed up and I was able to navigate around the UI! While I will almost certainly never use this, it's nice to see that it at least works, and didn't take too much extra effort to adjust things for Windows compatibility.
After checking that, I added some help windows to make it easier to know what the available hotkeys are in any given mode. I'd like to eventually do this in a more structured way such as implementing default keybinds in a KDL file rather than manually written out in Rust, but to implement it how I'm imagining would likely require me to add a scripting language (and not roll it back this time), which would also let me implement the keybinds as a file.
I think I'll explore Lua support eventually (since rhai was a bust), but since tomorrow is the last day of December Adventure, I think I'll try to top it off neatly by finishing the event offset plugin, bringing this adventure full-circle to what I started the month with.
It's the last day of December Adventure!
Today, I fixed event buffers so that CLAP events are properly passed
I assume there are still a bunch of problems with it (especially with
regards to non-serial signal paths and nested groups), but for the most
part, it seems to work as expected.
Most importantly, the
event-offset plugin works as intended!
If I put it between the
tracker processor and the
plugin, it shifts the pitch up by the designated amount.
Right now, that amount is just a default value of 20 because parameters
are kind of a hassle that I don't want to deal with at the moment.
But it works!
The first task (at least, the first "actual" task) that I set out to
complete for December Adventure is now finished!
The main thing I was surprised about when it came to December Adventure is how consistent I was. There were a few days where I did less than 10 minutes of work, and one day that I didn't do anything at all (though in my defence, I had a lot going on that day, and knew a few days in advance that I wouldn't be doing anything), but other than that, I made meaningful contributions towards my project every day this month! Looking back at my original plan, it's laughable that I ever thought I would be capable of "a brand new plugin each day", or the "meaningful feature every day" that I fell back on, but even with "just touch the code at least once every day", I'm really happy with all the progress I made on this project.
As for why I think I was able to do it, I think there are a few key benefits to December Adventure:
- Choose your own adventure - Whatever you decide to work on is something that you get to pick, so you're naturally more inclined to do it compared to, for example, the coding interview-esque challenges of Advent of Code.
- Relaxed - There isn't any competitive pressure involved, and you're just working on stuff because you want to.
- Minimal - There's a sort of sentiment that I picked up on that any amount of progress is valid progress. For me, the first minute of working on something every day is the hardest, usually because of decision paralysis, but by weighing 10 minutes of work against failing the task in the first week, or breaking a 2 week streak, I found it much easier to just get started on literally anything, which usually led to multiple hours spent happily chugging away.
There are a few things that I think could have been better:
- Community - While I did keep up with a few other peoples' December Adventures, mostly through the #DecemberAdventure Fediverse hashtag, there wasn't much of a dialogue. I would read other people's logs, other people would read mine (hopefully, though I don't blame them if they didn't; my log is over 6000 words at this point), but I didn't feel like I had anything meaningful to add to their progress, and therefore didn't find myself commenting on their progress.
- Refactoring - I personally found myself doing a lot of refactoring and bug fixing rather than exploring new things. I think to some extent, this was inevitable, but it would have been nice to somehow have been aware of this in advance so I could have more realistic expectations about how I was going to spend the month. Which isn't to say that I regret it! I'm really happy with everything I did during December Adventure.
Anyway, overall, it was a great month and I'm really glad I did it. Thanks to eli and all the other folks who did December Adventure! Here's hoping 2024 is the year of the tracker on the Linux desktop!