A Wayland logout menu you theme with real HTML, CSS, and JavaScript — zero GTK theme inheritance, ever.
glogout renders its entire UI inside a WebKitGTK webview mounted in a wlr-layer-shell overlay. The only GTK surface is the invisible window that carries the layer-shell handshake; everything you see is a web page you fully control. Backdrop blur, web fonts, inline SVG, keyframe animations, JS — the whole web platform, with nothing inherited from your system.
Every other Wayland logout menu — wlogout, wleave, waylogout, powermenu, nwg-bar — is built from GTK (or Qt) widgets and styled with that toolkit's narrow CSS dialect, so it inherits and fights your system theme. glogout sidesteps the problem by not using widgets at all.
Themable logout menus on Linux are unreasonably hard. The existing tools render with GTK widgets that pick up the system GTK theme by default, and "style.css" there means GTK's CSS subset, not the real thing. If your system theme is at odds with what you want the menu to look like, you lose — there's no clean escape hatch.
glogout rendering the menu in a webkit6 WebView inside a gtk4-layer-shell overlay is that escape hatch. The WebView is untouched by GTK theming, and you get a real browser engine: anything you can do on a web page, you can do to your logout menu.
- Zero GTK theme bleed. Your CSS is the only CSS.
- Hot reload. Edit
config.toml,style.css, ortemplate.htmland the menu re-renders in place. Debounced so editor save patterns don't spam reloads. - Daemon mode. A long-running
glogout daemonkeeps the webview warm;glogout showreveals the menu sub-frame. One-shot still works if you don't care about latency. - Multi-monitor aware. Every output dims and the menu floats on top wherever the compositor places it, so the modal feel covers the whole session no matter which screen it opens on.
- Sensible built-ins.
logout,reboot,shutdown,suspend,hibernate,lock,cancel. Everything else is an arbitrary shell command. - Single static binary. Rust. No runtime, no plugin host, no electron.
- A Wayland compositor that implements
wlr-layer-shell. Validated on Hyprland; KWin (Plasma 6) and Sway are expected to work. GNOME does not implement layer-shell and is not supported. - WebKitGTK 6.0 (
webkitgtk-6.0package on Arch;libwebkitgtk-6.0on Debian/Ubuntu). - GTK 4.14 or newer.
git clone https://github.com/synnode/glogout
cd glogout
make install # builds, installs to ~/.cargo/bin + the systemd user unitmake install warns if another glogout earlier in PATH would shadow the
one it just installed. For a system-wide install: make build && sudo make install PREFIX=/usr/local. Run make help for all targets.
Or — once published — cargo install glogout.
Then write the default config:
glogout initThis creates ~/.config/glogout/{config.toml,style.css,template.html}.
~/.config/glogout/config.toml:
[settings]
close_on_escape = true
# output = "DP-1" # pin to a specific monitor (needs daemon restart)
# dimmer_color = "#121216" # dimmer overlay color (#RRGGBB)
# dimmer_opacity = 0.6 # 0.0 = see-through (shows desktop), 1.0 = opaque
[[buttons]]
id = "logout"
label = "Log out"
icon = "⏻"
keybind = "l"
action = "logout" # built-in
[[buttons]]
id = "screenshot"
label = "Screenshot"
icon = "📸"
keybind = "s"
command = "grim ~/shot.png" # arbitrary shell commandBuilt-in actions map to standard system commands:
action |
runs |
|---|---|
logout |
loginctl terminate-user $USER |
reboot |
systemctl reboot |
shutdown |
systemctl poweroff |
suspend |
systemctl suspend |
hibernate |
systemctl hibernate |
lock |
loginctl lock-session |
cancel |
(closes the menu) |
Anything else? Use command = "..." instead of action = "...". Commands run via sh -c, so quoting, pipes, env vars, and & background-spawning all work.
Edit ~/.config/glogout/style.css directly. The defaults expose CSS variables for the common knobs:
:root {
--bg: rgba(18, 18, 22, 0.6);
--fg: #f2f2f2;
--accent: #7aa7ff;
--button-bg: rgba(255, 255, 255, 0.05);
--button-bg-hover: rgba(255, 255, 255, 0.1);
}For full layout control, edit template.html. Available placeholders:
{{stylesheet}}— your CSS, injected as<style>...</style>{{buttons}}— the rendered button list, in config order{{script}}— the click/keybind dispatcher (don't omit this){{username}}/{{hostname}}— current$USERand/etc/hostname
{{buttons}} expands to one <button> per [[buttons]] entry, wrapped in a
.menu container inside a .layout column. Each button is generated like this
— the .kbd and .icon spans appear only when you set keybind/icon,
and the label span has no class:
<div class="layout">
<div class="menu">
<button data-action="logout" data-keybind="l" autofocus>
<span class="kbd">l</span> <!-- only if keybind is set -->
<span class="icon">⏻</span> <!-- only if icon is set -->
<span>Log out</span> <!-- label: NO class -->
</button>
<!-- ...one <button> per config entry, in order... -->
</div>
<div class="hint">Press a key, click a button, or hit Esc to cancel</div>
</div>data-action is the button's id; the first button gets autofocus. Selector
reference for your style.css:
| Selector | Targets |
|---|---|
.layout |
outer column wrapping the buttons and the hint |
.menu |
the button container (default: a horizontal grid) |
button |
every button |
button[data-action="logout"] |
one specific button, by its config id |
button:focus-visible |
the keyboard-focused button |
.icon |
the icon span (present only if icon is set) |
.kbd |
the keybind badge (present only if keybind is set) |
button > span:last-child |
the label text (it has no class of its own) |
.hint |
the footer hint line |
So to restyle just the cancel button and the labels:
button[data-action="cancel"] { opacity: 0.7; }
button > span:last-child { font-weight: 600; letter-spacing: 0.02em; }CSS backdrop-filter: blur() cannot blur your desktop. A web engine only
samples its own document, so with a transparent page there is nothing behind
body to blur — it's a no-op for desktop show-through. Real frosted glass is
compositor blur. Every glogout surface sets its layer-shell namespace to
glogout, so you can target it with a Hyprland layer rule (needs the global
decoration { blur { enabled = true } }, which is on by default). Pair it with a
low dimmer_opacity.
The rule syntax is Hyprland-version-dependent:
# Hyprland < 0.53 (classic)
layerrule = blur, glogout
layerrule = ignorezero, glogout
# Hyprland 0.53–0.55 (structured rule blocks).
# The classic comma form errors here; ignorezero became ignore_alpha (0.0–1.0).
layerrule {
name = glogout-blur
match:namespace = ^glogout$
blur = true
ignore_alpha = 0.0
}On 0.55+ you can also use the Lua config form:
hl.layer_rule({ match = { namespace = "^glogout$" }, blur = true, ignore_alpha = 0.0 })Two modes, two tradeoffs.
glogoutCold-starts the webview every time. Simplest setup — bind a key in your compositor to glogout and you're done. Expect 150–400ms to first paint depending on hardware, dominated by WebKitGTK process spawn. Fine for occasional use.
glogout daemon & # long-running; usually in a systemd user unit
glogout show # client; binds to your hotkeyThe daemon builds the webview at startup and keeps it warm. glogout show opens a Unix socket connection at $XDG_RUNTIME_DIR/glogout.sock and the menu appears sub-frame. After dispatch (or Escape), the surfaces hide and the daemon goes back to standby — no restart needed.
When to prefer daemon:
- You invoke the menu often enough that 200ms+ feels annoying.
- You're scripting around it and want predictable latency.
- You're already running other warm-webview tools (e.g. eww) and don't mind one more.
When one-shot is fine:
- You log out once a day or less.
- You're tight on idle RSS budget (the daemon parks at ~50–150MB depending on WebKit version).
- You don't want a systemd unit in your dotfiles.
The daemon refuses to start if another instance is reachable on the socket, and cleans up the socket file on clean exit. A stale socket from a crashed daemon is auto-detected and unlinked at startup.
contrib/glogout.service ships a ready-to-go unit:
mkdir -p ~/.config/systemd/user
cp contrib/glogout.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now glogout.servicePartOf=graphical-session.target so it stops when your session ends; Restart=on-failure so a webview crash doesn't leave you without a menu.
The watcher is always on (in both one-shot and daemon mode) and triggers on changes to config.toml, style.css, or template.html in the resolved config directory. Reload is in-place: the webview's HTML is re-rendered, the button dispatcher is rebuilt, and [settings] are re-applied live — the dimmer (dimmer_color/dimmer_opacity) and close_on_escape all take effect on save, no process restart. The one exception is output, which rebuilds the layer surfaces and so needs a restart. Parse errors are logged and the previous state is kept, so a half-edited file never bricks an open overlay.
In one-shot mode the practical value is limited (the overlay covers all monitors, so you can't reach an editor while it's open). The feature exists mostly as scaffolding for daemon mode, where it actually matters: edit your config while the daemon is hidden, then glogout show to see the result.
glogout one-shot menu
glogout daemon run as a background service
glogout show tell a running daemon to show
glogout toggle show if hidden, hide if shown (one keybind, both ways)
glogout init write default config files to ~/.config/glogout/
Bind glogout toggle rather than glogout show if you want a single key to both summon and dismiss the menu.
glogout init --force overwrites existing files.
For the full picture see glogout_spec.md and the .wiki/ pages. The short version:
gtk4::Window ── promoted via gtk4-layer-shell to a wlr-layer-shell overlay
└─ webkit6::WebView (transparent background, loads HTML from config)
└─ JS posts action_id via UserContentManager
└─ Rust dispatcher resolves action_id → built-in or `sh -c`
The stack is Linux-only on purpose. No tao, no wry. Earlier prototypes used wry, which still pulls in webkit2gtk (GTK 3) and crashes on Hyprland's explicit-sync surfaces (GTK 3 bug, fixed in GTK 4, not backported). See .wiki/StackDecision.md for the full debugging trail.
outputplacement on Hyprland is timing-sensitive. The menu grabs the keyboard exclusively, and Hyprland places keyboard-grabbing layer surfaces on the focused output, racing theset_monitorrequest. The passive dimmers honoroutputreliably; the menu itself does not in every build. In practice a--releasebinary appliesset_monitorfast enough that the menu lands on the requested output (consistently observed across triggers from different screens), but a debug build often opens it on the cursor's monitor instead. Treat pinnedoutputas reliable on release builds, not guaranteed — verify on your own setup. This is also why every monitor is dimmed rather than just the menu's.outputchanges need a daemon restart. Re-anchoring rebuilds the layer surfaces, which the in-place reload path doesn't do.
MIT — see LICENSE. Do whatever you want with it; just keep the copyright notice.
v1.0.0. Config + theming, hot reload, daemon mode, multi-monitor, configurable dimmer, and fully hot-reloadable [settings] (except output). Daily-driven on Hyprland. Stable and considered feature-complete for its scope; bug reports and theming PRs welcome.