A keyboard shortcut utility for GNOME that lets you instantly show or hide any window with a single key press.
Ever wanted to hide a distracting window with one key press and bring it back just as quickly? window-toggle gives you a keyboard shortcut for each window, so you can:
- Hide a terminal or browser that's in your way
- Bring it back instantly with the same shortcut
- Works like "hide app" on macOS, but per-window
You have 3 terminals open on different workspaces.
You assign Ctrl+Alt+F1 to terminal 1, F2 to terminal 2, F3 to terminal 3.
Now you can:
- Press Ctrl+Alt+F1 → terminal 1 appears (was hidden)
- Press Ctrl+Alt+F1 → terminal 1 minimizes (was visible)
- Repeat anywhere in GNOME
# Setup build directory (only needed once)
meson setup build
# Compile the project
meson compile -C build
# Install to system (requires sudo)
sudo meson install -C buildAfter installation, run from anywhere:
window-toggle --configureIf you modify the source code and want to re-install:
# Recompile and install
meson compile -C build && sudo meson install -C buildOr in two steps:
meson compile -C build
sudo meson install -C build./window-toggle --configureThe program will:
- Ask you to press a key combination (e.g.,
Ctrl+Alt+F1) - Show a list of open windows - select one
- Automatically register the shortcut in GNOME
Press Ctrl+Alt+F1 anywhere in GNOME:
- Window hidden? → It appears on screen
- Window visible? → It minimizes
./window-toggle --show| Command | Description |
|---|---|
--configure |
Add a new shortcut for a window |
--run |
Toggle window (used by GNOME shortcut) |
--show |
List all configured shortcuts |
--clean |
Remove slot and viewer shortcuts (leaves --bind-app registrations intact) |
--start |
Clean and start fresh |
--stop |
Stop the daemon |
--status |
Show whether the daemon is running |
--version |
Print version and release highlights |
--key KEY |
Specify which key was pressed (used by the daemon-callback command) |
--config PATH |
Use a non-default config file path |
Starting with v1.8, the same Fx key (e.g. F1) can be bound to five different modifier combinations, each toggling an independent window:
- bare Fx — e.g.
F1 - Ctrl+Fx — e.g.
Ctrl+F1 - Ctrl+Alt+Fx — e.g.
Ctrl+Alt+F1 - Ctrl+Shift+Fx — e.g.
Ctrl+Shift+F1 - Super+Fx — e.g.
Super+F1
Run window-toggle --configure once per binding. The tool writes a separate
dconf slot for each one and the run-time --key argument carries the modifier
through to the lookup, so pressing Ctrl+F1 toggles the Ctrl+F1 binding
while pressing plain F1 toggles the bare-F1 binding.
- Uses X11
_NET_WM_STATE_HIDDENto detect window state - Sends
_NET_ACTIVE_WINDOWto show,XIconifyWindowto hide - Stores config in
/tmp/window-toggle-config.json - Registers shortcuts via GNOME's
dconfsettings
- GNOME Desktop (for keyboard shortcuts)
- libX11
- xkbcommon
- GCC
- meson, ninja (for building)
- Ubuntu 24.04 with GNOME
Bind a modifier shortcut to "launch this app + toggle this window":
# Bind Ctrl+F12 to launch nautilus, anchored to the first launched window
window-toggle --bind-app Ctrl+F12 nautilus org.gnome.Nautilus
# List bindings
window-toggle --show-app
# Remove a binding
window-toggle --unbind-app Ctrl+F12Behavior on press:
- If the anchored window is alive — toggle (minimize / activate), same as a normal slot binding.
- If the anchored window is gone —
fork+execlp <cmd>, poll for a window matchingWM_CLASS, anchor the first match, and stop. The newly-launched window is visible by default, so it is not toggled. - Multi-instance: opening a second instance of the same class does not drift the binding; the anchor stays on the originally launched window.
Storage: the same /tmp/window-toggle-config.json, in a section marked ### app_bindings ###. The slot pipeline stops at this delimiter, so app bindings and slot bindings do not interfere. --clean also removes the auto-registered viewer bindings (Pause / Scroll_Lock / Print) and any empty-slot residue in the dconf custom-keybindings array. It does NOT touch --bind-app registrations; use --unbind-app for those.
- fix:
--cleannow removes only ordinary window shortcuts created by--configure. App shortcuts (window-toggle-app) and viewer shortcuts (window-toggle-viewer) remain registered, and the running viewer is left untouched.
- chore: each app binding is now stored as a single config row. Previously every (modifier, key) pair wrote its own 6-line block, so the three sibling modifiers of one app occupied 18 lines; with five apps the config grew to 91 lines. The new format joins all three modifiers into one modifiers field separated by "|" and keeps one 6-line block per (app, Fx), so five apps now take ~30 lines. On disk the format is unambiguous: "" means bare Fx; a single name means single modifier; pipe-separated names mean multiple modifiers. The in-memory model still carries one AppBinding per (modifiers, key) — load splits pipe-separated entries back into individual rows, serialize merges them back together, so --show-app / --bind-app / --unbind-app behavior is unchanged. Legacy files (one modifier per row) keep loading.
- fix: merge duplicate rows and remove-one-row both used struct assignment to compact the AppBinding array, which left several rows sharing the same char* buffers. The subsequent app_binding_free then double-freed those buffers, and --unbind-app would collapse the file down to one row and lose every other binding. Both paths now deep-copy fields per row and clear the source row's pointers before free so the caller's free pass is a no-op on the cleared slots.
- fix: --clean now also clears the viewer's auto-registered bindings (Pause / Scroll_Lock / Print) and trims empty-slot residue from the dconf custom-keybindings array. Previously --clean only wiped slot bindings, so the viewer rows stuck around and the dconf array still showed the empty slots they had occupied.
- fix: --clean no longer touches --bind-app registrations. Those rows live in ~/.config/window-toggle/bindings.json under the name "window-toggle-app" and are removed by --unbind-app only. Without this split, --clean used to wipe user's app bindings along with the slot/viewer rows.
- feat:
--bind-app <Fx> <cmd> <wm_class>now registers three dconf shortcuts —Ctrl+Fx,Super+Fx, andAlt+Fx— instead of just one. Any of the three modifiers toggles the same window, so the user no longer has to pick one and stick with it. Bindings with an explicit modifier (e.g.Ctrl+Shift+F7) still register as a single row, so existing scripts keep working. - feat:
--show-appand the viewer popup now collapse the three siblings of one app into a single row. Modifier set is shown asCtrl(S+A)+Fx(Ctrl + Super + Alt),Ctrl+S+Fx(Ctrl + Super only), orCtrl+Fx(one entry), depending on which modifiers are present. Viewer badge state for the merged row isstarted > hidden > not-started, so a row never says "all dead" while one of the three siblings is alive. - fix: when launching a fresh app, the new anchor XID is also written back to the
Super+FxandAlt+Fxsiblings (same cmd + wm_class), so all three modifiers always point at the same window. Previously each modifier carried its own anchor that could go stale independently, leading to one of the three stopping to work after the window was closed and reopened.
- fix: pressing the hotkey now distinguishes "window is the topmost normal window" from "window is visible but buried under other windows". Previously both cases hit the minimize branch, so a hotkey press on a buried window did nothing the user could see (mutter only marks
_NET_WM_STATE_HIDDEN, not z-order). Now the hotkey first asks whether the window is on top of normal windows; if it is, minimize (unchanged); if it is buried, raise it to the front instead. Affects F1/F3/F6 (slot) and Ctrl+F10/F11/F12 (app binding). - fix: raising a window now sends
_NET_ACTIVE_WINDOWwithsource=2(pager) instead ofsource=1(app). Mutter honors the pager source by raising + giving focus; for app source it only gives focus and leaves the window where it was.
- fix: viewer popup now distinguishes "visible" from "minimized/hidden" from "gone". Previously both hidden and gone windows were rendered the same way, so pressing Ctrl+Fx to minimize a window looked like the key had no effect.
- Three states:
started(green dot, full opacity),hidden(gray dot, 40% opacity),not-started(orange dot, 25% opacity). - The popup polls every second while open, so closing/minimizing a window via Ctrl+Fx updates the popup in place — no need to close and reopen it.
- Three states:
- fix:
--cleannow also wipes the auto-registered viewer bindings (Pause / Scroll_Lock / Print) and any empty-slot residue in the dconf array.--bind-appregistrations are intentionally left alone — use--unbind-appto remove one.
- fix: dconf action parameter order (
--key X --run-appinstead of--run-app --key X) so the dconf callback actually fires - fix: app binding config now lives at
~/.config/window-toggle/bindings.json(XDG) instead of/tmp/window-toggle-config.json, so bindings survive reboot - chore: ensure parent dir is created on first write
- New:
--bind-app <key> <cmd> <wm_class>to bind a shortcut to "launch app + toggle window" - New:
--unbind-app <key>,--show-app,--run-app(dconf callback) - Anchor semantics: first launched window is remembered, never drifts
- Config: app bindings stored in a
### app_bindings ###section, isolated from slot data --cleanleaves app registrations alone; use--unbind-app <Ctrl+Fx>to remove a single one without touching the rest- Silent XErrorHandler added around stale-anchor checks
- Add
--versionflag with bilingual release notes - See v1.7 for the bulk of the modifier-matrix work
- Five modifier combinations can now coexist for the same Fx key (bare Fx, Ctrl+Fx, Ctrl+Alt+Fx, Ctrl+Shift+Fx, Super+Fx), each toggling an independent window
- Add persistent-X daemon mode (
--start/--stop/--status) to avoid the X-connection-exhaustion issue when Chrome has many windows. Seedoc/IMPLEMENT_DAEMON_MODE.mdfor the design notes - dconf command writes the full shortcut string (e.g.
--key Ctrl+F1) so the run-time lookup can recover the modifier - Fix dedup state-machine bug in
save_shortcut_mappingthat was corrupting the config file (the modifiers: line lost its trailing newline throughstrcspn) - Increase
line[]buffer from 512 to 4096 bytes in the config reader; the previous size truncated longwindow_titlefields
- Show
window_classin--showoutput
- Daemon-mode plumbing and assorted fixes; see
doc/GITHUB_STATUS.mdfor the historical commit list
- Fix config file format corruption when saving shortcuts
- Fix newline preservation in config file
- Support multiple independent shortcuts (F1, F2, etc.)
- Support shortcut override (reconfigure same key to different window)
- Add slot_id to config file for better tracking
- Add duplicate shortcut override support
- Initial release show
- Basic/hide window toggle functionality
MIT