Skip to content

Linux: pin the island to its display on layer-shell (Hyprland multi-monitor), display picker, click guard - #227

Merged
Louis-CFM merged 4 commits into
Louis-CFM:mainfrom
chuxclay:linux-hyprland-display-pin
Oct 7, 2026
Merged

Louis-CFM merged 4 commits into
Louis-CFM:mainfrom
chuxclay:linux-hyprland-display-pin

Conversation

@chuxclay

@chuxclay chuxclay commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Summary

On a two-monitor Hyprland desktop the Linux island opens on whichever monitor has keyboard focus when it starts, and the "Island lives on" setting does nothing. This PR pins the layer-shell surface to a display and lets the user pick which one. Two smaller fixes come with it.

One commit per topic, so you can review them separately, or ask me to split them into separate PRs (CONTRIBUTING asks for one topic per PR):

  1. Linux: pin the layer-shell island to its display. This is the bug fix.
  2. Settings: choose which display the island lives on.
  3. Island: ignore clicks on a fresh permission card for 600 ms.
  4. Settings: name the Secret Service as the key store on Linux. The text said "Windows Credential Manager" on Linux.

Environment

Distro Omarchy (Arch-based), kernel 7.2.5
Compositor Hyprland 0.56.2 (Lua config), uwsm session
Monitors 2 × Acer SA322QU A, 2560×1440, scale 1: DP-2 at 0,0 and DP-3 at 2592,0
GPU Intel HD 530 + NVIDIA Quadro M2200 Mobile
System libs gtk3 3.24.52, webkit2gtk-4.1 2.52.6, gtk-layer-shell 0.10.1, libayatana-appindicator 0.6.0
Toolchain rustc 1.98.1, Node 26.8.1
Coucou linux-v0.1.1 AppImage, then built from source (npm run tauri build -- --no-bundle) on main

What I observed

1. The island follows keyboard focus instead of the setting

screen is "primary" (the default). Each run below starts the app with keyboard focus on one monitor, then moves focus to the other monitor, then reads hyprctl layers to see where the coucou surface is:

AppImage 0.1.1         started with focus on DP-2 -> island on DP-2
AppImage 0.1.1         started with focus on DP-3 -> island on DP-3
this PR (at:2592,0)    started with focus on DP-2 -> island on DP-3
this PR (at:2592,0)    started with focus on DP-3 -> island on DP-3

Cause. apply_geometry calls set_position, but a layer surface ignores it. make_non_activating never calls gtk_layer_set_monitor, so the compositor picks the output itself, and Hyprland uses the focused one.

There is a second problem on Wayland. "Main display" maps to app.primary_monitor(), and GDK on Wayland has no primary monitor (is_primary() is false for both outputs here). So it falls back to whatever GDK lists first. On this machine that is DP-3, the right-hand screen, which the user never chose. "Display under the cursor" can't work on Wayland either: cursor_physical() returns None there, so it falls back the same way.

2. Monitor names can't tell displays apart

tao takes the monitor name from gdk_monitor_get_model(), and both monitors here report SA322QU A. GDK3 doesn't expose the connector name (DP-2/DP-3). So the setting stores the display's logical origin instead (at:<x>,<y>), the same coordinates GDK uses for monitor geometry. If that display is unplugged or moved, the island falls back to the primary one.

3. The permission card opens under a busy cursor

The card expands right under where the pointer already is. I was clicking around in a terminal under the top of the screen while testing, and the card appeared with the cursor sitting on Allow (screenshot below). The click that time was deliberate, but a click meant for the window underneath would approve the request. The card now ignores clicks for its first 600 ms, the same idea as a confirmation-delay guard.

I measured it by sending a PermissionRequest with the cursor parked on Deny and clicking every 150 ms: the first 5 clicks reached the card (mousedown logged) and were ignored, and the 6th registered decide deny.

