Skip to content

Give the window a screen of its own on the remote by default - #81

Open
Unayung wants to merge 20 commits into
omacom:mainfrom
Unayung:default-own-screen
Open

Unayung wants to merge 20 commits into
omacom:mainfrom
Unayung:default-own-screen

Conversation

@Unayung

@Unayung Unayung commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #57 and #58. This branch contains their commits, so review or merge those first. The change in this PR is the last commit, ad10410.

What

gliff user@host now creates a headless output on the remote, sized and scaled to the window, and moves the remote's workspaces onto it for the session. This is what --headless --move-workspaces did before. The screen follows the window as you resize it. On disconnect it is removed, and the workspaces go back to their own screens.

The other modes stay available:

  • --mirror: mirror the focused screen, the old default.
  • --output NAME: mirror a named screen, unchanged.
  • --keep-workspaces: a screen of its own that starts empty.
  • --headless and --move-workspaces are still accepted and hidden, since they are now the default.

Why

When I connect to my desktop from a laptop, I want my windows at the laptop window's size and scale. A 3440x1440 screen shrunk into a laptop window doesn't do that. The window-sized screen, holding the workspaces, also ends up the more useful mode in day-to-day use.

Compatibility

The client asks for the workspace move in the handshake, not on the server's command line. It does this with a move-workspaces entry in its ClientCaps.features (FEATURE_MOVE_WORKSPACES). A server that predates --move-workspaces ignores the entry and still connects, with an empty screen. If the client passed the flag on the command line, that server would refuse the unknown flag and the connection would fail. That is the failure I hit before making this change.

Testing

  • scripts/check.sh passes. A new test covers which server arguments and requests each set of flags produces.
  • In my fork, a Mac client connected to an RTX 3080 Hyprland desktop with the new default:
    • The desktop added a 3024x1672 output (the window at 2x) and moved workspace 2 onto it.
    • On disconnect, the output was removed and the workspace returned to DP-2.
  • The same client against an unmodified 0.4.0 server connected with an empty headless screen instead of failing.

🤖 Generated with Claude Code

A headless session starts on an empty workspace of its own, so the
windows open on the remote stay on its physical screens, out of view;
and on a machine whose monitor is asleep, mirroring it shows nothing.

`--move-workspaces` (server, and the client, which passes it on) moves
every regular workspace onto the headless output when the session starts
and shows there the one the main monitor was showing. At the end each
goes back to the monitor it came from and each monitor shows what it
did, before the output is removed; left to itself, Hyprland would pile
them all onto one monitor.

hypr-ipc gains workspace listing, each monitor's active workspace, and
the two dispatchers this needs, with the Lua fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bmaltais

Copy link
Copy Markdown
Contributor

Test report: PR #81 (1a3d827) on an RTX 3090 server and a Surface client

Setup

  • Server: desktop with an NVIDIA RTX 3090, Hyprland on one monitor (DP-1, 1920x1080, scale 1), Linux 7.2.8. It ran the PR's gliff-server, built with bin/build from the PR head.
  • Client: Microsoft Surface running Hyprland, eDP-1 at 2736x1824, scale 2, Linux 6.19.8. It ran the PR's gliff binary, copied over. gliff 0.4.0 from the package is installed on both machines.
  • Link: SSH over Tailscale, started with gliff --server-bin <path to PR gliff-server> user@3090, so the server end ran the PR build.
  • Server's open windows: three floating windows (two Ghostty at 1100x734, one Edge at 1100x716) on workspace 1.

Results

Check Result
Default gliff user@host creates a window-sized output Pass. A headless output appeared at 1752x1146 for a 876x620 window (scale 2), and at 2736x1824 with the window fullscreen.
Workspaces move onto the output Pass. Workspace 1 and its windows moved. Workspace 2, auto-created on DP-1, stayed there.
Disconnect removes the output and restores workspaces Pass, on four connect/disconnect cycles. The output was removed, workspace 1 returned to DP-1, and the windows were back at their original positions. No gliff-server was left running.
Works on an NVIDIA server Pass. No errors, and the stream ran. The PR's own test used a 3080.
Window resize The output size followed the window (876x620 window to fullscreen: 1752x1146 to 2736x1824). I did not test dragging a window to resize it.
Old 0.4.0 server falls back to an empty screen Not tested
--mirror, --keep-workspaces, DPMS keep-awake Not tested

