🤝 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.
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:
- Independent capability negotiation per client (resolution, DPI, scaling, monitor layout).
- Client-side
window_offsetthat locally repositions windows so they fit the local monitor layout — but the server-side window geometry is shared. --desktop-scalingper client (scale the whole forwarded display up/down on that client).--resize-display=WxHto 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.
- Config:
xpra/scripts/config.py#L742 - CLI parser:
xpra/scripts/parsing.py#L1391 - Server applies it:
xpra/server/subsystem/sharing.py parse_hello()decides whether a newcomer can coexist:sharing.py#L80-L163
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.
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:
GTK3 client computes a local offset per window to keep it visible on its own monitors:
xpra/client/gtk3/window/base.py#L339-L343— applies the offset on configure.calculate_window_offset()xpra/client/gtk3/window/base.py#L463-L525 — picks the monitor by max pixel overlap and shifts the window to fit.
This is purely client-side cosmetic adjustment; the server has no idea.
Related historical issues with offsets / multi-monitor: #1339, #2600.
--desktop-scaling=AUTO|2|0.5|...— scales the whole forwarded display on the client. Different clients can use different values.- Client option:
xpra/client/subsystem/display.py#L53-L91
- Client option:
--dpi=NNN— per-client; sent in caps:display.py#L196-L226. Docs:docs/Features/DPI.md.--resize-display=WxHon server — pin virtual size, avoid shrink-to-smallest. Docs:docs/Usage/Desktop.md#L32-L40.
Server (sharing on, fixed large virtual display):
xpra start :100 \
--start=xterm \
--sharing=yes \
--resize-display=3840x2160 \
--bind-tcp=0.0.0.0:14500Each 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=96Caveats:
- All clients still see windows at the same coordinates in the virtual root. The GTK client's
window_offsetwill 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-commandsto 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.
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.
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.
- Per-window offset slot already exists on the window object:
xpra/client/gtk3/window/base.py#L1485(self.window_offset), currently used only for override-redirect windows incalculate_window_offset(). - Per-window metadata storage:
xpra/client/gui/window_base.py#L196—self._metadatadict. - Pointer inverse already wired:
xpra/client/gtk3/window/pointer.py#L253-L269—_offset_pointer()andadjusted_pointer_data()already subtractself.window_offsetfrom outgoing pointer coordinates. Extending the offset to all windows (not just OR) reuses this for free. - Geometry dispatch entry-point:
xpra/client/subsystem/window/manager.py#L455-L472—_process_window_move_resizelooks up the window by wid and callsmove_resize(). A single hook here can apply the transform. - New-window dispatch:
xpra/client/subsystem/window/manager.py#L186—[wid, x, y, w, h, metadata, client_properties], so geometry and metadata arrive together.
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).
- 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_CLOSEEXITrules inxpra/client/subsystem/window/close.py— precedent for class-keyed client-side behavior.
- i3/sway
for_windowdirectives 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().
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:
- Per-client uniform transform (offset+scale, then matrix).
- Region-scoped matrices / rect-to-rect mapping.
- Per-window-class (and per-window-type) overrides on top, with the same transform shape.