From 4c629aaf537a85146d3fb7c6fdb937949629b9f9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 19 Jul 2026 19:38:31 -0400 Subject: [PATCH] feat(desktop): wire Tauri 2 builder, hotplug, and capabilities Hotplug registration moves from on_page_load to setup (runs once, so the AtomicBool guard is dropped). v1 allowlist replaced by a capability file granting dialog:allow-open and shell:allow-open. CSP from 0472908 carried forward to app.security.csp; bundle targets narrowed to deb + appimage. Also derives Clone on device::AddDevice: Tauri 2's Emitter::emit requires Serialize + Clone (v1's did not), and the addDevice event payload was missing the derive. No wire-format or IPC contract change. Co-Authored-By: Claude Opus 4.8 --- desktop/src-tauri/capabilities/default.json | 6 +- desktop/src-tauri/src/device.rs | 2 +- desktop/src-tauri/src/main.rs | 114 ++++++++++++++++++++ desktop/src-tauri/tauri.conf.json | 18 +++- 4 files changed, 132 insertions(+), 8 deletions(-) diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json index b9ec4c1..fda1fe9 100644 --- a/desktop/src-tauri/capabilities/default.json +++ b/desktop/src-tauri/capabilities/default.json @@ -1,9 +1,11 @@ { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", - "description": "Capability for the main window", + "description": "Core, dialog, and shell permissions for the main window", "windows": ["main"], "permissions": [ - "core:default" + "core:default", + "dialog:allow-open", + "shell:allow-open" ] } diff --git a/desktop/src-tauri/src/device.rs b/desktop/src-tauri/src/device.rs index 8e51560..bff3cc3 100644 --- a/desktop/src-tauri/src/device.rs +++ b/desktop/src-tauri/src/device.rs @@ -33,7 +33,7 @@ pub struct DevId { pub address: u8, } -#[derive(Debug, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AddDevice { #[serde(flatten)] diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 38c0477..86bebdc 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -1,11 +1,125 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use std::sync::Arc; +use std::time::Duration; + +use libnspire::{PID_CX2, VID}; +use rusb::{GlobalContext, Hotplug, UsbContext}; +use tauri::{Emitter, Manager, Runtime, WebviewWindow}; + +use crate::device::{add_device, AddDevice, DevId, DEVICES}; + mod commands; mod device; mod error; +struct DeviceMon { + window: WebviewWindow, +} + +impl Hotplug for DeviceMon { + fn device_arrived(&mut self, device: rusb::Device) { + let handle = self.window.clone(); + let is_cx_ii = device + .device_descriptor() + .map(|d| d.product_id() == PID_CX2) + .unwrap_or(false); + let device = Arc::new(device); + // The calculator often reports Busy immediately after enumeration, so + // retry until it settles. + std::thread::spawn(move || loop { + match add_device(device.clone()) { + Ok(dev) => { + let name = (dev.1).name.clone(); + let needs_drivers = (dev.1).needs_drivers; + DEVICES.write().unwrap().insert(dev.0, dev.1); + if let Err(msg) = handle.emit( + "addDevice", + AddDevice { + dev: DevId { + bus_number: (dev.0).0, + address: (dev.0).1, + }, + name, + is_cx_ii, + needs_drivers, + }, + ) { + eprintln!("{}", msg); + }; + return; + } + Err(rusb::Error::Busy) => { + println!("busy"); + } + Err(e) => { + eprintln!("{}", e); + return; + } + } + std::thread::sleep(Duration::from_millis(250)); + }); + } + + fn device_left(&mut self, device: rusb::Device) { + if let Some((dev, _)) = DEVICES + .write() + .unwrap() + .remove_entry(&(device.bus_number(), device.address())) + { + if let Err(msg) = self.window.emit( + "removeDevice", + DevId { + bus_number: dev.0, + address: dev.1, + }, + ) { + eprintln!("{}", msg); + }; + } + } +} + fn main() { tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_shell::init()) + .setup(|app| { + // Tauri 1 registered this in on_page_load, guarded by an AtomicBool to + // avoid double-registering on reload. In v2, setup runs exactly once, + // so the guard is unnecessary. + let window = app + .get_webview_window("main") + .expect("main window not found"); + if rusb::has_hotplug() { + if let Err(msg) = + GlobalContext::default().register_callback(Some(VID), None, None, Box::new(DeviceMon { window })) + { + eprintln!("{}", msg); + }; + std::thread::spawn(|| loop { + GlobalContext::default().handle_events(None).unwrap(); + }); + } else { + println!("no hotplug"); + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + commands::enumerate, + commands::open_device, + commands::close_device, + commands::update_device, + commands::list_dir, + commands::download_file, + commands::upload_file, + commands::upload_os, + commands::delete_file, + commands::delete_dir, + commands::create_nspire_dir, + commands::move_file, + commands::copy, + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 68fd1b5..2551495 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "n-link", - "version": "0.1.0", + "version": "0.1.6", "identifier": "com.lights0123.n-link", "build": { "beforeDevCommand": "npm run dev", @@ -12,13 +12,15 @@ "app": { "windows": [ { - "title": "n-link", + "title": "N-Link", "width": 800, - "height": 600 + "height": 600, + "resizable": true, + "fullscreen": false } ], "security": { - "csp": null + "csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src 'self' asset: http://asset.localhost blob: data:" } }, "bundle": { @@ -30,6 +32,12 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "resources": [], + "externalBin": [], + "copyright": "Copyright (c) 2021 Ben Schattinger. Licensed under GPL-3.0", + "category": "Utility", + "shortDescription": "Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire", + "longDescription": "Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire" } }