Observation: floating windows can be cropped

I saw a cropped view on the Surface: part of the remote's right and bottom edges was missing. The client wasn't cropping the frame. In fullscreen the output (1368x912 logical) was the same size as the window, and the whole frame was shown. The cause is the remote's floating windows. They keep their size when their workspace moves, so some are larger than the new output:

Window Right / bottom edge Fits in 1368x912?
Ghostty #1 1132 / 797 Yes
Ghostty #2 1292 / 873 Yes, barely
Edge 1534 / 935 No (about 166px off the right, about 23px off the bottom)

With a smaller Surface window (876x573 logical output), all three overflowed.

Comments

  • The PR does what it describes. The crop is an edge case it doesn't claim to handle, since the test window was as big as the desktop. Tiled windows are re-laid-out by Hyprland to fit, but floating ones aren't.
  • With floating windows and a small client window, parts of some windows can be out of reach. Options: shrink or reposition floating windows after the move and restore them on disconnect, document the limit and point to --mirror, or make the output at least as large as the biggest remote window. I'd suggest at least documenting it.
  • Not run: the old-server compatibility case, which the PR's compatibility claim relies on, and --mirror/--keep-workspaces.

@bmaltais

Copy link
Copy Markdown
Contributor

Follow-up: tiled windows and the remaining modes

Same setup as my earlier report (RTX 3090 Hyprland server, Surface client, PR head 1a3d827).

Tiled windows: no crop. With the 3090's three windows tiled (not floating), Hyprland re-laid-out the tiles to fit the window-sized output, both in the small Surface window and fullscreen. Nothing ran off the edge, and the original layout came back on disconnect. That supports the earlier finding that the crop only affects floating windows.

Surface window Remote output (logical) Tiles
876x620 876x573 419x255, 419x254, 419x523
1368x912 (fullscreen) 1368x912 665x424, 665x424, 665x862

The other modes

Case Result
Default client against an unmodified 0.4.0 gliff-server Pass. It connected with an empty headless output (1752x1146, scale 2). The windows stayed on DP-1, and the output was removed on disconnect.
--mirror Pass. The server ran --output auto, no new output was created, and nothing moved.
--keep-workspaces Pass. The server ran --headless. A new empty output appeared and the workspace with its 3 windows stayed on DP-1. The output was removed on disconnect.

Not tested: the DPMS keep-awake behaviour, and dragging a window to resize it live.

@bmaltais

Copy link
Copy Markdown
Contributor

Follow-up: live resize and DPMS keep-awake

Same setup as before (RTX 3090 Hyprland server, Surface client, PR head 1a3d827). Both checks I had left are done, and both pass.

Live resize. I resized the Surface window through Hyprland's window-resize dispatcher, not with the mouse (no pointer-control tool on that machine). It sends the client the same size changes as a drag. Each size produced exactly the expected output on the 3090 (2 × window width, 2 × (window height − 47) in pixels). The output followed within 0.2–0.4s of the command, including the SSH round trip.

Window Remote output
700x500 1400x906
1100x800 2200x1506
600x450 1200x806
1000x700 2000x1306
876x620 1752x1146

DPMS keep-awake. I turned every monitor on the 3090 off with hl.dsp.dpms({action="off"}) (what an idle daemon does before locking), with the stream running. I ran the same steps against an unmodified 0.4.0 server as a control.

PR server 0.4.0 server
Headless output after DPMS off Back on within about 1s; DP-1 stayed off Stayed off
Surface view Live at 6s and 12s Flat black (pixel std-dev 0.0002, against 0.254 before)
After DPMS on n/a Back to normal

So the keep-awake check fixes the freeze on black, and only the session's own output is switched on, since DP-1 stayed off. "Live" here means the view was not black and the output was on. I did not check that the content kept changing.

With this, everything in the PR's test plan has been run on this setup. The only open point is the floating-window crop from my first comment.

@bmaltais

Copy link
Copy Markdown
Contributor

This is a really cool addition!

Connecting now creates a headless output sized and scaled to the window
and moves the remote's workspaces onto it for the session, as --headless
--move-workspaces did; --mirror mirrors the focused screen as before,
--output a named one, and --keep-workspaces leaves the new screen empty.

The client asks for the move in the handshake, as a "move-workspaces"
entry in its features, rather than as a server flag, so a server without
--move-workspaces still connects (with an empty screen) instead of
refusing the unknown flag.

