Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ Flow Z13 via the `z13ctl` daemon. It slides in from the right edge of the screen
Armoury Crate button (KEY_PROG3) is pressed. The daemon broadcasts `gui-toggle` events over
a subscribe socket; this GUI listens for them.

It has two display backends:
It has three display backends:
- **Layer-shell** (KDE/Wayland): margin-based slide animation
- **Overlay** (GNOME and any compositor without layer-shell): fullscreen
transparent window, drawer right-aligned inside it, click-through everywhere else
- **Gamescope** (Steam Gaming Mode): X11 overlay via `STEAM_OVERLAY` atom

- Module: `github.com/dahui/z13gui`
Expand Down Expand Up @@ -45,6 +47,9 @@ internal/gui/fonts/
font.go Embedded Inter font loading
internal/gui/layershell/
layershell.go Layer-shell display backend (KDE/Wayland)
internal/gui/overlay/
overlay.go Fullscreen transparent click-through backend
(GNOME/Mutter and anything without layer-shell)
internal/gui/gamepad/
gamepad.go evdev gamepad reader; device classification + EVIOCGRAB
steam.go Steam PID discovery; drives the hidraw blocker
Expand Down Expand Up @@ -73,6 +78,7 @@ internal/keyrepeat/ Tracker: which held direction owns the gamepad a
internal/colorconv/ hex <-> HSL/RGB conversion and colour validation
internal/lighting/ RGB mode resolution, per-mode controls, defaults
internal/uiscale/ Gamescope UI scale factor (cannot live in the cgo package)
internal/panelgeom/ Overlay backend panel rectangle + slide interpolation
internal/startup/ CLI arg scanning + split-level slog handler
internal/togglegate/ Debounce helper for duplicate gui-toggle bursts
contrib/
Expand All @@ -82,6 +88,18 @@ contrib/

## Key architectural decisions

