Skip to content

Desktop: cycle through the Mac's displays with the Desktop button (#91) - #173

Open
addhumal wants to merge 2 commits into
Saffsanity:mainfrom
addhumal:desktop-displays
Open

addhumal wants to merge 2 commits into
Saffsanity:mainfrom
addhumal:desktop-displays

Conversation

@addhumal

@addhumal addhumal commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Closes #91.

Reworked from main (e4d3034), and merged with main at c4a331f (0.6.0, the locked Mac). This replaces the earlier version of this branch.

What it does

On a Mac with two displays or more, the Desktop button reads "Desktop 1", "Desktop 2"…, and tapping it while the Desktop streams moves it to the next display. The order is left to right, then top to bottom, as System Settings › Displays arranges them, wrapping round. Sill's virtual display is never one of them. With one display, or against an older Mac, the button looks and behaves as before. This is the tapping method Noah decided on in #91.

  • Wire (additive): kind 47 nextDisplay (empty payload), sent only to a host whose window list says desktopDisplays 2 or more. WindowList.desktopDisplays and desktopDisplay (1-based). Kinds 45 and 46 are held by open pull requests (the microphone, the pen, the camera). Older hosts skip kind 47; older devices skip the fields.
  • Host (WindowCatalog, StreamCoordinator):
    • The host works out the next display from its own newest look and keeps the choice by the display's UUID, so a new display ID after a sleep is still the same display. The device never names a display, so a stale ID can't reach the host.
    • Input and the pointer land on the display the capture runs on, not the one just chosen, so a tap made during the switch can't land on a screen the device isn't showing.
    • A Desktop whose display moves under it (the chosen display plugged in again, a new ID) restarts there. With nothing chosen, the Desktop stays on the display it is on.
    • If the new display doesn't stream (its start and the one more try fail), the Desktop goes back to the display it was on.
    • The Mac's pointer moves to the new display's centre.
    • One log line: "Desktop: display 2 of 3 (DELL U2723QE), asked by ‹device›."
    • A locked Mac (main's LockPolicy) lists no displays and refuses kind 47 as it refuses a window pick. The host checks the lock again before it steps and before it moves the pointer. lock-policy: 540 checks, 141 of 141 mutants.
  • Device (StreamClient.tapDesktop(), StreamScreen, PortraitStreamScreen): only the Desktop buttons send kind 47. The device's own Desktop requests and gestures still pick the Desktop, so nothing steps by itself. VoiceOver hears "Display 2 of 3" and the hint "Shows the next display."
  • DEBUG: -SillDesktopDisplays 3,2 puts the harness's mock on display 2 of 3.

Checks

  • Tests/checks/desktop-displays (new, in CI's mutants matrix): 56 checks and 31 of 31 mutants. It covers the order, the UUID choice, the next display, where input lands, falling back after a failed step, the button's words and tap, and the pointer, with 5,000 random arrangements.
  • compatibility: 159 checks and 36 of 36 mutants (the new fields and kind 47). display-wake: 199 checks and 62 of 62 mutants (kind 47 is not a person at the device, as a pick isn't). pointer-control: 160 checks and 33 of 33 mutants.
  • run-all.sh: all 64 checks pass. swift build -c release: only the CaptureProbe warning. The three iOS builds (Debug and Release for the simulator, Debug for a device) show only the known StreamClient capture warning.
  • Harness photos at 1000×710, 710×500, 710×1000, 466×678 and 320×568 (phone): "Desktop 2" fits on one line in every bar.

Tried by others

Untested

  • On a Mac with two or three monitors: stepping and wrapping, the label, the pointer moving, input landing on the shown display right after a tap, and a monitor unplugged and plugged in again.
  • VoiceOver reading the value and the hint.
  • A mirrored display (whether it shows up as another Desktop).

Problems found, and how they were fixed

  1. A tap right after switching could click the wrong monitor. Choosing the next display moved Sill's idea of the Desktop at once, while the stream still showed the old monitor until it restarted. Fix: input and the pointer land on the display the capture runs on (DesktopDisplays.landing).
  2. A monitor Sill can't capture would trap the Desktop. Nothing streamed, the choice was kept, and the button can only step while the Desktop streams. Fix: if the new display doesn't stream, the Desktop goes back to the one it was on (DesktopDisplays.afterStep).
  3. Display IDs change across sleeps (2026-10-28: 21 became 53 and 52), so a choice kept by ID would be lost. Fix: kept by the display's UUID.
  4. A choice could flip when macOS lists the displays in another order, restarting the Desktop on the other monitor. Fix: with nothing chosen, the Desktop stays on the display it's on.
  5. A locked Mac would have shown its screens and moved its pointer. This came up merging main's locked Mac. Fix: a locked Mac lists no displays and refuses kind 47 as a window pick, and the host checks the lock again before it steps and before it moves the pointer.
  6. The earlier version's findings (marliechorgan's, Desktop displays: follow the streamed display, number displays left to right (Saffsanity/sill#91) addhumal/sill#2). A pick that landed mid-switch was dropped, and monitors were numbered by ID. Fix: a second quick tap is ignored while a switch is under way, and the numbers follow the arrangement, left to right.

Saffsanity added a commit that referenced this pull request Oct 19, 2026
The plan's open pull requests gain #173 (the same issue built separately:
the pick beside the Desktop's object, the displays by ID, "Desktop 1" for
a label, a pick without a display keeping the last one), and rule 5 says
the displays are read again before a stopped capture's restart, as
7cc8bcc does; CLAUDE.md's entry says both and the merge of main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity added a commit that referenced this pull request Oct 20, 2026
…exactly

The plan's #173 section now says how the two differ (the pick's key beside
or inside the Desktop's object, "Desktop 2" against "1 of 2", a pick without
a display, a display gone, a pick during a restart, two quick taps), what
backs each, and which should land: this branch, which does what Noah decided
on #91 ("when there is another one it says so") and keeps devices from before
on the main display. Rule 3 says a pick that named no display keeps meaning
the main one through a change of the main display; the wire section says why
desktopDisplay sits beside the list rather than as a flag on each entry
(#66's shape). The checks README says which change brought the check.

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

@marliechorgan marliechorgan left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking up #91. I ran this on a Mac with three displays: a MacBook Pro's built-in screen (main) under two identical BenQ MA270U monitors. Two things came up, and both are fixed in addhumal#2, which goes into this branch.

1. A pick that lands mid-switch is dropped, but the device is told it happened. The Desktop started on display 4, display 2 was picked, then display 1 50 ms later. The host streamed two BenQ pipelines (3840×2160) and never the built-in screen, yet its window list said desktopDisplay=1. Input and the pointer read the same catalog.display, so taps land on a screen the device isn't showing. It happened in 4 of 4 runs. The cause is that pickDesktop moves catalog.display at once (WindowCatalog.swift:99), while select's defer drops the queued .desktop because pick != active is false (StreamCoordinator.swift:1237).

2. The numbers follow display IDs, not positions. Here the right-hand BenQ (macOS's "(2)") is ID 2 and the left-hand one ("(1)") is ID 4, so the button calls the left monitor Desktop 3, and tapping steps right, then left (WindowCatalog.swift:278).

The linked PR has the repro (a desktop:ID pick for sillclient.py), a fix for both, and checks built on this layout. Under the ID order those checks fail. With #173 and the fix merged onto main, all 46 pure checks pass.

Two smaller points about the description:

  • The default Desktop changes for everyone with more than one display, from displays.first to the main display. I think that's better, but users will notice it, so it deserves a line.
  • preferMainDisplay is now written but never read, and its comment no longer holds.

@addhumal

addhumal commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @marliechorgan for testing this on a 3-display setup and putting together addhumal#2!

Merged addhumal#2 into desktop-displays, removed the unused preferMainDisplay property (c15dd2b), merged latest main (4d990e9), and updated the PR description to note the main-display default and the new check/mutant counts (compatibility: 112 checks, 29 of 29 mutants).

…ty#91)

On a Mac with two displays or more the Desktop button reads "Desktop 1",
"Desktop 2"…, and a tap while the Desktop streams moves it to the next
display (left to right, then top to bottom, wrapping; never Sill's virtual
display). Noah's decision on Saffsanity#91: the tapping method.

- Wire (additive): kind 47 nextDisplay (empty), sent only to a host whose
  window list says desktopDisplays 2 or more; WindowList.desktopDisplays and
  desktopDisplay. 45 and 46 are held by open pull requests.
- Host: the next display is worked out from the host's own newest look and
  kept by the display's UUID (a new ID after a sleep is the same display);
  input and the pointer land on the display the capture runs on, not the one
  just chosen; a Desktop whose display moves under it restarts there; a
  display that does not stream sends the Desktop back to the one it was on;
  the Mac's pointer goes to the new display's centre. One log line.
- Device: StreamClient.tapDesktop(); VoiceOver's "Display 2 of 3" and hint.
- DesktopDisplays (pure), checked in Tests/checks/desktop-displays (56
  checks, 31 mutants); compatibility, display-wake and pointer-control
  extended for kind 47.

Supersedes this pull request's earlier version.
…ps no display

Conflicts, each kept both sides: the CI mutants matrix (desktop-displays
added to main's list), WindowList's new fields (desktopDisplays and
desktopDisplay beside locked and offConsole), listMessage (the display
fields before LockPolicy.list), StreamClient's window list handling.

With main's locked Mac (LockPolicy): a locked list carries no
desktopDisplays or desktopDisplay, so a device sends no kind 47 and learns
nothing of the Mac's screens; kind 47 is refused as a window pick is
(LockPolicy.Request.nextDisplay); the host checks the lock again before it
steps and before it moves the pointer, and the automatic restart leaves the
placeholder to LockPolicy. lock-policy 540 checks, 141 of 141 mutants (3 new);
run-all.sh all 64; swift build -c release and the three iOS builds, only the
known warnings.
@addhumal

addhumal commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@Saffsanity this is ready for your review: tap Desktop again for the next monitor (#91, your tapping method). It's reworked from main, and merged with main's locked Mac.

Decisions to confirm:

  • Monitors are numbered left to right, then top to bottom, as System Settings arranges them.
  • The choice is kept by display UUID while Sill runs.
  • The Mac's pointer jumps to the new monitor's centre.
  • The virtual display isn't a target (your words on Feature Request: Select which Mac display Desktop mode streams #91 named it; left out for now).

Untested, for you: on a Mac with two or three monitors:

  • Stepping and wrapping, and the "Desktop 2" label.
  • A tap right after switching landing on the shown monitor.
  • A monitor unplugged and plugged back in.
  • VoiceOver's "Display 2 of 3".

The pure rules are checked (desktop-displays: 56 checks, 31 of 31 mutants), and CI is green. Whichever of this and #200 lands second needs WindowCatalog's display choice reconciled.

@addhumal

addhumal commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Merging this beside #173 / #200: they conflict only in how the Desktop picks its display. A tested merge of the two is ready on addhumal:reconcile-173-200, for whichever lands second.

@xiaofeng-han

Copy link
Copy Markdown

please merge this, I tried it on my machine with 3 external monitors, working well. It would be great if it could provide a list to choose from, but I could work with current solution

@addhumal

addhumal commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @xiaofeng-han for testing it on three monitors! A list to choose from is in #296: hold the Desktop button and pick a display by name.

This branch has not been deployed

No deployments
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.

Feature Request: Select which Mac display Desktop mode streams

3 participants