Contributing¶
Thanks for wanting to make fm better. This guide gets you set up, explains the
layout, and covers the project's non-negotiables.
Dev setup¶
git clone https://github.com/jensenloke/macos-fanMonitor
cd macOS-fanMonitor
./install.sh # creates .venv, installs rich + textual, links fm
source .venv/bin/activate # optional, for running tools directly
Everything runs from the project's .venv. There's no global install.
Project layout¶
fm launcher (resolves symlink, sets PYTHONPATH, runs app)
install.sh venv + deps + ~/.local/bin symlink
requirements.txt runtime deps (rich, textual)
requirements-docs.txt docs deps (mkdocs-material)
smoke_test.py headless TUI test
mkdocs.yml docs site config
fanmon/
__main__.py python -m fanmon entry
cli.py arg parsing: default = TUI, --once = snapshot
app.py the Textual App: ABC boot, gauges, tabs, sort, kill
fanmon.tcss Textual stylesheet
brand.py ABC palette, wordmark, Textual theme, heat scale
engine.py sampler orchestration → one snapshot dict + histories
smc.py fan (+ fan presence) + temperature sensors (Stats.app smc)
cpu.py per-core busy % via Mach host_processor_info (ctypes)
thermal.py pmset -g therm throttle state
procs.py process snapshot, CPU-time delta, classification
memory.py RAM breakdown / swap / compressor / pressure / load / uptime
regime.py verdict + recommendation algorithm ← the brain
watchdog.py read-only watchdog log/config parsing
render.py rich layout used by --once
docs/ MkDocs Material site
Run it while developing¶
Tests¶
smoke_test.py drives the app headless via Textual's run_test(), twice: once
as-is and once with FANMON_FANLESS=1 FANMON_THROTTLE=72 to simulate a
throttling MacBook Air. It asserts the Close / CPU / Memory / Processes /
Watchdog tables populate, the FAN tile becomes COOLING when fanless, the ABC
boot remains visible for at least three seconds, [ / ] cycle the tabs, the
1/2/3 sort keys change the sort, and k opens (and cleanly declines) the
confirm modal from all four process tables. When you add UI behaviour, extend
the smoke test to cover it — the headless pilot is the closest thing to a real
terminal we have in CI.
To eyeball the ABC theme without a real terminal, app.save_screenshot() inside
run_test() writes an SVG; rsvg-convert turns it into the PNGs under
docs/assets/.
Documentation¶
Docs live in docs/, configured by mkdocs.yml. A GitHub Actions workflow builds
and publishes to GitHub Pages on pushes to main that touch docs/ or
mkdocs.yml. mkdocs build --strict is the bar — broken internal links fail
CI.
Code style¶
- Keep it in the standard library +
rich/textual; don't add heavy deps. - Type hints throughout;
from __future__ import annotationsis the norm here. - Blocking I/O stays in the worker thread (
_sample) — never sample on the UI thread, or the app stalls. - No new file unless the module has a clear single job.
Non-negotiables (please don't undo these)¶
These come from the reason the tool exists:
- Read-only to hardware. Only
smc fans/smc list -t. No SMC writes. - Killing is confirmed and
SIGTERM-only. Re-check liveness before sending. NoSIGKILL, no automatic/background killing. - System daemons are never killable. They're symptoms; add them to advisories, not the kill list.
- Never invent a cause. If the sample can't distinguish a legit build from a
runaway, the Verdict should say so (
watch), not guess.
Proposing a change¶
For anything beyond a small fix, open an issue first — especially for items on the Roadmap, so work isn't duplicated. For feature ideas, sketch how it interacts with the safety model above.
Committing¶
Conventional-ish messages (Add …, Fix …, Tune …) read well in this repo's
short history. Keep commits focused; a commit should leave make test green.
Open a pull request against main. CI builds the docs; the maintainer merges.