Skip to content

feat(desktop): port desktop app and platform integrations to macOS - #24

Merged
thtbee merged 9 commits into
thtbee:mainfrom
saivinay1805:feature/macos-port
Oct 20, 2026
Merged

thtbee merged 9 commits into
thtbee:mainfrom
saivinay1805:feature/macos-port

Conversation

@saivinay1805

@saivinay1805 saivinay1805 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This pull request ports the NectarLink desktop application to macOS, delivering on the roadmap goal in docs/PLAN.md §10.7 to bring the Qt Quick + Rust desktop core cross-platform.

It incorporates maintainer feedback from PR #24 and PR #23:


Feature Scope on macOS

Supported Today

  • Pairing & Security: Core discovery, QR / nearby code pairing, mutual TLS 1.3 authentication and device-key storage restricted to the current user.
  • Files & Photos: Two-way file and folder transfers, photo roll browser, timeline photo thumbnails, and full-resolution photo downloads.
  • Continuity Camera: Document scanning and camera photo capture directly to the Mac.
  • Clipboard: Two-way text clipboard synchronization; incoming image pasteboard copying (Phone → Mac) via AppleScript with format conversion.
  • Calls & Messages: In-call alerts, call history browsing, contacts search, and SMS conversation viewing & sending.
  • Device Controls: Phone quick toggles (volume, ringer, DND, etc.) and battery/ring interactions.
  • Diagnostics: System Connection Doctor network and firewall checks.

Intentionally Not Advertised / Not Supported Yet

  • Phone screen mirroring (video decoding): mirror.view is removed from BASE_CAPABILITIES and restricted to Windows, preventing unhandled video stream initiation.
  • MP4 recording & PC audio streaming: Explicit Err returns on unsupported stream initialization rather than silent success stubs.
  • System Media Transport Controls (media.control): Omitted from macOS desktop capabilities.
  • Phone storage mounting in Finder: Cloud Files sync root (storage.mount) is Windows-only.
  • Virtual webcam: Windows MF virtual camera driver only.

