feat(desktop): port device registry and error type to Tauri 2

Splits the former 478-line main.rs into focused modules. Drops hashbrown
in favour of std HashMap (the drain_filter API it was used for was
renamed with changed semantics) and drops vendored libusb1-sys in favour
of the system library, matching nlink-cli.

Also removes the Task 2 scaffold's lib.rs/tauri-plugin-opener indirection
(the brief's module layout is a plain binary with no lib target) and drops
the now-dangling "opener:default" permission from capabilities/default.json,
which otherwise fails the Tauri build script with an unresolved permission
error.
This commit is contained in:
Your Name 2026-07-19 09:44:22 -04:00
parent 4626858fa5
commit 816300fbcc
7 changed files with 784 additions and 430 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,25 +1,37 @@
[package] [package]
name = "n-link" name = "n-link"
version = "0.1.0" version = "0.1.6"
description = "A Tauri App" description = "Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire"
authors = ["you"] authors = ["Ben Schattinger <developer@lights0123.com>"]
license = "GPL-3.0"
repository = "https://github.com/lights0123/n-link"
default-run = "n-link"
edition = "2021" edition = "2021"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "n_link_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "2.11.5", features = [] }
tauri-plugin-opener = "2" tauri-plugin-dialog = "2.7.2"
serde = { version = "1", features = ["derive"] } tauri-plugin-shell = "2.3.5"
serde_json = "1" serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"
lazy_static = "1.4"
libnspire = "0.2.3"
rusb = "0.6.4"
clap = { version = "4", features = ["derive"] }
indicatif = "0.15"
[patch.crates-io]
libnspire-sys = { path = "../../vendor/libnspire-sys" }
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[[bin]]
name = "n-link"
path = "src/main.rs"

View file

@ -4,7 +4,6 @@
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": ["main"],
"permissions": [ "permissions": [
"core:default", "core:default"
"opener:default"
] ]
} }

View file

@ -0,0 +1,113 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
use libnspire::{PID, PID_CX2, VID};
use rusb::GlobalContext;
use serde::{Deserialize, Serialize};
pub enum DeviceState {
Open(
Arc<Mutex<libnspire::Handle<GlobalContext>>>,
libnspire::info::Info,
),
Closed,
}
pub struct Device {
pub name: String,
pub device: Arc<rusb::Device<GlobalContext>>,
pub state: DeviceState,
pub needs_drivers: bool,
}
lazy_static::lazy_static! {
pub static ref DEVICES: RwLock<HashMap<(u8, u8), Device>> = RwLock::new(HashMap::new());
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
pub struct DevId {
pub bus_number: u8,
pub address: u8,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AddDevice {
#[serde(flatten)]
pub dev: DevId,
pub name: String,
pub is_cx_ii: bool,
pub needs_drivers: bool,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgressUpdate {
#[serde(flatten)]
pub dev: DevId,
pub remaining: usize,
pub total: usize,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileInfo {
pub path: String,
pub is_dir: bool,
pub date: u64,
pub size: u64,
}
pub fn add_device(dev: Arc<rusb::Device<GlobalContext>>) -> rusb::Result<((u8, u8), Device)> {
let descriptor = dev.device_descriptor()?;
if !(descriptor.vendor_id() == VID && matches!(descriptor.product_id(), PID | PID_CX2)) {
return Err(rusb::Error::Other);
}
let (name, needs_drivers) = match dev.open() {
Ok(handle) => (
handle.read_product_string(
handle.read_languages(Duration::from_millis(100))?[0],
&descriptor,
Duration::from_millis(100),
)?,
false,
),
Err(rusb::Error::NotSupported) | Err(rusb::Error::Access) => (
if descriptor.product_id() == PID_CX2 {
"TI-Nspire CX II"
} else {
"TI-Nspire"
}
.to_string(),
true,
),
Err(other) => return Err(other),
};
Ok((
(dev.bus_number(), dev.address()),
Device {
name,
device: dev,
state: DeviceState::Closed,
needs_drivers,
},
))
}
pub fn get_open_dev(
dev: &DevId,
) -> Result<Arc<Mutex<libnspire::Handle<GlobalContext>>>, anyhow::Error> {
if let Some(dev) = DEVICES.read().unwrap().get(&(dev.bus_number, dev.address)) {
match &dev.state {
DeviceState::Open(handle, _) => Ok(handle.clone()),
DeviceState::Closed => anyhow::bail!("Device closed"),
}
} else {
anyhow::bail!("Failed to find device");
}
}

View file

@ -0,0 +1,12 @@
use serde::Serialize;
/// Wraps any displayable error into a serde-serializable form so it can
/// cross the Tauri IPC boundary.
#[derive(Serialize)]
pub struct SerializedError(String);
impl<T: std::fmt::Display> From<T> for SerializedError {
fn from(f: T) -> Self {
SerializedError(f.to_string())
}
}

View file

@ -1,14 +0,0 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View file

@ -1,6 +1,10 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod device;
mod error;
fn main() { fn main() {
n_link_lib::run() tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error while running tauri application");
} }