4. The AppImage on this system (for #95)

This matches #95. The AppImage printed gtk-layer-shell v0.7.0 may not work on GTK v3.24.33. Hook events reached Rust (hook PreToolUse and so on in coucou.log), but the page never reacted. Every PermissionRequest ended with island never acknowledged — terminal takes over. In the first run the main process went away after about 6 minutes, and the orphaned WebKitWebProcess died with SIGTRAP (WebProcess didn't exit as expected after the UI process connection was closed). Built from source against the system libraries, none of that happened in my testing: no warning, the page boots, and the card shows and answers.

Changes

  • platform/linux.rs: bind gtk_layer_set_monitor. New pin_to_monitor(win, x, y) finds the GdkMonitor whose geometry starts at that logical origin and pins the surface to it. It does nothing for a regular window (GNOME, COUCOU_LAYER_SHELL=0).
  • platform/windows.rs: pin_to_monitor does nothing, because set_position already places the window there.
  • island.rs: apply_geometry pins after placing. target_monitor accepts at:<x>,<y>. New monitor_choices() for Settings.
  • lib.rs: new list_monitors command.
  • settings/main.ts: "Island lives on" now lists each display as <model> — <w>×<h> at <x>,<y> after "Main display" and "Display under the cursor". The existing options and styling are unchanged.
  • core/state.ts, core/bridge.ts: screen widens to string. Existing "primary"/"cursor" values keep working.
  • views/views.ts: CLICK_GUARD_MS = 600 on the approval card's two buttons, reset per requestId.

How I tested

On the machine above:

  • Build. cargo test --release passes (12/12) and tsc --noEmit is clean. Each commit builds.
  • Hook flow. I fed coucou-hook synthetic SessionStart / UserPromptSubmit / PreToolUse / PostToolUse / PermissionRequest payloads on stdin, and also ran a real Claude Code session with the hooks installed. I checked coucou.log and the relay's stdout: {"behavior":"allow"} and {"behavior":"deny","message":"Denied from Coucou"} both came back.
  • Placement. I tried at:0,0 and at:2592,0, moving focus between outputs (hl.dsp.focus({monitor=...})) and starting the app with focus on each. The surface stayed on the chosen output every time (table above).
  • Pointer. I drove it with hl.dsp.cursor.move and ydotool: hover from hidden goes to compact, a click expands it, and the gear and "Settings…" open the Settings window.
  • Screenshots. Taken with grim.

Screenshots

The permission card on Hyprland, opened under the pointer:

permission card

The display picker in Settings:

display picker

Related

Noticed but not changed

  • Y/N on the approval card. The keyboard badges are shown, but nothing on Windows/Linux handles those keys, and the layer surface is KEYBOARD_NONE.
  • "Display under the cursor" can't work on Wayland, as above. It could be hidden on Linux.
  • Island over Settings. The island (overlay layer) covers the top of the Settings window when that window is tiled under it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BNUKEFycKsFCP5fKkhY1Uf

chuxclay and others added 4 commits October 4, 2026 23:06
A layer surface ignores set_position, so the island landed on whichever
output had focus when it was mapped. Pin it with gtk_layer_set_monitor to the
display target_monitor picked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNUKEFycKsFCP5fKkhY1Uf
"Main display" means little on Wayland: GDK has no primary monitor, so it is
whichever output GDK lists first. List every display in "Island lives on" and
store the choice as at:<x>,<y> (logical origin). Names cannot be the key: two
monitors of the same model report the same one. A display that is gone falls
back to the primary one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNUKEFycKsFCP5fKkhY1Uf
The card opens under a cursor that was busy with the window underneath, so a
click meant for that window could land on Allow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNUKEFycKsFCP5fKkhY1Uf
@Louis-CFM
Louis-CFM merged commit 8761b36 into Louis-CFM:main Oct 7, 2026
JhoanG956 pushed a commit to JhoanG956/coucou that referenced this pull request Oct 7, 2026
…M#299

- Linux pointer watch (Louis-CFM#160) now also runs for a regular window (GNOME, X11),
  not only on layer-shell, so auto-close works there too (Louis-CFM#136).
- GNOME: force GDK_BACKEND=x11 (Louis-CFM#149) only when XWayland is there (DISPLAY set).
- GNOME large text (Louis-CFM#122): undo text-scaling-factor for the island through
  GDK_DPI_SCALE, unless the user set it.
- Display choice (Louis-CFM#227): remember a display by place, name and size, so it is
  found again after the layout is rearranged or the resolution changes; keys
  saved before still match.
- PKGBUILD (Louis-CFM#299): license matches LICENSE-ASSETS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JhoanG956 pushed a commit to JhoanG956/coucou that referenced this pull request Oct 7, 2026
…desktop

Includes port/mochi-wardrobe. Conflicts: settings fields, commands, tray
entries (Open, Weekly recap, Wardrobe…, Settings…, Pause, Quit), bridge
calls, views and styles kept from every branch; save_settings still keeps
Mochi's desktop spot whatever a page sends, next to the shortcuts check; the
duplicate gtk_layer_set_monitor binding (Louis-CFM#227 and this branch) is declared
once.

Wired up after the merge:
- alertActive() says what it now covers: the one card for every agent's
  permission and for Claude Code's questions (Louis-CFM#216), folded or not; a
  question card flies Mochi home like a permission does (tested).
- The wardrobe shortcut's open-wardrobe event opens the wardrobe from any
  state, or closes it, through the same path as the desktop Mochi's
  right-click.
- Only the main tool's pill dresses Mochi (it was hard-wired to VS Code's).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants