{"id":50887476,"url":"https://github.com/nim444/featherbar","last_synced_at":"2026-06-15T18:01:08.869Z","repository":{"id":362944448,"uuid":"1261439017","full_name":"nim444/featherbar","owner":"nim444","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-06T17:59:26.000Z","size":0,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-06T18:08:06.257Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nim444.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-06T17:27:05.000Z","updated_at":"2026-06-06T17:59:29.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nim444/featherbar","commit_stats":null,"previous_names":["nim444/featherbar"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/nim444/featherbar","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nim444%2Ffeatherbar","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nim444%2Ffeatherbar/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nim444%2Ffeatherbar/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nim444%2Ffeatherbar/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nim444","download_url":"https://codeload.github.com/nim444/featherbar/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nim444%2Ffeatherbar/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34374146,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-15T18:01:07.020Z","updated_at":"2026-06-15T18:01:08.824Z","avatar_url":"https://github.com/nim444.png","language":"Rust","funding_links":[],"categories":["System Monitor"],"sub_categories":[],"readme":"![Rust](https://img.shields.io/badge/rust-%23000000.svg?style=for-the-badge\u0026logo=rust\u0026logoColor=white)\n![macOS](https://img.shields.io/badge/mac%20os-000000?style=for-the-badge\u0026logo=macos\u0026logoColor=F0F0F0)\n![Apple Silicon](https://img.shields.io/badge/apple%20silicon-333333?style=for-the-badge\u0026logo=apple\u0026logoColor=white)\n\n[![crates.io](https://img.shields.io/crates/v/featherbar.svg)](https://crates.io/crates/featherbar)\n[![downloads](https://img.shields.io/crates/d/featherbar.svg)](https://crates.io/crates/featherbar)\n[![license](https://img.shields.io/github/license/nim444/featherbar)](LICENSE)\n\n____\n\u003cbr\u003e\n\n\u003cdiv align=\"center\"\u003e\n\n# featherbar\n\n**A featherweight macOS menu-bar system monitor that stays featherweight.**\n\n\u003c/div\u003e\n\nA tiny, modular menu-bar (NSStatusItem) app in Rust that shows live stats on two compact, color-coded lines:\n\n\u003cdiv align=\"center\"\u003e\n\n![featherbar in the menu bar](https://raw.githubusercontent.com/nim444/featherbar/main/assets/menubar.png)\n\n\u003c/div\u003e\n\nUpdates every 2 seconds. Right-click for launch-at-login and Quit. That's the whole app — and that's the point.\n\n\u003e **The premise.** Most menu-bar monitors slowly become what they measure: background threads, growing buffers, tens of MB of RSS. featherbar runs **zero background threads** and allocates **nothing that accumulates** — one main-thread event loop wakes on a timer, takes one sample, redraws the display, and re-arms. Memory stays flat for as long as it runs.\n\n## Features\n\n- **Live stats on two stacked lines**: CPU % + battery watts on top, RAM % + CPU temperature below, refreshed every 2s\n- **Color-coded severity**: each value renders green / orange / red by its own thresholds (CPU 40/70%, RAM 60/80%, power 10/20W, temp 60/80°C); labels stay neutral\n- **Perfectly gridded**: SF Mono with fixed-width value fields — nothing shifts when a value changes digit count\n- **No Dock icon, no window**: `ActivationPolicy::Accessory` — it exists only in the menu bar\n- **No background threads**: a single main-thread `tao` event loop with `ControlFlow::WaitUntil` timer wakes — even sysinfo's rayon pool is compiled out\n- **Launch at login toggle**: right-click menu check item backed by `SMAppService` (when running as the `.app` bundle)\n- **Flat memory by design**: one `Sampler` owns all sampling state, one `Renderer` owns all drawing state; every tick's ObjC temporaries die in an explicit autorelease pool\n- **Measured footprint**: 11 MB at launch, settling at a flat ~20 MB steady state (`phys_footprint`, the Activity Monitor number) on an M-series MacBook Pro — soak-profiled leak-free with `leaks` (live heap ~6.5 MB; the rest is one-time allocator high-water, and it stops moving)\n- **Modular metrics**: adding a stat is an enum variant + a match arm — nothing else changes\n- **Tiny binary**: ~800 KB release build (`opt-level = \"z\"`, LTO, stripped)\n\n```mermaid\nflowchart LR\n    T[\"Timer wake\u003cbr/\u003e(every 2s)\"] --\u003e S[\"Sampler\u003cbr/\u003eone owner, no allocation\"]\n    S --\u003e C[\"CPU % + temp\u003cbr/\u003esysinfo\"]\n    S --\u003e R[\"RAM %\u003cbr/\u003esysinfo\"]\n    S --\u003e P[\"Power W\u003cbr/\u003estarship-battery\"]\n    C --\u003e O[\"Renderer\u003cbr/\u003etwo color-coded lines\u003cbr/\u003edrawn into the status icon\"]\n    R --\u003e O\n    P --\u003e O\n    O --\u003e T\n```\n\n___\n\n\u003cbr\u003e\n\u003cdetails\u003e\n  \u003csummary\u003e1. Requirements\u003c/summary\u003e\n\n- macOS on Apple Silicon (M-series)\n- Rust **1.89+** (required by `starship-battery`)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e2. Installation\u003c/summary\u003e\n\n#### As an .app bundle (recommended — enables the launch-at-login toggle)\n\n```bash\ngit clone https://github.com/nim444/featherbar.git\ncd featherbar\n\n# Build the release binary and assemble Featherbar.app (ad-hoc signed)\n./scripts/bundle.sh\ncp -R target/Featherbar.app /Applications/\nopen /Applications/Featherbar.app\n```\n\n#### As a bare binary\n\n```bash\n# From crates.io\ncargo install featherbar\nfeatherbar\n\n# Or from a checkout\ncargo run --release\n```\n\nThe reading appears in your menu bar immediately. There is no Dock icon and no window — right-click the menu-bar text for the menu and **Quit**.\n\n#### Launch at login\n\nRight-click the menu-bar reading and check **Launch at login**. The toggle uses Apple's `SMAppService` API, which only works from a real `.app` bundle — from a bare `cargo run` binary the item is shown disabled. Verify the registration anytime in **System Settings → General → Login Items**.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e3. Project Structure\u003c/summary\u003e\n\n```\n├── src/\n│   ├── main.rs          # Metric enum, Sampler, thresholds, event loop, menu\n│   ├── two_line.rs      # Renderer: two color-coded lines drawn into the icon\n│   └── login_item.rs    # Launch-at-login via SMAppService\n├── scripts/\n│   └── bundle.sh        # Assemble Featherbar.app from the release binary\n├── assets/\n│   └── menubar.png\n├── Cargo.toml           # size-optimized release profile, trimmed features\n├── Cargo.lock\n├── LICENSE              # Apache-2.0\n└── README.md\n```\n\nThree source files on purpose. The app is small enough that splitting it up further would only add indirection.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e4. How It Works\u003c/summary\u003e\n\nThe hard macOS constraints this design satisfies:\n\n- The `tao` event loop must run on the **main thread**, and the tray icon must be created on that same thread.\n- The tray icon is created **after the loop is running** — on `StartCause::Init`, not before.\n- `ActivationPolicy::Accessory` keeps it out of the Dock and the app switcher.\n\nThe loop itself:\n\n1. `StartCause::Init` — create the tray icon, locate its `NSStatusBarButton`, draw the first reading, arm a 2s `ControlFlow::WaitUntil` timer.\n2. `StartCause::ResumeTimeReached` — drain the menu-event channel (Quit? login toggle?), take one sample per enabled metric, redraw, re-arm.\n3. Nothing else. No threads, no channels to background workers, no history buffers.\n\n**Why the display is an image:** NSStatusItem text titles are vertically centered by the button cell with no working override, and a single-line title can't stack two rows. So the `Renderer` draws both lines into an `NSImage` each tick — glyph positions computed from real font metrics (cap height, descent), colors per severity, Retina-sharp. The button can't fight pixels.\n\n**Why memory stays flat:** one `Sampler` owns the `sysinfo::System` (created empty — no process table), the component list, and a single battery handle refreshed in place; one `Renderer` owns the font and four prebuilt attribute dictionaries. Per tick only the strings and the image are created, and an explicit `autoreleasepool` kills them before the loop sleeps again. The footprint rises from 11 MB at launch to ~20 MB as the malloc zones reach their high-water mark, then stops: a profiled soak shows ~6.5 MB live heap, no featherbar leaks, and a flat line from there.\n\nMeasure it yourself while it runs (same metric Activity Monitor shows):\n\n```bash\nfootprint $(pgrep -x featherbar)\n# featherbar [pid]: 64-bit    Footprint: 11 MB   (launch; settles at ~20 MB and stays flat)\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e5. Adding a Metric\u003c/summary\u003e\n\nThree edits, all in `src/main.rs`:\n\n```rust\n// 1. Add a variant\nenum Metric {\n    Ram,\n    Cpu,\n    Power,\n    Temp,\n    DiskFree, // new\n}\n\n// 2. Add a match arm in Sampler::fragment that pushes label/value Segs\nMetric::DiskFree =\u003e {\n    out.push(Seg::new(\"D\", Level::Neutral));\n    out.push(Seg::new(format!(\"{}G\", pad(free_gb, 3, 0)), disk_level(free_gb)));\n}\n\n// 3. Put it on a line\nconst LINE_TOP: \u0026[Metric] = \u0026[Metric::Cpu, Metric::Power];\nconst LINE_BOTTOM: \u0026[Metric] = \u0026[Metric::Ram, Metric::Temp, Metric::DiskFree];\n```\n\nGood candidates with maintained crates and no reverse engineering: network up/down (`sysinfo` networks), disk free (`sysinfo` disks), battery % (`starship_battery`).\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e6. Behavior Notes (not bugs)\u003c/summary\u003e\n\n- **Power reads `0W` on AC or at full charge.** The watt figure is the battery charge/discharge rate (`energy_rate`), so it is only meaningful while running on battery. It is NOT total system/SoC power.\n- **Power may read `0W` for a minute right after unplugging.** The battery fuel gauge reports `InstantAmperage = 0` until real discharge current registers — featherbar shows exactly what the SMC reports. Verify the OS-side value with `ioreg -rn AppleSmartBattery | grep InstantAmperage`.\n- **`—W` is shown** when no battery is available; **`—°C`** when no die sensor is found.\n- **Temperature is the hottest CPU die sensor** (`PMU tdie*`), which is what \"CPU temp\" colloquially means — individual sensors run cooler.\n- **The first CPU sample may be off** for one tick until the second refresh lands.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e7. Scope — what featherbar will not do\u003c/summary\u003e\n\nFans and total SoC/package power are **out of scope**. They require undocumented IOKit/SMC keys that break with each new Apple Silicon generation — the exact maintenance treadmill this project exists to avoid. (CPU temperature *is* shown, but through `sysinfo`'s maintained Components API — the PMU die sensors — not hand-rolled SMC keys.) If you need fans or package watts, [Stats](https://github.com/exelban/stats) does them well and pays that maintenance cost for you.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e8. Tech Stack\u003c/summary\u003e\n\n| Layer | Crate |\n|---|---|\n| Menu-bar icon (NSStatusItem) | `tray-icon` 0.21 |\n| Main-thread event loop | `tao` 0.34 |\n| RAM / CPU sampling | `sysinfo` 0.33 |\n| Battery power draw | `starship-battery` 0.10 |\n\nRelease profile: `opt-level = \"z\"`, `lto = true`, `strip = true` → ~800 KB binary.\n\n\u003c/details\u003e\n\n## Roadmap\n\n- SMC `PSTR` (system total power) reader behind `Metric::Power`, with fallback to battery watts — total consumption with or without AC, accepting the undocumented-key tradeoff\n- Network up/down and disk-free metrics\n- A settings menu to toggle which metrics are shown at runtime\n\n____\n\u003cbr\u003e\n\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnim444%2Ffeatherbar","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnim444%2Ffeatherbar","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnim444%2Ffeatherbar/lists"}