macos-fanmon 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jensen Loke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,254 @@
1
+ Metadata-Version: 2.4
2
+ Name: macos-fanmon
3
+ Version: 0.1.0
4
+ Summary: macOS fan monitor โ€” why is the fan spinning, and what to close.
5
+ Author: Jensen Loke
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Jensen Loke
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/jensenloke/macos-fanMonitor
29
+ Project-URL: Repository, https://github.com/jensenloke/macos-fanMonitor
30
+ Project-URL: Changelog, https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/changelog.md
31
+ Keywords: macos,fan,monitor,tui,textual,thermal,diagnostics
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Environment :: Console
34
+ Classifier: Intended Audience :: End Users/Desktop
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: MacOS
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Topic :: System :: Monitoring
39
+ Requires-Python: >=3.10
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Requires-Dist: rich>=13.0
43
+ Requires-Dist: textual>=0.60
44
+ Dynamic: license-file
45
+
46
+ # macOS Fan Monitor (`fm`)
47
+
48
+ [![docs](https://github.com/jensenloke/macos-fanMonitor/actions/workflows/docs.yml/badge.svg)](https://github.com/jensenloke/macos-fanMonitor/actions/workflows/docs.yml)
49
+ [![license: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/jensenloke/macos-fanMonitor/blob/main/LICENSE)
50
+ [![platform: macOS](https://img.shields.io/badge/platform-macOS%20(Apple%20Silicon)-black)](#requirements)
51
+ [![python: 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/getting-started.md)
52
+ [![tui: textual](https://img.shields.io/badge/TUI-textual-8833ff)](https://textual.textualize.io/)
53
+
54
+ ๐Ÿ“– **Full documentation:** <https://jensenloke.github.io/macos-fanMonitor/>
55
+
56
+ > **Why is my fan spinning โ€” and what should I close?**
57
+ > An interactive terminal app that answers that at a glance, without asking an LLM.
58
+
59
+ A lazygit / yazi-style **Textual** TUI. Runs natively on macOS โ€” **not Docker**
60
+ (see [why](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/how-it-works.md#the-two-regimes) Docker can't work here).
61
+
62
+ ```
63
+ โ”Œ macOS Fan Monitor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 23:30 ยท up 18d ยท 1126 procs โ”
64
+ โ”‚ FAN 6097 RPM โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘ 93% TEMP 88ยฐC[TCMz] LOAD 15.6/10c MEM swap 98%โ”‚
65
+ โ”œ Verdict โ€” why is the fan spinning? โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
66
+ โ”‚ ๐Ÿ’พ Swap thrash โ€” fan is spinning from memory pressure, not CPU โ”‚
67
+ โ”‚ load 15.6 on 10 cores but only 12% of a core busy; procs blocked on โ”‚
68
+ โ”‚ disk, not computing. Close memory hogs below. โ”‚
69
+ โ”œ [ Close ] [ Processes ] [ Watchdog ] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
70
+ โ”‚ # process / group RSS CPU age why โ”‚
71
+ โ”‚ 1 claude@session-476111c5 x3 1626M 20% 24h frees RAM thrash โ”‚
72
+ โ”‚ 2 Google x57 2120M 10% 190h frees RAM thrash โ”‚
73
+ โ”‚ โ–ฒ select a row and press [k] to SIGTERM it โ”‚
74
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Contents
80
+
81
+ - [Why not Docker](#why-a-native-cli-instead-of-docker)
82
+ - [The core idea](#the-core-idea)
83
+ - [Install](#install)
84
+ - [Run & keys](#run)
85
+ - [Data sources](#data-sources-all-read-only)
86
+ - [Documentation](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/index.md) โ€” getting started, user guide, algorithm, roadmap, and more
87
+ - [Contributing](https://github.com/jensenloke/macos-fanMonitor/blob/main/CONTRIBUTING.md)
88
+
89
+ ## Requirements
90
+
91
+ - **macOS** on Apple Silicon (Intel untested)
92
+ - **Python 3.10+**
93
+ - **[Stats.app](https://github.com/exelban/stats)** โ€” `fm` reuses its read-only
94
+ SMC helper to read fan RPM + temperatures, so no new privileged code. Without
95
+ it, fan/temp tiles are blank but everything else works.
96
+
97
+ ## Why a native CLI instead of Docker
98
+
99
+ Docker Desktop on macOS runs containers inside a **Linux VM**. From inside a
100
+ container you **cannot** see:
101
+
102
+ - the **SMC** (fan RPM, temps) โ€” that's macOS IOKit, not exposed to the VM;
103
+ - the **macOS process list** (`ps` shows the VM's processes, not your apps);
104
+ - `vm_stat` / `sysctl vm.swapusage` / `memory_pressure` (macOS memory internals);
105
+ - the **watchdog** logs (they live on the macOS host).
106
+
107
+ Everything this tool needs is host-side, so the correct architecture is a small
108
+ native TUI that reads the SMC + `ps` + `vm_stat` + watchdog logs directly.
109
+
110
+ ## The core idea
111
+
112
+ A spinning fan here comes from **one of two very different causes**, and the fix
113
+ for each is different. The app decides which one is active, then ranks what to
114
+ close accordingly.
115
+
116
+ | Regime | Signal | What's really happening | Fix |
117
+ |---|---|---|---|
118
+ | **CPU** | load high **and** measured CPU% high | something is genuinely computing | close / wait on the CPU hog |
119
+ | **MEMORY** (swap thrash) | load high **but** measured CPU% **low** | system ran out of RAM and is paging; processes are **blocked on disk**, not computing | close **memory** hogs to stop the thrash |
120
+
121
+ The memory regime is the sneaky one: `ps %cpu` and the watchdog's "top CPU"
122
+ attribution both **miss it**, because thrashing processes show low CPU. That is
123
+ the exact failure mode from the real incidents that motivated this tool.
124
+
125
+ ### The recommendation algorithm
126
+
127
+ 1. **Classify** every process: `agent` (claude / codex / omp / devin / node_repl),
128
+ `browser`, `chat`, `app`, or `system` (protected).
129
+ 2. **Group** swarm siblings (all agents in one `session-โ€ฆ`) into one batch.
130
+ 3. **Detect the regime** from swap %, compressor ratio, RAM-free %, load vs.
131
+ core count, and measured CPU%.
132
+ 4. **Score** closeable processes with regime-appropriate weights (memory regime
133
+ weights RAM + age; CPU regime weights CPU%), times a category prior.
134
+ 5. **Never** recommend killing `system` daemons (WindowServer, Spotlight,
135
+ `suggestd`, โ€ฆ) โ€” those are symptoms; those get an *advisory* instead.
136
+
137
+ Full, exact thresholds and the scoring formula:
138
+ [docs โ†’ The Algorithm](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/algorithm.md).
139
+
140
+ ## Install
141
+
142
+ ```bash
143
+ pipx install macos-fanmon # one command; fm lands on your PATH
144
+ ```
145
+
146
+ (No pipx? `brew install pipx`, or from a clone: `./install.sh` builds a venv
147
+ and links `fm` into `~/.local/bin` โ€” make sure that is on your `PATH`.)
148
+
149
+ ## Run
150
+
151
+ ```bash
152
+ fm # interactive TUI
153
+ fm --once # single snapshot frame, then exit (for scripts / quick look)
154
+ fm --interval 3 # live refresh every 3s
155
+ ```
156
+
157
+ ### Keys (TUI)
158
+
159
+ | key | action |
160
+ |---|---|
161
+ | `q` | quit |
162
+ | `r` | refresh now |
163
+ | `1` / `2` / `3` | sort Processes by CPU / memory / age |
164
+ | `k` | **SIGTERM the selected row** (asks to confirm first) |
165
+ | `tab` | move focus between the Close / Processes tables |
166
+
167
+ `k` re-checks each PID is still alive before sending `SIGTERM`, and shows a
168
+ confirm prompt โ€” killing stays a deliberate action, never automatic. It only ever
169
+ sends `SIGTERM`, never `SIGKILL`.
170
+
171
+ ## Data sources (all read-only)
172
+
173
+ | Data | Source |
174
+ |---|---|
175
+ | fan RPM / target / range, temps | `/Applications/Stats.app/Contents/Resources/smc` |
176
+ | process list, CPU-time delta, RSS, age | `ps -axww -o pid,ppid,rss,etime,time,command` |
177
+ | swap / compressor / free / page I/O | `sysctl vm.swapusage`, `vm_stat`, `memory_pressure` |
178
+ | load / cores / uptime | `sysctl vm.loadavg`, `hw.ncpu`, `kern.boottime` |
179
+ | fan events, probe state, thresholds | `~/watchdogs/state/events/*.log`, `runner.log`, `watchdogs.json` |
180
+
181
+ `fm` never writes to the SMC. Sampling runs in a background worker thread so the
182
+ UI never blocks.
183
+
184
+ ## Watchdog integration
185
+
186
+ The **Watchdog** tab reads `dev.jensen.watchdog` state (read-only): current
187
+ `fan-activity` probe status (OK / WARN / CRIT), trigger / re-arm thresholds from
188
+ `watchdogs.json`, and recent fan events with their recorded attribution โ€” so you
189
+ can correlate the live verdict against what the watchdog has been logging.
190
+ Details: [docs โ†’ Watchdog Integration](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/watchdog.md).
191
+
192
+ ## Documentation
193
+
194
+ The full site is built from `docs/` and published to GitHub Pages. Browse
195
+ [online](https://jensenloke.github.io/macos-fanMonitor/) or preview locally:
196
+
197
+ ```bash
198
+ make docs # serve at http://127.0.0.1:8000
199
+ ```
200
+
201
+ | Page | What's in it |
202
+ |---|---|
203
+ | [Getting Started](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/getting-started.md) | install & first run |
204
+ | [User Guide](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/user-guide.md) | every screen, tile, and key |
205
+ | [How It Works](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/how-it-works.md) | the two-regime diagnosis logic |
206
+ | [The Algorithm](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/algorithm.md) | exact thresholds & scoring |
207
+ | [Watchdog Integration](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/watchdog.md) | correlating with your watchdog |
208
+ | [Troubleshooting](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/troubleshooting.md) | common issues |
209
+ | [Roadmap](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/roadmap.md) | shipped / planned / won't-do |
210
+ | [Contributing](https://github.com/jensenloke/macos-fanMonitor/blob/main/CONTRIBUTING.md) | dev setup, tests, safety rules |
211
+ | [Changelog](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/changelog.md) | release history |
212
+
213
+ ## Development
214
+
215
+ ```bash
216
+ make test # headless TUI smoke test (Textual run_test pilot)
217
+ make docs-build # strict docs build (what CI runs)
218
+ ```
219
+
220
+ `smoke_test.py` drives the app headless via Textual's `run_test()`: it asserts the
221
+ Close / Processes / Watchdog tables populate, the `1/2/3` sort keys change the
222
+ sort, and `k` opens the confirm modal from both tables (and declines cleanly).
223
+
224
+ ## Layout
225
+
226
+ ```
227
+ macOS-fanMonitor/
228
+ pyproject.toml # PyPI packaging: `fm` console script, package data
229
+ fm # launcher -> .venv/bin/python -m fanmon (dev clone)
230
+ Makefile # run / once / test / docs / docs-build / clean
231
+ install.sh # venv + deps + PATH link (dev clone)
232
+ scripts/verify-package.sh # wheel build + clean-venv install rehearsal
233
+ requirements.txt # rich, textual (dev clone)
234
+ requirements-docs.txt # mkdocs-material
235
+ smoke_test.py # headless TUI test
236
+ mkdocs.yml # docs site config
237
+ docs/ # documentation site
238
+ fanmon/
239
+ __main__.py # python -m fanmon
240
+ cli.py # entry: default = TUI, --once = snapshot
241
+ app.py # Textual App: gauges, tabs, kill, sort
242
+ fanmon.tcss # Textual stylesheet
243
+ engine.py # shared sampler (snapshot dict)
244
+ smc.py # fan + temperature sensors
245
+ procs.py # process snapshot + CPU delta + classification
246
+ memory.py # swap / compressor / pressure / load / uptime
247
+ regime.py # verdict + recommendation algorithm
248
+ watchdog.py # read-only watchdog log/config parsing
249
+ render.py # rich layout used by --once
250
+ ```
251
+
252
+ ## License
253
+
254
+ MIT โ€” see [LICENSE](https://github.com/jensenloke/macos-fanMonitor/blob/main/LICENSE).
@@ -0,0 +1,209 @@
1
+ # macOS Fan Monitor (`fm`)
2
+
3
+ [![docs](https://github.com/jensenloke/macos-fanMonitor/actions/workflows/docs.yml/badge.svg)](https://github.com/jensenloke/macos-fanMonitor/actions/workflows/docs.yml)
4
+ [![license: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/jensenloke/macos-fanMonitor/blob/main/LICENSE)
5
+ [![platform: macOS](https://img.shields.io/badge/platform-macOS%20(Apple%20Silicon)-black)](#requirements)
6
+ [![python: 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/getting-started.md)
7
+ [![tui: textual](https://img.shields.io/badge/TUI-textual-8833ff)](https://textual.textualize.io/)
8
+
9
+ ๐Ÿ“– **Full documentation:** <https://jensenloke.github.io/macos-fanMonitor/>
10
+
11
+ > **Why is my fan spinning โ€” and what should I close?**
12
+ > An interactive terminal app that answers that at a glance, without asking an LLM.
13
+
14
+ A lazygit / yazi-style **Textual** TUI. Runs natively on macOS โ€” **not Docker**
15
+ (see [why](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/how-it-works.md#the-two-regimes) Docker can't work here).
16
+
17
+ ```
18
+ โ”Œ macOS Fan Monitor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 23:30 ยท up 18d ยท 1126 procs โ”
19
+ โ”‚ FAN 6097 RPM โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘ 93% TEMP 88ยฐC[TCMz] LOAD 15.6/10c MEM swap 98%โ”‚
20
+ โ”œ Verdict โ€” why is the fan spinning? โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
21
+ โ”‚ ๐Ÿ’พ Swap thrash โ€” fan is spinning from memory pressure, not CPU โ”‚
22
+ โ”‚ load 15.6 on 10 cores but only 12% of a core busy; procs blocked on โ”‚
23
+ โ”‚ disk, not computing. Close memory hogs below. โ”‚
24
+ โ”œ [ Close ] [ Processes ] [ Watchdog ] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
25
+ โ”‚ # process / group RSS CPU age why โ”‚
26
+ โ”‚ 1 claude@session-476111c5 x3 1626M 20% 24h frees RAM thrash โ”‚
27
+ โ”‚ 2 Google x57 2120M 10% 190h frees RAM thrash โ”‚
28
+ โ”‚ โ–ฒ select a row and press [k] to SIGTERM it โ”‚
29
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Contents
35
+
36
+ - [Why not Docker](#why-a-native-cli-instead-of-docker)
37
+ - [The core idea](#the-core-idea)
38
+ - [Install](#install)
39
+ - [Run & keys](#run)
40
+ - [Data sources](#data-sources-all-read-only)
41
+ - [Documentation](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/index.md) โ€” getting started, user guide, algorithm, roadmap, and more
42
+ - [Contributing](https://github.com/jensenloke/macos-fanMonitor/blob/main/CONTRIBUTING.md)
43
+
44
+ ## Requirements
45
+
46
+ - **macOS** on Apple Silicon (Intel untested)
47
+ - **Python 3.10+**
48
+ - **[Stats.app](https://github.com/exelban/stats)** โ€” `fm` reuses its read-only
49
+ SMC helper to read fan RPM + temperatures, so no new privileged code. Without
50
+ it, fan/temp tiles are blank but everything else works.
51
+
52
+ ## Why a native CLI instead of Docker
53
+
54
+ Docker Desktop on macOS runs containers inside a **Linux VM**. From inside a
55
+ container you **cannot** see:
56
+
57
+ - the **SMC** (fan RPM, temps) โ€” that's macOS IOKit, not exposed to the VM;
58
+ - the **macOS process list** (`ps` shows the VM's processes, not your apps);
59
+ - `vm_stat` / `sysctl vm.swapusage` / `memory_pressure` (macOS memory internals);
60
+ - the **watchdog** logs (they live on the macOS host).
61
+
62
+ Everything this tool needs is host-side, so the correct architecture is a small
63
+ native TUI that reads the SMC + `ps` + `vm_stat` + watchdog logs directly.
64
+
65
+ ## The core idea
66
+
67
+ A spinning fan here comes from **one of two very different causes**, and the fix
68
+ for each is different. The app decides which one is active, then ranks what to
69
+ close accordingly.
70
+
71
+ | Regime | Signal | What's really happening | Fix |
72
+ |---|---|---|---|
73
+ | **CPU** | load high **and** measured CPU% high | something is genuinely computing | close / wait on the CPU hog |
74
+ | **MEMORY** (swap thrash) | load high **but** measured CPU% **low** | system ran out of RAM and is paging; processes are **blocked on disk**, not computing | close **memory** hogs to stop the thrash |
75
+
76
+ The memory regime is the sneaky one: `ps %cpu` and the watchdog's "top CPU"
77
+ attribution both **miss it**, because thrashing processes show low CPU. That is
78
+ the exact failure mode from the real incidents that motivated this tool.
79
+
80
+ ### The recommendation algorithm
81
+
82
+ 1. **Classify** every process: `agent` (claude / codex / omp / devin / node_repl),
83
+ `browser`, `chat`, `app`, or `system` (protected).
84
+ 2. **Group** swarm siblings (all agents in one `session-โ€ฆ`) into one batch.
85
+ 3. **Detect the regime** from swap %, compressor ratio, RAM-free %, load vs.
86
+ core count, and measured CPU%.
87
+ 4. **Score** closeable processes with regime-appropriate weights (memory regime
88
+ weights RAM + age; CPU regime weights CPU%), times a category prior.
89
+ 5. **Never** recommend killing `system` daemons (WindowServer, Spotlight,
90
+ `suggestd`, โ€ฆ) โ€” those are symptoms; those get an *advisory* instead.
91
+
92
+ Full, exact thresholds and the scoring formula:
93
+ [docs โ†’ The Algorithm](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/algorithm.md).
94
+
95
+ ## Install
96
+
97
+ ```bash
98
+ pipx install macos-fanmon # one command; fm lands on your PATH
99
+ ```
100
+
101
+ (No pipx? `brew install pipx`, or from a clone: `./install.sh` builds a venv
102
+ and links `fm` into `~/.local/bin` โ€” make sure that is on your `PATH`.)
103
+
104
+ ## Run
105
+
106
+ ```bash
107
+ fm # interactive TUI
108
+ fm --once # single snapshot frame, then exit (for scripts / quick look)
109
+ fm --interval 3 # live refresh every 3s
110
+ ```
111
+
112
+ ### Keys (TUI)
113
+
114
+ | key | action |
115
+ |---|---|
116
+ | `q` | quit |
117
+ | `r` | refresh now |
118
+ | `1` / `2` / `3` | sort Processes by CPU / memory / age |
119
+ | `k` | **SIGTERM the selected row** (asks to confirm first) |
120
+ | `tab` | move focus between the Close / Processes tables |
121
+
122
+ `k` re-checks each PID is still alive before sending `SIGTERM`, and shows a
123
+ confirm prompt โ€” killing stays a deliberate action, never automatic. It only ever
124
+ sends `SIGTERM`, never `SIGKILL`.
125
+
126
+ ## Data sources (all read-only)
127
+
128
+ | Data | Source |
129
+ |---|---|
130
+ | fan RPM / target / range, temps | `/Applications/Stats.app/Contents/Resources/smc` |
131
+ | process list, CPU-time delta, RSS, age | `ps -axww -o pid,ppid,rss,etime,time,command` |
132
+ | swap / compressor / free / page I/O | `sysctl vm.swapusage`, `vm_stat`, `memory_pressure` |
133
+ | load / cores / uptime | `sysctl vm.loadavg`, `hw.ncpu`, `kern.boottime` |
134
+ | fan events, probe state, thresholds | `~/watchdogs/state/events/*.log`, `runner.log`, `watchdogs.json` |
135
+
136
+ `fm` never writes to the SMC. Sampling runs in a background worker thread so the
137
+ UI never blocks.
138
+
139
+ ## Watchdog integration
140
+
141
+ The **Watchdog** tab reads `dev.jensen.watchdog` state (read-only): current
142
+ `fan-activity` probe status (OK / WARN / CRIT), trigger / re-arm thresholds from
143
+ `watchdogs.json`, and recent fan events with their recorded attribution โ€” so you
144
+ can correlate the live verdict against what the watchdog has been logging.
145
+ Details: [docs โ†’ Watchdog Integration](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/watchdog.md).
146
+
147
+ ## Documentation
148
+
149
+ The full site is built from `docs/` and published to GitHub Pages. Browse
150
+ [online](https://jensenloke.github.io/macos-fanMonitor/) or preview locally:
151
+
152
+ ```bash
153
+ make docs # serve at http://127.0.0.1:8000
154
+ ```
155
+
156
+ | Page | What's in it |
157
+ |---|---|
158
+ | [Getting Started](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/getting-started.md) | install & first run |
159
+ | [User Guide](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/user-guide.md) | every screen, tile, and key |
160
+ | [How It Works](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/how-it-works.md) | the two-regime diagnosis logic |
161
+ | [The Algorithm](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/algorithm.md) | exact thresholds & scoring |
162
+ | [Watchdog Integration](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/watchdog.md) | correlating with your watchdog |
163
+ | [Troubleshooting](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/troubleshooting.md) | common issues |
164
+ | [Roadmap](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/roadmap.md) | shipped / planned / won't-do |
165
+ | [Contributing](https://github.com/jensenloke/macos-fanMonitor/blob/main/CONTRIBUTING.md) | dev setup, tests, safety rules |
166
+ | [Changelog](https://github.com/jensenloke/macos-fanMonitor/blob/main/docs/changelog.md) | release history |
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ make test # headless TUI smoke test (Textual run_test pilot)
172
+ make docs-build # strict docs build (what CI runs)
173
+ ```
174
+
175
+ `smoke_test.py` drives the app headless via Textual's `run_test()`: it asserts the
176
+ Close / Processes / Watchdog tables populate, the `1/2/3` sort keys change the
177
+ sort, and `k` opens the confirm modal from both tables (and declines cleanly).
178
+
179
+ ## Layout
180
+
181
+ ```
182
+ macOS-fanMonitor/
183
+ pyproject.toml # PyPI packaging: `fm` console script, package data
184
+ fm # launcher -> .venv/bin/python -m fanmon (dev clone)
185
+ Makefile # run / once / test / docs / docs-build / clean
186
+ install.sh # venv + deps + PATH link (dev clone)
187
+ scripts/verify-package.sh # wheel build + clean-venv install rehearsal
188
+ requirements.txt # rich, textual (dev clone)
189
+ requirements-docs.txt # mkdocs-material
190
+ smoke_test.py # headless TUI test
191
+ mkdocs.yml # docs site config
192
+ docs/ # documentation site
193
+ fanmon/
194
+ __main__.py # python -m fanmon
195
+ cli.py # entry: default = TUI, --once = snapshot
196
+ app.py # Textual App: gauges, tabs, kill, sort
197
+ fanmon.tcss # Textual stylesheet
198
+ engine.py # shared sampler (snapshot dict)
199
+ smc.py # fan + temperature sensors
200
+ procs.py # process snapshot + CPU delta + classification
201
+ memory.py # swap / compressor / pressure / load / uptime
202
+ regime.py # verdict + recommendation algorithm
203
+ watchdog.py # read-only watchdog log/config parsing
204
+ render.py # rich layout used by --once
205
+ ```
206
+
207
+ ## License
208
+
209
+ MIT โ€” see [LICENSE](https://github.com/jensenloke/macos-fanMonitor/blob/main/LICENSE).
@@ -0,0 +1,2 @@
1
+ """macOS Fan Monitor โ€” why is the fan spinning, and what to close."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ sys.exit(main())