- **Never call into gtk4-layer-shell without checking `IsSupported()` first.**
`zwlr_layer_shell_v1` is a **wlroots** extension, not part of `wayland-protocols`:
KWin, Hyprland and Sway implement it, GNOME's Mutter never has and has no plan to.
Installing `gtk4-layer-shell` does not help — that is the *client* library; the
protocol has to come from the compositor. The failure is silent, which is what
made issue #16 hard: `gtk_layer_init_for_window` logs one `G_LOG_LEVEL_WARNING`
and returns, then every `SetLayer`/`SetAnchor`/`SetMargin`/`SetMonitor`/
`SetKeyboardMode` warns once and no-ops. Nothing aborts, so the drawer came up
as an unanchored window — and since the anchors were the only thing supplying a
height, it collapsed to a ~320px box in the middle of the screen. `gui.go`'s
`layerShellUsable()` gates this, and checks the GDK backend *before* calling
`IsSupported()`, which asserts on a non-Wayland display.
- **Layer-shell** (KDE): `github.com/diamondburned/gotk4-layer-shell/pkg/gtk4layershell`
(NOT `gtklayershell` which is GTK3). pkg-config name: `gtk4-layer-shell-0`.
- **Anchor**: right + top + bottom edges. Top/bottom margins set to 5% of screen height
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ GTK4 overlay drawer for [z13ctl](https://github.com/dahui/z13ctl) on Wayland.

`z13gui` is the graphical companion to z13ctl — a slide-out drawer triggered by the
Armoury Crate button (KEY_PROG3). It renders as a Wayland layer-shell overlay (KDE Plasma,
Hyprland, Sway) or as a gamescope X11 overlay in Steam Gaming Mode. All hardware
communication goes through the z13ctl daemon.
Hyprland, Sway), as a transparent click-through overlay on compositors without layer-shell
(GNOME — see [GNOME support](https://dahui.github.io/z13gui/#gnome-support)), or as a
gamescope X11 overlay in Steam Gaming Mode. All hardware communication goes through the
z13ctl daemon.

## Install

Expand Down
91 changes: 89 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,106 @@ touches HID devices or sysfs directly.

## Display backends

Two backends are supported, selected automatically based on the session
Three backends are supported, selected automatically based on the session
environment:

- **Layer-shell** (KDE Plasma, Hyprland, Sway) — Wayland layer-shell overlay
with margin-based slide animation and focus-loss dismiss
- **Overlay** (GNOME, and any compositor without layer-shell) — a fullscreen,
transparent window with the drawer against the right edge. Everything outside
the drawer is click-through, so the rest of the desktop stays usable.
See [GNOME support](#gnome-support)
- **Gamescope** (Steam Gaming Mode) — X11 overlay via the `STEAM_OVERLAY` atom
with opacity-based visibility and a click-to-dismiss backdrop

---

## GNOME support

z13gui works on GNOME, but with a slightly degraded experience compared with KDE
Plasma, Hyprland or Sway. This section covers why, and exactly what differs.

### Why a workaround is needed

The drawer is normally anchored to the screen edge using the **layer-shell**
protocol (`zwlr_layer_shell_v1`). That protocol is a **wlroots extension** — it
is not part of `wayland-protocols`, and not every Wayland compositor implements
it:

| Compositor | Wayland | `zwlr_layer_shell_v1` |
|---|---|---|
| KWin (KDE Plasma) | yes | yes |
| Hyprland, Sway (wlroots) | yes | yes |
| **Mutter (GNOME)** | **yes** | **no** |

GNOME supports Wayland perfectly well. What Mutter has never implemented is this
one extension, on the long-standing position that panels and docks belong in
GNOME Shell extensions rather than in a client-side protocol.

!!! warning "Installing `gtk4-layer-shell` does not help"

`gtk4-layer-shell` is the **client** side of the protocol — it is what z13gui
uses to *speak* layer-shell, and the packages already depend on it. The
protocol itself has to come from the compositor, and Mutter offers no way to
add Wayland protocols (GNOME Shell extensions cannot register Wayland
globals). No package can add layer-shell to GNOME.

### How the workaround works

Core Wayland deliberately gives a client no way to position its own window — the
compositor decides placement. So without layer-shell, an ordinary window simply
lands wherever Mutter puts it, which is the middle of the screen. That is
[exactly what used to happen](https://github.com/dahui/z13gui/issues/16).

Instead, z13gui takes a **fullscreen** window — a standard request every
compositor honours, and one that needs no positioning — makes it fully
transparent, and draws the drawer against the right edge inside it. The window's
*input region* is then restricted to the drawer's rectangle, so every pixel
outside the drawer is click-through: clicks and scrolls pass straight to the
windows underneath, and the rest of the desktop keeps working normally.

The missing protocol is detected at startup and this backend is selected
automatically. There is nothing to configure.

### What is unchanged

- The drawer sits against the right edge at full height, less 5% top and bottom
- The slide-in and slide-out animation
- Every control, theme and gamepad navigation behaviour
- Escape dismisses, as does clicking another window

### What is degraded

Because the drawer is an ordinary window rather than a compositor-managed overlay
layer:

- **It cannot be drawn above a fullscreen application.** Under layer-shell the
drawer lives on the overlay layer, above everything; here it is a normal window
and the compositor decides stacking.
- **It belongs to the current workspace**, rather than being present on every
workspace the way a layer surface is.
- **It may appear in the window switcher (Alt-Tab) while open.** The window is
unmapped when the drawer is closed, so it only shows up while on screen.
- **GNOME's top bar may be hidden while the drawer is open**, since Mutter hides
it for fullscreen windows.
- **There is no dedicated click-outside backdrop.** Dismissal is Escape, or
clicking another window — which works because that window takes focus.

None of this affects the controls themselves: every hardware feature behaves
identically on GNOME.

### Prefer the full experience?

Log into a session whose compositor implements layer-shell — KDE Plasma, Hyprland
and Sway are all packaged for Fedora and every other major distribution. z13gui
switches back to the layer-shell backend automatically.

---

## Requirements

- Wayland compositor with layer-shell support, or gamescope (Steam Gaming Mode)
- A Wayland compositor (layer-shell is used when available; GNOME and others get
the overlay backend), or gamescope (Steam Gaming Mode)
- GTK 4 and gtk4-layer-shell libraries (see [Installation](installation.md#runtime-dependencies) for distro package names)
- [z13ctl](https://github.com/dahui/z13ctl) daemon running

Expand Down
46 changes: 44 additions & 2 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
## Prerequisites

- Linux kernel (x86_64)
- Wayland compositor with layer-shell support, or gamescope (Steam Gaming Mode)
- A Wayland compositor, or gamescope (Steam Gaming Mode). Compositors with
layer-shell support (KDE Plasma, Hyprland, Sway) get the drawer as a true
edge-anchored panel; GNOME and any other compositor without it get the overlay
backend instead, which works but is slightly degraded — see
[GNOME support](index.md#gnome-support)
- [z13ctl](https://github.com/dahui/z13ctl) installed and daemon running

### Runtime dependencies
Expand Down Expand Up @@ -244,6 +248,43 @@ Make sure the z13ctl daemon is running:
systemctl --user status z13ctl.service
```

Then check the log — see the note below on `--user`, which is required.

**Which display backend am I on?**

The startup log names it. Run z13gui from a terminal and read the first lines:

```sh
z13gui --debug
```

Look for `backend mode=layer-shell`, `mode=overlay` or `mode=gamescope`. On
GNOME, `mode=overlay` is expected and correct: Mutter does not implement the
`zwlr_layer_shell_v1` protocol, so the drawer is drawn as a transparent
click-through overlay rather than an edge-anchored panel. See
[GNOME support](index.md#gnome-support) for what that changes.

**The drawer is a small box in the middle of the screen**

This affects v1.3.0 and earlier on GNOME. Those versions called into layer-shell
without checking whether the compositor implements it; every anchoring call
silently did nothing, and since the anchors were the only thing giving the drawer
a height, it collapsed into a small unusable window
([#16](https://github.com/dahui/z13gui/issues/16)). Upgrade to the latest
release, which detects this and uses the overlay backend instead.

**Reading the log: `--user` is required**

z13gui runs as a systemd **user** unit (installed to `/usr/lib/systemd/user/` by
the distro packages), so its output goes to the user journal. Without `--user`,
`journalctl` searches system units, finds no such unit, and prints
`-- No entries --` — which looks like the program never ran:

```sh
journalctl --user -u z13gui -n 50 # correct
sudo journalctl -u z13gui # WRONG: reads system units, prints nothing
```

**Service fails to start**

Check the journal:
Expand Down Expand Up @@ -279,4 +320,5 @@ ls "$XDG_RUNTIME_DIR/$GAMESCOPE_WAYLAND_DISPLAY"
```

If the socket is missing (stale environment from a previous Gaming Mode session),
z13gui automatically falls back to Wayland layer-shell mode.
z13gui automatically falls back to the Wayland path — layer-shell where the
compositor supports it, the overlay backend otherwise.
11 changes: 7 additions & 4 deletions internal/gui/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ package gui
import "github.com/diamondburned/gotk4/pkg/gtk/v4"

// Backend abstracts display-mode-specific window management.
// Implementations: layershell.Backend (Wayland compositors) and
// gamescope.Backend (X11 overlay for Steam Gaming Mode).
// Implementations: layershell.Backend (Wayland compositors implementing
// zwlr_layer_shell_v1 — KWin, Hyprland, Sway), overlay.Backend (a fullscreen
// transparent click-through window for those that do not, GNOME/Mutter above
// all), and gamescope.Backend (X11 overlay for Steam Gaming Mode).
type Backend interface {
// Configure sets up the window for this display mode. Must be called
// before the window is realized.
Expand All @@ -18,7 +20,8 @@ type Backend interface {

// WrapContent optionally wraps the drawer widget for this display mode.
// Layer-shell returns it as-is; gamescope wraps it in a fullscreen
// container with a click-to-dismiss backdrop.
// container with a click-to-dismiss backdrop; overlay wraps it in a
// fullscreen GtkFixed so the drawer can be slid in from the right edge.
WrapContent(drawer gtk.Widgetter) gtk.Widgetter

// Show makes the drawer visible (animation, atom toggle, etc).
Expand All @@ -28,7 +31,7 @@ type Backend interface {
Hide()

// Scale returns the factor the drawer's CSS pixel sizes are multiplied by.
// Layer-shell returns 1.0 — GTK handles scaling there. Gamescope scales its
// Layer-shell and overlay return 1.0 — GTK handles scaling there. Gamescope scales its
// own CSS because GDK_SCALE would be applied twice, and anything drawn
// directly rather than styled has to apply the same factor by hand or it
// stays at its 1x size while everything around it grows. Valid after
Expand Down
40 changes: 32 additions & 8 deletions internal/gui/controls.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,36 @@ func addTouchActivate(widget gtk.Widgetter, onTap func()) {
gtk.BaseWidget(widget).AddController(gesture)
}

// scrollMinContentHeight is the floor every view's scroller reports. It matches
// .fan-curve-area's min-height so the custom view's chart is never the thing
// being crushed when the drawer is short.
const scrollMinContentHeight = 240

// newDrawerScroll returns the vertical scroller every view uses.
//
// SetMinContentHeight is the load-bearing call. A GtkScrolledWindow reports a
// minimum height of 0, and SetVExpand only distributes surplus height rather
// than requesting any, so with nothing imposing a height from outside the
// toplevel's natural height collapses to the title row plus the bottom bar —
// which is the ~320px box in issue #16. Layer-shell hid that for two years by
// anchoring top and bottom, and gamescope hid it by going fullscreen; neither
// is a height the widget tree ever asked for.
//
// Deliberately not SetPropagateNaturalHeight(true): that would make the custom
// view (eight fan-curve points, three TDP sliders, telemetry) request a natural
// height taller than the screen, trading no default size for a bad one.
//
// The three views shared five identical lines before this existed, which is the
// shape of bug where a fix lands on two call sites out of three.
func newDrawerScroll(child gtk.Widgetter) *gtk.ScrolledWindow {
scroll := gtk.NewScrolledWindow()
scroll.SetPolicy(gtk.PolicyNever, gtk.PolicyAutomatic)
scroll.SetVExpand(true)
scroll.SetMinContentHeight(scrollMinContentHeight)
scroll.SetChild(child)
return scroll
}

// buildContent builds the scrolled content box and returns it as the window child.
// Content, theme view, and color picker view are in a gtk.Stack so views can be
// swapped for gamepad navigation (and in gamescope where popovers don't work).
Expand Down Expand Up @@ -91,10 +121,7 @@ func (w *Window) buildContent() gtk.Widgetter {
// Set initial visibility based on default mode (static).
w.syncModeVis()

scroll := gtk.NewScrolledWindow()
scroll.SetPolicy(gtk.PolicyNever, gtk.PolicyAutomatic)
scroll.SetVExpand(true)
scroll.SetChild(inner)
scroll := newDrawerScroll(inner)
w.mainScroll = scroll

// Stack with main, theme, and color views — used in both modes.
Expand Down Expand Up @@ -211,10 +238,7 @@ func (w *Window) buildThemeView() *gtk.Box {
content.SetMarginEnd(12)
w.appendThemeChoices(content)

scroll := gtk.NewScrolledWindow()
scroll.SetPolicy(gtk.PolicyNever, gtk.PolicyAutomatic)
scroll.SetVExpand(true)
scroll.SetChild(content)
scroll := newDrawerScroll(content)
w.themeScroll = scroll
view.Append(scroll)
return view
Expand Down
Loading