Contributor guide for window-toggle, a GNOME/X11 utility that toggles windows on per-key shortcuts.
Flat C99 codebase built with Meson; the working tree root is the project root.
window-toggle.c— CLI entry point, argument parsing, subcommand dispatch.config.c/.h— JSON config at/tmp/window-toggle-config.json; slot lookup/save.window-manager.c/.h— X11 window scan, state, show/hide via_NET_ACTIVE_WINDOWandXIconifyWindow.daemon.c/.h— persistent X-connection daemon (PID at/tmp/window-toggle-daemon.pid).ipc.c/.h— UNIX-socket protocol between CLI client and daemon.app-binding.c/.h— App binding module (v1.9+): registers "launch cmd + wm_class" pairs and anchors the first-launched XID. Persisted in the same config file under a### app_bindings ###section delimiter; the slot parser stops at this delimiter, so slot data and app bindings are isolated.doc/— design notes (IMPLEMENT_DAEMON_MODE.md,GITHUB_WORKFLOW.md,GITHUB_STATUS.md).build/— Meson/Ninja output; gitignored.README.md(English) andREADME_zh.md(Chinese) — keep both in sync on user-facing changes.
meson setup build # configure once; re-run with --reconfigure if needed
meson compile -C build # build the `window-toggle` binary
sudo meson install -C build # install to ${prefix}/bin (default /usr/local/bin)Smoke checks: ./build/window-toggle --version, --status, --show, --show-app. Re-run meson compile -C build after edits; reinstall only when the system binary needs refreshing.
- C, GNU99 (
-std=gnu99inmeson.build); implicit declarations are intentional — do not "fix" them. warning_level=1; keep the build warning-clean.- 4-space indent, no tabs; brace style matches existing files.
- Functions:
snake_case(e.g.save_shortcut_mapping). Types:PascalCase(e.g.WindowState,IPCRequest,AppBinding). Macros/constants:UPPER_SNAKE_CASE(e.g.CONFIG_PATH,APP_BINDINGS_DELIM). - No external formatter or linter is configured; do not add one. Match the surrounding style.
No dedicated test suite. meson.build registers only a smoke test('basic test', window_toggle) confirming the binary exists. Validate by:
- Running
./build/window-toggle --versionafter each build. - Exercising new shortcuts end-to-end on GNOME (configure → trigger → hide/show → reconfigure).
- Verifying
--start,--status,--stop, and a subsequent toggle when touching the daemon or IPC. - For app bindings: register with
--bind-app <key> <cmd> <wm_class>, verify anchor is updated on first launch and reused on subsequent toggles.pkill <cmd>to test relaunch semantics.
History follows Conventional Commits in English: feat:, fix:, chore:, docs:. PRs should:
- Use a Conventional Commits title.
- Describe the user-visible change and testing performed; reference any related issue.
- Include a screenshot or terminal transcript when CLI output or shortcut behavior changes.
- For
feat:/fix:that change observable behavior: bump the version string inwindow-toggle.cand add a changelog entry to bothREADME.mdandREADME_zh.md.
Two modes share the binary. Direct mode (--run, --configure, --show, …) opens a short-lived X11 connection. Daemon mode (--start / --stop / --status) keeps a persistent Display *; the CLI talks to it over a UNIX socket via ipc.c using the IPCRequest / IPCResponse framing. Use daemon mode when the host (e.g. Chrome) opens enough X connections to exhaust the default limit.
A second binding type, kept strictly separate from the slot pipeline:
- CLI subcommands:
--bind-app <key> <cmd> <wm_class>registers a dconf shortcut (action =window-toggle --run-app --key <key>) and writes one row into theapp_bindingssection.--unbind-app <key>removes both.--show-applists them.--run-appis the dconf callback. - Storage: same config file as slots. The
### app_bindings ###delimiter marks the section start;load_config/find_window_by_keystop reading at the delimiter, so the slot pipeline is unaffected.app_binding_add/app_binding_remove/app_binding_update_anchorrewrite the file via a<path>.app.tmpatomic rename.save_shortcut_mapping(used by--configure) preserves the app section across its own rewrite by appending the post-delimiter bytes into its temp file before rename. - Runtime (
--run-app): opens a direct X connection (does not go through the daemon — see rationale below). If the anchored XID is still alive, runs the existingminimize_window/activate_windowtoggle. If the anchor is dead or absent,fork+execlps the cmd, polls_NET_CLIENT_LISTfor a window whoseWM_CLASSmatches, anchors the first match (XID written back to the config), and exits without toggling — a freshly-launched window is visible by default, so toggling would be counter-intuitive. - Multi-instance semantics: the anchor is set on first launch and never drifts. If the user opens a second instance of the same class manually, the binding still acts on the anchored window.
- Why not daemon: launching an application is a lifecycle event, not a toggle. Letting the daemon fork apps would expand its role into "application supervisor".
--run-appopens a fresh X connection per press, which is acceptable for low-frequency key presses.
run_app_mode_with_path and show_app_mode_with_path install a silent XErrorHandler (silent_xerror_handler) right after XOpenDisplay. This swallows BadWindow from XGetWindowAttributes against a dead anchor instead of letting Xlib dump a fatal error. The handler is local to those code paths and is reset to default at process exit.