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.
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"
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
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,
}
Plain u64
bytes, no units baked in. Formatting is the UI's problem, not the collector's.
/proc/meminfo
looks like this:
MemTotal: 14177796 kB
MemFree: 497404 kB
MemAvailable: 7490900 kB
Buffers: 1235656 kB
Cached: 5146132 kB
SwapCached: 81068 kB
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,
_ => {}
}
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
.
Look at the value column again:
13.5 GiB
6.42 GiB
271 MiB
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
};
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.
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);
}
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.
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"]
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);
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
.
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:
| 🇩🇰 Dansk | 🇪🇸 Español | 🇮🇷 Farsi | 🇫🇮 Suomi | 🇯🇵 日本語 | 🇳🇴 Norsk | 🇵🇹 Português | 🇷🇺 Русский | 🇦🇱 Shqip | 🇨🇳 中文 | 🇮🇳 हिन्दी |
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 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…