A solo dev, a plastic-welding day job, and the cleanup that a Microsoft Store
listing forced on a script that had quietly become real software.
I Had a 6,533-Line Python File. Then My App Hit the Microsoft Store.
That evening is PC Workman,
It started as a small script to watch my own overheating PC, because I could not afford to keep guessing why it throttled, and I could not afford to be wrong about it either. A while later it has a thermal model that learns the norm of my specific machine instead of an averaged spec sheet, an assistant that answers in Polish or English from live sensor data, and a listing on the Microsoft Store.
It runs fully local: no account, no cloud, nothing about your machine ever leaves it. That part matters to me more than any single feature.
Store Listning did not change the code. It changed how I read it.
The day the Store listing went live, nothing in the repository moved.
Same files, functions, and me. And yet I opened the project that evening and saw it differently. A stranger in another country could now click Install and run this on their own machine, their own PSU, their own temperatures. It had stopped being my toy.
That is a quiet, enormous feeling, and it arrived with a bill attached.
Because when you suddenly see your code as something other people depend on, you cannot keep scrolling past the one file you have been avoiding for a year.
The file I had been avoiding
It was called **builder.py**. One class, ResponseBuilder, 6,533 lines long. Its job is the heart of the assistant:
take a parsed intent, like "what's my temperature" or "can I run this game",
and turn it into a bilingual answer built from live data.
There are 96 of those handlers, one per intent: _resp_temperature, _resp_why_slow, _resp_game_can_run``, and ninety-three more. Every single one lived in the same class, in the same file.I want to be honest about how it got that big, because it is not a dramatic story. It grew one handler at a time. Each new thing the assistant learned to say was ten more lines, and ten more lines never feels like a problem.
You do it a hundred times and one day the file makes your editor pause for a beat when it opens.\A monolith like that costs you in ways that do not show up on any screen. Every change touches a wall of unrelated code, so you stop making changes you should make.
You cannot work near your past self without a merge fight. And bugs love it there, because nobody reads six thousand lines at once, so there is always somewhere dark to hide.
A file you are afraid to open is a file you have already stopped improving.
A facade is a polite lie about where code lives
Here is the constraint that shaped the whole fix. The rest of the app talks to this thing through exactly one method: `
response_builder.build(result, lang)
`.
Chat panel calls it. The proactive monitor calls it. Tests call it.
I did not want to touch that surface at all. A refactor that forces every caller to change is not a cleanup, it is a second project.So the class stayed. Only its body moved out. What is left in **builder.py** is a facade: a class that still answers to every old name, but keeps almost none of the code itself.
class ResponseBuilder(HardwareResponses, UpgradeResponses),
ThermalResponses,
GamingResponses, SystemResponses,
PerformanceResponses,
"""Builds bilingual responses for every parsed intent."""
Each of those parents is amixin: a small class that carries nothing but one slice of the handlers.
HardwareResponses holds the CPU, GPU, RAM and disk answers. ThermalResponses holds temperatures, fans and voltage baselines. And so on down the line. Python composes them by inheritance, so from the outside ResponseBuilder still has all 96 methods hanging off it, exactly as it did when they shared one file.
The dispatch never moved a single character
This is the part I am most quietly proud of. The way a message becomes an answer did not change at all:
intent = self._INTENT_ALIASES.get(result.intent, result.intent)
handler = getattrgetattr(self, f"_resp_{intent}", None)
if handler is None:
return None
out = handler(result, lang)
getattr looks the method up by name, at runtime.
It does not know or care which file that method is written in. Python's method resolution order walks the mixins and finds it.
So " temperature" still resolves to `
`_resp_temperature, whether that method sits in the old monolith or in the newr_thermal.py. The move was invisible to every caller and to every test that goes throughbuild()``. That was the entire goal: relocate six thousand lines and change nothing a single other file can observe.### How I actually cut it
By subject, not by size. I did not slice the file into equal chunks. I grouped handlers by what they talk about, because that is how I reach for them when I add a new one. It fell into eight modules almost on its own:
| Module | Handlers | What it answers |
|---|---|---|
| r_hardware.py | 25 | CPU, GPU, RAM, disk, motherboard, full specs |
| r_insights.py | 15 | trends, comparisons, session digests, what changed |
| r_performance | 13 | load, throttling, why is it slow |
| r_system | 13 | health check, processes, startup, security |
| r_thermal | 11 | temperatures, fans, voltage baselines |
| r_assistant.py | 11 | greetings, help, about, recall earlier answers |
| r_gaming | 6 | can this game run, gaming versus work time |
| r_upgrade | 2 | offline part compatibility (new this release) |
Two more files carry the shared weight. common.py holds the helpers every module reaches for:
bilingual picker _t(lang, pl, en), follow-up hint pools, the little delta label that turns a raw number into
" +23% above your norm". `
flows.py`` holds the guided, multi-step conversations.## Andbuilder.py`, once 6,533 lines, is now under 600 and does exactly one job:
compose the mixins and route to them. r_upgrade.py is youngest of the eight, and it tells the whole story in miniature.
When I built the offline part-compatibility engine for this release, it got its own module from the first line, two handlers today with plenty of room to grow. A year ago I would have pasted it into whichever file was already open.
Its difference maturity makes: I gave a new feature a home instead of a corner.
Two tests so it can never grow back
Splitting a monolith is an afternoon's work. Keeping it split is hard part, because the pressure that built the first one is still there. Next handler will always be easier to paste into an existing file than to think about.
**So I wrote two guards that fail the build, not a note-to-self that I will ignore.\The first caps every response module at 1,600 lines:
def test_monolith_guard(self):
for p in glob.glob(_RESPONSES_GLOB):
n = _read(p).count("\n") + 1
self.assertLess(n, 1600,
f"{os.path.basename(p)} has {n} lines - split it "
f"instead of growing another monolith")
If any module crosses that line, the build turns red. Failure message is not " error", it is an instruction aimed at my future self:
split it, do not start a new monolith.
Second guard exists because mixins have a sharp edge. If two of them define a method with the same name, one silently wins by inheritance order and the other vanishes with no warning at all. That is the kind of bug you find three weeks later, staring at an answer that changed for no reason you can see.
So the second test greps every module for handler definitions and fails if any name appears twice:
all_defs = []
for p in glob.glob(_RESPONSES_GLOB):
all_defs += re.findall(r'def (_resp_[a-z0-9_]+\(', _read(p))
dupes = {d for d in all_defs if all_defs.count(d) > 1}
self.assertEqual(dupes, set())
The split took one afternoon. These two tests are the reason it will still be split a year from now. A refactor without a ratchet is just a delay.
It was not only the AI
Builder was the headline, but the whole release was a maturity pass, and the other pieces rhyme with it.My PC page opened in about two seconds. It now re-opens in 1 to 17 milliseconds. Real culprit was not the AI or the charts.
It was two wmic calls, one for the CPU name and one for the disk model, sitting on the interface thread with a three-second timeout each, and on current Windows wmic is slow. They now read a hardware identity that is warmed once at startup.
Page is also built a single time and kept alive, instead of being torn down and rebuilt on every visit.The version number used to live in eight files, and it had already drifted.
Window titled itself one version while the single-instance check searched for another, so launching the app twice quietly stopped focusing the copy already running. It now lives in exactly one file, and a test fails the build on any hardcoded duplicate.
The test suite went from 21 in June to 194.
\ None of that is a feature. You cannot take a screenshot of "the version now lives in one file". But this is what a project looks like when it decides to grow up. It spends weeks making itself cheaper to change, so the next real feature is easy instead of frightening.
What this kind of work actually costs
I will not pretend it is free, because a piece that hides price is not worth reading.
A refactor ships nothing the user can see. For weeks the changelog says things like " builder split into mixins", and a fair person asks what they got for it.
Honest answer is: nothing today, and a faster everything after. That is a hard sell, most of all to yourself at eleven at night, after a full day over the welding gun.
It is not risk-free either.
Moving six thousand lines is exactly the change that sneaks a subtle bug in, precisely because it is supposed to change nothing. That is why every step of it went behind tests that call real public method, and why the two guards exist at all.
**I trust the boring, provable moves. I do not trust my own memory of a 6,533-line file. \ But I would do it again without hesitating, because the alternative is worse.
Alternative is a file that grows until you are scared of it, inside an app that strangers now install. Fear is not a maintenance strategy.
Back in the workshop, good welding is never the flashy part. It is the joint you cannot see coming apart later.
Software is same. The work I am proudest of this release leaves no mark on the screen.
The Microsoft Store did not make PC Workman a real project. It just held up a mirror, and I finally cleaned up what I saw.
*PC Workman is free and open-source. The builder split, the guard tests and the My PC speed-up all ship in v1.8.5.
You can read every file on GitHub
or grab the build from the Microsoft Store
I'm Marcin "HCK" Firmuga, a self-taught developer from Radom, Poland. I weld plastic by day and build PC Workman by night, in public, one commit and one honest post at a time. Everything I write is about what I actually shipped, with real numbers and real code, including the parts that broke. PC Workman is free, open-source, and offline by design. If that is your kind of software, the source and the whole story are at )'
❤️ If you want to support my work :) - Here! ❤️