Day 27: Planting a tree in a ball of mud
I made progress today on displaying the tree structure described in my initial article on the TUI. I can now display the state of each item within a synchronizing channel. This works, but left me somewhat unsatisfied — the code is not pretty, and reflects a more general shakiness found in the rest of the synchronization code. I’ll start by describing the progress I made, then reflect on the meta-level approach I’ve taken, its benefits, and its drawbacks.
Branching out
While synchronizing, Séance now displays the titles of the pending items (episodes), if present. Here’s how that looks:
Never Post
│
├── Posting from Inside (pending)
│
╰── Mailbag #11: Someone To Tell Us How To Be (pending)
The “(pending)” text reflects that the request for the item has been made, but we haven’t yet received a response. Once the response is received, it will switch to a loading bar that tracks the progress (not yet implemented). This UI is backed by an extended series of item states, as follows:
#[derive(Debug, Clone, PartialEq, Eq)]
enum Item {
Unset,
Broken,
Pending {
title: String,
},
Loading {
title: String,
bytes_loaded: u64,
bytes_total: u64,
},
Loaded {
title: String
}
}
Note the distinction between pending and loading. I render this UI using an extension of the zero-allocation widget rendering technique that I previously used for rendering each channel title. Each channel now contains a nested loop that renders the box-drawing characters and title for the nested item, like so:
let mut item_y = start_y + 1;
for item_offset in 0..ITEMS_PER_CHANNEL {
let last_item = item_offset == ITEMS_PER_CHANNEL - 1;
let item = &self.items[channel_index * ITEMS_PER_CHANNEL + item_offset];
frame.render_widget(
Line::raw(" │"),
Rect {
x: start_x,
y: item_y,
width: full_width,
height: 1,
}
);
frame.render_widget(
if last_item {
Line::raw(" ╰──")
} else {
Line::raw(" ├──")
},
Rect {
x: start_x,
y: item_y + 1,
width: 8,
height: 1,
}
);
frame.render_widget(match item {
Item::Unset => Line::raw("(unset)"),
Item::Broken => Line::raw("broken item"),
Item::Pending { title } => Line::raw(format!("{title} (pending)")),
Item::Loading { title, bytes_loaded, bytes_total } => {
Line::raw(format!("{title} {0:.0}", 100 * *bytes_loaded as u64 / *bytes_total as u64))
},
Item::Loaded { title } => Line::raw(title),
}, Rect {
x: 8,
y: item_y + 1,
width: full_width,
height: 1,
});
item_y += 2;
}
This works, but I don’t like it, for reasons I’ll expand upon in the next section.
When to build abstractions
As I’ve detailed previously, the strategy I first used for improving at building software was reading book after book and article after article on design patterns and best practices. I would start a project by scaffolding every pre-baked idea that I had heard of, without regard for whether it made sense in context. This didn’t work work. I’d get caught in cycles of building a beautiful abstraction, only to get a third of the way into it and realize a core shortcoming. Or I would keep extending the design in a series of endless branches, never getting to the actually distinctive parts. Ultimately, I never finished a single sizeable project.
Learning from this mistake, I now try to design abstractions late. I grit my teeth through the early parts of the process, building just enough to meet the next milestone. The downside though, which I’ve started to run into now, is it makes the intermediate work a slog through hacky code I’m not proud of. I believe in my heart of hearts that there is a simple and beautiful way to express any useful idea, and I wince when my work doesn’t reflect that.
The logic for the tree structure rendering above is a prime example of that. It uses magic numbers and offsets, mutable state across the loops, repeated method calls with only subtle differences, and is dropped in the middle of some other equally dense yet distinct code. Often when you write out the duplicate code by hand it makes the correct abstraction blindingly obvious. I don’t think that’s the case here, though I am starting to see the outline. With this session, though, I think I’ve reached the limit of my pain tolerance. Next up, I’ll extract the tree widget into its own definition, with its own file, built the way I know it should be.