Key Changes

  1. macOS Platform Subsystem (desktop/app/src/mac/):

    • Implemented native modules matching the desktop application platform contracts:
      • clipboard: Two-way text synchronization via pbcopy/pbpaste, plus AppleScript «class PNGf» pasteboard image writing with automatic format conversion and collision-free temporary files.
      • image: Real bitmap decoding using Qt QImage::loadFromData into BGRA pixels.
      • audio: Syntax-safe AppleScript speaker volume and mute status query parser.
      • single_instance: Unix domain socket-based single instance detection (nectarlink.sock) with automatic stale socket cleanup and activation wakeups.
      • toast: Native macOS system notifications via osascript.
      • http, browser, shell, autostart, doctor, and wol.
      • Explicit unsupported error handling in h264.rs, mp4.rs, and audio_out.rs.
  2. Module Layout (matching PR feat(linux): Linux desktop & CLI implementation (stacked on #23) #12):

    • Kept storage.rs, pc_audio_stream.rs, pc_mirror.rs, and play_on_phone.rs as single files with internal #[cfg(windows)] and #[cfg(not(windows))] blocks.
    • Platform module mounted in main.rs via #[cfg(target_os = "macos")] #[path = "mac/mod.rs"] mod win;.
  3. Capability Negotiation:

    • Removed mirror.view from BASE_CAPABILITIES in nectarlink-core so non-decoding desktop builds can opt out.
    • Scoped Windows-only capabilities (media.control, storage.mount, vcam, etc.) to #[cfg(windows)] in core_host.rs.
    • Desktop reports os: std::env::consts::OS.into() ("macos").
    • Added unit tests in core (macos_desktop_matrix_reflects_supported_and_unsupported_features) and desktop (macos_advertises_only_supported_capabilities).
  4. Warnings & CI:

    • Removed blanket #![cfg_attr(not(windows), allow(unused))] across desktop and spike crates.
    • Preserved desktop-macos GitHub Actions CI workflow with Qt 6.12.
  5. Documentation:

    • Updated README.md with macOS build requirements and a comprehensive Platform Support matrix.

Testing & Verification

  • cargo test -p nectarlink-protocol: 62 passed; 0 failed
  • cargo test -p nectarlink-core --lib: 112 passed; 0 failed
  • cargo test -p nectarlink-desktop: 148 passed; 0 failed
  • cargo clippy --locked -p nectarlink-desktop --all-targets -- -D warnings: 0 warnings
  • cargo clippy --locked -p s1-qt-rust --all-targets -- -D warnings: 0 warnings
  • cargo fmt --all --check & cargo xtask strings --check: Clean
  • git diff --check: Clean
  • All commits signed off (Signed-off-by) adhering to DCO guidelines.

@saivinay1805
saivinay1805 requested a review from thtbee as a code owner October 18, 2026 10:40
@saivinay1805

Copy link
Copy Markdown
Contributor Author

Closed for local testing

@jochenkirstaetter

Copy link
Copy Markdown
Contributor

Hello @saivinay1805

Cool stuff.
Maybe you might have a look at the code changes in core provided in #23 which prepares the common areas for Linux? Eventually this could assist your work as a stacked branch like it does for my Linux port?

Really like seeing this.

Cheers

@saivinay1805

Copy link
Copy Markdown
Contributor Author

Thanks for reaching out @jochenkirstaetter !

I looked through #23 — having the shared core/QML changes (Unix key permissions, window geometry persistence, RPC timeouts, and std::env::consts::OS) broken out is super clean and directly benefits macOS too.

Once #23 lands in main, rebasing the macOS platform layer on top of it will make everything even smoother. Really appreciate you preparing the common foundations!

@saivinay1805

Copy link
Copy Markdown
Contributor Author

Reopening after successful local testing on macOS (tested device pairing, UI, and test suite on Apple Silicon).

@saivinay1805 saivinay1805 reopened this Oct 18, 2026
@thtbee

thtbee commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Thanks for this, and for testing it on Apple Silicon. A macOS build is something people have asked for, so it's great to see it started.

Before you go further, a few things about how it should fit in, so the work doesn't have to be redone later:

  1. Base it on core, desktop, android: cross-platform Linux preparation & USB session reliability #23. core, desktop, android: cross-platform Linux preparation & USB session reliability #23 holds the shared groundwork for other platforms and will land first. Please wait for it to merge, then rebase onto main.

  2. Use the same structure as the Linux port. feat(linux): Linux desktop & CLI implementation (stacked on #23) #12 adds Linux by putting its platform code in desktop/app/src/linux/, mounted in place of win with the same functions, and keeps the shared modules (pc_mirror.rs, pc_audio_stream.rs, play_on_phone.rs, storage.rs) as single files with cfg blocks. This PR splits those four into windows.rs / non_windows.rs folders instead, which conflicts with feat(linux): Linux desktop & CLI implementation (stacked on #23) #12 on the same files and leaves the project with two patterns. Please follow the Linux layout: mac/ in place of win, and the shared modules kept as they are.

  3. No silent stubs. Several functions do nothing and report nothing, for example everything in mac/input.rs, and pc_mirror / play_on_phone outside Windows. A phone's remote mouse, keyboard or Deck tiles would then look as if they work on a Mac and quietly do nothing. The rule in this project is that the app never offers a feature that can't work: anything not implemented on macOS should not advertise its capability, so the capability matrix shows it as unavailable on both apps (see docs/architecture/capabilities.md).

  4. No crate-wide allow(unused). #![cfg_attr(not(windows), allow(unused))] in main.rs hides warnings on every non-Windows build, Linux included. Please scope cfg attributes to the items that need them instead.

  5. Scope in the description. A short list of what works on macOS today and what doesn't yet would help reviewing, and the README, since we can't test on a Mac ourselves. The desktop-macos CI job is a good start.

If it helps, splitting it like #23 and #12 (shared changes first, then the macOS code) also makes it much easier to review. Happy to answer questions on any of this.

@thtbee

thtbee commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Hi @saivinay1805, #23 is merged now, so main has the shared groundwork for other platforms. Whenever you're ready, rebase onto main and follow the Linux layout from #12 as described above. Happy to look at it again then.

@saivinay1805
saivinay1805 force-pushed the feature/macos-port branch 2 times, most recently from 422f40b to 96370de Compare October 19, 2026 07:11
@saivinay1805

Copy link
Copy Markdown
Contributor Author

Hi @thtbee,

Thanks for merging #23! I have rebased this PR cleanly onto main and addressed your feedback:

  1. Module Layout (matching Linux feat(linux): Linux desktop & CLI implementation (stacked on #23) #12):

    • Restored storage.rs, pc_audio_stream.rs, pc_mirror.rs, and play_on_phone.rs back to single files with internal #[cfg(windows)] and #[cfg(not(windows))] blocks.
    • Cleaned up macOS platform dispatch in main.rs via #[cfg(target_os = "macos")] #[path = "mac/mod.rs"] mod win;.
    • Removed blanket crate-wide allow(unused) attributes across desktop and spike crates.
  2. Capability Negotiation & Honest Error Handling:

    • Removed mirror.view from BASE_CAPABILITIES in core so non-decoding desktop platforms can cleanly opt out.
    • Scoped Windows-only desktop capabilities (media.control, storage.mount, vcam, etc.) to #[cfg(windows)] in core_host.rs and report "macos" as the OS.
    • Added unit tests in core (macos_desktop_matrix_reflects_supported_and_unsupported_features) and desktop (macos_advertises_only_supported_capabilities) asserting that unsupported features evaluate to unavailable and are not advertised.
    • Replaced silent success stubs with explicit unsupported Err returns for H.264 decoding, MP4 recordings, and streamed audio playback.
    • Implemented real bitmap decoding via Qt QImage::loadFromData and AppleScript pasteboard image copy with automatic format conversion and collision-free temp files.
  3. Documentation & Validation:

    • Added macOS build requirements and a comprehensive Platform Support matrix to README.md.
    • Updated PR description.
    • Local validation: 148 desktop tests, 112 core tests, and 62 protocol tests all passing; Clippy strict zero warnings; cargo fmt clean. All commits signed off per DCO.

Local checks passed; awaiting CI approval and review whenever convenient!

@thtbee thtbee left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for the rebase. I compared the four wrapped files with main, and the Windows code is the same apart from the cfg wrapper and rustfmt.

What needs fixing:

  1. Windows x64 CI fails clippy: decode_image is unused on Windows, and use super::* in core_host's tests is unused there since the only test is macOS-only.
  2. Clipboard privacy. Sending the PC clipboard is on by default, and the macOS watcher reads pbpaste without checking org.nspasteboard.ConcealedType / TransientType, so a password copied from a password manager goes to the phone. Windows skips those (Clip::Private). NSPasteboard would fix it, and its changeCount avoids starting a process twice a second. The first poll also sends whatever is on the clipboard at startup.
  3. Close to tray is on by default and the tray is a stub, so closing the window leaves the app running with no way back. Turn it off on macOS or handle the Dock reopen.
  4. Some stubs still report success: the Doctor firewall/network checks and fix_firewall, elevated::start, wol::wake (and pc.wake is advertised with no adapters), and lock(), which doesn't check whether osascript was allowed. These should return unsupported or not be offered.

Smaller: stop_ringing kills every afplay the user has running, unmuting the mic sets 75 instead of the old level, query_live_speaker_state needs real audio hardware, the LaunchAgent plist doesn't escape the path, and mac/mod.rs still has a blanket allow(dead_code, unused_imports).

@alexfoxtm

Copy link
Copy Markdown

Hi @saivinay1805 and @thtbee — are additional contributors welcome?

We’re working locally from this macOS port, targeting Apple silicon Macs and Galaxy Fold phones. We’d like to contribute reusable macOS fixes and features, along with Android app improvements, back upstream rather than maintain a separate implementation.

Happy to coordinate and submit small, focused PRs that follow the project’s architecture and contribution guidelines. Are there particular areas or priorities you’d prefer us to help with, so we avoid duplicating ongoing work?

@saivinay1805

Copy link
Copy Markdown
Contributor Author

Updated in e71dee4:

  • Windows CI Clippy: Scoped macOS unit tests and bridge functions with platform cfgs and conditional shadowing, eliminating all dead-code and unused-mut warnings under -D warnings.
  • Clipboard & Privacy: Replaced subprocess calls with native C++ NSPasteboard helpers querying changeCount, wrapped in autorelease pools for thread safety, respecting ConcealedType privacy markers, and ignoring startup clipboard state.
  • Window Geometry: Implemented NativeWindow::setScreenByName to cleanly restore saved screen placement without QML QScreen* type mismatches, and resolved the visibility change binding.
  • Honest Stubs: Removed pc.wake from macOS capabilities, returned explicit unsupported errors for doctor/elevation/WoL, and tracked individual child PIDs for sound ringing.
  • macOS Tray Fallbacks: Defaulted and enforced close_to_tray: false on macOS (quitting cleanly on window close), disabled the tray toggle in Settings on macOS, and removed --minimized from the autostart LaunchAgent plist.
  • Audio & Animations: Preserved exact microphone volume restoration on unmute and restored comb_motion preference initialization.

All local checks pass (112 core unit tests, 148 desktop tests, zero Clippy warnings, fmt, and whitespace checks). Awaiting CI approval and review.

alexfoxtm added a commit to alexfoxtm/nectarlink that referenced this pull request Oct 19, 2026
…ter gating, audio status, download limits)

Merge canonical thtbee/nectarlink refs/pull/24/head at exact 9c0f13d, direct child of previous pin e71dee4 (already in product), preserving upstream author/history. No rebase, no snapshot copy. Fetched only that one HTTPS ref with prompts disabled; merge-base resolved to e71dee4, so no deepen/unshallow.

One upstream commit, four files. Clean takes: desktop/app/src/mac/audio.rs (osascript exit-status checks, mic-volume read failure is now an honest error) and desktop/app/src/mac/http.rs (bounded curl download: pre/post size checks, --max-filesize, exit-63 too-large deletes, partials kept on other failures). desktop/app/cpp/app_helpers.cpp also merged cleanly: changeCount-verified clipboard reads (two attempts, torn/private-race reads discarded) and setString result propagation; this C++ bridge is dead_code with no Rust callers, and the live Rust objc2 clipboard in mac/clipboard.rs (stronger: torn-snapshot discard, both private markers checked with fail-closed clear, legacy transient type) is untouched and not replaced.

1 conflict file, desktop/app/src/updater.rs (2 hunks): keep the fork's macOS Sparkle route first in can_update() and install() (bundled WaypairUpdater helper + Sparkle.framework, SUFeedURL/SUPublicEDKey from the host bundle), and apply upstream's non-Windows disable only to the GitHub Windows-installer route (newer_release returns None off Windows, can_update false off Windows/macOS, install unsupported off Windows/macOS), so a Mac can never discover or install a Windows installer. Windows paths unchanged. Identities, keys, feeds and license notices untouched; no new capabilities.

Not built or tested here by instruction: Root is sole compiler and cross-reviews (focused commands in UPSTREAM-INTEGRATION.md wave report).

Co-authored-by: Cursor <cursoragent@cursor.com>
alexfoxtm added a commit to alexfoxtm/nectarlink that referenced this pull request Oct 19, 2026
…y1805 watch policy

UPSTREAM_BASE.json: move the macos-pr24 pin from e71dee4 to 9c0f13d (one reviewed commit on top of e71dee4, author Signed-off-by present) and record that it enters via merge 435ffa2 on alex.s/upstream-9c0-20261019, preserving upstream parentage.

UPSTREAM_SYNC.md: update the pin table and record Alex's latest watch policy: saivinay1805's macOS updates are always incorporated after exact-SHA review via merge (never auto-merge, never an unpinned fork branch), resolving against the fork's Sparkle R2 updater, native objc2 clipboard privacy, identities/keys and license notices.

Co-authored-by: Cursor <cursoragent@cursor.com>

@thtbee thtbee left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks, this covers the last review. I read the NSPasteboard code: the concealed/transient check and recording changeCount at startup look right, and the stubs fail honestly now.

What's left:

  1. It conflicts with main in MainWindow.qml. I pushed a fix there this morning (e4b279a): on Windows the window from #29 didn't open at all, because NativeWindow's QML type doesn't expose onWindowVisibilityChanged or screen. Main now uses a Connections block like yours and drops the screen assignment, since a position in desktop coordinates already picks the monitor. Please take main's version when you rebase. setScreenByName shouldn't be needed then; if macOS does need it to move between monitors, call it after bringToFront().
  2. #34 (Linux part 1) changes 16 of the same files in a different way. I'd like to merge this one first, since it has had two rounds and is close, and then have #34 rebase onto it. Your #[cfg(windows)] mod imp with one #[cfg(not(windows))] stub module works well for that, since Linux can start from the same stubs and replace them part by part.

Small: I think CGSession is gone since Big Sur, so lock() will always take the osascript path. That's fine, but the comment says otherwise.

@thtbee

thtbee commented Oct 19, 2026

Copy link
Copy Markdown
Owner

@alexfoxtm yes, welcome. Small, focused PRs are the best way to do it.

To avoid doing the same work twice: #24 is the macOS port and #34 starts the Linux series. For macOS, it's best to wait until #24 is merged and then pick up what it leaves unsupported, like screen mirroring and H.264 decoding, PC audio streaming, and the tray. For Android, including Fold layouts, please open an issue first with what you have in mind so we can agree on the approach before you write it. CONTRIBUTING.md has the rest (sign-off and checks).

@thtbee

thtbee commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Heads-up for the rebase: main now has photo backup and joining the phone's hotspot, which add a few things under crate::win that macOS will need stubs for: win::local_offset_ms, win::free_space and the win::wifi module (saved_networks, scan, reach, connect, Reach). photo_backup.rs also opens the folder with explorer.exe. Returning 0, None, an empty list and an unsupported error is fine for now.

Signed-off-by: saivinay1805 <saivinayreddyvavilla@gmail.com>
- Guard Qt::ExpandedClientAreaHint with #ifdef _WIN32 so the QML client area sits below the native macOS titlebar
- Hide CaptionButtons on non-Windows platforms and collapse its width to 0
- Allow unused items on non-Windows in main.rs to silence dead-code warnings for platform stubs

Signed-off-by: saivinay1805 <saivinayreddyvavilla@gmail.com>
- Add macos-latest to core CI matrix for locked clippy and unit tests
- Add desktop-macos job with Qt 6.12 to test and build nectarlink-desktop

Signed-off-by: saivinay1805 <saivinayreddyvavilla@gmail.com>
…on non-Windows

Signed-off-by: saivinay1805 <saivinayreddyvavilla@gmail.com>
…ust_strings.h

Signed-off-by: saivinay1805 <saivinayreddyvavilla@gmail.com>
…pability advertising for macOS

- Restore storage.rs, pc_audio_stream.rs, pc_mirror.rs, and play_on_phone.rs to single files with internal cfg blocks (matching Linux thtbee#12 architecture).
- Mount macOS platform code via #[cfg(target_os = "macos")] #[path = "mac/mod.rs"] mod win in main.rs and remove crate-wide allow(unused) attributes from desktop and spike crates.
- Remove mirror.view from BASE_CAPABILITIES in core so non-decoding desktop platforms can cleanly opt out.
- Restrict Windows-only capabilities (media.control, storage.mount, etc.) to cfg(windows) in core_host.rs and report std::env::consts::OS.
- Implement real QImage bitmap decoding and AppleScript pasteboard image copy with automatic format conversion and collision-free temporary files.
- Return explicit unsupported errors for H.264 video decoding, MP4 recordings, and streamed audio playback instead of silent success stubs.
- Fix AppleScript speaker volume and mute status query parser.
- Add capability matrix unit test in core and live capability assertion test in desktop.
- Add macOS platform support summary and build requirements to README.md.

Signed-off-by: saivinay1805 <saivinayreddyvavilla@gmail.com>
…d macOS tray fallbacks

- Implement NativeWindow::setScreenByName to map QML screen names to native QScreen pointers without QObject type mismatch errors.
- Wrap native NSPasteboard C++ helpers in AutoReleasePool to prevent thread-local memory accumulation.
- Restore exact microphone volume on unmute without artificial minimum clamping.
- Preserve comb_motion preference initialization across platforms.
- Disable close_to_tray toggle and enforce quit-on-close on macOS where system tray is unavailable.
- Avoid unused_mut on non-macOS platforms in on_close_to_tray_changed listener.
- Guard macOS-only unit test module in core_host.rs and allow unused bridge helpers to resolve Windows CI Clippy failures.
- Remove pc.wake from macOS advertised capabilities and supply honest error responses for unimplemented stubs.
- Clean up granular dead_code allowances across mac/ platform modules.

Signed-off-by: saivinay1805 <saivinayreddyvavilla@gmail.com>
…us, and download limits

- Verify changeCount before and after reading clipboard text to avoid race condition with private data
- Propagate pasteboard write results in mac_clipboard_write_text
- Disable updater and Windows installer discovery on non-Windows
- Enforce HTTP download size limits while preserving partial files on network errors
- Check osascript execution status in audio commands and fail if reading microphone state fails

Signed-off-by: saivinay1805 <saivinayreddyvavilla@gmail.com>
…t latest upstream APIs

- Rebase onto upstream main, taking main's MainWindow.qml without conflict
- Remove setScreenByName from NativeWindow as window positioning in desktop coordinates handles monitor placement
- Update CGSession comment in mac/shell.rs regarding modern macOS fallback
- Add macOS stubs and helpers for new upstream features (wifi module for hotspot, local_offset_ms and free_space for photo backup, preserving Some(0) on full drive)

Signed-off-by: saivinay1805 <saivinayreddyvavilla@gmail.com>
@saivinay1805

Copy link
Copy Markdown
Contributor Author

Rebased onto main (5bdcadb) and updated in 749b7fd:

  • MainWindow & Screen Placement: Rebased cleanly onto main, taking main's MainWindow.qml without conflict, and removed NativeWindow::setScreenByName as desktop coordinate placement handles monitors.
  • Screen Lock: Updated the comment in mac/shell.rs to clarify that CGSession was removed after Big Sur (macOS 11), with modern macOS taking the osascript keycode path.
  • Upstream Feature Support:
    • Added mac/wifi.rs stubs for Instant Hotspot.
    • Implemented local_offset_ms (via QTimeZone) and free_space (via QStorageInfo) for Photo Backup.
    • Handled the full-drive edge case in free_space: query failures return None, while a 100% full drive returns Some(0), ensuring photo backup's free-space margin checks work correctly.

All local checks pass (164 desktop tests, 112 core tests, zero Clippy warnings, fmt, and whitespace checks).

@thtbee

thtbee commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Thanks, the rebase looks right: MainWindow.qml is the same as main's, nothing from main was undone, and local_offset_ms and free_space through QTimeZone and QStorageInfo look good. CI is green, including the new UI check.

Two small things: photo backup's Open button runs explorer.exe directly (bridge/photo_backup.rs, open_folder), so on macOS it does nothing; going through a shell::open_folder on each platform would fix it. And native_window.h still has a stray blank line from the removed setScreenByName.

Jochen is checking how #34 and the rest of the Linux series line up with this, so I'll wait for that before merging either one.

@jochenkirstaetter

jochenkirstaetter commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Hi @thtbee

It, meaning #34 , lines up beautifully and matches this PR's structure perfectly. It's ready for review and waiting for your feedback.

Cheers

@thtbee thtbee left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Went through it once more as a whole: the Windows code in the shared files is the same as main's apart from the cfg wrappers, the updater's Windows path is unchanged, and CI is green on all three platforms. Thanks for sticking with all the rounds.

@thtbee
thtbee merged commit e723260 into thtbee:main Oct 20, 2026
8 checks passed
thtbee added a commit to jochenkirstaetter/nectarlink that referenced this pull request Oct 20, 2026
Both ports' CI jobs, link libraries and platform modules side by side; vcam keeps this branch's shm, which serves macOS too, instead of thtbee#24's stubs.
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.

4 participants