Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v7
- name: Install the pinned Rust toolchain
Expand Down Expand Up @@ -132,6 +132,30 @@ jobs:
path: target/package/*-setup.exe
retention-days: 14

desktop-macos:
name: macOS app
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- name: Install the pinned Rust toolchain
run: rustup show active-toolchain || rustup toolchain install
- uses: Swatinem/rust-cache@v2
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: ${{ env.QT_VERSION }}
host: mac
target: desktop
arch: clang_64
cache: true
aqtsource: git+https://github.com/miurahr/aqtinstall.git
- name: Clippy
run: cargo clippy --locked -p nectarlink-desktop --all-targets -- -D warnings
- name: Tests
run: cargo test --locked -p nectarlink-desktop
- name: Release build
run: cargo build --locked --release -p nectarlink-desktop

android:
name: Android app
runs-on: ubuntu-latest
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,23 @@ searchable **Timeline** of everything that moved, a **Command Palette**
Four themes, **Hive**, **Bloom**, **Glass** and **Graphite**, each in light
and dark.

### Platform support

| Feature | Windows | macOS (Preview) |
|---|---|---|
| Pairing & mutual TLS | Yes | Yes |
| Files & folder transfer | Yes | Yes |
| Photos browser & download | Yes | Yes |
| Continuity Camera | Yes | Yes |
| Calls & SMS viewer | Yes | Yes |
| Phone quick toggles & volume | Yes | Yes |
| Clipboard text sync | Yes | Yes (two-way text, incoming photo) |
| System notifications | Yes | Yes (system banners) |
| Phone screen mirroring | Yes | Not yet (in development) |
| Screen recording (MP4) | Yes | Not yet |
| PC audio stream / phone mic | Yes | Not yet |
| Storage mount in Explorer | Yes | Windows Cloud Files only |

## Private by design

- **Local-first.** Devices talk directly over your network or a USB cable.
Expand Down Expand Up @@ -228,6 +245,7 @@ docs/ Guide, technical docs, protocol, design system (CC B
|---|---|
| Core and CLI | [Rust](https://rustup.rs) (stable, pinned in `rust-toolchain.toml`) |
| Windows app | Windows 11, Visual Studio 2022 Build Tools (C++), [Qt 6.12](https://www.qt.io/download-open-source) for MSVC 2022 x64 |
| macOS app | macOS 14+, Xcode Command Line Tools, [Qt 6.12](https://www.qt.io/download-open-source) (or Homebrew Qt 6) |
| Android app | Android SDK (platform 37, NDK 30.0.16248370), JDK 17 or newer, `cargo-ndk` (see [android/README.md](android/README.md)) |

```sh
Expand Down
69 changes: 69 additions & 0 deletions core/nectarlink-core/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1461,4 +1461,73 @@ mod tests {
);
assert_eq!(matrix.state("device.find_phone"), Some(FeatureState::Available));
}

#[test]
fn macos_desktop_matrix_reflects_supported_and_unsupported_features() {
const MAC_DESKTOP: &[&str] = &[
"core.ping",
"device.battery",
"device.ring",
"files.transfer",
"clip.image",
"media.remote",
"link.open",
"task.notify",
"camera.continuity",
"pc.power",
"photos.show",
"calls.show",
"contacts.show",
"sms.show",
"toggles.show",
];
let mac = facts(DeviceKind::Desktop, "macos", "15.0", PowerLevel::NotApplicable, MAC_DESKTOP);
let mut phone_caps = BASIC_PHONE.to_vec();
phone_caps.extend(["link.open", "storage.read", "storage.write"]);
let phone = facts(DeviceKind::Phone, "android", "16", PowerLevel::Elevated, &phone_caps);
let m = matrix_with(&phone, &mac, &HashMap::new());

// Supported macOS features are Available:
assert_eq!(m.state("device.find_phone"), Some(FeatureState::Available));
assert_eq!(m.state("device.battery"), Some(FeatureState::Available));
assert_eq!(m.state("device.pc_actions"), Some(FeatureState::Available));
assert_eq!(m.state("device.links_to_pc"), Some(FeatureState::Available));
assert_eq!(m.state("device.links_to_phone"), Some(FeatureState::Available));
assert_eq!(m.state("camera.continuity"), Some(FeatureState::Available));
assert_eq!(m.state("files.send"), Some(FeatureState::Available));
assert_eq!(m.state("files.recent_photos"), Some(FeatureState::Available));
assert_eq!(m.state("calls.alerts"), Some(FeatureState::Available));
assert_eq!(m.state("contacts.read"), Some(FeatureState::Available));
assert_eq!(m.state("toggles.volume"), Some(FeatureState::Available));
assert_eq!(m.state("clipboard.pc_to_phone"), Some(FeatureState::Available));

// Unsupported macOS features are NOT Available:
assert_eq!(
m.state("mirroring.view"),
Some(locked(UpgradeAction::UpdateApp(Role::Desktop), Effort::Minutes(2)))
);
assert_ne!(m.state("mirroring.control"), Some(FeatureState::Available));
assert_ne!(m.state("mirroring.app_windows"), Some(FeatureState::Available));
assert_ne!(m.state("mirroring.audio"), Some(FeatureState::Available));
assert_eq!(
m.state("media.pc_control"),
Some(locked(UpgradeAction::UpdateApp(Role::Desktop), Effort::Minutes(2)))
);
// With default toggles, storage is opt-in (toggle off):
assert_eq!(
m.state("files.storage"),
Some(locked(UpgradeAction::EnableDeviceToggle("storage"), Effort::Instant))
);
// With storage toggle enabled, missing desktop("storage.mount") locks behind UpdateApp:
let toggles_with_storage = HashMap::from([("storage".to_owned(), true)]);
let m_storage = matrix_with(&phone, &mac, &toggles_with_storage);
assert_eq!(
m_storage.state("files.storage"),
Some(locked(UpgradeAction::UpdateApp(Role::Desktop), Effort::Minutes(2)))
);
assert_eq!(
m.state("camera.webcam"),
Some(locked(UpgradeAction::EnableAddon("vcam"), Effort::Minutes(1)))
);
}
}
1 change: 0 additions & 1 deletion core/nectarlink-core/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ const BASE_CAPABILITIES: &[&str] = &[
"media.remote",
"link.open",
"task.notify",
"mirror.view",
];

/// This device's mutable description, sent to peers.
Expand Down
18 changes: 13 additions & 5 deletions desktop/app/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,17 @@ fn main() {
])
.build();

// The C++ window code calls DWM, Shell property store, and user32 directly.
println!("cargo:rustc-link-lib=dwmapi");
println!("cargo:rustc-link-lib=ole32");
println!("cargo:rustc-link-lib=shell32");
println!("cargo:rustc-link-lib=user32");
// The C++ window code calls DWM, Shell property store, and user32 directly on Windows.
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
println!("cargo:rustc-link-lib=dwmapi");
println!("cargo:rustc-link-lib=ole32");
println!("cargo:rustc-link-lib=shell32");
println!("cargo:rustc-link-lib=user32");
} else if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
println!("cargo:rustc-link-lib=framework=AppKit");
println!("cargo:rustc-link-lib=framework=CoreGraphics");
println!("cargo:rustc-link-lib=framework=CoreAudio");
println!("cargo:rustc-link-lib=framework=AudioToolbox");
println!("cargo:rustc-link-lib=framework=IOKit");
}
}
Loading
Loading