Hello, I’m Maneshwar. I’m building git-lrc, a Micro AI code reviewer that runs on every commit. It is free and source-available on Github. Star git-lrc to help devs discover the project. Do give it a try and share your feedback.
Yesterday I got the CPU box working in ratatui and wrote up my first impressions coming from Bubble Tea.
Today: memory.
I assumed this would be the easy one.
CPU needed delta tracking between samples, per-core bookkeeping, braille bit twiddling. Memory is just a file with numbers in it.
Read it, print it, go home.
Reader, it was not the easy one.
Free memory is not free memory
Look at that box again. Total 13.5 GiB. Free: 271 MiB.
Two percent free. That sounds like a machine about to fall over.
It is completely fine. I have 7.10 GiB available right now.
This is the thing that trips up everyone reading /proc/meminfo for the first time, and it tripped me up too.
MemFree means memory the kernel is doing absolutely nothing with. Not caching a file, not buffering a write, nothing. Just sitting there.
And the kernel hates that. Unused RAM is wasted RAM. So it fills spare memory with page cache, and hands it back the instant a process actually needs it.
So MemFree on a healthy machine trends toward zero, by design. It is a measure of how much RAM your kernel is failing to put to work.
The number you actually want is MemAvailable, added to the kernel in 3.14 precisely because everyone was misreading MemFree.
It is an estimate of how much you can allocate right now without swapping, and it accounts for the fact that most of that cache is reclaimable.
Note that Used and Available add up to Total, but Cached and Free do not slot in neatly beside them.
Cached memory is counted inside available. They overlap.
Trying to make all four sum to total is a trap, and I definitely tried for a bit.
Meme 1
Template: Bernie Sanders “I am once again asking”
Text: “I am once again asking you to stop reading MemFree”
So how do you compute “used”?
Here is where I went looking at how btop does it instead of inventing my own.
The obvious approach is to add up the parts.
Total minus free minus cached minus buffers, something like that.
This is what a lot of tools did historically, and it is where the old “Linux ate my RAM” confusion comes from.
btop does the simple thing instead:
used = MemTotal - MemAvailable
Enter fullscreen mode Exit fullscreen mode
That is it. Let the kernel do the hard estimate, because it is the only thing that actually knows what is reclaimable.
In Rust, my collector boils down to this:
pub struct Memory {
pub total: u64,
pub used: u64,
pub available: u64,
pub cached: u64,
pub free: u64,
}
Enter fullscreen mode Exit fullscreen mode
Plain u64 bytes, no units baked in. Formatting is the UI’s problem, not the collector’s.
The bug that a substring gave me for free
/proc/meminfo looks like this:
MemTotal: 14177796 kB
MemFree: 497404 kB
MemAvailable: 7490900 kB
Buffers: 1235656 kB
Cached: 5146132 kB
SwapCached: 81068 kB
Enter fullscreen mode Exit fullscreen mode
Spot the landmine.
SwapCached contains the substring Cached.
If you parse this with a contains or a loose prefix match, SwapCached arrives four lines later and quietly overwrites your real cached value with something roughly 60 times smaller.
Your box renders. No error. No panic. Just a wrong number sitting there looking plausible.
The fix is boring: split on the colon, match the label exactly.
let Some((label, rest)) = line.split_once(':') else {
continue;
};
match label {
"MemTotal" => mem.total = bytes,
"MemFree" => mem.free = bytes,
"MemAvailable" => available = Some(bytes),
"Cached" => mem.cached = bytes,
_ => {}
}
Enter fullscreen mode Exit fullscreen mode
I wrote a test named swap_cached_does_not_get_mistaken_for_cached purely so the next person who “simplifies” this parser gets caught.
Also worth knowing: the kB suffix in that file is a lie too. Those are KiB, 1024 bytes, not 1000. So the multiply is << 10, not * 1000.
Three significant figures, and why
Look at the value column again:
13.5 GiB
6.42 GiB
271 MiB
Enter fullscreen mode Exit fullscreen mode
Different decimal counts. That is deliberate, and it is what btop does.
The rule is three significant figures, always. Under 10, you get two decimals. Between 10 and 100, one. Above 100, none. The number is always exactly three digits wide, so the column stays aligned no matter what the values do.
let decimals = if unit == 0 || value >= 100.0 {
0
} else if value >= 10.0 {
1
} else {
2
};
Enter fullscreen mode Exit fullscreen mode
Small thing. But a column that jitters between 6.4 GiB and 13.52 GiB looks amateurish in a way you feel before you can name it.
One thing I got wrong on the first pass: my loop divided by 1024 while the value was too large, with no guard on the unit table.
Feed that a u64::MAX and it walks straight off the end of the array.
Now it stops at TiB, with a test that passes u64::MAX specifically.
Where ratatui made this pleasant
Two things stood out building a second box.
Each field gets its own gradient. btop’s theme gives used, available, cached and free their own three-stop colour ramps, and I lifted the exact hex values out of btop_theme.cpp.
Because a Gradient is just a value in my code, colouring a meter is passing a different one in:
for (label, value, gradient) in [
("Used:", mem.used, theme::MEM_USED),
("Available:", mem.available, theme::MEM_AVAILABLE),
("Cached:", mem.cached, theme::MEM_CACHED),
("Free:", mem.free, theme::MEM_FREE),
] {
render_value(inner, &mut y, label, value, buf);
render_meter(inner, &mut y, mem.percent(value), gradient, buf);
}
Enter fullscreen mode Exit fullscreen mode
Immediate mode really pays off here.
There is no widget to construct, register, style and later mutate.
You are in a loop, you have a Rect, you draw. Four fields, four iterations, done.
The meter widget was already written. I built it for the CPU bar on day one, and reusing it for memory took zero changes.
That is the nice consequence of ratatui’s Widget trait being tiny: a widget is a struct plus one render method, so a generic one you wrote yesterday just works today.
I did briefly add a Meter::braille() method to make the memory bars look different. Then I noticed it returned self completely unchanged, doing nothing but making the call site read fancier. Deleted it. A method that lies about doing something is worse than no method.
Restructuring, because two boxes is a crowd
Day one had everything in one ui.rs. That was fine for one box and immediately wrong for two.
flowchart TD
M["ui/mod.rs<br/>splits the terminal, paints background"]
M --> C["ui/cpu.rs<br/>bar, cores, load"]
M --> Mem["ui/mem.rs<br/>values, meters, formatting"]
C --> W["widgets/<br/>BrailleGraph, Meter"]
Mem --> W
C --> Col["collect/cpu.rs"]
Mem --> Col2["collect/mem.rs"]
Enter fullscreen mode Exit fullscreen mode
The useful change was moving layout decisions up.
Previously main.rs computed the box rectangle itself, which was fine when there was exactly one box and completely wrong the moment memory needed the leftover height.
Now main.rs hands over the entire terminal and ui::render decides who gets what:
let [cpu_area, rest] = Layout::vertical([
Constraint::Length(cpu_height),
Constraint::Min(0),
])
.areas(area);
Enter fullscreen mode Exit fullscreen mode
CPU takes the top, memory takes a fixed width below it, and the rest stays empty for the network and process boxes.
Adding a box is now a matter of carving another Rect out of rest.
What’s next
Network, probably. /proc/net/dev is another counter file, so it needs the same delta treatment the CPU box uses, plus auto-scaling units because network speeds swing across five orders of magnitude.
Then processes, which is the big one. That is where /proc/[pid]/stat lives, and where I suspect I will learn a great deal about how many edge cases a process table has.
Code so far: lovestaco/ratatop.
If you take one thing from this post, make it this: stop looking at free. Look at available. Your machine is fine.
AI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.
git-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.
Any feedback or contributors are welcome! It’s online, source-available, and ready for anyone to use.
⭐ Star it on GitHub:
GenAI today is a race car without brakes. It accelerates fast — you describe something, and large blocks of code appear instantly. But AI agents silently break things: they remove logic, relax constraints, introduce expensive cloud calls, leak credentials, and change behavior — without telling you. You often find out in production.
git-lrc is your braking system. It hooks into git commit and runs an AI review on every diff before it lands. 60-second setup. Completely free.
In short, git-lrc helps Prevent Outages, Breaches, and Technical Debt Before They Happen
At a glance: 10 risk categories · 100+ failure patterns tracked · every commit…





답글 남기기