Skip to content

feat(linux): Linux desktop & CLI implementation (stacked on #23) - #12

Draft
jochenkirstaetter wants to merge 5 commits into
thtbee:mainfrom
jochenkirstaetter:linux
Draft

jochenkirstaetter wants to merge 5 commits into
thtbee:mainfrom
jochenkirstaetter:linux

Conversation

@jochenkirstaetter

@jochenkirstaetter jochenkirstaetter commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What and why

Provides the full NectarLink desktop and CLI experience on Linux (desktop/app/src/linux/, desktop/packaging/linux/, core/nectarlink-cli linux_watch, and ubuntu-latest CI job).

Note: As requested in #12 (comment), the shared cross-platform changes in core, ffi, android, and cross-platform desktop Rust/QML files have been split out into #23 (linux-shared). This PR is stacked directly on top of #23 (f24da46) and contains only the Linux-specific implementation and desktop-linux CI job.

Summary of Linux-specific changes (25f069f on top of #23)

  • Linux Platform Backend (desktop/app/src/linux/* — 31 modules):
    • Audio capture/playback (audio.rs, audio_out.rs), autostart (autostart.rs), BLE proximity (ble_beacon.rs), browser tab handoff (browser.rs), Bluetooth HFP (bt_hfp.rs), Wayland/X11 clipboard (clipboard.rs), diagnostics (doctor.rs), polkit elevation (elevated.rs), H.264/MP4 (h264.rs, mp4.rs), HTTP/updater helper (http.rs), icon/image helpers (icon.rs, image.rs), input injection (input.rs), XDG desktop actions (jumplist.rs), MPRIS2 media control (media_sessions.rs, smtc.rs), Netlink network watcher (net.rs), share target (share_target.rs), shell integration (shell.rs, shortcut.rs), flock single-instance IPC (single_instance.rs), Freedesktop notifications (toast.rs), StatusNotifierItem tray (tray.rs), V4L2/PipeWire virtual webcam & microphone (vcam.rs, vmic.rs), wallpaper (wallpaper.rs), widget state (widget.rs), per-window capture (window_capture.rs), and Wake-on-LAN (wol.rs).
  • Linux CLI Process Watcher (core/nectarlink-cli/src/main.rs):
    • linux_watch (pidfd_open + /proc/<pid>/stat) and DesktopDir IPC for nectarlink notify-when.
  • Native Frameless Window (desktop/app/cpp/native_window.cpp):
    • #else path for Linux using Qt startSystemMove() and startSystemResize().
  • Linux Packaging (desktop/packaging/linux/*):
    • build-tarball.sh, build-deb.sh, build-appimage.sh, nectarlink.desktop, and org.nectarlink.desktop.metainfo.xml.
  • CI (.github/workflows/ci.yml):
    • Added desktop-linux (Linux app (x64) on ubuntu-latest) running cargo clippy, cargo test, and cargo build --release for nectarlink-desktop, nectarlink-vcam, and s1-qt-rust.

How it was tested

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace (626 Rust unit and integration tests across all crates)
  • cargo xtask tokens --check & cargo xtask strings --check
  • ./scripts/check-headers.ps1 (SPDX license header verification)
  • ./android/gradlew -p android testDebugUnitTest
  • Full E2E verification on Linux with a physical Samsung S20 FE (0.1.4) over both USB and LAN.

Checklist

  • ./scripts/check.ps1 passes (or the CI equivalent for the parts I changed)
  • User-facing strings are translatable, and new UI works with keyboard and screen readers
  • No personal content (notification text, messages, file names, keys) is logged
  • Commits are signed off (git commit -s), see CONTRIBUTING.md

@thtbee thtbee mentioned this pull request Oct 16, 2026
@thtbee

thtbee commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Hi Jochen, great progress, and thanks for keeping it as a draft while it grows. I approved CI on this PR so you get real feedback as you go.

A few things before this can land:

  1. Keep this PR Linux-only. Please move the Android and wording changes (strings.xml, HomeScreen.kt) out of it. They overlap with the Simplified Chinese PR (Android: localize the UI into Simplified Chinese #13), which lands first.
  2. On "Windows PC" to "Desktop PC": I'd rather use plain "PC" / "your PC". Most people pair a laptop, and "Desktop PC" reads like a tower. It also works for Windows and Linux. "Windows 11" stays only where it's a requirement (install steps, the "needs" lines in the guide). I'll do that rename myself in its own PR after Android: localize the UI into Simplified Chinese #13, so the Chinese translation is updated at the same time.
  3. Please sign off your commits (git commit -s, see CONTRIBUTING.md). For the existing ones, git rebase --signoff main does it in one go.
  4. Since this touches core (identity.rs, features.rs, deck.rs), I'll review those parts closely once it's out of draft, so small focused commits there help a lot.

Mark it ready for review whenever the Linux app does the basics end to end (pairing, notifications, clipboard, files).

@jochenkirstaetter

jochenkirstaetter commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi @thtbee

Everything well noted. I'm going to comply with the contributing constraints. A few notes on your remarks on how I'm going to proceed.

  1. Unfortunately, there are some implementations in the shared/common files that are Windows specific and I had to modify them. As for the PRs happening while working in this branch. I usually rebase onto main when I see that there are differences. This shall keep everything nicely fresh and alive without causing any trouble for the PR.
  2. Agreed on the plain "PC", I'm going to revert my labels accordingly. No worries, I'm on it.
  3. Ahh, yes, perfect, thanks for the heads up. Will be duly done. No worries.
  4. Yes, agreed, the majority of changes are mainly about adding "linux" as a platform. But also resolving typical Windows implementations like using "Windows Explorer", .lnk files, etc. I'm going to assemble a full report with more context for this whole port. Hope this is going to make it easier for everyone.
  5. Running scripts/check.ps1 is happening as I have PowerShell Core on Linux anyway. Fixed the -Android flag for Linux. 😸

Ohh, it's ready since milestone 3 already. I'm currently working on milestone 5 - reverse mirroring, Phone-to-PC features, and so forth.

Cheers, JoKi

@jochenkirstaetter

Copy link
Copy Markdown
Contributor Author

Hi @thtbee

Seen the build errors and will address them.
Nothing serious as it looks like.

Cheers

@jochenkirstaetter

Copy link
Copy Markdown
Contributor Author

Hello @thtbee

I know it's a huge PR and the majority of changes are focusing on Linux and new tests.
However, as already mentioned to you there were changes and additions necessary in the common, shared code areas and some Android parts.

Following is a summary of the changes made to the non-Linux areas.

Kindly have a look and give me any feedback that needs further review and improvement. The whole branch has already been rebased onto main v0.1.4 earlier today.

Summary of Changes to Shared & Non-Linux Areas

1. Core Engine, CLI & FFI (core/)

features.rs:
• Added "linux" alongside "windows" and "macos" in role_from(DeviceKind::Desktop, os) so a Linux PC is recognized as Role::Desktop during capability evaluation.

identity.rs & Cargo.toml:
• Added SecretServiceKeyProtector (id = 2, gated behind #[cfg(target_os = "linux")]) using the Linux kernel
@u/@s keyring (keyctl) + ChaCha20-Poly1305 alongside Windows DpapiKeyProtector (id = 1) and PlainKeyProtector
(id = 0), plus 0600 file permissions on Unix. Windows DPAPI behavior is unchanged.

deck.rs:
• Extended is_valid_app_path to accept .desktop, .appimage, .sh, and safe absolute Unix paths (/usr/bin/...
without .. traversal) in addition to Windows .exe and .lnk.

node.rs & session.rs (Cross-platform E2E reliability fixes):
• USB tunnel priority & dial interruption (dial_with_usb_interrupt): If an outbound LAN/relay dial is in
flight when an ADB USB tunnel comes up, the dial is interrupted immediately so the low-latency USB link
connects without waiting for a dial timeout; skips DEFER_DIAL when a USB tunnel is ready.
• Duplicate session race prevention: Added CloseOnDrop guard on canceled dials and listener-side deduplication
so a late incoming LAN connection does not tear down an active outgoing or USB session.
• Loopback filtering: Excluded 127.0.0.1 (stale ADB tunnel ports) when persisting LAN peer addresses to peers.
json.
• Non-blocking real-time PCM audio (try_send_realtime): Prevented high-rate AUDIO_PCM frames from saturating
the control stream outbox and starving heartbeats or RPCs; added REQUEST_TIMEOUT on control-stream writes.

clipboard.rs, handoff.rs, timeline.rs:
• Added is_likely_binary_or_corrupted_image_text in timeline.rs (used by clipboard.rs and handoff.rs) so raw
binary image headers (JFIF, Exif, \x89PNG) accidentally placed into a text clipboard slot are classified as
"Copied image" instead of rendering binary garbage or triggering URI handoff prompts.

lib.rs:
• When the broadcast event channel reports RecvError::Lagged, the FFI bridge now automatically re-emits
LinkChanged, WakeInfoChanged, PeerInfoChanged, Capabilities, and QueueChanged for all paired devices so the
Android UI never stays stuck on stale connection state after a burst of events.

main.rs & Cargo.toml:
• Enabled TaskTransport::DesktopDir and notify-when on #[cfg(any(windows, target_os = "linux"))], adding
#[cfg(target_os = "linux")] mod linux_watch (pidfd_open + /proc//stat) alongside existing #[cfg(windows)]
mod win_watch.

──────

2. Android App (android/)

PhoneClipboard.kt:
• Fixed a clipboard image bug found during E2E testing where Samsung/Android clipboard items with image/* MIME
types or content:// image URIs fell through to coerceToText(appContext) (which dumped raw JPEG/PNG binary
bytes as UTF-8 text). readCurrent() now checks hasImageMime / looksLikeImageUri before coerceToText, resolves
content:// streams into PNG bytes via BitmapFactory, and rejects binary-corrupted text via
looksLikeBinaryOrCorruptedText.

ThemeAndLocalizationTest.kt:
• New JUnit test suite verifying AppTheme (Bloom, Hive, Glass, Graphite), ColorMode (System, Light, Dark),
dark/light/AMOLED palette generation, and 100% key/placeholder parity between values/strings.xml and values-
zh-rCN/strings.xml.

──────

3. Shared Desktop Rust Modules (desktop/app/ & desktop/vcam/)

Cargo.toml & build.rs:
• Moved the windows crate dependency under [target.'cfg(windows)'.dependencies] and gated Win32 link libraries
(dwmapi, ole32, shell32, user32) to Windows targets.
• Added Linux Qt 6 sysroot/linker discovery and cross-platform .ts → .qm translation compilation (using
lrelease when available or a built-in pure-Rust .qm compiler fallback).

main.rs, core_host.rs, local_api.rs, icons.rs:
• Added #[cfg(target_os = "linux")] #[path = "linux/mod.rs"] mod win; so all callers of crate::win::* work
unchanged on both OSes.
• Replaced hardcoded "windows" OS strings in core_host.rs with std::env::consts::OS, and routed local_api.rs
token generation through crate::win::secure_random.

Platform-Gated Desktop Subsystems (all existing #[cfg(windows)] code paths preserved intact, with #[cfg(target_os = "linux")] implementations added alongside):
• mirror.rs: Gated XInput/WinRT gamepad polling to #[cfg(windows)] and added Linux /dev/input/js* evdev
gamepad polling under #[cfg(target_os = "linux")].
• pc_mirror.rs: Gated DXGI Desktop Duplication + Media Foundation H.264 encoder to #[cfg(windows)] and added
Linux PipeWire/X11 + FFmpeg/VAAPI H.264 streaming under #[cfg(target_os = "linux")].
• pc_audio_stream.rs: Gated WASAPI loopback to #[cfg(windows)] and added PipeWire/PulseAudio monitor source
loopback under #[cfg(target_os = "linux")].
• pc_notifications.rs: Gated WinRT UserNotificationListener to #[cfg(windows)] and added org.freedesktop.
Notifications D-Bus monitor under #[cfg(target_os = "linux")].
• storage.rs: Gated Windows Cloud Filter API (CfApi) to #[cfg(windows)] and added Linux FUSE3 / on-demand
folder mount under #[cfg(target_os = "linux")].
• recordings.rs, setup.rs, updater.rs: Gated Windows Media Foundation transcoding, Windows Registry/Start Menu
installer logic, and .exe updater asset matching to #[cfg(windows)], with Linux equivalents under
#[cfg(target_os = "linux")].

Desktop State, Settings & Bridge Improvements (settings.rs, prefs.rs, app.rs, timeline.rs, transfers.rs,
send_to.rs):
• Added main window geometry persistence (window_x, window_y, window_width, window_height, window_maximized)
to Settings and Preferences so the desktop window restores its size and position across restarts.
• Added clipboard::mark_sent_files_text in transfers.rs and send_to.rs so sending a file does not also get
echoed by the clipboard watcher as a duplicate file:// text clip.
• Enabled image thumbnails in bridge/timeline.rs for TimelineKind::File entries when the transferred file has
an image extension (.jpg, .png, .webp, .gif).

Virtual Camera Shared Memory Crate (desktop/vcam/):
• Gated Win32 CreateFileMappingW / MapViewOfFile to #[cfg(windows)] and added POSIX /dev/shm mmap backing
under #[cfg(unix)] using the identical FrameHeader binary layout.

──────

4. Desktop C++ Windowing & QML UI (desktop/app/cpp/ & desktop/app/qml/)

native_window.cpp & app_helpers.cpp:
• Guarded Win32 DWM/DXGI frameless window hooks with #ifdef _WIN32 and added #else Linux Qt
FramelessWindowHint + startSystemMove() / startSystemResize() event filter.

Cross-Platform QML Rendering & Font Compatibility (desktop/app/qml/):
• CaptionButtons.qml & SetupWindow.qml: Replaced Windows-only "Segoe Fluent Icons" private-use glyph
characters (\uE921, \uE922, \uE8BB) with crisp vector Shape / PathSvg icons so minimize, maximize/restore, and
close buttons render identically on Windows and Linux without missing-glyph boxes (□).
• Txt.qml: Uses "Segoe UI Variable Text" on Windows and falls back to the system UI font ("Ubuntu" /
system sans-serif) on Linux.
• Qt 6.4+ QML Compatibility (App.qml, Icon.qml, RoundedImage.qml, Spinner.qml, HomePage.qml, SadBee.qml):
• Set preferredRendererType = 2 (CurveRenderer) dynamically via Component.onCompleted: if
("preferredRendererType" in this) preferredRendererType = 2 so it remains enabled on Qt 6.6+ (Windows)
while loading cleanly on Qt 6.4/6.5 (Ubuntu 24.04 LTS).
• Replaced MultiEffect mask in RoundedImage.qml with a hardware-accelerated Shape + TextureFill rounded
rectangle that works across all Qt 6 versions without QtQuick.Effects.
• MainWindow.qml: Added window geometry save/restore bindings (Preferences.save_window_geometry) and
clamped minimum initial window size.

──────

5. Build Scripts & Spikes (scripts/check.ps1, spikes/s1-qt-rust/)

check.ps1: Updated the Android check step to invoke ./gradlew on Linux/macOS and .\gradlew.bat on Windows.

spikes/s1-qt-rust/: Guarded the Windows D3D11 spike code with #[cfg(windows)] / #ifdef _WIN32 so cargo test
--workspace and cargo clippy --workspace succeed on Linux.

@jochenkirstaetter
jochenkirstaetter marked this pull request as ready for review October 17, 2026 17:37
@thtbee

thtbee commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Hi @jochenkirstaetter, thank you for the summary of the shared-code changes. It makes this much easier to review.

A heads-up before you do more: main moved today by 47 commits (0.1.4 fixes and new features, not released yet), and several touch the same files as this branch:

  • core/nectarlink-core/src/session.rs: the control stream is now sent ahead of video. That's related to your try_send_realtime change, so please check the two fit together.
  • core/nectarlink-core/src/storage.rs and node.rs
  • desktop/app/src/pc_mirror.rs, mirror.rs, play_on_phone.rs, doctor.rs, updater.rs, setup.rs
  • desktop/app/src/i18n.rs: text the app builds in Rust now goes through tr! (see CONTRIBUTING), so new Linux-side text should too.
  • desktop/app/qml/MirrorWindow.qml, HomePage.qml, Theme.qml

Please rebase onto the current main when you can. I'm starting on the review now, beginning with the shared code and the Android changes, and will post it on the rebased branch.

@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.

Hi @jochenkirstaetter, thank you for this, and for the summary of the shared-code changes. Getting NectarLink onto Linux is something a lot of people have asked for, and the amount of work here shows. This review covers what you asked about: the changes outside the Linux-only code (core, FFI, Android, and the desktop code that also runs on Windows). The Linux-only modules get their own pass once the branch is split (see the end).

Must change before merge

  1. The Linux key isn't actually protected (core/nectarlink-core/src/identity.rs). The wrapping key is derived from /etc/machine-id and the uid. Any local user can read both, and the key is derived the same way again after every reboot, so the keyring adds nothing: anyone who can read the key file can rebuild the key. The type is also named SecretServiceKeyProtector but never talks to the Secret Service. Please either store a random key in the real Secret Service (libsecret / org.freedesktop.secrets), falling back to plain + 0600 when there isn't one, or keep plain + 0600 and name it for what it is.

  2. Windows gets weaker (identity.rs, decode and unprotect_with_known). A Windows build now accepts a plain (method 0) key file where it used to refuse one, so swapping the key file for an unprotected one would go unnoticed. Please limit the fallback to the migration it's meant for (Linux, method 0 to 2), and re-seal with the build's own protector after loading.

  3. Rounded images lose their corners on Windows too (qml/components/RoundedImage.qml). The summary says the mask became a Shape + texture fill, but the code only sets clip: true, so album art and photos are square everywhere. Please keep the MultiEffect mask where QtQuick.Effects is available, and use the fallback only where it isn't.

  4. Deck launch tiles get around the commands opt-in (core/nectarlink-core/src/deck.rs). LaunchApp now accepts .sh scripts and any absolute Unix path. Windows refuses .bat and .ps1 for this action on purpose: scripts belong to RunCommand, behind the off-by-default "Run Deck shell commands" toggle. Please keep .desktop, .AppImage and plain executables, and drop .sh and arbitrary paths (or send them through RunCommand).

  5. Real clipboard text gets dropped (core/nectarlink-core/src/timeline.rs, used by clipboard.rs). is_likely_binary_or_corrupted_image_text rejects any clip whose first 512 characters mention "Exif", "IHDR", "IDAT", "JFIF" or "ICC_PROFILE", which catches a photographer's notes or code that reads PNGs. It runs in the core, so it affects every platform. Your PhoneClipboard.kt change fixes the garbage where it starts, so I'd remove the core check. If you'd rather keep it, keep only file signatures at the very start plus the replacement-character ratio, with tests for normal text that mentions those words.

  6. Reconnecting after a network change gets slower (node.rs accept and session.rs handshake_listener). Both now refuse an incoming connection whenever this side's own outgoing session still looks alive. When the other device changes networks and dials again, this side's old session is already dead but not yet detected, so the new connection is refused until the heartbeat gives up (several seconds), and the duplicate tie-break in register_session never runs. Please leave that decision to register_session, or refuse only when the existing session has received something in the last second or two. These changes also need a two-device test (core/nectarlink-core/tests/two_devices.rs). None of the node.rs / session.rs changes have one yet.

Worth changing

  • Windows title-bar buttons (CaptionButtons.qml): the hand-drawn paths also replace Windows 11's own Segoe Fluent glyphs on Windows, and the maximize button is now also registered as a caption hole, which may stop Snap Layouts from appearing when hovering it. Please keep the glyphs and the current maximize handling on Windows, and use the paths elsewhere.
  • The .qm compiler in build.rs: a second implementation of Qt's binary format to keep in step. I'd rather require lrelease (packaging already does) and fail with a clear message when it's missing.
  • preferredRendererType = 2 in Component.onCompleted (Icon, Spinner, App, HomePage, SadBee): a magic number, and on Windows the first frame now draws with the default renderer. A file selector or a small Linux variant of these components would keep Windows as it is.
  • Android clipboard (isImageUriByMagic): it opens every copied content link to check whether it's an image. That's fine as a fallback, but please skip it when the clip's description already says what it is.
  • Window geometry persistence: new behavior on Windows. Please check it with two monitors, and when the monitor it was saved on is gone.

Good as they are

  • handoff.rs: no more panic on multi-byte text. That's a real crash, and it comes with a test.
  • features.rs: Linux as a desktop OS, with a test that the feature matrix matches Windows.
  • session.rs request: the timeout now covers sending too.
  • node.rs: USB preferred over LAN in the duplicate tie-break, loopback addresses no longer persisted, cancelled dials closed, and connected() waking the supervisor.
  • FFI: sending state again after the event channel lags.
  • try_send_realtime for PC audio frames. main now also sends the control stream ahead of media streams, and the two fit together.

How to move this forward

At 106 files and 19,000 lines this is hard to review in one go and hard to keep rebased. Could you split it?

  1. PR A, shared changes: core, FFI, Android, and the desktop code and QML that also run on Windows, with the fixes above and a two-device test for the session and dial changes. It's small enough to review properly, and it merges first.
  2. PR B, Linux: the Linux-only code on top of A. Please add a CI job that builds the desktop app on Ubuntu, so Linux keeps building after it's merged. Today CI builds only the core there.

Both on top of the current main, please (it moved by 47 commits today, as mentioned above). Thanks again, this is a big step for the project.

@jochenkirstaetter

Copy link
Copy Markdown
Contributor Author

Hi @thtbee

Thank you very much for taking the time to review this huge amount of information. Appreciate your time. I'll start with the rebase onto main, and then your input to improve the code, in particular the commonly shared and Windows related parts.

Afterwards, I'll see how this can be split into two pull requests. In general, it shall be feasible given that the Linux sources have there own folder. I'll see about that.

And yes, the automated build shall be extended to provide all packages.

Again, thanks for your time and I'm looking forward to seeing this fully merged soon.

Cheers

@jochenkirstaetter jochenkirstaetter changed the title Linux feat(linux): Linux desktop & CLI implementation (stacked on #23) Oct 18, 2026
@jochenkirstaetter

jochenkirstaetter commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @thtbee!

I have rebased onto main (c7191ee), addressed all review comments, split the PR into two stacked branches/PRs, and added the ubuntu-latest desktop CI job:

  1. PR core, desktop, android: cross-platform Linux preparation & USB session reliability #23 (linux-shared): Shared changes in core, ffi, android, and cross-platform desktop code/QML (28 files changed), plus the two_devices.rs USB upgrade + realtime PCM test.
  2. PR feat(linux): Linux desktop & CLI implementation (stacked on #23) #12 (linux, stacked on core, desktop, android: cross-platform Linux preparation & USB session reliability #23): Linux-only code (desktop/app/src/linux/, desktop/packaging/linux/, docs/LINUX_PLAN.md, #ifdef / #[cfg(target_os = "linux")] blocks) + the desktop-linux (ubuntu-latest) build/clippy/test job in .github/workflows/ci.yml.

Summary of Review Fixes

Must-Change Items

  1. core/nectarlink-core/src/identity.rs (SecretServiceKeyProtector): Removed SecretServiceKeyProtector (mod keyring) and libc from nectarlink-core/Cargo.toml. Linux uses PlainKeyProtector (id = 0) + 0o600 (#[cfg(unix)]) file permissions on identity.key.
  2. core/nectarlink-core/src/identity.rs (unprotect_with_known): Removed unprotect_with_known and restored the strict method != protector.id() check in decode().
  3. desktop/app/qml/components/RoundedImage.qml: Restored to exact upstream main (QtQuick.Effects + MultiEffect mask).
  4. core/nectarlink-core/src/deck.rs & desktop/app/src/linux/shell.rs: Removed .sh and arbitrary / paths from is_valid_app_path (allowing only .exe, .lnk, .desktop, .appimage, or non-.sh single-filename binaries in /usr/bin/, /usr/local/bin/, /snap/bin/, /var/lib/flatpak/exports/bin/), and removed the sh <path> execution branch from linux/shell.rs::launch_app.
  5. core/nectarlink-core/src/timeline.rs & clipboard.rs: Removed is_likely_binary_or_corrupted_image_text across core and desktop, keeping the fix in PhoneClipboard.kt and Changes::CLIPBOARD | Changes::TIMELINE in desktop/app/src/clipboard.rs.
  6. core/nectarlink-core/src/node.rs & session.rs: Replaced expect(...) in upgrade_to with error!() + early return, updated spawn_session to wait on a oneshot channel if an existing live session is already present (so USB upgrade never double-opens control), kept the 15s timeout on Session::rpc, and added usb_upgrade_reconnect_and_realtime_pcm_do_not_starve_control_rpcs in core/nectarlink-core/tests/two_devices.rs.

Worth-Changing Items

  1. desktop/app/qml/components/CaptionButtons.qml & SetupWindow.qml: Preserved Windows Segoe Fluent Icons Text (visible: Qt.platform.os === "windows"), removed addCaptionHole(maximize) so Windows 11 Snap Layout continues to work via HTMAXBUTTON, and bound nativeHovered / nativePressed.
  2. desktop/app/build.rs: Removed the hand-rolled compile_ts_to_qm_rust parser/writer and used Qt's lrelease tool directly.
  3. Icon.qml, Spinner.qml, HomePage.qml, SadBee.qml: Reverted to exact upstream main (preferredRendererType: Shape.CurveRenderer).
  4. PhoneClipboard.kt: Skipped isImageUriByMagic when resolvedMime is already known and non-image/*, and removed isLikelyBinaryText.
  5. MainWindow.qml: Checked Qt.application.screens in restoreAndBringToFront() before clamping so positions on secondary monitors are preserved.
  6. desktop/app/src/updater.rs: Wrapped "The update couldn't be started. Try again." in tr!("Updater", ...) and regenerated desktop/app/i18n/rust_strings.h via cargo xtask strings.

Kindly let me know whether this fits your requirements.

Cheers.

@jochenkirstaetter

Copy link
Copy Markdown
Contributor Author

Hi @thtbee

Going to check what's broken here and address it accordingly. No worries, I'm on it.

Cheers

@jochenkirstaetter

Copy link
Copy Markdown
Contributor Author

Hi @thtbee

Back to draft as I want to handle 6c71204 first properly and don't want to risk an early PR merge. This shall be helpful to keep the right sequence of PRs in place.

Minor delay but I won't touch the code base for now, just taking care of the intermediate branch linux-shared-followupto render individual issue-PR pairs instead of one messy block.

Hope that's OK with you.

Cheers

@jochenkirstaetter
jochenkirstaetter force-pushed the linux branch 2 times, most recently from 94d272e to 40add6b Compare October 18, 2026 22:14
@jochenkirstaetter
jochenkirstaetter marked this pull request as ready for review October 18, 2026 22:36
thtbee pushed a commit that referenced this pull request Oct 18, 2026
From @jochenkirstaetter: the parts of the Linux port that the core and the
Windows app share, plus fixes for USB sessions, split out of #12 so they
can land first.

Core:
- features.rs: a "linux" device of unknown kind is a desktop, and
  DeviceFacts::for_os builds facts for an OS, with a test that Linux gets
  the same feature matrix as Windows.
- deck.rs: launch tiles also take .desktop and .AppImage launchers and
  absolute or ~/ Unix paths. The check is about the path's shape only, so
  a saved tile is kept or dropped the same way however the app starts:
  no "..", no shells or .sh scripts, nothing under /etc, /tmp, /var/tmp,
  /dev, /proc or /sys. The PC expands ~/ when it launches.
- identity.rs: the key file is written 0600 on Unix.
- handoff.rs: no panic on text with multi-byte characters at the byte
  positions it slices.
- node.rs:
  - A USB tunnel to a paired device starts and wakes its supervisor
    rather than dialing on the side, and a LAN dial in flight gives way
    when a tunnel comes up (dial_with_usb_interrupt). A dial that's
    dropped closes its connection (CloseOnDrop).
  - The higher ID skips its 750 ms wait when a tunnel is already there.
  - register_session skips connections that are already closed, and
    publish_online only reports a session that's still alive.
  - Loopback addresses from USB tunnels are no longer saved as a device's
    last addresses.
  - The event channel holds 2048 events instead of 512.
- session.rs: PC audio sent over the control stream drops frames when the
  outbox is half full (try_send_realtime), so it can't hold up heartbeats
  and requests. Session::request's timeout covers sending as well as the
  reply.
- A two-device test for a LAN session moving to USB, audio frames dropped
  under load while a ring gets through, and reconnecting after a restart.

FFI: after the event stream lags, the app gets fresh link, wake, info,
capability and queue state for every paired device.

PC app:
- file_url handles Unix paths and keeps Windows drive and UNC paths as
  they were.
- The local API token comes from win::secure_random.
- Off Windows: no DWM backdrop, vector caption glyphs instead of Segoe
  Fluent Icons, and Linux wording and file filters for launch tiles.

Signed-off-by: Jochen Kirstätter <7329802+jochenkirstaetter@users.noreply.github.com>
… Ubuntu 24.04

Signed-off-by: Jochen Kirstätter <7329802+jochenkirstaetter@users.noreply.github.com>
Signed-off-by: Jochen Kirstätter <7329802+jochenkirstaetter@users.noreply.github.com>
…ual mic hookup

Signed-off-by: Jochen Kirstätter <7329802+jochenkirstaetter@users.noreply.github.com>
…on for Command Palette

Signed-off-by: Jochen Kirstätter <7329802+jochenkirstaetter@users.noreply.github.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.

Hi @jochenkirstaetter, thanks for this, and for rebasing it onto #23 so quickly. It's a huge amount of work, and a lot of it is careful: the virtual microphone already lives in $XDG_RUNTIME_DIR with a quoted, sanitized description, the updater still checks the release's SHA-256 before it installs anything, and the phone mic hooks straight into the changes from main.

I went through it file by file, starting with what Windows shares, then the Linux parts that run as root or touch files, and checked a few things against Qt 6.12 rather than guess. Here's what I found.

Must change before merge

  1. Turning on a phone's storage also shares this PC's folders (desktop/app/src/bridge/app.rs, set_toggle). When storage is turned on for a device, it now calls set_share_pc_storage(true). That's the same issue as point 1 in the first #23 review: on a PC that toggle means "browse that phone here", and Your PC in your pocket has to stay its own choice. It came back through this PR; please remove those three lines.

  2. The .deb update runs as root on a file the user can swap (updater.rs, Linux install_with_prompt). The download is checked against its SHA-256 in ~/.cache/nectarlink/updates, then pkexec dpkg -i runs on that same file. Anything running as the user can replace it between the check and dpkg, and its maintainer scripts then run as root. Windows avoids this on purpose: the task copies the installer to a folder only SYSTEM can write and checks it there. The Linux equivalent is to have the root step copy the file somewhere only root can write and check the hash there before dpkg reads it. (Related: since every release has an AppImage, LINUX_FALLBACK_SUFFIXES picks it first, so a .deb install never gets to the .deb branch at all.)

  3. A Qt 6.4 guard changes Windows rendering (IslandWindow.qml, two places). preferredRendererType = 2 was meant to be Shape.CurveRenderer, but I checked on Qt 6.12: Shape.CurveRenderer is 4, and 2 is the old NVPR renderer, so the Island's shapes lose curve rendering on Windows and on Linux alike. Icon.qml, Spinner.qml, HomePage.qml, CaptionButtons.qml and SadBee.qml still use Shape.CurveRenderer directly, so the Qt 6.4 support is also incomplete. This is really the question from the first round: ship our pinned Qt with the Linux build, or support the distribution's Qt. If we bundle Qt (see 4), none of these guards are needed and the shared QML can stay as it is. I'd rather go that way.

  4. The packages don't include Qt. build-appimage.sh puts only the binary, the .qm files and a placeholder hexagon icon into the AppDir, and build-deb.sh doesn't list any Qt 6 packages in Depends. On a clean system neither starts. The AppImage needs Qt and its QML modules bundled (linuxdeploy with its Qt plugin does this), the .deb needs the Qt 6 runtime and QML packages in Depends or a bundled copy, the icon should be the real one from assets/brand/ rather than the inline hexagon, the .qm files belong under /usr/share/nectarlink rather than /usr/bin, and the maintainer address should be one that reaches someone (nectarlink.org isn't a domain the project uses).

  5. FFmpeg structs read at fixed offsets without a version check (linux/h264.rs). The offsets for AVPacket (0x18, 0x20) and AVFrame (0x40, 0x68, 0x6c, 0x74) do match FFmpeg 4 to 7, but the loader also accepts a bare libavcodec.so, which can be any version, and nothing checks avcodec_version() before reading them. A future FFmpeg would mean reading the wrong memory rather than an error. Please check the major version against the ones the offsets were taken from and refuse anything else, and do the same for libopenh264 (WelsGetCodecVersion), whose hand-written structs in pc_mirror.rs also follow one version's headers.

  6. The Linux CI job fails. Six tests need libraries the job doesn't install: libopenh264 for the encoder, libavcodec for the decoder, and the ffmpeg command for converting recordings. I'd install them in the job (ffmpeg and libopenh264-7 on Ubuntu 24.04) so the tests keep running the real code rather than skipping. Two of them are still named after Media Foundation (…_with_mft, media_foundation_…) and could use neutral names now. Also, without ffmpeg, choosing MP3 for recordings saves a different format without saying so; the app should tell the user.

  7. Uninstalling goes around the package manager (setup.rs, Linux remove). It runs pkexec sh -c "rm -f /usr/bin/nectarlink-desktop ...", which leaves dpkg thinking the package is still installed. For a package install it should be dpkg -r (or apt remove), and only the tarball install should remove files by hand. The uninstall.exe check above it can go on Linux, and the messages there ("Uninstall stopped with code …") aren't translatable yet.

Worth changing

  • Phone storage on Linux is a sync loop, not a mount. Each pass lists the phone's whole tree, downloads every file up to 512 KB, never shows larger files at all, and pushes local edits back. On a phone with a large Download or WhatsApp folder that's a lot of traffic and battery, and a folder that silently leaves out big files is confusing. On Windows the files are placeholders that download when opened. On Linux, a GVfs backend or a small FUSE filesystem would give the same on-demand behavior. Either way, I'd move it into linux/storage.rs: storage.rs went from 2 cfg attributes to 81.
  • Wayland. Screen capture uses XGetImage, and input and window activation use xdotool, wmctrl and xprop, which only see XWayland windows. Ubuntu and Fedora default to Wayland, so PC mirroring and remote input fail there without saying why. Until there's a portal or libei path, please report those capabilities as unavailable on Wayland so the phone doesn't offer them.
  • External tools. The Linux backend runs about 60 different programs. A few things stand out: remote mouse movement starts an xdotool process per event, which adds latency at 60 events a second; the tray is a Python GTK script, while Qt's QSystemTrayIcon already speaks StatusNotifierItem on Linux; and the updater downloads through curl, where --proto =https --proto-redir =https and a -- before the URL would be good to have. Whatever stays should be listed in the package's Depends or Recommends, and the app should say when one is missing.
  • #[allow(dead_code)] without a cfg in mirror.rs, pc_audio_stream.rs and core_host.rs also hides real dead code on Windows. #[cfg_attr(target_os = "linux", allow(dead_code))] keeps the warning there.
  • build.rs bakes an absolute path into .qt6-sysroot as DT_RPATH (which also overrides LD_LIBRARY_PATH), hard-codes x86_64-linux-gnu and forces -fuse-ld=bfd. For packaging, a relative $ORIGIN rpath (or none, with the libraries bundled) works on more systems.
  • setup::installed() on Linux reports this program's own version, not the installed one.
  • Two Windows behavior changes that the description doesn't mention: the Command Palette hotkey now closes the palette when it's open, and the Shelf also shows images from received files. Both seem fine to me, but they should be in the description, or better, in their own PRs like #29 to #33.
  • CLI process watching polls /proc. pidfd_open (Linux 5.3 and later) gives a race-free wait if you want it.

Good as they are

  • The virtual microphone: $XDG_RUNTIME_DIR, the quoted and sanitized device.description with the phone's name, and the test with a hostile name.
  • The phone mic Sink building on ACTIVE and restart() from main, and choosing the virtual mic as the preferred "cable".
  • The updater keeping the strict asset name check and the SHA-256 check.
  • Storage names from the phone go through the core's is_valid_storage_name, so the sync can't write outside its folder.
  • The only new crate is libc.

How to move this forward

At almost 19,000 lines this is hard to review well in one piece, and the split you did for #29 to #33 worked really nicely. I'd suggest the same here, roughly in this order, so each part can land on its own:

  1. Build, packaging with a bundled Qt, and the CI job (which also settles point 3).
  2. Basics: tray, notifications, clipboard, autostart, single instance.
  3. Input and window handling, with the Wayland checks.
  4. Media: mirroring, audio, the mic and the camera, with the version checks from point 5.
  5. Phone storage.
  6. Updater and setup.

Happy to review each as it comes. Thanks again for pushing the Linux side this far.

…alignment and +9 dB makeup gain

Signed-off-by: Jochen Kirstätter <7329802+jochenkirstaetter@users.noreply.github.com>
@jochenkirstaetter
jochenkirstaetter marked this pull request as draft October 19, 2026 00:21
saivinay1805 added a commit to saivinay1805/nectarlink that referenced this pull request Oct 19, 2026
…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.
saivinay1805 added a commit to saivinay1805/nectarlink that referenced this pull request Oct 19, 2026
…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>
@jochenkirstaetter

Copy link
Copy Markdown
Contributor Author

Hey @thtbee and hello https://github.com/saivinay1805

As suggested, I fanned out this mega PR into six (6) smaller, fully isolated and stacked milestones.
Starting with #34 - this PR is an umbrella only.

Rebasing shall be relatively straight-forward.

Cheers

saivinay1805 added a commit to saivinay1805/nectarlink that referenced this pull request Oct 20, 2026
…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>
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