{"slug": "ratatop-the-network-box-and-why-your-isp-lies-with-units", "title": "ratatop: the network box, and why your ISP lies with units", "summary": "Maneshwar, a developer building git-lrc, a Micro AI code reviewer, has implemented the network box for a btop-style system monitor in Rust using ratatui. The feature displays both bits and bytes per second to avoid ISP unit confusion, uses three-significant-figure formatting to prevent layout shifts, and mirrors the upload graph for a unified visual. The project is open source on GitHub.", "body_md": "*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.*\n\nDay four of building a btop-style system monitor in Rust with [ratatui](https://ratatui.rs).\n\nCPU, memory, disks are done. Today the network box lands, which means three of btop's four boxes are up.\n\nThree things in there took actual thought.\n\nThe unit situation, the shared scale, and the fact that the upload graph is upside down on purpose.\n\nLook at that first row again.\n\n```\n▼ 566 Byte/s  (4.42 Kibps)\n```\n\nSame number. Twice. Different units.\n\nbtop does this deliberately, and once you notice why, you cannot unsee it.\n\nYour ISP sells you bandwidth in **bits** per second.\n\n100 Mbps, 300 Mbps, whatever the sticker says.\n\nYour download manager, your browser, and every file transfer you have ever watched report progress in **bytes** per second.\n\nThe factor between them is 8.\n\nSo the \"100 Mbps\" plan you pay for tops out around 12.5 MB/s in the progress bar, and every few months someone concludes their internet is broken when it is in fact working exactly as sold.\n\nbtop refuses to pick a side.\n\nIt shows both, so you can compare against your plan and against your download at the same time without doing mental arithmetic.\n\nThe conversion in code is unremarkable, which is the point:\n\n``` php\nfn format_bits(bytes_per_sec: f64) -> String {\n    const UNITS: [&str; 4] = [\"bps\", \"Kibps\", \"Mibps\", \"Gibps\"];\n    let (value, unit) = scale(bytes_per_sec * 8.0, 1024.0, UNITS.len());\n    let decimals = decimals(value, unit);\n    format!(\"({value:.decimals$} {})\", UNITS[unit])\n}\n```\n\nNote `Kibps`\n\nand not `Kbps`\n\n. Dividing by 1024 gives you kibibits; dividing by 1000 gives you kilobits.\n\nbtop uses 1024, so the label has to say so.\n\nGetting this wrong is a 2.4% error that nobody would ever notice and that would still be wrong.\n\nSmall thing I got wrong and had to fix.\n\nMy first version formatted rates with a fixed two decimals.\n\nPerfectly reasonable, and it produced this:\n\n```\n▼ Top:  (4.42 Kibps)\n▼ Top:  (43.80 Kibps)\n▼ Top:  (438.00 Kibps)\n```\n\nThe number changes width as traffic grows, so the whole column shuffles sideways every time the value crosses a power of ten.\n\nIn a display that redraws twice a second, that is genuinely distracting.\n\nThe fix is the same three-significant-figure rule the memory box already used:\n\n``` php\nfn decimals(value: f64, unit: usize) -> usize {\n    if unit == 0 || value >= 100.0 {\n        0\n    } else if value >= 10.0 {\n        1\n    } else {\n        2\n    }\n}\n```\n\n`4.42`\n\n, `43.8`\n\n, `438`\n\n. Always three digits. Always the same width.\n\nThis is the second time in this project I have written that exact function, which is usually a sign it should be shared.\n\nFor now it lives in both places because the memory box counts bytes and this one counts bits, and I would rather have two small honest copies than one abstraction that takes four parameters to explain itself.\n\nThe two graphs on the left are download on top and upload underneath, and the bottom one is mirrored: it hangs from its top edge and grows downward.\n\n```\nflowchart TD\n    subgraph BOX[\" \"]\n        D[\"download graph<br/>grows up from the middle\"]\n        M[\"shared baseline\"]\n        U[\"upload graph<br/>grows down from the middle\"]\n    end\n    D --- M\n    M --- U\n```\n\nIt reads as one figure with traffic flowing outward from a centre line, rather than two unrelated charts stacked by coincidence.\n\nMy `BrailleGraph`\n\nwidget from day one only knew how to fill upward from the bottom, so this needed a new option.\n\nThe change turned out to be four lines, because the only thing that differs is which end you count the dot rows from:\n\n``` js\nlet offset = if self.inverted {\n    dot\n} else {\n    dot_rows - 1 - dot\n};\n```\n\nThat is the whole feature. Everything else, the braille bit packing, the gradient, the two-samples-per-cell trick, is unchanged.\n\nHere is the part that is easy to get wrong and produces a chart that actively lies.\n\nEach direction has its own peak.\n\nThe obvious move is to scale each graph to its own maximum, so both always use the full height of their box.\n\nDo that and you get a display where a 3 KB/s upload trickle and a 3 MB/s download flood are drawn at **exactly the same height**.\n\nTwo graphs, sitting adjacent, implying a comparison that is off by a factor of a thousand.\n\nSo the scale is shared, computed once from whichever direction peaked higher:\n\n``` php\npub fn graph_max(&self) -> f64 {\n    self.download.top.max(self.upload.top).max(10.0 * 1024.0)\n}\n```\n\nTwo details worth pointing out.\n\nIt scales to the **peak seen**, not the current value.\n\nScaling to the current value means the graph rescales every frame, so a flat line and a spike look identical and the shape carries no information at all.\n\nAnd there is a 10 KiB floor.\n\nWithout it, an idle interface scales to its own noise, and three stray bytes of ARP traffic render as a mountain range.\n\nThe floor is why the label in my screenshot reads `20K`\n\neven though nothing much is happening.\n\nDay three needed a syscall because filesystem usage has no file. Today the same thing happened again, and I did not expect it to.\n\nThe rates all come from `/proc/net/dev`\n\n. Easy. But the `192.168.29.14`\n\nin the title has no file behind it.\n\n`/proc/net/route`\n\ngives you the gateway, not your address.\n\n`/proc/net/fib_trie`\n\ncontains local addresses, but it is a serialised routing trie, not an interface listing, and parsing it is exactly as unpleasant as that sounds.\n\n`/sys/class/net/wlo1/`\n\nhas the MAC, the MTU, the link speed, and no IP.\n\nSo it is `getifaddrs(3)`\n\n, and my second `unsafe`\n\nblock.\n\n``` js\nunsafe {\n    let mut list: *mut libc::ifaddrs = std::ptr::null_mut();\n    if libc::getifaddrs(&mut list) != 0 {\n        return addresses;\n    }\n\n    let mut node = list;\n    while !node.is_null() {\n        let entry = &*node;\n        node = entry.ifa_next;\n        // ...\n    }\n\n    libc::freeifaddrs(list);\n}\n```\n\nThis one is chunkier than day three's `statvfs`\n\n, because it is a linked list. You walk it, you filter to `AF_INET`\n\nbecause an interface carries several addresses and only IPv4 fits the box, you cast a generic `sockaddr`\n\nto a `sockaddr_in`\n\nto reach the actual four bytes, and you free the list exactly once through the same pointer you were handed.\n\nThe line I am most pleased with is this one:\n\n```\nnode = entry.ifa_next;\n```\n\nIt advances the cursor **before** the body of the loop touches anything. Do it at the bottom instead and every `continue`\n\nin the loop becomes an infinite loop. There are two `continue`\n\ns in there.\n\nA parsing trap for anyone reading `/proc/net/dev`\n\n:\n\n```\nInter-|   Receive                                     |  Transmit\n face |bytes  packets errs drop fifo frame compressed multicast|bytes packets ...\n  wlo1: 319815680  240000    0    0    0     0     0    0  137363456  180000 ...\n```\n\nReceived bytes is the first counter.\n\nTransmitted bytes is the **ninth**, because receive gets eight columns before transmit starts.\n\nGrab field 1 for upload and you will be reporting received packets as your upload speed, and it will look plausible enough that you might not catch it.\n\nThe two header lines take care of themselves, incidentally.\n\nNeither contains a colon, and the parser splits each line on the colon to separate the name from the counters, so they drop out before any field indexing happens.\n\nI originally wrote a comment claiming the header needed special handling, then actually checked, and it does not.\n\nWorth checking your own comments occasionally.\n\nbtop puts `<b wlo1 n>`\n\nin the corner, which is both a label and a keymap: `b`\n\nfor the previous interface, `n`\n\nfor the next.\n\n``` js\nKeyCode::Char('n') => app.net.cycle(true),\nKeyCode::Char('b') => app.net.cycle(false),\n```\n\nLoopback is filtered out of the list.\n\nIt is always present, always busy, and never the thing you opened a monitor to look at.\n\nThe default selection comes from `/proc/net/route`\n\n: whichever interface carries the route with a destination of all zeroes is the default route, and that is almost always the one you care about.\n\nIf that lookup fails, it falls back to whichever interface has moved the most bytes.\n\nProcesses. The last box, and by a distance the biggest.\n\nReading `/proc/[pid]/stat`\n\nfor every process on every tick, computing per-process CPU deltas, sorting by any column, and making the list scroll with a selection cursor.\n\nhtop and btop both have a lot of hard-won edge case handling in that area, so I expect to be reading their source more than usual.\n\nCode so far:\n\nA reimplementation of [btop](https://github.com/aristocratos/btop) in Rust, built on\n[ratatui](https://ratatui.rs).\n\nThe goal is a resource monitor that feels like btop: the four-box layout, braille graphs with gradient fills, mouse-driven navigation, and a themable process manager, but written as a modern Rust TUI.\n\n**The CPU box works.** It samples real data and renders btop's layout:\n\nPer-core braille history graphs, a gradient load bar with sub-cell resolution frequency, package temperature, battery, and load averages.\n\nEverything else, the memory, network, and process boxes, is still to come.\n\n`/proc`\n\nand `/sys`\n\non Linux first, read directly rather than\nshelling out. btop's `src/linux/btop_collect.cpp`\n\nis the reference for what to\nread and how often; the BSD/macOS collectors are out of scope for v1.Today's takeaway is small but general: when you draw two things next to each other, you have implied they are comparable.\n\nIf they are on different scales, you have drawn a lie. Share the axis or separate the charts.\n\nAI agents write code fast. They also silently remove logic, change behavior, and introduce bugs — without telling you. You often find out in production.\n\ngit-lrc fixes this. It hooks into git commit and reviews every diff before it lands. 60-second setup. Completely free.\n\nAny feedback or contributors are welcome! It's online, source-available, and ready for anyone to use.\n\n⭐ Star it on GitHub:\n\n| [🇩🇰 Dansk](https://github.com/HexmosTech/git-lrc/readme/README.da.md) | [🇪🇸 Español](https://github.com/HexmosTech/git-lrc/readme/README.es.md) | [🇮🇷 Farsi](https://github.com/HexmosTech/git-lrc/readme/README.fa.md) | [🇫🇮 Suomi](https://github.com/HexmosTech/git-lrc/readme/README.fi.md) | [🇯🇵 日本語](https://github.com/HexmosTech/git-lrc/readme/README.ja.md) | [🇳🇴 Norsk](https://github.com/HexmosTech/git-lrc/readme/README.nn.md) | [🇵🇹 Português](https://github.com/HexmosTech/git-lrc/readme/README.pt.md) | [🇷🇺 Русский](https://github.com/HexmosTech/git-lrc/readme/README.ru.md) | [🇦🇱 Shqip](https://github.com/HexmosTech/git-lrc/readme/README.sq.md) | [🇨🇳 中文](https://github.com/HexmosTech/git-lrc/readme/README.zh.md) | [🇮🇳 हिन्दी](https://github.com/HexmosTech/git-lrc/readme/README.hi.md) |\n\nGenAI 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.\n\n** git-lrc is your braking system.** It hooks into\n\n`git commit`\n\nand runs an AI review on every diff In short, git-lrc helps **Prevent Outages, Breaches, and Technical Debt Before They Happen**\n\n**At a glance:** [10 risk categories](https://github.com/HexmosTech/git-lrc#what-git-lrc-checks-for) · [100+ failure patterns tracked](https://github.com/HexmosTech/git-lrc#what-git-lrc-checks-for) · every commit…", "url": "https://wpnews.pro/news/ratatop-the-network-box-and-why-your-isp-lies-with-units", "canonical_source": "https://dev.to/lovestaco/ratatop-the-network-box-and-why-your-isp-lies-with-units-4lpo", "published_at": "2026-08-01 16:48:16+00:00", "updated_at": "2026-08-01 17:10:49.864381+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Maneshwar", "git-lrc", "ratatui", "Rust", "btop", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/ratatop-the-network-box-and-why-your-isp-lies-with-units", "markdown": "https://wpnews.pro/news/ratatop-the-network-box-and-why-your-isp-lies-with-units.md", "text": "https://wpnews.pro/news/ratatop-the-network-box-and-why-your-isp-lies-with-units.txt", "jsonld": "https://wpnews.pro/news/ratatop-the-network-box-and-why-your-isp-lies-with-units.jsonld"}}