Hyprland lays tiled windows out again on the new screen but keeps a
floating window's size and position, so one larger than the client
window runs off its edge; the README and --mirror's help say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JjGxUHRAF1wya1dVp5ScR4
Claude-Session: https://claude.ai/code/session_012Pxk3MdNTDNQ3CPySqTmfT
@Unayung
Unayung force-pushed the default-own-screen branch from 1a3d827 to ad10410 Compare October 21, 2026 13:56
@Unayung

Unayung commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for all three reports, and for running the 0.4.0 fallback especially, since the compatibility claim depends on it and I'd only tried it from a Mac. The DPMS run also covers #57, which this branch carries: the control against 0.4.0 is the before-and-after I didn't have.

The floating-window crop is real, and as you say, it comes from Hyprland keeping floating windows at their size and position while it re-lays-out the tiles. I've pushed a note for it. The README paragraph on the default now says a floating window larger than your window runs off its edge, and to tile it or use --mirror. The --mirror help says the same.

I'd rather not shrink and move floating windows in this PR. Restoring them on disconnect means tracking each one's geometry, and if the server dies before it cleans up, the windows stay shrunk. That's worse than a crop that goes away when you disconnect. It's also a lot to add to a PR that sits on top of #57 and #58. If it's wanted, I'll do it as a follow-up on the same restore path the workspaces use.

@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no blocking issue remains from this review.

Summary

The PR makes a window-sized remote screen the default and asks the server to move workspaces through the handshake.

  • Gliff opens a window-sized remote screen with workspaces by default.
  • The server returns borrowed workspaces to their monitors when a session ends.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Client connects] --> B{Mirror requested?}
    B -->|Yes| C[Mirror existing screen]
    B -->|No| D[Create window-sized screen]
    D --> E{Move requested and lease available?}
    E -->|Yes| F[Move regular workspaces]
    E -->|No| G[Start with empty screen]
    F --> H[Session ends]
    G --> H
    H --> I[Restore borrowed workspaces]
    I --> J[Remove temporary screen]
Loading

Reviews (5) · Last reviewed commit: "Merge commit '79395738cdbe54bc99b927e392..." · Reviewed by Greptile

Comment thread crates/gliff-server/src/session.rs Resolved
Unayung and others added 5 commits October 23, 2026 03:09
Hyprland gives named workspaces negative IDs, like the scratchpads, so
skipping every ID at or below zero left named workspaces and their
windows on the physical screens. Scratchpads are now told apart by their
"special" name, and named workspaces are moved and shown by
"name:<name>".

A workspace's home was recorded only after the move answered ok. A move
whose answer timed out could still happen, and then nothing put that
workspace back. The home is now recorded before the move; restoration
already moves only the workspaces still on the session's output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JauiFwn6qx2NBFZJ7FvEva
Restoration retried its snapshot but sent each move once. If IPC stalled
then, the move failed silently and the output was removed anyway,
leaving that workspace wherever Hyprland put it. Each move is now
retried like the snapshot, and one that still fails is logged with the
monitor it should have gone back to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JauiFwn6qx2NBFZJ7FvEva
Comment thread crates/hypr-ipc/src/lib.rs Resolved
Unayung and others added 4 commits October 23, 2026 04:36
A named workspace reaches the Lua dispatchers as "name:<name>", and the
name went into the Lua string unescaped. A quote or backslash in it broke
the call or changed the name Hyprland read, so that workspace was neither
moved nor returned. Both calls now quote the selector as a Lua string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JauiFwn6qx2NBFZJ7FvEva
Main's keep-awake cancellation gave the dispatcher a third argument, the
check that lets teardown skip a request. Workspace moves and focus are
never cancelled, so they pass `|| true`, as set_dpms does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWLtaPFL1Y2jfFD7qm2QjZ
Brings in current main through omacom#58. Main's session test still passed a
bare VideoMode; it now passes Wants, without moving workspaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWLtaPFL1Y2jfFD7qm2QjZ
Comment thread crates/hypr-ipc/src/lib.rs Resolved
Showing the restored workspace now retries like the moves before it,
and warns if it still fails. Special workspaces are recognized by the
"special:" prefix Hyprland gives them, so a regular named workspace
such as "special-mail" is moved like any other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWLtaPFL1Y2jfFD7qm2QjZ
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.

3 participants