Shipping display-profiles: What I Learned From Releasing My First Open Source Tool
From a personal workaround to open source release: what the gap between 'works for me' and 'works for anyone' looks like, and what I'd do differently.
I published display-profiles last week. It’s a display-switching tool that started as a set of personal scripts for working around an Nvidia driver bug, grew to cover panel layout persistence, and eventually became something I thought someone else might find useful. Releasing it taught me more than I expected about the distance between code that works for me and code that works for anyone.
What “works for me” actually means
When the scripts were personal, a lot of things were implicit. Monitor output names were hardcoded to my specific DisplayPort connections. Cinnamon was assumed throughout because that’s what I run. The home directory path was hardcoded in several places. The number of display profiles was fixed at two because I only ever needed two.
None of this was carelessness. It’s the natural shape of code written for a single environment where the author controls every variable. The problem is that none of those assumptions are visible until someone else runs it on something different.
Before publishing, I had to audit every implicit assumption in the codebase and decide whether to generalise it, document it, or remove it. Generalising was usually right. Hardcoded paths became variables. Monitor names became profile-driven configuration derived from what the user defines during setup. The number of profiles became however many directories exist under ~/.config/display-profiles/.
The DE plugin interface
The original scripts had Cinnamon-specific logic scattered throughout. dconf write calls lived alongside xrandr invocations. The cinnamon --replace restart lived in the main switch function.
For the release I extracted all DE-specific logic into a hooks/cinnamon/ directory with a defined interface: save-panels.sh takes a profile directory and snapshots dconf into it; restart-de.sh does whatever restarts the DE. The main scripts call the hooks if they exist and skip gracefully if they don’t. Adding support for another desktop environment means adding a hooks/<de>/ directory with the same two files.
This is the change I would have made first if I’d known I was going to publish. Writing the DE abstraction before writing the Cinnamon implementation would have been cleaner than extracting it after. The current structure works, but the seams from the refactor are visible if you look.
Writing a README that actually helps
The first README I wrote assumed the reader already knew what xrandr was, why you’d want named profiles rather than just running commands directly, and what the Nvidia driver bug was. It jumped straight to the installation steps.
The README the project shipped with explains the problem first. It describes the display switching use case, explains why persistence across reboots requires more than a bare xrandr invocation, and walks through what happens during profile creation before the installation section. The installation and usage steps stayed concise, but they’re preceded by enough context that a reader who doesn’t already know the problem space understands why the tool exists.
Writing a good README is harder than writing the code, because the reader’s state of knowledge is entirely unknown. The code can make assumptions about the machine state because those are controlled. The README can’t assume anything, and the gap between what the author knows and what the reader needs is easy to misjudge.
CI for a shell scripting project
What does useful CI look like for a project that’s entirely shell scripts? Shellcheck runs on all scripts and catches a meaningful class of real bugs: unquoted variables, missing exit codes, bad substitutions, word splitting issues. An install smoketest runs the installer and verifies the expected symlinks and desktop files end up where they should.
1
2
3
4
5
- name: Run shellcheck
uses: ludeeus/action-shellcheck@master
- name: Smoke test install
run: bash install.sh && [ -x ~/bin/display-switch.sh ]
That’s nearly the whole CI workflow. Small projects don’t need complex CI, and adding more than what’s actually useful is its own kind of overhead.
The resistance to publishing
There was a long stretch before I published where I kept finding new reasons not to. It is too specific to my hardware. The README is not ready yet. The DE plugin interface is not clean enough to hand to anyone else. The coordinate normalisation has an edge case I have not fully tested. The list went on, and I sat with it for longer than I want to admit.
Some of those reasons were real and worth caring about. Most of them were the kind of pre-emptive self-editing that quietly keeps useful things private indefinitely, and I want to name that pattern out loud, because I think a lot of us recognise it when we see it in ourselves. The Nvidia display bug on Linux is a known and persistent frustration that has cost real evenings to people I will never meet. The xrandr fix is well documented in pieces, but the persistence and panel layout work is scattered across forum posts and partial solutions, and having it all in one place with a setup wizard is useful even if the tool itself is not perfect.
Publishing it, in the end, was the right call, and I am genuinely glad I did it. The README is good enough. The code is the right shape for what it does. That is sufficient, and I am learning, slowly, to let sufficient be sufficient.
There is a version of quality control that is actually quality control, and there is a version of it that is indefinite deferral dressed up as standards. Knowing which one you are doing on any given evening is most of the work, and being kind to yourself about the answer is the rest of it.