Skip to content

Instantly share code, notes, and snippets.

@gwpl
Last active May 11, 2026 23:35
Show Gist options
  • Select an option

  • Save gwpl/b07b93e6a07091d1a30d5fd0cc97d831 to your computer and use it in GitHub Desktop.

Select an option

Save gwpl/b07b93e6a07091d1a30d5fd0cc97d831 to your computer and use it in GitHub Desktop.
Xpra sharing=yes with multiple clients & different screen layouts — codebase analysis

Xpra sharing=yes with multiple clients and different screen layouts

🤝 AI + Greg — Greg provided the question, scenario, and direction; AI (Claude Code) did the codebase deep-dive and wrote these notes.

Analysis based on xpra @ commit 6071759.

TL;DR

There is one virtual display on the server. Every attached client sees the same windows at the same (x, y) positions in that virtual root. Xpra does not support telling client A "shift everything by +1920, +0" and client B "leave alone" from the server side. What it does support:

  1. Independent capability negotiation per client (resolution, DPI, scaling, monitor layout).
  2. Client-side window_offset that locally repositions windows so they fit the local monitor layout — but the server-side window geometry is shared.
  3. --desktop-scaling per client (scale the whole forwarded display up/down on that client).
  4. --resize-display=WxH to lock the virtual display to a chosen size instead of letting it shrink to the smallest client.

So: you can make it usable, but you cannot make windows land in genuinely different spots on different clients — clients fundamentally share one coordinate space.


How sharing actually works

Sharing option

Per-client connection state

The server keeps one ClientConnection (with sub-*Connection mixins) per attached client in _server_sources:

Each client has its own DisplayConnection storing that client's desktop_size, screen_sizes, monitors, DPI, vrefresh, ICC, etc. — they are not merged across clients.

When more than one display-capable client is connected, DPI / antialias are zeroed out rather than picked from one client: xpra/server/subsystem/display.py#L153-L171.

The "shared root" problem

Server-side desktop size negotiation picks essentially min(clients):

This is the known pain point — tracked as Xpra issue #4430: "virtual screen shrinks to accommodate the smallest client". When a small-screen client joins, large-screen clients get a coordinate space smaller than their monitor.

Per-window state is per-client (each client encodes/decodes independently), but window geometry in the virtual root is global:

Client-side window_offset (the closest thing to per-client offsets)

GTK3 client computes a local offset per window to keep it visible on its own monitors:

This is purely client-side cosmetic adjustment; the server has no idea.

Related historical issues with offsets / multi-monitor: #1339, #2600.

Per-client knobs that do work


Practical recipe for your scenario

Server (sharing on, fixed large virtual display):

xpra start :100 \
  --start=xterm \
  --sharing=yes \
  --resize-display=3840x2160 \
  --bind-tcp=0.0.0.0:14500

Each client (different scaling/DPI to suit local screens):

# 4K laptop
xpra attach tcp://host:14500/100 --desktop-scaling=auto --dpi=192

# Dual-1080p workstation — let client-side window_offset move things on-screen
xpra attach tcp://host:14500/100 --desktop-scaling=1 --dpi=96

Caveats:

  • All clients still see windows at the same coordinates in the virtual root. The GTK client's window_offset will only nudge windows so they're not off-screen on that client — not move them to a different monitor of choice.
  • If you want each user to arrange windows independently, that's not a sharing-mode use case. Run separate xpra sessions per user (or use start-new-commands to launch a per-user app inside the same session and let each user move "their" windows).
  • Modal/global dialogs become global across all clients in sharing mode.

What would actually be needed for true per-client offsets

A server-side feature where each DisplayConnection carries a (dx, dy) translation applied when sending window-create/configure packets to that client, and the inverse applied to pointer events from that client. This does not exist today. Filing it as a feature request against #4430 (which already covers the related "shrink to smallest" problem) is probably the right starting point.


Could it be done purely client-side, even per-window or per-WM_CLASS?

Short answer: yes, and the infrastructure is already partly in place. Putting the transform on the xpra client (server stays unaware) appears to be a low-risk path. Adding per-window and per-window-class rules on top is a natural extension.

Existing client-side plumbing (commit 6071759)

Window metadata available for matching

Defined in xpra/constants.py#L132-L145. Useful for rule-matching:

Field Stability Use
class-instance (WM_CLASS) stable "if app == firefox, transform = X"
window-type (normal/dialog/toolbar/…) stable place dialogs differently
role usually stable finer grain inside one app
pid / command stable per-process rules
title dynamic matches but may change at runtime
transient-for, group-leader structural inherit rule from parent window

Class-instance is set before window realization (window_base.py#L370-L373), so a rule lookup at create-time has all the stable fields in hand. Dynamic fields (title, role) would need re-evaluation if matched on — they arrive via _process_window_metadata (manager.py#L447).

Existing WM_CLASS-aware machinery in xpra

  • Server-side window filters can already match on WM_CLASS (trac #1934, #3454) — same matching primitive a client-side rule engine would want.
  • Client-side XPRA_WINDOW_GROUPING="group-leader-xid,class-instance,pid,command" already keys behavior on these fields (xpra/client/gtk3/client_base.py).
  • Per-window scaling exists historically (commit b1ac5619d2).
  • Hard-coded WM_CLASS_CLOSEEXIT rules in xpra/client/subsystem/window/close.py — precedent for class-keyed client-side behavior.

Prior art (validates the design)

  • i3/sway for_window directives keyed on WM_CLASS / app_id.
  • KWin Window Rules — per-WM_CLASS placement, size, state.
  • devilspie2 — Lua callbacks fired on window open, with get_class_instance_name() / get_class_group_name().

Conclusion

Per-window / per-WM_CLASS translation overrides are not overreach as a follow-on layer to a simple affine-per-client transform: the per-window offset slot exists, the pointer-inverse path is already correct for any non-zero window_offset, the metadata for matching is already received at create-time, and prior art shows the pattern is well-understood and useful. The natural staging is:

  1. Per-client uniform transform (offset+scale, then matrix).
  2. Region-scoped matrices / rect-to-rect mapping.
  3. Per-window-class (and per-window-type) overrides on top, with the same transform shape.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment