Skip to content

feat: optionally share workspaces across displays - #603

Draft
acsandmann wants to merge 20 commits into
mainfrom
feat/multidisplayv2
Draft

acsandmann wants to merge 20 commits into
mainfrom
feat/multidisplayv2

Conversation

@acsandmann

Copy link
Copy Markdown
Owner

No description provided.

- One owner for which workspace a window is in
- Moving windows between desktops
- Which workspace each desktop shows
- navigation
- Move workspace to display
- Config reload is validated before anything changes
- One layout per workspace…
- Displays disconnecting and reconnecting
@sj-cstar

sj-cstar commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Automated testing and notes, btw (on other note, I love the new stackline ❤️ ):

Ran #603 (e2a0c7f) today with display_mode = "shared", the built-in panel below a DELL U3225QE, and tested with rift-cli:

     +--------------------------+
     | DELL      (77,-1049)     |  workspaces 6-9
     |           1920x1049      |
     +--------------------------+
  +--------------------------------+
  | Built-in  (0,40) 2056x1289     |  workspaces 1-5
  +--------------------------------+
  1. Hidden windows on the upper display land on the lower one. Rift parks them at (1996, 39), just past the DELL's bottom-right corner and over the built-in's right edge. macOS moves the window to the built-in's Space 20–170 ms later, and rift adopts it into the built-in's visible workspace. Moving a window to a hidden DELL workspace, or switching the DELL away from a workspace with windows, does it every time. sj-cstar/rift@166695c parks on the side edges instead and picks the placement that overlaps other displays least; it applies cleanly on feat/multidisplayv2.
  2. A native move of a window rift parked counts as a user move. Even with better parking, a window rift hid itself should keep its workspace. WindowStore membership is meant to be authoritative.
  3. Back-and-forth after a cross-display switch does nothing. With the DELL on 8 and the built-in on 4, switching to 2 from the DELL makes set_active_workspace_from set the built-in's previous to 8, dropping 4. workspace last then targets 8, already shown on the DELL, and nothing happens. With workspace_auto_back_and_forth, pressing 2 again does the same.
  4. Switching to an empty workspace on another display keeps focus where it was. From the built-in, switching to 9 (empty, on the DELL) shows it there, but command focus stays on the built-in, so the next command acts there. Works when 9 has a window.
  5. A command right after a cross-display move-node acts on the old display. move-node up then move-node down 17 ms apart leaves the window on the DELL: macOS moves its active display about 100 ms after the first move. sj-cstar/rift@07d4153 makes the window's new display the command context straight away.
  6. move-workspace on a rule-bound workspace reverts later. Moving workspace 4 (bound to the built-in) to the DELL works, then reconcile_workspace_placements puts it back on the next display change: sleep/wake, plugging a display, a desktop switch. Either refuse the move, as feat: bind workspaces to displays #545 did, or let it update the pin.
  7. workspace move-window <ws> <window-id> only finds windows on the focused display. find_window_by_idx filters on the command space, so a window on the other display is a no-op that still prints "Command executed successfully". sj-cstar/rift@ce6f6b9 resolves the window on any display.
  8. rift-cli query workspaces is ambiguous in shared mode. It returns the whole catalog with one is_active per display and no display field, so select(.is_active == true) yields 1 and 8. The rift plugin in sketchybar-config does exactly that and highlights nothing. A display_uuid/space_id field and an is_focused flag would cover it.

Not carried over from #545:

  1. settings.new_window_display (sj-cstar/rift@f26d998). My terminal hotkey activates Ghostty and sends ⌘N. With focus and the mouse on the DELL, the window opens on the built-in next to Ghostty's other windows, and the built-in switches workspace to show it.
  2. Retrying apps that refused accessibility (sj-cstar/rift@732c76e). Registration still has no grace period outside JetBrains and Emacs, and activating an app without an app actor doesn't retry. fix: recover untracked windows on focus and activation #590 only recovers windows of apps that already have one.

Config:

  1. A rule's display is ignored in independent mode with no validation warning. Identical monitors share a name, so a name-based rule picks the first.

Working well: move-window --follow and display move-window across displays with no snap-back, next/prev staying on each display's workspaces, focus across displays. Not tested yet: unplug/replug, sleep/wake, swipes, overview drops.

Happy to open PRs against feat/multidisplayv2 for 1, 5, 7, 9 and 10.

@sj-cstar

Copy link
Copy Markdown
Contributor

Corrections to my list above:

  • 3 and 4 were wrong. Back-and-forth is meant to return to the previous selection on the other display (shared_workspace_back_and_forth_follows_cross_display_selection), and switching to an empty workspace on another display does move command focus and the pointer there. I read keyboard focus staying on the old display's window as rift not moving; with no window in the target workspace, that is expected.
  • 2 conflicts with topology_snapshot_preserves_workspace_placement_with_incomplete_delta. Its scenario 3 expects a window from a hidden workspace, reported on another connected display with no display change, to follow macOS. That is the same signal as the parking bounce, so I left it alone. With 1 fixed it no longer happens on two displays.

Fixes for the rest are on sj-cstar/rift:feat/multidisplayv2-sjpatches, one commit each on top of e2a0c7f:

  • 1: park hidden windows where they overlap no other display
  • 5: act on the display move-node just carried the focused window to
  • 6: keep a rule-bound workspace on its display in move_workspace_to_display
  • 7: find a window named by id on any display
  • 8: display_uuid and is_focused in workspace queries
  • 9: in shared mode, open new windows on the pointer's display, with no setting
  • 10: retry attaching to apps that refused accessibility
  • 11: warn about display rules outside shared mode and about display names matching several displays

Happy to open them as PRs against feat/multidisplayv2, together or one by one.

sj-cstar and others added 3 commits October 22, 2026 16:16
* fix: park hidden windows where they overlap no other display

Rift hides the windows of inactive workspaces by parking them past the
bottom-left or bottom-right corner of their display, leaving a sliver on
screen. With a display arranged above another, a window parked past the
upper display's bottom edge sits on the lower display. Rift then adopts it
there, and it turns up in the wrong display's workspace.

- add right-edge and left-edge placements, top-aligned with the display
  and clamped to its height
- pick the placement that overlaps other displays least
- treat a sliver in either dimension as hidden

🤖 Generated with Claude Code

* fix: act on the display move-node just carried the focused window to

After move-node carried the focused window onto another display, the
next command still acted on the display the window had left: macOS
moves its active display along with the key window only later. Make the
window's new display the command context, as focus_display would, so a
second move-node or a focus command acts where the window now is.

🤖 Generated with Claude Code

* fix: retry attaching to apps that refused accessibility

An app actor gives up when registering for an app's accessibility
notifications fails, and nothing tries again until the app relaunches or
opens a new window, so its windows stay unmanaged. Registration had no
grace period outside JetBrains IDEs and Emacs.

- give every app a two second grace period for the first registration
- retry attaching whenever the user activates an app that has no app
  actor

🤖 Generated with Claude Code

* fix: find a window named by id on any display

move_window_to_workspace with a window id only searched the workspaces of
the display holding the command context. A window on another display was
not found, so the command did nothing while rift-cli still reported
success. Fall back to the window's assignment in any workspace; the rest
of the command already works out the window's own display.

🤖 Generated with Claude Code

* feat: open new windows on the pointer's display in shared mode

A new window opens wherever macOS and the app put it. Launchers and
hotkeys often activate an app before asking it for a window, so the new
window lands next to the app's other windows, on whichever display those
are on, and joins that display's visible workspace instead of the one the
user works in. Keyboard focus has moved with the activation by then; the
pointer has not, and with mouse_follows_focus it also follows keyboard
moves.

In shared mode, move a new standard window to the display under the
pointer when macOS put it on another one, through the same transfer as
move_window_to_display. A window whose app rule names a workspace stays
where the rule puts it. Independent mode is unchanged.

🤖 Generated with Claude Code

* fix: keep a rule-bound workspace on its display in move_workspace_to_display

In shared mode, move_workspace_to_display moved a workspace whose rule
binds it to a connected display, and the move held until the next display
change. reconcile_workspace_placements then put it back on the rule's
display: after sleep and wake, plugging a display or a desktop switch,
the workspace jumped back with its windows.

Refuse the move while the rule's display is connected, and log why. An
unbound workspace still moves, and so does a bound one while its display
is away.

🤖 Generated with Claude Code

* feat: name each workspace's display and the focused one in workspace queries

In shared mode an unscoped workspace query returns the whole catalog, with
one is_active workspace per display and nothing saying which display each
belongs to. Clients that pick the active workspace, such as status bars
selecting .is_active, got one name per display and could not tell which
one has focus.

Add display_uuid, the display a workspace belongs to, and is_focused,
true for the workspace active on the display holding command focus. Both
default when absent, so older clients and servers still decode.

🤖 Generated with Claude Code

* fix: warn about workspace rule displays rift ignores or cannot pin down

A workspace rule's display only applies with display_mode = "shared";
in independent mode rift dropped it without a word. And a rule naming a
display by name matched the first of several identical monitors.

Log a warning for each display rule outside shared mode when the settings
load, and when a rule's display name matches more than one connected
display. Neither is a validation error: switching display_mode in the
settings UI must not be refused because rules still name displays.

🤖 Generated with Claude Code

* refactor: slim down multidisplay patches

Keep every fix but trim the diff: side-edge parking reuses the corner
geometry, the ignored display rule warning lives in one place, the
pointer-display and move-node helpers are condensed, and the tests are
merged into a smaller set that each fail without their fix.

---------

Co-authored-by: acsandmann <[email protected]>
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