8
.gitignore
vendored
|
|
@ -23,3 +23,11 @@ pnpm-debug.log*
|
|||
*.sw?
|
||||
|
||||
*.tns
|
||||
.superpowers/
|
||||
|
||||
# build artifacts
|
||||
node_modules/
|
||||
target/
|
||||
dist/
|
||||
desktop/src-tauri/icons/android/
|
||||
desktop/src-tauri/icons/ios/
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/essential',
|
||||
'eslint:recommended',
|
||||
'@vue/typescript/recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? ['warn', { allow: ['warn', 'error'] }] : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
}
|
||||
}
|
||||
24
desktop/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -1,32 +1,99 @@
|
|||
# N-Link
|
||||
|
||||
Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire
|
||||
Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire.
|
||||
|
||||
## Project setup
|
||||
You'll need to install [Node.js](https://nodejs.org/en/download/package-manager),
|
||||
[Yarn](https://classic.yarnpkg.com/en/docs/install/) (which may be included with your installation of Node.js), and
|
||||
[Rust](https://rustup.rs/). You'll also need a
|
||||
[C compiler](https://github.com/alexcrichton/cc-rs#compile-time-requirements) available. On Linux, you'll need
|
||||
`webkit2gtk` and `squashfs-tools`, which, on Ubuntu, can be installed with
|
||||
`sudo apt install webkit2gtk-4.0 squashfs-tools`.
|
||||
```
|
||||
yarn
|
||||
This package is a **Tauri 2 + Vue 3 + Vite** application. It was ported from
|
||||
Tauri 1.0.0-beta.8 / Vue CLI, which linked `webkit2gtk-4.0` and could not build
|
||||
or run on current distributions (Ubuntu 26.04 ships only `webkit2gtk-4.1`).
|
||||
|
||||
Currently Linux-only: bundle targets are `deb` and `appimage`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) 22+ (npm ships with it — this project uses
|
||||
**npm workspaces**, not yarn)
|
||||
- [Rust](https://rustup.rs/) 1.97+
|
||||
- System libraries (Ubuntu/Debian):
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
libsoup-3.0-dev \
|
||||
librsvg2-dev \
|
||||
libxdo-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
libusb-1.0-0-dev \
|
||||
build-essential curl wget file
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn tauri:serve
|
||||
Verify they resolve before building:
|
||||
|
||||
```bash
|
||||
for p in webkit2gtk-4.1 javascriptcoregtk-4.1 libsoup-3.0 gtk+-3.0 libusb-1.0; do
|
||||
printf "%-24s " "$p"; pkg-config --modversion "$p" || echo MISSING
|
||||
done
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn tauri:build
|
||||
## Setup
|
||||
|
||||
Run from the **repository root** (this is a workspace member):
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn lint
|
||||
`--legacy-peer-deps` is no longer needed — `n-link-core` is on Vue 3 as of the
|
||||
frontend port.
|
||||
|
||||
Note that the root `package.json` workspaces list contains only `desktop` and
|
||||
`n-link-core`. `web/` is deliberately excluded: it is still on Nuxt 2 and pulls
|
||||
`tailwindcss@1.9.6`, which npm hoists to the root and which then shadows the
|
||||
`tailwindcss@3` that `desktop` and `@tailwindcss/forms` require. Restore the
|
||||
entry when `web` is migrated.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cd desktop
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
## Production build
|
||||
|
||||
```bash
|
||||
cd desktop
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
Produces `src-tauri/target/release/bundle/deb/*.deb` and
|
||||
`.../appimage/*.AppImage`.
|
||||
|
||||
## Headless CLI
|
||||
|
||||
The same binary works without a GUI:
|
||||
|
||||
```bash
|
||||
./src-tauri/target/release/n-link --help
|
||||
./src-tauri/target/release/n-link ls /
|
||||
```
|
||||
|
||||
## Notes and gotchas
|
||||
|
||||
- **`cargo` commands need `dist/` to exist.** Tauri's `generate_context!` macro
|
||||
validates the `frontendDist` path at compile time, so a bare
|
||||
`cargo check` / `cargo build` in `src-tauri/` fails on a fresh clone until the
|
||||
frontend has been built once: `npm run build --workspace desktop`.
|
||||
`npm run tauri build` does this for you.
|
||||
- **Do not run the binary from a snap-confined shell** (for example, the
|
||||
integrated terminal of VS Code installed as a snap). Snap injects an
|
||||
`LD_LIBRARY_PATH` whose `libpthread` shadows the system one, producing
|
||||
`undefined symbol: __libc_pthread_init`. Use a normal terminal.
|
||||
- **AppImage needs FUSE.** Without it, run the AppImage with
|
||||
`--appimage-extract` and launch `squashfs-root/AppRun` instead.
|
||||
- **`vendor/libnspire-sys`** at the repo root is a locally patched crate wired in
|
||||
via `[patch.crates-io]`. It raises the CX II NNSE handshake retry limit
|
||||
(10 → 30). The build will not reproduce upstream behaviour without it.
|
||||
- **Known upstream limitation:** libnspire's CX II support has been frozen since
|
||||
2020. Against current CX II firmware, data transfers may return `Busy` or hang.
|
||||
This is independent of the Tauri version and is not a defect in this port.
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
13
desktop/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + Vue + Typescript App</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,44 +1,34 @@
|
|||
{
|
||||
"name": "desktop",
|
||||
"description": "Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire",
|
||||
"version": "0.1.6",
|
||||
"name": "n-link",
|
||||
"private": true,
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "vue-cli-service lint",
|
||||
"tauri:build": "vue-cli-service tauri:build",
|
||||
"tauri:serve": "vue-cli-service tauri:serve"
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/custom-forms": "^0.2.1",
|
||||
"core-js": "^3.6.5",
|
||||
"element-ui": "^2.13.2",
|
||||
"feather-icons": "^4.28.0",
|
||||
"filesize": "^6.1.0",
|
||||
"n-link-core": "0.0.0",
|
||||
"@tauri-apps/api": "^1.0.0-beta.8",
|
||||
"vue": "^2.6.11",
|
||||
"vue-async-computed": "^3.9.0",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-property-decorator": "^9.0.2",
|
||||
"vue-router": "^3.2.0"
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.2",
|
||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||
"element-plus": "^2.14.3",
|
||||
"feather-icons": "^4.29.2",
|
||||
"filesize": "^10.1.6",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
||||
"@typescript-eslint/parser": "^4.4.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"raw-loader": "^4.0.1",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typescript": "~4.0.3",
|
||||
"vue-cli-plugin-tailwind": "~1.5.0",
|
||||
"vue-cli-plugin-tauri": "^1.0.0-beta.6",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"autoprefixer": "^10.5.4",
|
||||
"postcss": "^8.5.21",
|
||||
"sass": "^1.101.3",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.3",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
desktop/postcss.config.cjs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
module.exports = require('n-link-core/postcss.config');
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
|
|
@ -1,17 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
11
desktop/src-tauri/.gitignore
vendored
|
|
@ -1,12 +1,7 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
WixTools
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
config.json
|
||||
bundle.json
|
||||
|
||||
/.cargo
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
|
|
|
|||
5351
desktop/src-tauri/Cargo.lock
generated
|
|
@ -2,32 +2,35 @@
|
|||
name = "n-link"
|
||||
version = "0.1.6"
|
||||
description = "Free, cross-platform, CX-II compatible computer linking program for the TI-Nspire"
|
||||
authors = [ "Ben Schattinger <developer@lights0123.com>" ]
|
||||
authors = ["Ben Schattinger <developer@lights0123.com>"]
|
||||
license = "GPL-3.0"
|
||||
repository = "https://github.com/lights0123/n-link"
|
||||
default-run = "n-link"
|
||||
edition = "2018"
|
||||
build = "src/build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.32"
|
||||
serde_json = "1.0"
|
||||
libnspire = "0.2.2"
|
||||
lazy_static = "1.4.0"
|
||||
rusb = "0.6.4"
|
||||
serde = { version = "1.0", features = [ "derive" ] }
|
||||
tauri = { version = "1.0.0-beta.8", features = ["dialog-open", "dialog-save", "notification-all", "shell-open"] }
|
||||
clap = "3.0.0-beta.2"
|
||||
indicatif = "0.15"
|
||||
libusb1-sys = { version = "0.4.2", features = [ "vendored" ] }
|
||||
hashbrown = "0.11"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.0.0-beta.4" }
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.11.5", features = [] }
|
||||
tauri-plugin-dialog = "2.7.2"
|
||||
tauri-plugin-shell = "2.3.5"
|
||||
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]
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
default = [ "custom-protocol" ]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[[bin]]
|
||||
name = "n-link"
|
||||
|
|
|
|||
3
desktop/src-tauri/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
11
desktop/src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Core, dialog, and shell permissions for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"dialog:allow-open",
|
||||
"shell:allow-open"
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
desktop/src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 47 KiB |
|
|
@ -1,13 +0,0 @@
|
|||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
newline_style = "Auto"
|
||||
use_small_heuristics = "Default"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
remove_nested_parens = true
|
||||
edition = "2018"
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
|
@ -3,18 +3,18 @@ use std::io::{Read, Write};
|
|||
use std::path::PathBuf;
|
||||
use std::{fs::File, path::Path};
|
||||
|
||||
use clap::Clap;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use libnspire::{dir::EntryType, PID, PID_CX2, VID};
|
||||
|
||||
#[derive(Clap, Debug)]
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, about, version)]
|
||||
struct Opt {
|
||||
#[clap(subcommand)]
|
||||
cmd: Option<SubCommand>,
|
||||
}
|
||||
|
||||
#[derive(Clap, Debug)]
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum SubCommand {
|
||||
Upload(Upload),
|
||||
Download(Download),
|
||||
|
|
@ -29,31 +29,31 @@ enum SubCommand {
|
|||
}
|
||||
|
||||
/// Upload files to the calculator
|
||||
#[derive(Clap, Debug)]
|
||||
#[derive(Args, Debug)]
|
||||
struct Upload {
|
||||
/// Files to upload
|
||||
#[clap(required = true, parse(from_os_str))]
|
||||
#[clap(required = true)]
|
||||
files: Vec<PathBuf>,
|
||||
/// Destination path
|
||||
dest: String,
|
||||
}
|
||||
|
||||
/// Download files from the calculator
|
||||
#[derive(Clap, Debug)]
|
||||
#[derive(Args, Debug)]
|
||||
struct Download {
|
||||
/// Files to download
|
||||
#[clap(required = true)]
|
||||
files: Vec<String>,
|
||||
/// Destination path
|
||||
#[clap(required = true, parse(from_os_str))]
|
||||
#[clap(required = true)]
|
||||
dest: PathBuf,
|
||||
}
|
||||
|
||||
/// Upload and install a .tcc/.tco/.tcc2/.tco2/.tct2 OS file
|
||||
#[derive(Clap, Debug)]
|
||||
#[derive(Args, Debug)]
|
||||
struct UploadOS {
|
||||
/// Path to the OS file
|
||||
#[clap(required = true, parse(from_os_str))]
|
||||
#[clap(required = true)]
|
||||
file: PathBuf,
|
||||
|
||||
/// Disables the file extension check
|
||||
|
|
@ -62,7 +62,7 @@ struct UploadOS {
|
|||
}
|
||||
|
||||
/// Copy a file to a different location
|
||||
#[derive(Clap, Debug)]
|
||||
#[derive(Args, Debug)]
|
||||
struct Copy {
|
||||
/// Path to file
|
||||
#[clap(required = true)]
|
||||
|
|
@ -74,7 +74,7 @@ struct Copy {
|
|||
}
|
||||
|
||||
/// Move a file or directory to a new location
|
||||
#[derive(Clap, Debug)]
|
||||
#[derive(Args, Debug)]
|
||||
struct Move {
|
||||
/// Path to file
|
||||
#[clap(required = true)]
|
||||
|
|
@ -86,7 +86,7 @@ struct Move {
|
|||
}
|
||||
|
||||
/// Create a directory
|
||||
#[derive(Clap, Debug)]
|
||||
#[derive(Args, Debug)]
|
||||
struct Mkdir {
|
||||
/// Path to directory
|
||||
#[clap(required = true)]
|
||||
|
|
@ -94,7 +94,7 @@ struct Mkdir {
|
|||
}
|
||||
|
||||
/// Delete a directory
|
||||
#[derive(Clap, Debug)]
|
||||
#[derive(Args, Debug)]
|
||||
struct Rmdir {
|
||||
/// Path to directory
|
||||
#[clap(required = true)]
|
||||
|
|
@ -102,7 +102,7 @@ struct Rmdir {
|
|||
}
|
||||
|
||||
/// List the contents of a directory
|
||||
#[derive(Clap, Debug)]
|
||||
#[derive(Args, Debug)]
|
||||
struct Ls {
|
||||
/// Path to directory
|
||||
#[clap(required = true)]
|
||||
|
|
|
|||
421
desktop/src-tauri/src/commands.rs
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use libnspire::dir::EntryType;
|
||||
use libnspire::PID_CX2;
|
||||
use serde::Serialize;
|
||||
use tauri::{Emitter, Runtime, WebviewWindow};
|
||||
|
||||
use crate::device::{
|
||||
add_device, get_open_dev, AddDevice, DevId, DeviceState, FileInfo, ProgressUpdate, DEVICES,
|
||||
};
|
||||
use crate::error::SerializedError;
|
||||
|
||||
/// If the device vanished mid-operation, drop it from the registry and tell
|
||||
/// the frontend, then pass the original result through unchanged.
|
||||
fn err_wrap<T, R: Runtime>(
|
||||
res: Result<T, libnspire::Error>,
|
||||
dev: DevId,
|
||||
window: &WebviewWindow<R>,
|
||||
) -> Result<T, libnspire::Error> {
|
||||
if let Err(libnspire::Error::NoDevice) = res {
|
||||
DEVICES
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&(dev.bus_number, dev.address));
|
||||
if let Err(msg) = window.emit("removeDevice", dev) {
|
||||
eprintln!("{}", msg);
|
||||
};
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Emits a `progress` event on every 6th callback, and always on completion,
|
||||
/// to avoid flooding the IPC channel during large transfers.
|
||||
fn progress_sender<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
dev: DevId,
|
||||
total: usize,
|
||||
) -> impl FnMut(usize) + '_ {
|
||||
let mut i = 0;
|
||||
move |remaining| {
|
||||
if i > 5 {
|
||||
i = 0;
|
||||
}
|
||||
if i == 0 || remaining == 0 {
|
||||
if let Err(msg) = window.emit(
|
||||
"progress",
|
||||
ProgressUpdate {
|
||||
dev,
|
||||
remaining,
|
||||
total,
|
||||
},
|
||||
) {
|
||||
eprintln!("{}", msg);
|
||||
};
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn enumerate<R: Runtime>(window: WebviewWindow<R>) -> Result<Vec<AddDevice>, SerializedError> {
|
||||
let devices: Vec<_> = rusb::devices()?.iter().collect();
|
||||
let mut map = DEVICES.write().unwrap();
|
||||
|
||||
// Replaces hashbrown's `drain_filter`: collect the keys of devices that are
|
||||
// no longer present on the bus, then remove them. std HashMap has no stable
|
||||
// drain-with-predicate, and this is clearer than the original anyway.
|
||||
let stale: Vec<(u8, u8)> = map
|
||||
.keys()
|
||||
.filter(|k| {
|
||||
devices
|
||||
.iter()
|
||||
.all(|d| d.bus_number() != k.0 || d.address() != k.1)
|
||||
})
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
for key in stale {
|
||||
map.remove(&key);
|
||||
if let Err(msg) = window.emit(
|
||||
"removeDevice",
|
||||
DevId {
|
||||
bus_number: key.0,
|
||||
address: key.1,
|
||||
},
|
||||
) {
|
||||
eprintln!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
let filtered: Vec<_> = devices
|
||||
.into_iter()
|
||||
.filter(|d| !map.contains_key(&(d.bus_number(), d.address())))
|
||||
.collect();
|
||||
|
||||
Ok(
|
||||
filtered
|
||||
.into_iter()
|
||||
.filter_map(|dev| add_device(Arc::new(dev)).ok())
|
||||
.map(|dev| {
|
||||
let msg = AddDevice {
|
||||
dev: DevId {
|
||||
bus_number: (dev.0).0,
|
||||
address: (dev.0).1,
|
||||
},
|
||||
name: (dev.1).name.clone(),
|
||||
is_cx_ii: (dev.1)
|
||||
.device
|
||||
.device_descriptor()
|
||||
.map(|d| d.product_id() == PID_CX2)
|
||||
.unwrap_or(false),
|
||||
needs_drivers: (dev.1).needs_drivers,
|
||||
};
|
||||
map.insert(dev.0, dev.1);
|
||||
msg
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_device(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
) -> Result<libnspire::info::Info, SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let device = if let Some(dev) = DEVICES.read().unwrap().get(&(bus_number, address)) {
|
||||
if !matches!(dev.state, DeviceState::Closed) {
|
||||
return Err("Already open".into());
|
||||
};
|
||||
dev.device.clone()
|
||||
} else {
|
||||
return Err("Failed to find device".into());
|
||||
};
|
||||
let handle = libnspire::Handle::new(device.open()?)?;
|
||||
let info = handle.info()?;
|
||||
{
|
||||
let mut guard = DEVICES.write().unwrap();
|
||||
let device = guard
|
||||
.get_mut(&(bus_number, address))
|
||||
.ok_or_else(|| anyhow::anyhow!("Device lost"))?;
|
||||
device.state = DeviceState::Open(Arc::new(std::sync::Mutex::new(handle)), info.clone());
|
||||
}
|
||||
Ok(info)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn close_device(bus_number: u8, address: u8) -> Result<impl Serialize, SerializedError> {
|
||||
let mut guard = DEVICES.write().unwrap();
|
||||
let device = guard
|
||||
.get_mut(&(bus_number, address))
|
||||
.ok_or_else(|| anyhow::anyhow!("Device lost"))?;
|
||||
device.state = DeviceState::Closed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_device<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<libnspire::info::Info, SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
let info = err_wrap(handle.info(), dev, &window)?;
|
||||
Ok(info)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_dir<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: String,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<Vec<FileInfo>, SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
let dir = err_wrap(handle.list_dir(&path), dev, &window)?;
|
||||
|
||||
Ok(
|
||||
dir
|
||||
.iter()
|
||||
.map(|file| FileInfo {
|
||||
path: file.name().to_string_lossy().to_string(),
|
||||
is_dir: file.entry_type() == EntryType::Directory,
|
||||
date: file.date(),
|
||||
size: file.size(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn download_file<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: (String, u64),
|
||||
dest: String,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<(), SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let (file, size) = path;
|
||||
let dest = PathBuf::from(dest);
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
let mut buf = vec![0; size as usize];
|
||||
err_wrap(
|
||||
handle.read_file(
|
||||
&file,
|
||||
&mut buf,
|
||||
&mut progress_sender(&window, dev, size as usize),
|
||||
),
|
||||
dev,
|
||||
&window,
|
||||
)?;
|
||||
if let Some(name) = file.split('/').last() {
|
||||
File::create(dest.join(name))?.write_all(&buf)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn upload_file<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: String,
|
||||
src: String,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<(), SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let file = PathBuf::from(src);
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
let mut buf = vec![];
|
||||
File::open(&file)?.read_to_end(&mut buf)?;
|
||||
let name = file
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get file name"))?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
err_wrap(
|
||||
handle.write_file(
|
||||
&format!("{}/{}", path, name),
|
||||
&buf,
|
||||
&mut progress_sender(&window, dev, buf.len()),
|
||||
),
|
||||
dev,
|
||||
&window,
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn upload_os<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
src: String,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<(), SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
let mut buf = vec![];
|
||||
File::open(&src)?.read_to_end(&mut buf)?;
|
||||
err_wrap(
|
||||
handle.send_os(&buf, &mut progress_sender(&window, dev, buf.len())),
|
||||
dev,
|
||||
&window,
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_file<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: String,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<(), SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
err_wrap(handle.delete_file(&path), dev, &window)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_dir<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: String,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<(), SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
err_wrap(handle.delete_dir(&path), dev, &window)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_nspire_dir<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: String,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<(), SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
err_wrap(handle.create_dir(&path), dev, &window)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn move_file<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
src: String,
|
||||
dest: String,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<(), SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
err_wrap(handle.move_file(&src, &dest), dev, &window)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn copy<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
src: String,
|
||||
dest: String,
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<(), SerializedError> {
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
err_wrap(handle.copy_file(&src, &dest), dev, &window)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| SerializedError::from(e))?
|
||||
}
|
||||
|
|
@ -1,12 +1,29 @@
|
|||
use std::sync::Arc;
|
||||
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};
|
||||
use tauri::{Runtime, Window};
|
||||
|
||||
use crate::{Device, DeviceState, SerializedError};
|
||||
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")]
|
||||
|
|
@ -16,6 +33,34 @@ pub struct DevId {
|
|||
pub address: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, 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, Clone, 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)) {
|
||||
|
|
@ -54,80 +99,15 @@ pub fn add_device(dev: Arc<rusb::Device<GlobalContext>>) -> rusb::Result<((u8, u
|
|||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn enumerate<R: Runtime>(handle: Window<R>) -> Result<Vec<AddDevice>, SerializedError> {
|
||||
let devices: Vec<_> = rusb::devices()?.iter().collect();
|
||||
let mut map = crate::DEVICES.write().unwrap();
|
||||
map
|
||||
.drain_filter(|k, _v| {
|
||||
devices
|
||||
.iter()
|
||||
.all(|d| d.bus_number() != k.0 || d.address() != k.1)
|
||||
})
|
||||
.for_each(|d| {
|
||||
if let Err(msg) = handle.emit(
|
||||
"removeDevice",
|
||||
DevId {
|
||||
bus_number: (d.0).0,
|
||||
address: (d.0).1,
|
||||
},
|
||||
) {
|
||||
eprintln!("{}", msg);
|
||||
}
|
||||
});
|
||||
let filtered: Vec<_> = devices
|
||||
.into_iter()
|
||||
.filter(|d| !map.contains_key(&(d.bus_number(), d.address())))
|
||||
.collect();
|
||||
Ok(
|
||||
filtered
|
||||
.into_iter()
|
||||
.filter_map(|dev| add_device(Arc::new(dev)).ok())
|
||||
.map(|dev| {
|
||||
let msg = AddDevice {
|
||||
dev: DevId {
|
||||
bus_number: (dev.0).0,
|
||||
address: (dev.0).1,
|
||||
},
|
||||
name: (dev.1).name.clone(),
|
||||
is_cx_ii: (dev.1)
|
||||
.device
|
||||
.device_descriptor()
|
||||
.map(|d| d.product_id() == PID_CX2)
|
||||
.unwrap_or(false),
|
||||
needs_drivers: (dev.1).needs_drivers,
|
||||
};
|
||||
map.insert(dev.0, dev.1);
|
||||
msg
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[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 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");
|
||||
}
|
||||
}
|
||||
12
desktop/src-tauri/src/error.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +1,21 @@
|
|||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use libnspire::{PID_CX2, VID};
|
||||
use rusb::{GlobalContext, Hotplug, UsbContext};
|
||||
use serde::Serialize;
|
||||
use tauri::{Runtime, Window};
|
||||
use tauri::{Emitter, Manager, Runtime, WebviewWindow};
|
||||
|
||||
use crate::cmd::{add_device, AddDevice, DevId, ProgressUpdate};
|
||||
use crate::device::{add_device, AddDevice, DevId, DEVICES};
|
||||
|
||||
mod cli;
|
||||
mod cmd;
|
||||
mod commands;
|
||||
mod device;
|
||||
mod error;
|
||||
|
||||
pub enum DeviceState {
|
||||
Open(
|
||||
Arc<Mutex<libnspire::Handle<GlobalContext>>>,
|
||||
libnspire::info::Info,
|
||||
),
|
||||
Closed,
|
||||
}
|
||||
|
||||
pub struct Device {
|
||||
name: String,
|
||||
device: Arc<rusb::Device<GlobalContext>>,
|
||||
state: DeviceState,
|
||||
needs_drivers: bool,
|
||||
}
|
||||
lazy_static::lazy_static! {
|
||||
static ref DEVICES: RwLock<HashMap<(u8, u8), Device>> = RwLock::new(HashMap::new());
|
||||
}
|
||||
struct DeviceMon<R: Runtime> {
|
||||
window: Window<R>,
|
||||
window: WebviewWindow<R>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> Hotplug<GlobalContext> for DeviceMon<R> {
|
||||
|
|
@ -47,6 +26,8 @@ impl<R: Runtime> Hotplug<GlobalContext> for DeviceMon<R> {
|
|||
.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) => {
|
||||
|
|
@ -100,378 +81,48 @@ impl<R: Runtime> Hotplug<GlobalContext> for DeviceMon<R> {
|
|||
}
|
||||
}
|
||||
|
||||
fn err_wrap<T, R: Runtime>(
|
||||
res: Result<T, libnspire::Error>,
|
||||
dev: DevId,
|
||||
window: &Window<R>,
|
||||
) -> Result<T, libnspire::Error> {
|
||||
if let Err(libnspire::Error::NoDevice) = res {
|
||||
DEVICES
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&(dev.bus_number, dev.address));
|
||||
if let Err(msg) = window.emit("removeDevice", dev) {
|
||||
eprintln!("{}", msg);
|
||||
};
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn progress_sender<R: Runtime>(
|
||||
window: &Window<R>,
|
||||
dev: DevId,
|
||||
total: usize,
|
||||
) -> impl FnMut(usize) + '_ {
|
||||
let mut i = 0;
|
||||
move |remaining| {
|
||||
if i > 5 {
|
||||
i = 0;
|
||||
}
|
||||
if i == 0 || remaining == 0 {
|
||||
if let Err(msg) = window.emit(
|
||||
"progress",
|
||||
ProgressUpdate {
|
||||
dev,
|
||||
remaining,
|
||||
total,
|
||||
},
|
||||
) {
|
||||
eprintln!("{}", msg);
|
||||
};
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SerializedError(String);
|
||||
|
||||
impl<T: std::fmt::Display> From<T> for SerializedError {
|
||||
fn from(f: T) -> Self {
|
||||
SerializedError(f.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
mod invoked {
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use libnspire::dir::EntryType;
|
||||
use serde::Serialize;
|
||||
use tauri::{Runtime, Window};
|
||||
|
||||
use crate::cmd::{DevId, FileInfo};
|
||||
use crate::{err_wrap, get_open_dev, progress_sender, DeviceState, SerializedError};
|
||||
|
||||
use super::DEVICES;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn open_device(bus_number: u8, address: u8) -> Result<impl Serialize, SerializedError> {
|
||||
let device = if let Some(dev) = DEVICES.read().unwrap().get(&(bus_number, address)) {
|
||||
if !matches!(dev.state, DeviceState::Closed) {
|
||||
return Err("Already open".into());
|
||||
};
|
||||
dev.device.clone()
|
||||
} else {
|
||||
return Err("Failed to find device".into());
|
||||
};
|
||||
let handle = libnspire::Handle::new(device.open()?)?;
|
||||
let info = handle.info()?;
|
||||
{
|
||||
let mut guard = DEVICES.write().unwrap();
|
||||
let device = guard
|
||||
.get_mut(&(bus_number, address))
|
||||
.ok_or_else(|| anyhow::anyhow!("Device lost"))?;
|
||||
device.state = DeviceState::Open(Arc::new(Mutex::new(handle)), info.clone());
|
||||
}
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn close_device(bus_number: u8, address: u8) -> Result<impl Serialize, SerializedError> {
|
||||
let mut guard = DEVICES.write().unwrap();
|
||||
let device = guard
|
||||
.get_mut(&(bus_number, address))
|
||||
.ok_or_else(|| anyhow::anyhow!("Device lost"))?;
|
||||
device.state = DeviceState::Closed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_device<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
window: Window<R>,
|
||||
) -> Result<impl Serialize, SerializedError> {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
let info = err_wrap(handle.info(), dev, &window)?;
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_dir<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: String,
|
||||
window: Window<R>,
|
||||
) -> Result<impl Serialize, SerializedError> {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
let dir = err_wrap(handle.list_dir(&path), dev, &window)?;
|
||||
|
||||
Ok(
|
||||
dir
|
||||
.iter()
|
||||
.map(|file| FileInfo {
|
||||
path: file.name().to_string_lossy().to_string(),
|
||||
is_dir: file.entry_type() == EntryType::Directory,
|
||||
date: file.date(),
|
||||
size: file.size(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn download_file<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: (String, u64),
|
||||
dest: String,
|
||||
window: Window<R>,
|
||||
) -> Result<impl Serialize, SerializedError> {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let (file, size) = path;
|
||||
let dest = PathBuf::from(dest);
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
let mut buf = vec![0; size as usize];
|
||||
err_wrap(
|
||||
handle.read_file(
|
||||
&file,
|
||||
&mut buf,
|
||||
&mut progress_sender(&window, dev, size as usize),
|
||||
),
|
||||
dev,
|
||||
&window,
|
||||
)?;
|
||||
if let Some(name) = file.split('/').last() {
|
||||
File::create(dest.join(name))?.write_all(&buf)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn upload_file<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: String,
|
||||
src: String,
|
||||
window: Window<R>,
|
||||
) -> Result<impl Serialize, SerializedError> {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let file = PathBuf::from(src);
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
let mut buf = vec![];
|
||||
File::open(&file)?.read_to_end(&mut buf)?;
|
||||
let name = file
|
||||
.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to get file name"))?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
err_wrap(
|
||||
handle.write_file(
|
||||
&format!("{}/{}", path, name),
|
||||
&buf,
|
||||
&mut progress_sender(&window, dev, buf.len()),
|
||||
),
|
||||
dev,
|
||||
&window,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn upload_os<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
src: String,
|
||||
window: Window<R>,
|
||||
) -> Result<impl Serialize, SerializedError> {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
let mut buf = vec![];
|
||||
File::open(&src)?.read_to_end(&mut buf)?;
|
||||
err_wrap(
|
||||
handle.send_os(&buf, &mut progress_sender(&window, dev, buf.len())),
|
||||
dev,
|
||||
&window,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_file<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: String,
|
||||
window: Window<R>,
|
||||
) -> Result<impl Serialize, SerializedError> {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
err_wrap(handle.delete_file(&path), dev, &window)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_dir<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: String,
|
||||
window: Window<R>,
|
||||
) -> Result<impl Serialize, SerializedError> {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
err_wrap(handle.delete_dir(&path), dev, &window)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_nspire_dir<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
path: String,
|
||||
window: Window<R>,
|
||||
) -> Result<impl Serialize, SerializedError> {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
err_wrap(handle.create_dir(&path), dev, &window)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn move_file<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
src: String,
|
||||
dest: String,
|
||||
window: Window<R>,
|
||||
) -> Result<impl Serialize, SerializedError> {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
err_wrap(handle.move_file(&src, &dest), dev, &window)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn copy<R: Runtime>(
|
||||
bus_number: u8,
|
||||
address: u8,
|
||||
src: String,
|
||||
dest: String,
|
||||
window: Window<R>,
|
||||
) -> Result<impl Serialize, SerializedError> {
|
||||
let dev = DevId {
|
||||
bus_number,
|
||||
address,
|
||||
};
|
||||
let handle = get_open_dev(&dev)?;
|
||||
let handle = handle.lock().unwrap();
|
||||
err_wrap(handle.copy_file(&src, &dest), dev, &window)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if cli::run() {
|
||||
return;
|
||||
}
|
||||
let has_registered_callback = AtomicBool::new(false);
|
||||
tauri::Builder::default()
|
||||
.on_page_load(move |window, _p| {
|
||||
if !has_registered_callback.swap(true, Ordering::SeqCst) {
|
||||
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");
|
||||
}
|
||||
.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![
|
||||
cmd::enumerate,
|
||||
invoked::open_device,
|
||||
invoked::close_device,
|
||||
invoked::update_device,
|
||||
invoked::list_dir,
|
||||
invoked::download_file,
|
||||
invoked::upload_file,
|
||||
invoked::upload_os,
|
||||
invoked::delete_file,
|
||||
invoked::delete_dir,
|
||||
invoked::create_nspire_dir,
|
||||
invoked::move_file,
|
||||
invoked::copy,
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -1,47 +1,15 @@
|
|||
{
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "msi", "appimage", "dmg"],
|
||||
"identifier": "com.lights0123.n-link",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"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",
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"useBootstrapper": false,
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"entitlements": null
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"allowlist": {
|
||||
"shell": {
|
||||
"open": true
|
||||
},
|
||||
"dialog": {
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "n-link",
|
||||
"version": "0.1.6",
|
||||
"identifier": "com.lights0123.n-link",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "N-Link",
|
||||
|
|
@ -52,7 +20,24 @@
|
|||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
|
||||
"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": {
|
||||
"active": true,
|
||||
"targets": ["deb", "appimage"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~n-link-core/assets/tailwind.css';
|
||||
@import 'n-link-core/assets/tailwind.css';
|
||||
|
||||
#app {
|
||||
user-select: none;
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import {invoke} from '@tauri-apps/api/tauri';
|
||||
import {listen} from '@tauri-apps/api/event';
|
||||
import {open as openDialog} from '@tauri-apps/api/dialog';
|
||||
import {Component, Vue} from 'vue-property-decorator';
|
||||
import type {GenericDevices} from 'n-link-core/components/devices';
|
||||
|
||||
|
||||
export type DevId = { address: number; busNumber: number };
|
||||
import { reactive } from 'vue';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { open as openDialog } from '@tauri-apps/plugin-dialog';
|
||||
import type {
|
||||
GenericDevices,
|
||||
Device,
|
||||
DevId,
|
||||
FileInfo,
|
||||
Info,
|
||||
Progress,
|
||||
PartialCmd,
|
||||
Cmd,
|
||||
} from 'n-link-core/components/devices';
|
||||
|
||||
function devToString(dev: DevId) {
|
||||
return `${dev.busNumber}:${dev.address}`;
|
||||
|
|
@ -17,43 +23,6 @@ function stringToDev(dev: string): DevId {
|
|||
return {busNumber: Number.parseInt(parts[0]), address: Number.parseInt(parts[1])};
|
||||
}
|
||||
|
||||
export type Version = { major: number; minor: number; patch: number; build: number };
|
||||
export type Lcd = { width: number; height: number; bpp: number; sample_mode: number };
|
||||
export type HardwareType =
|
||||
| "Cas"
|
||||
| "NonCas"
|
||||
| "CasCx"
|
||||
| "NonCasCx"
|
||||
| { Unknown: number };
|
||||
// The current state of the calculator.
|
||||
export type RunLevel =
|
||||
| "Recovery"
|
||||
| "Os"
|
||||
| { Unknown: number };
|
||||
export type Battery =
|
||||
| "Powered"
|
||||
| "Low"
|
||||
| "Ok"
|
||||
| { Unknown: number };
|
||||
export type Info = { free_storage: number; total_storage: number; free_ram: number; total_ram: number; version: Version; boot1_version: Version; boot2_version: Version; hw_type: HardwareType; clock_speed: number; lcd: Lcd; os_extension: string; file_extension: string; name: string; id: string; run_level: RunLevel; battery: Battery; is_charging: boolean };
|
||||
|
||||
export type FileInfo = { path: string; isDir: boolean; date: number; size: number };
|
||||
|
||||
export type Progress = { remaining: number; total: number };
|
||||
|
||||
export type PartialCmd = { action: 'download'; path: [string, number]; dest: string }
|
||||
| { action: 'upload'; path: string; src: string }
|
||||
| { action: 'uploadOs'; src: string }
|
||||
| { action: 'deleteFile'; path: string }
|
||||
| { action: 'deleteDir'; path: string }
|
||||
| { action: 'createDir'; path: string }
|
||||
| { action: 'move'; src: string; dest: string }
|
||||
| { action: 'copy'; src: string; dest: string };
|
||||
|
||||
export type Cmd = { id: number } & PartialCmd;
|
||||
|
||||
export type Device = { name: string; isCxIi: boolean; needsDrivers: boolean; info?: Info; progress?: Progress; queue?: Cmd[]; running?: boolean };
|
||||
|
||||
async function downloadFile(dev: DevId | string, path: [string, number], dest: string) {
|
||||
if (typeof dev === 'string') dev = stringToDev(dev);
|
||||
await invoke('download_file', {...dev, path, dest});
|
||||
|
|
@ -117,138 +86,70 @@ async function listAll(dev: DevId | string, path: FileInfo): Promise<FileInfo[]>
|
|||
|
||||
let queueId = 0;
|
||||
|
||||
@Component
|
||||
class Devices extends Vue implements GenericDevices {
|
||||
devices: Record<string, Device> = {};
|
||||
enumerating = false;
|
||||
hasEnumerated = false;
|
||||
|
||||
created() {
|
||||
this.enumerate().then(() => this.hasEnumerated = true, console.error);
|
||||
listen('addDevice', dev => {
|
||||
const payload = dev.payload as Device & DevId;
|
||||
const str = devToString(payload);
|
||||
const existing = this.devices[str] || {};
|
||||
this.$set(this.devices, str, {...existing, ...payload});
|
||||
});
|
||||
listen('removeDevice', dev => {
|
||||
this.$delete(this.devices, devToString(dev.payload as DevId));
|
||||
});
|
||||
listen('progress', dev => {
|
||||
const payload = dev.payload as Progress & DevId;
|
||||
const str = devToString(payload);
|
||||
this.$set(this.devices[str], 'progress', payload);
|
||||
});
|
||||
}
|
||||
|
||||
async runQueue(dev: DevId | string) {
|
||||
if (typeof dev !== 'string') dev = devToString(dev);
|
||||
const device = this.devices[dev];
|
||||
if (!device?.queue || device.running) return;
|
||||
this.$set(device, 'running', true);
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
// The device has been removed
|
||||
if (!this.devices[dev]) return;
|
||||
|
||||
const cmd = device.queue[0];
|
||||
if (!cmd) {
|
||||
device.running = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (cmd.action === 'download') {
|
||||
await downloadFile(dev, cmd.path, cmd.dest);
|
||||
} else if (cmd.action === 'upload') {
|
||||
await uploadFile(dev, cmd.path, cmd.src);
|
||||
} else if (cmd.action === 'uploadOs') {
|
||||
await uploadOs(dev, cmd.src);
|
||||
} else if (cmd.action === 'deleteFile') {
|
||||
await deleteFile(dev, cmd.path);
|
||||
} else if (cmd.action === 'deleteDir') {
|
||||
await deleteDir(dev, cmd.path);
|
||||
} else if (cmd.action === 'createDir') {
|
||||
await createDir(dev, cmd.path);
|
||||
} else if (cmd.action === 'move') {
|
||||
await move(dev, cmd.src, cmd.dest);
|
||||
} else if (cmd.action === 'copy') {
|
||||
await copy(dev, cmd.src, cmd.dest);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
if ('progress' in device) this.$delete(device, 'progress');
|
||||
device.queue.shift();
|
||||
await this.update(dev);
|
||||
}
|
||||
}
|
||||
|
||||
private addToQueue(dev: string, ...cmds: PartialCmd[]) {
|
||||
const device = this.devices[dev];
|
||||
if (!device) return;
|
||||
if (!device.queue) {
|
||||
this.$set(device, 'queue', []);
|
||||
}
|
||||
device.queue?.push(...cmds.map(cmd => ({...cmd, id: queueId++} as Cmd)));
|
||||
this.runQueue(dev);
|
||||
}
|
||||
const state = reactive<GenericDevices>({
|
||||
devices: {},
|
||||
enumerating: false,
|
||||
hasEnumerated: false,
|
||||
|
||||
async enumerate() {
|
||||
this.enumerating = true;
|
||||
state.enumerating = true;
|
||||
try {
|
||||
for (const dev of await invoke('enumerate') as (Device & DevId)[]) {
|
||||
this.$set(this.devices, devToString(dev as DevId), dev);
|
||||
state.devices[devToString(dev as DevId)] = dev;
|
||||
}
|
||||
} finally {
|
||||
this.enumerating = false;
|
||||
state.enumerating = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async open(dev: DevId | string) {
|
||||
if (typeof dev === 'string') dev = stringToDev(dev);
|
||||
const info = await invoke('open_device', {...dev});
|
||||
this.$set(this.devices[devToString(dev)], 'info', info);
|
||||
}
|
||||
const info = await invoke<Info>('open_device', {...dev});
|
||||
state.devices[devToString(dev)].info = info;
|
||||
},
|
||||
|
||||
async close(dev: DevId | string) {
|
||||
if (typeof dev === 'string') dev = stringToDev(dev);
|
||||
await invoke('close_device', {...dev});
|
||||
this.$delete(this.devices[devToString(dev)], 'info');
|
||||
}
|
||||
delete state.devices[devToString(dev)].info;
|
||||
},
|
||||
|
||||
async update(dev: DevId | string) {
|
||||
if (typeof dev === 'string') dev = stringToDev(dev);
|
||||
const info = await invoke('update_device', {...dev});
|
||||
this.$set(this.devices[devToString(dev)], 'info', info);
|
||||
}
|
||||
const info = await invoke<Info>('update_device', {...dev});
|
||||
state.devices[devToString(dev)].info = info;
|
||||
},
|
||||
|
||||
async listDir(dev: DevId | string, path: string) {
|
||||
return await listDir(dev, path);
|
||||
}
|
||||
},
|
||||
|
||||
async promptUploadFiles(dev: DevId | string, path: string) {
|
||||
if (typeof dev !== 'string') dev = devToString(dev);
|
||||
const files = await openDialog({filters:[{extensions:['tns'], name:'TNS files'}], multiple: true});
|
||||
const files = await openDialog({filters: [{extensions: ['tns'], name: 'TNS files'}], multiple: true});
|
||||
// The user can cancel the dialog, in which case `files` is null.
|
||||
// Iterating a null value here would throw at runtime.
|
||||
if (!files) return;
|
||||
for (const src of files) {
|
||||
this.addToQueue(dev, {action: 'upload', path, src});
|
||||
addToQueue(dev, {action: 'upload', path, src});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async uploadOs(dev: DevId | string, filter: string) {
|
||||
if (typeof dev !== 'string') dev = devToString(dev);
|
||||
const src = await openDialog({filters:[{extensions:[filter], name:'TI Nspire OS upgrade files'}]}) as string | null;
|
||||
const src = await openDialog({filters: [{extensions: [filter], name: 'TI Nspire OS upgrade files'}]}) as string | null;
|
||||
if (!src) return;
|
||||
this.addToQueue(dev, {action: 'uploadOs', src});
|
||||
}
|
||||
addToQueue(dev, {action: 'uploadOs', src});
|
||||
},
|
||||
|
||||
async downloadFiles(dev: DevId | string, files: [string, number][]) {
|
||||
if (typeof dev !== 'string') dev = devToString(dev);
|
||||
const dest = await openDialog({directory: true}) as string | null;
|
||||
if (!dest) return;
|
||||
for (const path of files) {
|
||||
this.addToQueue(dev, {action: 'download', path, dest});
|
||||
addToQueue(dev, {action: 'download', path, dest});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async delete(dev: DevId | string, files: FileInfo[]) {
|
||||
if (typeof dev !== 'string') dev = devToString(dev);
|
||||
|
|
@ -257,35 +158,123 @@ class Devices extends Vue implements GenericDevices {
|
|||
toDelete.push(...await listAll(dev, file));
|
||||
}
|
||||
for (const file of toDelete) {
|
||||
this.addToQueue(dev, {action: file.isDir ? 'deleteDir' : 'deleteFile', path: file.path});
|
||||
addToQueue(dev, {action: file.isDir ? 'deleteDir' : 'deleteFile', path: file.path});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async createDir(dev: DevId | string, path: string) {
|
||||
if (typeof dev !== 'string') dev = devToString(dev);
|
||||
this.addToQueue(dev, {action: 'createDir', path});
|
||||
}
|
||||
addToQueue(dev, {action: 'createDir', path});
|
||||
},
|
||||
|
||||
async copy(dev: DevId | string, src: string, dest: string) {
|
||||
if (typeof dev !== 'string') dev = devToString(dev);
|
||||
this.addToQueue(dev, {action: 'copy', src, dest});
|
||||
}
|
||||
addToQueue(dev, {action: 'copy', src, dest});
|
||||
},
|
||||
|
||||
async move(dev: DevId | string, src: string, dest: string) {
|
||||
if (typeof dev !== 'string') dev = devToString(dev);
|
||||
this.addToQueue(dev, {action: 'move', src, dest});
|
||||
}
|
||||
addToQueue(dev, {action: 'move', src, dest});
|
||||
},
|
||||
|
||||
uploadFiles(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
// The desktop app resolves uploads through a native OS file-picker dialog
|
||||
// (see promptUploadFiles/uploadOs above), which yields a filesystem path
|
||||
// that the Rust side reads directly. It has no way to turn a browser
|
||||
// File object back into such a path, so — exactly as in the original
|
||||
// Vue 2 desktop store — these two interface members are intentionally
|
||||
// left unimplemented here. The web host (n-link/web) provides the real
|
||||
// File-based implementation.
|
||||
async uploadFiles(): Promise<void> {
|
||||
},
|
||||
|
||||
uploadOsFile(): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
async uploadOsFile(): Promise<void> {
|
||||
},
|
||||
});
|
||||
|
||||
async function runQueue(dev: DevId | string) {
|
||||
if (typeof dev !== 'string') dev = devToString(dev);
|
||||
const device = state.devices[dev];
|
||||
const queue = device?.queue;
|
||||
if (!queue || device.running) return;
|
||||
device.running = true;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
// The device has been removed
|
||||
if (!state.devices[dev]) return;
|
||||
|
||||
const cmd = queue[0];
|
||||
if (!cmd) {
|
||||
device.running = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (cmd.action === 'download') {
|
||||
if ('dest' in cmd && cmd.dest) {
|
||||
await downloadFile(dev, cmd.path, cmd.dest);
|
||||
} else {
|
||||
console.error('Missing destination for download command', cmd);
|
||||
}
|
||||
} else if (cmd.action === 'upload') {
|
||||
if ('src' in cmd) {
|
||||
await uploadFile(dev, cmd.path, cmd.src);
|
||||
} else {
|
||||
console.error('File uploads are not supported by the desktop app', cmd);
|
||||
}
|
||||
} else if (cmd.action === 'uploadOs') {
|
||||
if ('src' in cmd) {
|
||||
await uploadOs(dev, cmd.src);
|
||||
} else {
|
||||
console.error('File uploads are not supported by the desktop app', cmd);
|
||||
}
|
||||
} else if (cmd.action === 'deleteFile') {
|
||||
await deleteFile(dev, cmd.path);
|
||||
} else if (cmd.action === 'deleteDir') {
|
||||
await deleteDir(dev, cmd.path);
|
||||
} else if (cmd.action === 'createDir') {
|
||||
await createDir(dev, cmd.path);
|
||||
} else if (cmd.action === 'move') {
|
||||
await move(dev, cmd.src, cmd.dest);
|
||||
} else if (cmd.action === 'copy') {
|
||||
await copy(dev, cmd.src, cmd.dest);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
if ('progress' in device) delete device.progress;
|
||||
queue.shift();
|
||||
await state.update(dev);
|
||||
}
|
||||
}
|
||||
|
||||
function addToQueue(dev: string, ...cmds: PartialCmd[]) {
|
||||
const device = state.devices[dev];
|
||||
if (!device) return;
|
||||
if (!device.queue) {
|
||||
device.queue = [];
|
||||
}
|
||||
device.queue.push(...cmds.map(cmd => ({...cmd, id: queueId++} as Cmd)));
|
||||
runQueue(dev);
|
||||
}
|
||||
|
||||
const devices = new Devices();
|
||||
export default devices;
|
||||
Vue.prototype.$devices = devices;
|
||||
state.enumerate().then(() => {
|
||||
state.hasEnumerated = true;
|
||||
}, console.error);
|
||||
|
||||
listen('addDevice', dev => {
|
||||
const payload = dev.payload as Device & DevId;
|
||||
const str = devToString(payload);
|
||||
const existing = state.devices[str] || {};
|
||||
state.devices[str] = {...existing, ...payload};
|
||||
});
|
||||
|
||||
listen('removeDevice', dev => {
|
||||
delete state.devices[devToString(dev.payload as DevId)];
|
||||
});
|
||||
|
||||
listen('progress', dev => {
|
||||
const payload = dev.payload as Progress & DevId;
|
||||
const str = devToString(payload);
|
||||
state.devices[str].progress = payload;
|
||||
});
|
||||
|
||||
export default state;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import AsyncComputed from 'vue-async-computed';
|
||||
import router from './router'
|
||||
import 'n-link-core/assets/tailwind.css'
|
||||
import './components/devices';
|
||||
Vue.use(AsyncComputed);
|
||||
Vue.config.productionTip = false
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import devices from './components/devices';
|
||||
import { DEVICES_KEY } from 'n-link-core/components/devices';
|
||||
import 'n-link-core/assets/tailwind.css';
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
// Two registrations, both required:
|
||||
// globalProperties -> templates across n-link-core reference $devices
|
||||
// provide -> <script setup> blocks have no `this`, so they inject
|
||||
app.config.globalProperties.$devices = devices;
|
||||
app.provide(DEVICES_KEY, devices);
|
||||
app.mount('#app');
|
||||
|
|
|
|||
|
|
@ -1,29 +1,10 @@
|
|||
import Vue from 'vue'
|
||||
import VueRouter, { RouteConfig } from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import Home from '../views/Home.vue';
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes: Array<RouteConfig> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: process.env.BASE_URL,
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
export default createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'Home', component: Home },
|
||||
{ path: '/about', name: 'About', component: () => import('../views/About.vue') },
|
||||
],
|
||||
});
|
||||
|
|
|
|||
13
desktop/src/shims-tsx.d.ts
vendored
|
|
@ -1,13 +0,0 @@
|
|||
import Vue, { VNode } from 'vue'
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
// tslint:disable no-empty-interface
|
||||
interface Element extends VNode {}
|
||||
// tslint:disable no-empty-interface
|
||||
interface ElementClass extends Vue {}
|
||||
interface IntrinsicElements {
|
||||
[elem: string]: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
desktop/src/shims-vue.d.ts
vendored
|
|
@ -1,54 +0,0 @@
|
|||
declare module '*.vue' {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
||||
|
||||
import Vue, { PluginFunction } from 'vue';
|
||||
|
||||
interface IAsyncComputedOptions {
|
||||
errorHandler?: (error: string[]) => void;
|
||||
useRawError?: boolean;
|
||||
default?: any;
|
||||
}
|
||||
|
||||
export default class AsyncComputed {
|
||||
constructor(options?: IAsyncComputedOptions);
|
||||
static install: PluginFunction<never>;
|
||||
static version: string;
|
||||
}
|
||||
|
||||
type AsyncComputedGetter<T> = () => Promise<T> | T;
|
||||
export interface IAsyncComputedProperty<T> {
|
||||
default?: T | (() => T);
|
||||
get: AsyncComputedGetter<T>;
|
||||
watch?: () => void;
|
||||
shouldUpdate?: () => boolean;
|
||||
lazy?: boolean;
|
||||
}
|
||||
|
||||
interface IAsyncComputedProperties {
|
||||
[K: string]: AsyncComputedGetter<any> | IAsyncComputedProperty<any>;
|
||||
}
|
||||
|
||||
declare module 'vue/types/options' {
|
||||
interface ComponentOptions<V extends Vue> {
|
||||
// @ts-ignore
|
||||
asyncComputed?: IAsyncComputedProperties;
|
||||
}
|
||||
}
|
||||
|
||||
interface IASyncComputedState {
|
||||
state: 'updating' | 'success' | 'error';
|
||||
updating: boolean;
|
||||
success: boolean;
|
||||
error: boolean;
|
||||
exception: Error | null;
|
||||
update: () => void;
|
||||
}
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
// tslint:disable-next-line:interface-name
|
||||
interface Vue {
|
||||
$asyncComputed: { [K: string]: IASyncComputedState };
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="home h-full overflow-hidden">
|
||||
<div class="flex flex-row h-full">
|
||||
<div class="flex-shrink-0 border-r w-64">
|
||||
<device-select :selected.sync="selectedCalculator"/>
|
||||
<device-select v-model:selected="selectedCalculator"/>
|
||||
<div class="overflow-auto h-full px-4 py-4">
|
||||
<div v-if="needsDrivers && !isLinux">
|
||||
<h1 class="text-3xl">Drivers required</h1>
|
||||
|
|
@ -23,9 +23,9 @@
|
|||
<div class="lds-dual-ring"/>
|
||||
</div>
|
||||
<div v-else-if="calculator && calculator.info">
|
||||
<calc-info :info="calculator.info" :dev="selectedCalculator"/>
|
||||
<calc-info :info="calculator.info" :dev="selectedCalculator!"/>
|
||||
<label class="inline-flex items-center cursor-pointer mr-2 mt-4">
|
||||
<input type="checkbox" class="form-checkbox h-5 w-5 text-blue-600 cursor-pointer" v-model="showHidden">
|
||||
<input type="checkbox" class="h-5 w-5 text-blue-600 cursor-pointer" v-model="showHidden">
|
||||
<span class="mx-2 text-gray-700 select-none">Include hidden files</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -33,82 +33,69 @@
|
|||
</div>
|
||||
<div class="w-full">
|
||||
<div class="h-full">
|
||||
<file-browser v-if="calculator && calculator.info" :dev="selectedCalculator" :show-hidden="showHidden"/>
|
||||
<file-browser v-if="calculator && calculator.info" :dev="selectedCalculator!" :show-hidden="showHidden"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Vue, Watch} from 'vue-property-decorator';
|
||||
import {open} from '@tauri-apps/api/shell';
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import CalcInfo from 'n-link-core/components/CalcInfo.vue';
|
||||
import FileBrowser from 'n-link-core/components/FileBrowser.vue';
|
||||
import DeviceSelect from "n-link-core/components/DeviceSelect.vue";
|
||||
import DeviceSelect from 'n-link-core/components/DeviceSelect.vue';
|
||||
import devices from '../components/devices';
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
DeviceSelect,
|
||||
FileBrowser,
|
||||
CalcInfo,
|
||||
},
|
||||
})
|
||||
export default class Home extends Vue {
|
||||
selectedCalculator: string | null = null;
|
||||
showHidden = false;
|
||||
const selectedCalculator = ref<string | null>(null);
|
||||
const showHidden = ref(false);
|
||||
|
||||
@Watch('$devices.hasEnumerated')
|
||||
onEnumerated() {
|
||||
const first = Object.keys(this.$devices.devices)[0];
|
||||
if (first) this.selectedCalculator = first;
|
||||
function onEnumerated() {
|
||||
const first = Object.keys(devices.devices)[0];
|
||||
if (first) selectedCalculator.value = first;
|
||||
}
|
||||
|
||||
async function onDeviceChange() {
|
||||
if (!selectedCalculator.value) {
|
||||
await sleep(1000);
|
||||
if (selectedCalculator.value) return;
|
||||
const first = Object.keys(devices.devices)[0];
|
||||
if (first) selectedCalculator.value = first;
|
||||
} else if (!Object.keys(devices.devices).includes(selectedCalculator.value)) {
|
||||
selectedCalculator.value = null;
|
||||
// go back and choose the first if available
|
||||
onDeviceChange();
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('$devices.devices')
|
||||
async onDeviceChange() {
|
||||
if (!this.selectedCalculator) {
|
||||
await sleep(1000);
|
||||
if (this.selectedCalculator) return;
|
||||
const first = Object.keys(this.$devices.devices)[0];
|
||||
if (first) this.selectedCalculator = first;
|
||||
} else if (!Object.keys(this.$devices.devices).includes(this.selectedCalculator)) {
|
||||
this.selectedCalculator = null;
|
||||
// go back and choose the first if available
|
||||
this.onDeviceChange();
|
||||
async function onSelectCalculator(dev: string | null) {
|
||||
if (dev && !devices.devices[dev].info && !devices.devices[dev].needsDrivers) {
|
||||
try {
|
||||
await devices.open(dev);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
selectedCalculator.value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('selectedCalculator')
|
||||
async onSelectCalculator(dev: string | null) {
|
||||
if (dev && !this.$devices.devices[dev].info && !this.$devices.devices[dev].needsDrivers) {
|
||||
try {
|
||||
await this.$devices.open(dev);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.selectedCalculator = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
watch(() => devices.hasEnumerated, onEnumerated);
|
||||
watch(() => devices.devices, onDeviceChange, { deep: true });
|
||||
watch(selectedCalculator, onSelectCalculator);
|
||||
|
||||
get calculator() {
|
||||
return this.selectedCalculator && this.$devices.devices[this.selectedCalculator];
|
||||
}
|
||||
const calculator = computed(() => selectedCalculator.value && devices.devices[selectedCalculator.value]);
|
||||
|
||||
get needsDrivers() {
|
||||
return this.selectedCalculator && this.$devices.devices[this.selectedCalculator]?.needsDrivers;
|
||||
}
|
||||
const needsDrivers = computed(() => selectedCalculator.value && devices.devices[selectedCalculator.value]?.needsDrivers);
|
||||
|
||||
get isLinux() {
|
||||
return navigator.platform.includes('Linux');
|
||||
}
|
||||
const isLinux = computed(() => navigator.platform.includes('Linux'));
|
||||
|
||||
installDrivers(platform = 'windows') {
|
||||
open(`https://lights0123.com/n-link/#${platform}`);
|
||||
}
|
||||
function installDrivers(platform = 'windows') {
|
||||
open(`https://lights0123.com/n-link/#${platform}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
7
desktop/src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
8
desktop/tailwind.config.cjs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
content: [
|
||||
'./index.html',
|
||||
'./src/**/*.{vue,js,ts}',
|
||||
'../n-link-core/components/**/*.{vue,js,ts}',
|
||||
],
|
||||
presets: [require('../n-link-core/tailwind.config.js')],
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
module.exports = require('n-link-core/tailwind.config');
|
||||
|
|
@ -1,41 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"typeRoots": ["@/types", "./node_modules/@types"]
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
|
|
|||
10
desktop/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
24
desktop/vite.config.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"n-link-core": fileURLToPath(new URL("../n-link-core", import.meta.url)),
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
// Vite ignores watching src-tauri; Tauri expects a fixed port.
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host ? { protocol: "ws", host, port: 1421 } : undefined,
|
||||
watch: { ignored: ["**/src-tauri/**"] },
|
||||
},
|
||||
}));
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
module.exports = {
|
||||
transpileDependencies: ['n-link-core']
|
||||
}
|
||||
1484
docs/superpowers/plans/2026-07-19-desktop-tauri2-backend.md
Normal file
1145
docs/superpowers/plans/2026-07-21-desktop-vue3-frontend.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
# Design: Port n-link desktop to Tauri 2 + Vue 3
|
||||
|
||||
Date: 2026-07-19
|
||||
Status: Approved
|
||||
Branch: `port/tauri2-vue3`
|
||||
|
||||
## Problem
|
||||
|
||||
The n-link desktop app cannot build or run on Ubuntu 26.04. `desktop/src-tauri`
|
||||
pins `tauri = "1.0.0-beta.8"`, which links against `webkit2gtk-4.0`. That library
|
||||
is not available on this system — only `webkit2gtk-4.1` is. The prebuilt 0.1.6
|
||||
`.deb` fails for the same reason, plus a missing `libssl.so.1.1` (only
|
||||
`libssl.so.3` is present).
|
||||
|
||||
This is the blocker that caused `nlink-cli` to be written as a GUI-free
|
||||
workaround. This project removes the blocker properly so the GUI works.
|
||||
|
||||
Tauri 2.x links against `webkit2gtk-4.1`, which is what makes the port the fix.
|
||||
|
||||
### Verified environment
|
||||
|
||||
| Component | State |
|
||||
|---|---|
|
||||
| `webkit2gtk-4.0` | absent, not installable |
|
||||
| `libwebkit2gtk-4.1-dev` | available, 2.52.3-0ubuntu0.26.04.2 (not yet installed) |
|
||||
| `libsoup-3.0-dev` | available, 3.6.6-1 (not yet installed) |
|
||||
| `libssl` | 3 only; no 1.1 |
|
||||
| `libusb-1.0` | 1.0.29, installed |
|
||||
| rustc / cargo | 1.97.1 |
|
||||
| node | v22.22.1 |
|
||||
| yarn | **not installed** |
|
||||
| TI-Nspire CX II | connected, USB `0451:e022` |
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
1. `n-link-core` — port 8 components from Vue 2 to Vue 3.
|
||||
2. `desktop` — port to Tauri 2, Vue 3, and Vite; bundle deb + appimage.
|
||||
|
||||
Out of scope (deferred to a separate project):
|
||||
|
||||
- `web` (Nuxt 2). It shares `n-link-core`, so porting core **knowingly breaks
|
||||
it**. `web/` is left untouched at its current commit rather than
|
||||
half-migrated. It is a browser/WebUSB product and does not bear on the
|
||||
Linux GUI problem.
|
||||
- Windows and macOS targets. `msi` and `dmg` are dropped from the bundle
|
||||
config; they cannot be verified from this machine.
|
||||
|
||||
## Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| Platform targets | Linux only; deb + appimage | Fully testable here |
|
||||
| Frontend stack | Vue 3 + Vite | Vue CLI 4.5 / webpack 4 fails on Node 22 (OpenSSL 3); `vue-cli-plugin-tauri` is Tauri-v1-era and abandoned |
|
||||
| Shared core | Port `n-link-core` to Vue 3 | `web` deferred, accepted as broken |
|
||||
| Component style | `<script setup>` + Composition API | `vue-class-component` has no Vue 3 future |
|
||||
| Migration method | Scaffold clean, port code across | v1→v2 config is a near-total rewrite; a generated baseline means only our code is ever in question |
|
||||
| Verification | Manual, `n-link-cli` as oracle | Repo has zero tests; risk concentrates in USB behavior needing real hardware |
|
||||
|
||||
## Approach
|
||||
|
||||
Scaffold a fresh Tauri 2 + Vue 3 + Vite + TypeScript app, then move logic into
|
||||
it. Backend goes first — this is forced, not preference: the Vue 3 frontend
|
||||
cannot be exercised until a Tauri 2 shell runs, because Tauri 1 cannot launch
|
||||
on this machine at all.
|
||||
|
||||
"Scaffold clean" means the generated baseline **replaces the contents of
|
||||
`desktop/`** in place, on the `port/tauri2-vue3` branch — it is not a new
|
||||
top-level package. The workspace keeps its three members (`desktop`,
|
||||
`n-link-core`, `web`). The old `desktop/src` and `desktop/src-tauri` are
|
||||
removed in the same commit that adds the scaffold, so history shows one
|
||||
coherent replacement rather than a half-migrated intermediate state.
|
||||
|
||||
Trade-off accepted: per-file `git blame` continuity in `desktop/` is lost.
|
||||
Given that `tauri.conf.json`, the build tooling, and every component's syntax
|
||||
are all being replaced anyway, that continuity is largely notional.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
desktop/
|
||||
src-tauri/
|
||||
Cargo.toml tauri 2.x, patched libnspire-sys
|
||||
tauri.conf.json v2 schema
|
||||
capabilities/default.json replaces v1 allowlist
|
||||
src/
|
||||
main.rs thin: CLI passthrough + Builder
|
||||
device.rs Device/DeviceState/DEVICES registry
|
||||
commands.rs the 13 #[tauri::command] wrappers
|
||||
error.rs SerializedError
|
||||
cli.rs headless mode
|
||||
src/ Vue 3 + Vite
|
||||
```
|
||||
|
||||
`main.rs` is currently 478 lines holding the device registry, hotplug monitor,
|
||||
all 13 commands, and `main()`. Splitting it into focused modules is a targeted
|
||||
improvement to code being rewritten end-to-end anyway — not unrelated
|
||||
refactoring.
|
||||
|
||||
The patched `vendor/libnspire-sys` (CX II NNSE handshake retry limit raised
|
||||
10 → 30) is wired via `[patch.crates-io]`, exactly as `nlink-cli` already does.
|
||||
|
||||
## IPC contract — held invariant
|
||||
|
||||
Deliberately unchanged, so the frontend and backend ports can be verified
|
||||
independently.
|
||||
|
||||
**13 commands:** `enumerate`, `open_device`, `close_device`, `update_device`,
|
||||
`list_dir`, `download_file`, `upload_file`, `upload_os`, `delete_file`,
|
||||
`delete_dir`, `create_nspire_dir`, `move_file`, `copy`
|
||||
|
||||
**3 events:** `addDevice`, `removeDevice`, `progress`
|
||||
|
||||
**Payloads:** `DevId{busNumber,address}`, `AddDevice`, `ProgressUpdate`,
|
||||
`FileInfo` — all `camelCase` via serde, unchanged.
|
||||
|
||||
## Data flow
|
||||
|
||||
`rusb` hotplug thread → `DeviceMon` → emits `addDevice`/`removeDevice` → Vue
|
||||
store in `devices.ts`. Commands mutate the global `DEVICES` map behind a
|
||||
`RwLock`. Long transfers emit `progress`.
|
||||
|
||||
One frontend change: the ~10 `$set`/`$delete` calls in `devices.ts` become
|
||||
plain assignment and `delete`. Vue 3 proxy reactivity makes them unnecessary —
|
||||
a deletion, not a rewrite.
|
||||
|
||||
## Error handling
|
||||
|
||||
Unchanged in shape. `SerializedError` wraps `libnspire::Error` and
|
||||
`rusb::Error` into a serde-serializable form for crossing IPC. Kept as-is,
|
||||
relocated to `error.rs`.
|
||||
|
||||
## Migration hazards
|
||||
|
||||
Specific traps identified in the existing code:
|
||||
|
||||
| Hazard | Detail |
|
||||
|---|---|
|
||||
| `on_page_load` | Signature changed in v2 (`&Webview` + payload). Hotplug registration at `main.rs:440` must move; `setup()` is the better home |
|
||||
| `Window` → `WebviewWindow` | `emit` additionally requires `use tauri::Emitter` in scope |
|
||||
| `drain_filter` | `cmd.rs:62`, hashbrown 0.11 → renamed `extract_if`, **with changed semantics** |
|
||||
| `allowlist` → capabilities | allowlist block deleted; `dialog:allow-open` + `shell:allow-open` become plugin permissions |
|
||||
| Plugins | `dialog` and `shell` are separate crates + JS packages in v2. `notification-all` is in Cargo features but unused by the frontend — dropped |
|
||||
| CSP | Commit `0472908` fixed a CSP blank-screen in release mode. v2 moves CSP to `app.security.csp`; this fix must be carried forward or release builds go blank again |
|
||||
| `clap` 3.0.0-beta.2 | `cli.rs` uses it — but `nlink-cli/src/cli.rs` is **already** the clap-4 port of this same file. Reuse it instead of redoing the migration |
|
||||
| element-ui | Only `el-popover`, 5 usages across 3 files. Swap to element-plus |
|
||||
| Vue class components | 9 `@Component`, 18 `@Prop`, 6 `@Watch` across 9 files → `<script setup>` |
|
||||
| yarn missing | Root `package.json` declares yarn workspaces but yarn is absent. Migrating to **npm workspaces** (npm ships with Node 22; the root `yarn.lock` is invalidated by this port's dependency changes regardless) |
|
||||
|
||||
## Build sequence
|
||||
|
||||
1. Install system deps (`libwebkit2gtk-4.1-dev`, `libsoup-3.0-dev`,
|
||||
`librsvg2-dev`, `libxdo-dev`, `libayatana-appindicator3-dev`). Requires sudo.
|
||||
2. Scaffold Tauri 2 + Vue 3 + Vite shell. **Go/no-go gate.**
|
||||
3. Port Rust backend; wire patched `libnspire-sys`.
|
||||
4. Port `n-link-core` components to Vue 3 `<script setup>`.
|
||||
5. Port `desktop/src` (`devices.ts`, `Home.vue`, router); drop `$set`/`$delete`.
|
||||
6. Bundle deb + appimage.
|
||||
|
||||
## Verification
|
||||
|
||||
Each gate runs against the connected CX II.
|
||||
|
||||
1. **Go/no-go** — an empty Tauri 2 window opens on webkit2gtk-4.1. Cheap, and
|
||||
everything downstream is wasted if it fails, so it runs first.
|
||||
2. **Backend** — `enumerate` sees the device; `list_dir /` matches
|
||||
`n-link-cli ls /`.
|
||||
3. **Frontend** — components render; device appears in the UI.
|
||||
4. **End-to-end** — download a `.tns`; `sha256sum` matches the same file
|
||||
fetched via `n-link-cli download`.
|
||||
5. **Bundle** — deb and appimage build; installed deb launches.
|
||||
|
||||
### Known ceiling, not a regression
|
||||
|
||||
Per `nlink-cli/README.md`: libnspire's CX II support has been frozen upstream
|
||||
since 2020, and against 2026 firmware, multi-packet transfers are unreliable —
|
||||
long operations can fail mid-transfer with `Busy`.
|
||||
|
||||
This ceiling is inherited by the GUI. Step 4 therefore uses a **short**
|
||||
transfer, and any failure is compared against `n-link-cli` behavior before
|
||||
being treated as a port regression.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Go/no-go fails.** If webkit2gtk-4.1 cannot produce a window, the whole
|
||||
approach is void. Mitigated by making it step 2, at near-zero cost.
|
||||
- **Vue 3 reactivity drift.** Removing `$set`/`$delete` is where silent
|
||||
behavioral regressions are most likely. Caught by oracle cross-check at
|
||||
gate 2 and 4, not by tests.
|
||||
- **`web` stays broken.** Accepted and explicit; tracked as follow-up work.
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
</div>
|
||||
</button>
|
||||
<button class="mt-4 button gray-button"
|
||||
@click="nativeUpload ? $refs.upload.click() : $devices.uploadOs(dev, info.os_extension.split('.').pop())">
|
||||
@click="nativeUpload ? upload?.click() : $devices.uploadOs(dev, osExtension)">
|
||||
Upload OS
|
||||
</button>
|
||||
<input v-if="nativeUpload" ref="upload" type="file" class="hidden" :accept="info.os_extension"
|
||||
|
|
@ -39,40 +39,49 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import type {Info, Version} from './devices';
|
||||
import fileSize from "filesize";
|
||||
<script setup lang="ts">
|
||||
import {computed, inject, ref} from 'vue';
|
||||
import {DEVICES_KEY, type GenericDevices, type Info, type Version} from './devices';
|
||||
import {filesize} from 'filesize';
|
||||
|
||||
@Component
|
||||
export default class FileView extends Vue {
|
||||
@Prop({type: Object, required: true}) private info!: Info;
|
||||
@Prop({type: String, required: true}) private dev!: string;
|
||||
@Prop({type: Boolean, default: false}) private nativeUpload!: boolean;
|
||||
refreshing = false;
|
||||
const props = withDefaults(defineProps<{
|
||||
info: Info;
|
||||
dev: string;
|
||||
nativeUpload?: boolean;
|
||||
}>(), {
|
||||
nativeUpload: false,
|
||||
});
|
||||
|
||||
formatSize(size: number) {
|
||||
return fileSize(size, {round: 1});
|
||||
const devices = inject(DEVICES_KEY) as GenericDevices;
|
||||
|
||||
const refreshing = ref(false);
|
||||
|
||||
// `.split('.').pop()` is string | undefined; uploadOs takes a string. The
|
||||
// extension is always present in practice (e.g. ".tcc2"), so fall back to ''.
|
||||
const osExtension = computed(() => props.info.os_extension.split('.').pop() ?? '');
|
||||
const upload = ref<HTMLInputElement | null>(null);
|
||||
|
||||
function formatSize(size: number) {
|
||||
return filesize(size, {round: 1});
|
||||
}
|
||||
|
||||
function formatVersion(version: Version) {
|
||||
return `${version.major}.${version.minor}.${version.patch}.${version.build}`;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
refreshing.value = true;
|
||||
try {
|
||||
await devices.update(props.dev);
|
||||
} catch (e) {
|
||||
/* */
|
||||
}
|
||||
refreshing.value = false;
|
||||
}
|
||||
|
||||
formatVersion(version: Version) {
|
||||
return `${version.major}.${version.minor}.${version.patch}.${version.build}`;
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.refreshing = true;
|
||||
try {
|
||||
await this.$devices.update(this.dev);
|
||||
} catch (e) {
|
||||
/* */
|
||||
}
|
||||
this.refreshing = false;
|
||||
}
|
||||
|
||||
uploadNative(e: Event & { target: HTMLInputElement }) {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) this.$devices.uploadOsFile(this.dev, file);
|
||||
}
|
||||
function uploadNative(e: Event) {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) devices.uploadOsFile(props.dev, file);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -20,25 +20,28 @@
|
|||
</div>
|
||||
<div class="ml-2 flex-shrink-0">
|
||||
<button class="focus:outline-none" :disabled="i===0" :class="i===0 && 'cursor-not-allowed'"
|
||||
@click="device.queue.splice(i, 1)">
|
||||
<img src="~feather-icons/dist/icons/x-circle.svg" :class="i===0 && 'opacity-25'"/>
|
||||
@click="device.queue?.splice(i, 1)">
|
||||
<img :src="xCircle" :class="i===0 && 'opacity-25'"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg slot="reference" viewBox="-1 -1 2 2" class="circle focus:outline-none">
|
||||
<circle r="1" class="bg"/>
|
||||
<path class="fg" :d="pathData"/>
|
||||
</svg>
|
||||
<template #reference>
|
||||
<svg viewBox="-1 -1 2 2" class="circle focus:outline-none">
|
||||
<circle r="1" class="bg"/>
|
||||
<path class="fg" :d="pathData"/>
|
||||
</svg>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import ElPopover from 'element-ui/packages/popover/src/main.vue';
|
||||
import 'element-ui/lib/theme-chalk/popover.css';
|
||||
import type {Device} from "./devices";
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue';
|
||||
import {ElPopover} from 'element-plus';
|
||||
import 'element-plus/es/components/popover/style/css';
|
||||
import type {Device} from './devices';
|
||||
import xCircle from 'feather-icons/dist/icons/x-circle.svg';
|
||||
|
||||
function getCoordinatesForPercent(percent: number) {
|
||||
const x = Math.cos(2 * Math.PI * percent);
|
||||
|
|
@ -51,45 +54,45 @@ function trimPath(path: string) {
|
|||
return path.split(/[\\/]/).pop() as string;
|
||||
}
|
||||
|
||||
@Component({components: {ElPopover}})
|
||||
export default class FileView extends Vue {
|
||||
@Prop({type: Object, required: true}) private device!: Device;
|
||||
const props = defineProps<{
|
||||
device: Device;
|
||||
}>();
|
||||
|
||||
get pathData() {
|
||||
const {progress, queue} = this.device;
|
||||
const length = (queue?.length || 0);
|
||||
let percent = progress ? (1 - progress.remaining / progress.total) : 1 / (length);
|
||||
if (!Number.isFinite(percent)) percent = 0;
|
||||
const pathData = computed(() => {
|
||||
const {progress, queue} = props.device;
|
||||
const length = (queue?.length || 0);
|
||||
let percent = progress ? (1 - progress.remaining / progress.total) : 1 / (length);
|
||||
if (!Number.isFinite(percent)) percent = 0;
|
||||
|
||||
const [startX, startY] = getCoordinatesForPercent(0);
|
||||
const [endX, endY] = getCoordinatesForPercent(percent);
|
||||
const [startX, startY] = getCoordinatesForPercent(0);
|
||||
const [endX, endY] = getCoordinatesForPercent(percent);
|
||||
|
||||
const largeArcFlag = percent > .5 ? 1 : 0;
|
||||
return [
|
||||
`M ${startX} ${startY}`,
|
||||
`A 1 1 0 ${largeArcFlag} 1 ${endX} ${endY}`,
|
||||
`L 0 0`,
|
||||
].join(' ');
|
||||
}
|
||||
const largeArcFlag = percent > .5 ? 1 : 0;
|
||||
return [
|
||||
`M ${startX} ${startY}`,
|
||||
`A 1 1 0 ${largeArcFlag} 1 ${endX} ${endY}`,
|
||||
`L 0 0`,
|
||||
].join(' ');
|
||||
});
|
||||
|
||||
get queue() {
|
||||
if (!this.device.queue) return [];
|
||||
return this.device.queue.map(item => {
|
||||
let desc = '';
|
||||
if (item.action === 'download') desc = `Download ${trimPath(item.path[0])}`;
|
||||
else if (item.action === 'upload') desc = `Upload ${'src' in item ? trimPath(item.src) : item.file.name}`;
|
||||
else if (item.action === 'uploadOs') desc = 'Upload OS';
|
||||
else if (item.action === 'deleteFile') desc = `Delete file ${item.path}`;
|
||||
else if (item.action === 'deleteDir') desc = `Delete directory ${item.path}`;
|
||||
else if (item.action === 'createDir') desc = `Create directory ${item.path}`;
|
||||
else if (item.action === 'copy') desc = `Copy file ${item.src} to ${item.dest}`;
|
||||
else if (item.action === 'move') desc = `Move file ${item.src} to ${item.dest}`;
|
||||
return {
|
||||
desc,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
const queue = computed(() => {
|
||||
if (!props.device.queue) return [];
|
||||
return props.device.queue.map(item => {
|
||||
let desc = '';
|
||||
if (item.action === 'download') desc = `Download ${trimPath(item.path[0])}`;
|
||||
else if (item.action === 'upload') desc = `Upload ${'src' in item ? trimPath(item.src) : item.file.name}`;
|
||||
else if (item.action === 'uploadOs') desc = 'Upload OS';
|
||||
else if (item.action === 'deleteFile') desc = `Delete file ${item.path}`;
|
||||
else if (item.action === 'deleteDir') desc = `Delete directory ${item.path}`;
|
||||
else if (item.action === 'createDir') desc = `Create directory ${item.path}`;
|
||||
else if (item.action === 'copy') desc = `Copy file ${item.src} to ${item.dest}`;
|
||||
else if (item.action === 'move') desc = `Move file ${item.src} to ${item.dest}`;
|
||||
return {
|
||||
id: item.id,
|
||||
desc,
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
|||
|
|
@ -2,31 +2,33 @@
|
|||
<div class="header border-b px-2 py-2 flex w-full">
|
||||
<button @click="$devices.enumerate()" class="flex-shrink-0 mr-2 focus:outline-none"
|
||||
:class="$devices.enumerating && 'cursor-not-allowed opacity-25'" :disabled="$devices.enumerating">
|
||||
<img src="~feather-icons/dist/icons/refresh-cw.svg" class="w-5"/>
|
||||
<img :src="refreshCw" class="w-5"/>
|
||||
<div v-if="scanHint && Object.keys($devices.devices).length === 0" class="p-4 refresh-popup">
|
||||
Click to connect a device
|
||||
</div>
|
||||
</button>
|
||||
<el-popover width="239" :visible-arrow="false" popper-class="focus:outline-none dev-select-pop" v-model="active"
|
||||
<el-popover width="239" :show-arrow="false" popper-class="focus:outline-none dev-select-pop" v-model:visible="active"
|
||||
class="w-full overflow-hidden">
|
||||
<div slot="reference" class="relative w-full focus:outline-none">
|
||||
<div
|
||||
class="block w-full bg-white border border-gray-400 hover:border-gray-500 px-4 py-3/2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline h-8 truncate">
|
||||
<span v-if="selectedCalculator">
|
||||
<small class="tabular-nums">{{ selectedCalculator }}</small>
|
||||
<span v-if="calc && calc.info"> {{ calc.info.name }}</span>
|
||||
<span v-else> {{ calc.name }}</span>
|
||||
</span>
|
||||
<span v-else class="text-gray-700 text-sm">
|
||||
Select a device...
|
||||
</span>
|
||||
<template #reference>
|
||||
<div class="relative w-full focus:outline-none">
|
||||
<div
|
||||
class="block w-full bg-white border border-gray-400 hover:border-gray-500 px-4 py-3/2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline h-8 truncate">
|
||||
<span v-if="selectedCalculator">
|
||||
<small class="tabular-nums">{{ selectedCalculator }}</small>
|
||||
<span v-if="calc && calc.info"> {{ calc.info.name }}</span>
|
||||
<span v-else> {{ calc!.name }}</span>
|
||||
</span>
|
||||
<span v-else class="text-gray-700 text-sm">
|
||||
Select a device...
|
||||
</span>
|
||||
</div>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ul>
|
||||
<li v-for="(device, id) in $devices.devices" :key="id"
|
||||
class="p-2 hover:bg-blue-500 hover:text-white w-full cursor-pointer" @click="select(id)">
|
||||
|
|
@ -42,30 +44,36 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, PropSync, Vue} from 'vue-property-decorator';
|
||||
import ElPopover from 'element-ui/packages/popover/src/main.vue';
|
||||
import 'element-ui/lib/theme-chalk/popover.css';
|
||||
<script setup lang="ts">
|
||||
import {computed, inject, ref} from 'vue';
|
||||
import {ElPopover} from 'element-plus';
|
||||
import 'element-plus/es/components/popover/style/css';
|
||||
import {DEVICES_KEY, type GenericDevices} from './devices';
|
||||
import refreshCw from 'feather-icons/dist/icons/refresh-cw.svg';
|
||||
|
||||
@Component({components: {ElPopover}})
|
||||
export default class DeviceSelect extends Vue {
|
||||
@PropSync('selected', {type: [String]}) selectedCalculator!: string | null;
|
||||
@Prop({type: Boolean, default: false}) scanHint!: boolean;
|
||||
active = false;
|
||||
withDefaults(defineProps<{
|
||||
scanHint?: boolean;
|
||||
}>(), {
|
||||
scanHint: false,
|
||||
});
|
||||
|
||||
select(dev: string) {
|
||||
this.selectedCalculator = dev;
|
||||
this.active = false;
|
||||
}
|
||||
const selectedCalculator = defineModel<string | null>('selected');
|
||||
|
||||
get calc() {
|
||||
return this.selectedCalculator && this.$devices.devices[this.selectedCalculator];
|
||||
}
|
||||
const devices = inject(DEVICES_KEY) as GenericDevices;
|
||||
|
||||
get anyCalcs() {
|
||||
return !!Object.keys(this.$devices.devices).length;
|
||||
}
|
||||
const active = ref(false);
|
||||
|
||||
function select(dev: string) {
|
||||
selectedCalculator.value = dev;
|
||||
active.value = false;
|
||||
}
|
||||
|
||||
const calc = computed(() => {
|
||||
const id = selectedCalculator.value;
|
||||
return id ? devices.devices[id] : undefined;
|
||||
});
|
||||
|
||||
const anyCalcs = computed(() => !!Object.keys(devices.devices).length);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<li v-for="(part, i) in parts" :key="i" class="" :class="{active: i === parts.length - 1}">
|
||||
<span class="inline-block cursor-pointer bg-gray-200 px-4 py-1 rounded-full"
|
||||
@click="goToIndex(i)">{{ part || 'Home' }}</span>
|
||||
<img v-if="i < parts.length - 1" class="inline img-fix" src="~feather-icons/dist/icons/chevron-right.svg"/>
|
||||
<img v-if="i < parts.length - 1" class="inline img-fix" :src="chevronRight"/>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="flex-grow"/>
|
||||
|
|
@ -26,59 +26,59 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue, Watch} from 'vue-property-decorator';
|
||||
<script setup lang="ts">
|
||||
import {computed, inject, ref, watch} from 'vue';
|
||||
import FileView from './FileView.vue';
|
||||
import type {FileInfo} from "./devices";
|
||||
import FileData from "./FileData.vue";
|
||||
import DeviceQueue from "./DeviceQueue.vue";
|
||||
import {DEVICES_KEY, type GenericDevices, type FileInfo} from './devices';
|
||||
import FileData from './FileData.vue';
|
||||
import DeviceQueue from './DeviceQueue.vue';
|
||||
import chevronRight from 'feather-icons/dist/icons/chevron-right.svg';
|
||||
|
||||
@Component({
|
||||
asyncComputed: {
|
||||
files: {
|
||||
async get(this: FileBrowser) {
|
||||
try {
|
||||
this.updateIndex;
|
||||
return (await this.$devices.listDir(this.dev, this.path)).map(file => ({
|
||||
...file,
|
||||
path: `${this.path}/${file.path}`
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
components: {DeviceQueue, FileData, FileView}
|
||||
})
|
||||
export default class FileBrowser extends Vue {
|
||||
@Prop({type: String, required: true}) private dev!: string;
|
||||
@Prop({type: Boolean, default: false}) private showHidden!: boolean;
|
||||
@Prop({type: Boolean, default: false}) private nativeUpload!: boolean;
|
||||
path = '';
|
||||
updateIndex = 0;
|
||||
selected: FileInfo[] = [];
|
||||
files!: FileInfo[] | null;
|
||||
const devices = inject(DEVICES_KEY) as GenericDevices;
|
||||
|
||||
@Watch('path')
|
||||
onPathChange() {
|
||||
this.selected = [];
|
||||
const props = withDefaults(defineProps<{
|
||||
dev: string;
|
||||
showHidden?: boolean;
|
||||
nativeUpload?: boolean;
|
||||
}>(), {
|
||||
showHidden: false,
|
||||
nativeUpload: false,
|
||||
});
|
||||
|
||||
const path = ref('');
|
||||
const updateIndex = ref(0);
|
||||
const selected = ref<FileInfo[]>([]);
|
||||
|
||||
const files = ref<FileInfo[] | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
async function loadFiles() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const contents = await devices.listDir(props.dev, path.value);
|
||||
files.value = contents.map((file) => ({
|
||||
...file,
|
||||
path: `${path.value}/${file.path}`,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
files.value = null;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
get parts() {
|
||||
return this.path.split('/');
|
||||
}
|
||||
watch([() => props.dev, path, updateIndex], loadFiles, {immediate: true});
|
||||
|
||||
get loading() {
|
||||
return this.$asyncComputed.files.updating;
|
||||
}
|
||||
watch(path, () => {
|
||||
selected.value = [];
|
||||
});
|
||||
|
||||
goToIndex(i: number) {
|
||||
this.path = this.parts.slice(0, i + 1).join('/');
|
||||
this.updateIndex++;
|
||||
}
|
||||
const parts = computed(() => path.value.split('/'));
|
||||
|
||||
function goToIndex(i: number) {
|
||||
path.value = parts.value.slice(0, i + 1).join('/');
|
||||
updateIndex.value++;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
</button>
|
||||
<el-popover
|
||||
width="190"
|
||||
popper-class="focus:outline-none" @show="newName = formatPath(files[0])" v-model="renamePopup">
|
||||
popper-class="focus:outline-none" @show="newName = formatPath(files[0])" v-model:visible="renamePopup">
|
||||
<form @submit.prevent="rename">
|
||||
<input v-model="newName"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
|
|
@ -21,9 +21,11 @@
|
|||
Rename
|
||||
</button>
|
||||
</form>
|
||||
<button slot="reference" class="mt-4 button w-full">
|
||||
Rename
|
||||
</button>
|
||||
<template #reference>
|
||||
<button class="mt-4 button w-full">
|
||||
Rename
|
||||
</button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div v-else-if="files.length && files.every(file => !file.isDir)">
|
||||
|
|
@ -32,7 +34,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div v-if="files.length">
|
||||
<el-popover width="170" popper-class="focus:outline-none" v-model="deletePopup">
|
||||
<el-popover width="170" popper-class="focus:outline-none" v-model:visible="deletePopup">
|
||||
<div>
|
||||
Delete {{ files.length }} file{{ files.length === 1 ? '' : 's' }}?
|
||||
<div class="flex w-full justify-between">
|
||||
|
|
@ -44,17 +46,21 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button slot="reference" class="mt-4 button danger w-full">
|
||||
Delete {{ files.length > 1 ? `${files.length} files` : '' }}
|
||||
</button>
|
||||
<template #reference>
|
||||
<button class="mt-4 button danger w-full">
|
||||
Delete {{ files.length > 1 ? `${files.length} files` : '' }}
|
||||
</button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="flex-grow"/>
|
||||
<div class="pb-4">
|
||||
<el-popover width="170" popper-class="focus:outline-none" v-model="createDirPopup" @show="newName = ''">
|
||||
<button slot="reference" class="mt-4 button w-full text-sm">
|
||||
Create directory
|
||||
</button>
|
||||
<el-popover width="170" popper-class="focus:outline-none" v-model:visible="createDirPopup" @show="newName = ''">
|
||||
<template #reference>
|
||||
<button class="mt-4 button w-full text-sm">
|
||||
Create directory
|
||||
</button>
|
||||
</template>
|
||||
<form @submit.prevent="$devices.createDir(dev, `${path}/${newName}`)">
|
||||
<input v-model="newName"
|
||||
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
||||
|
|
@ -65,7 +71,7 @@
|
|||
</button>
|
||||
</form>
|
||||
</el-popover>
|
||||
<button class="mt-4 button w-full" @click="nativeUpload ? $refs.upload.click() : $devices.promptUploadFiles(dev, path)">
|
||||
<button class="mt-4 button w-full" @click="nativeUpload ? upload?.click() : $devices.promptUploadFiles(dev, path)">
|
||||
Upload files
|
||||
</button>
|
||||
<input v-if="nativeUpload" ref="upload" type="file" class="hidden" multiple accept=".tns" @change="uploadNative" />
|
||||
|
|
@ -73,66 +79,71 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
import ElPopover from 'element-ui/packages/popover/src/main.vue';
|
||||
import 'element-ui/lib/theme-chalk/popover.css';
|
||||
import fileSize from "filesize";
|
||||
import type {FileInfo} from './devices';
|
||||
import FileIcon from "./FileIcon.vue";
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { ElPopover } from 'element-plus';
|
||||
import 'element-plus/es/components/popover/style/css';
|
||||
import { DEVICES_KEY, type GenericDevices, type FileInfo } from './devices';
|
||||
import { filesize } from 'filesize';
|
||||
import FileIcon from './FileIcon.vue';
|
||||
|
||||
@Component({
|
||||
components: {FileIcon, ElPopover}
|
||||
})
|
||||
export default class FileData extends Vue {
|
||||
@Prop({type: Array, default: () => ([])}) private files!: FileInfo[];
|
||||
@Prop({type: String, required: true}) private path!: string;
|
||||
@Prop({type: Boolean, default: false}) private showHidden!: boolean;
|
||||
@Prop({type: String, required: true}) private dev!: string;
|
||||
@Prop({type: Boolean, default: false}) private nativeUpload!: boolean;
|
||||
newName = '';
|
||||
renamePopup = false;
|
||||
deletePopup = false;
|
||||
createDirPopup = false;
|
||||
const devices = inject(DEVICES_KEY) as GenericDevices;
|
||||
|
||||
formatSize(size: number) {
|
||||
return fileSize(size, {round: 1});
|
||||
const props = withDefaults(defineProps<{
|
||||
files: FileInfo[];
|
||||
dev: string;
|
||||
path: string;
|
||||
showHidden?: boolean;
|
||||
nativeUpload?: boolean;
|
||||
}>(), {
|
||||
showHidden: false,
|
||||
nativeUpload: false,
|
||||
});
|
||||
|
||||
const newName = ref('');
|
||||
const renamePopup = ref(false);
|
||||
const deletePopup = ref(false);
|
||||
const createDirPopup = ref(false);
|
||||
const upload = ref<HTMLInputElement | null>(null);
|
||||
|
||||
function formatSize(size: number) {
|
||||
return filesize(size, {round: 1});
|
||||
}
|
||||
|
||||
function formatPath({path, isDir}: FileInfo) {
|
||||
const name = path.split('/').pop() as string;
|
||||
if (props.showHidden || isDir) return name;
|
||||
return name.substring(0, name.length - 4);
|
||||
}
|
||||
|
||||
function download() {
|
||||
devices.downloadFiles(props.dev, props.files.map(file => [file.path, file.size]));
|
||||
}
|
||||
|
||||
function deleteFiles() {
|
||||
deletePopup.value = false;
|
||||
devices.delete(props.dev, props.files);
|
||||
}
|
||||
|
||||
const isValidName = computed(() => {
|
||||
if (props.showHidden && !props.files[0]?.isDir && !newName.value.endsWith('.tns')) return false;
|
||||
return newName.value.length > 0 && !newName.value.includes('/');
|
||||
});
|
||||
|
||||
function rename() {
|
||||
if (isValidName.value) {
|
||||
renamePopup.value = false;
|
||||
const path = props.files[0].path;
|
||||
const newPath = path.split('/');
|
||||
newPath.pop();
|
||||
newPath.push(newName.value + (!props.showHidden && !props.files[0].isDir ? '.tns' : ''));
|
||||
devices.move(props.dev, path, newPath.join('/'));
|
||||
}
|
||||
}
|
||||
|
||||
formatPath({path, isDir}: FileInfo) {
|
||||
const name = path.split('/').pop() as string;
|
||||
if (this.showHidden || isDir) return name;
|
||||
return name.substring(0, name.length - 4);
|
||||
}
|
||||
|
||||
download() {
|
||||
this.$devices.downloadFiles(this.dev, this.files.map(file => [file.path, file.size]));
|
||||
}
|
||||
|
||||
deleteFiles() {
|
||||
this.deletePopup = false;
|
||||
this.$devices.delete(this.dev, this.files);
|
||||
}
|
||||
|
||||
get isValidName() {
|
||||
if (this.showHidden && !this.files[0]?.isDir && !this.newName.endsWith('.tns')) return false;
|
||||
return this.newName.length && !this.newName.includes('/');
|
||||
}
|
||||
|
||||
rename() {
|
||||
if (this.isValidName) {
|
||||
this.renamePopup = false;
|
||||
const path = this.files[0].path;
|
||||
const newPath = path.split('/');
|
||||
newPath.pop();
|
||||
newPath.push(this.newName + (!this.showHidden && !this.files[0].isDir ? '.tns' : ''));
|
||||
this.$devices.move(this.dev, path, newPath.join('/'));
|
||||
}
|
||||
}
|
||||
|
||||
uploadNative(e: Event & {target: HTMLInputElement}) {
|
||||
this.$devices.uploadFiles(this.dev, this.path, [...(e.target.files || [])])
|
||||
}
|
||||
function uploadNative(e: Event) {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
devices.uploadFiles(props.dev, props.path, [...(files || [])]);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,161 +1,97 @@
|
|||
<template>
|
||||
<img :width="width" :src="icon" alt="name"/>
|
||||
<img :width="width" :src="icon" alt=""/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue} from 'vue-property-decorator';
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
@Component
|
||||
export default class FileView extends Vue {
|
||||
@Prop({type: String, required: true}) private path!: string;
|
||||
@Prop({type: Boolean, default: false}) private dir!: string;
|
||||
@Prop({type: Number, default: 64}) private width!: number;
|
||||
const props = withDefaults(defineProps<{
|
||||
path: string;
|
||||
dir?: boolean;
|
||||
width?: number;
|
||||
}>(), {
|
||||
dir: false,
|
||||
width: 64,
|
||||
});
|
||||
|
||||
get name() {
|
||||
return this.path.split('/').pop() as string;
|
||||
}
|
||||
// Vite replacement for webpack's require(). Eagerly resolves every SVG in
|
||||
// assets/ to its emitted URL at build time, keyed by module path.
|
||||
const assets = import.meta.glob<string>('../assets/**/*.svg', {
|
||||
eager: true,
|
||||
query: '?url',
|
||||
import: 'default',
|
||||
});
|
||||
|
||||
get ext() {
|
||||
const parts = this.name.split('.');
|
||||
if (parts[parts.length - 1] === 'tns') parts.pop();
|
||||
if (parts.length < 2) return '';
|
||||
return parts.pop() as string;
|
||||
}
|
||||
|
||||
get icon() {
|
||||
if (this.dir) {
|
||||
switch (this.name.trim().toLowerCase()) {
|
||||
case 'games':
|
||||
return require('../assets/folders/games.svg');
|
||||
case 'images':
|
||||
case 'photos':
|
||||
return require('../assets/folders/images.svg');
|
||||
case 'music':
|
||||
case 'sound':
|
||||
case 'sounds':
|
||||
return require('../assets/folders/music.svg');
|
||||
case 'script':
|
||||
case 'scripts':
|
||||
case 'program':
|
||||
case 'programs':
|
||||
return require('../assets/folders/scripts.svg');
|
||||
case 'template':
|
||||
case 'templates':
|
||||
return require('../assets/folders/templates.svg');
|
||||
case 'video':
|
||||
case 'videos':
|
||||
return require('../assets/folders/video.svg');
|
||||
default:
|
||||
return require('../assets/folders/folder.svg');
|
||||
}
|
||||
}
|
||||
if (this.name === 'ndless_resources.tns') return require('../assets/files/resources.svg');
|
||||
if (this.name.startsWith('ndless_installer')) return require('../assets/files/installer.svg');
|
||||
switch (this.ext.toLowerCase()) {
|
||||
case 'bin':
|
||||
return require('../assets/files/binary.svg');
|
||||
case 'ical':
|
||||
case 'ics':
|
||||
case 'ifb':
|
||||
case 'icalendar':
|
||||
return require('../assets/files/calendar.svg');
|
||||
case 'cfg':
|
||||
return require('../assets/files/cfg.svg');
|
||||
case 'vcf':
|
||||
return require('../assets/files/contact.svg');
|
||||
case 'csv':
|
||||
case 'log':
|
||||
case 'logs':
|
||||
return require('../assets/files/csv.svg');
|
||||
case 'sql':
|
||||
case 'db':
|
||||
case 'sqlite':
|
||||
return require('../assets/files/db.svg');
|
||||
case 'epub':
|
||||
return require('../assets/files/epub.svg');
|
||||
case 'eot':
|
||||
case 'otf':
|
||||
case 'ttf':
|
||||
case 'woff':
|
||||
case 'woff2':
|
||||
return require('../assets/files/font.svg');
|
||||
case 'png':
|
||||
case 'bmp':
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
case 'jpe':
|
||||
case 'jif':
|
||||
case 'jfif':
|
||||
case 'jfi':
|
||||
case 'jp2':
|
||||
case 'j2k':
|
||||
case 'jpf':
|
||||
case 'jpx':
|
||||
case 'jpm':
|
||||
case 'mj2':
|
||||
case 'gif':
|
||||
case 'tiff':
|
||||
case 'tif':
|
||||
case 'ppm':
|
||||
case 'pgm':
|
||||
case 'pbm':
|
||||
case 'pnm':
|
||||
case 'webp':
|
||||
case 'heif':
|
||||
case 'heifs':
|
||||
case 'heic':
|
||||
case 'heics':
|
||||
case 'avci':
|
||||
case 'avcs':
|
||||
case 'avif':
|
||||
case 'avifs':
|
||||
case 'ico':
|
||||
case 'icon':
|
||||
case 'icns':
|
||||
case 'pcx':
|
||||
case 'pgf':
|
||||
case 'tga':
|
||||
case 'psd':
|
||||
case 'xcf':
|
||||
return require('../assets/files/image.svg');
|
||||
case 'js':
|
||||
case 'mjs':
|
||||
return require('../assets/files/js.svg');
|
||||
case 'json':
|
||||
case 'json5':
|
||||
return require('../assets/files/json.svg');
|
||||
case 'lua':
|
||||
return require('../assets/files/lua.svg');
|
||||
case 'pdf':
|
||||
return require('../assets/files/pdf.svg');
|
||||
case 'py':
|
||||
return require('../assets/files/python.svg');
|
||||
case 'rom':
|
||||
case '89u':
|
||||
case 'smc':
|
||||
case 'sfc':
|
||||
case 'srm':
|
||||
case 'img':
|
||||
return require('../assets/files/rom.svg');
|
||||
case 'svg':
|
||||
case 'svgz':
|
||||
return require('../assets/files/svg.svg');
|
||||
case 'txt':
|
||||
return require('../assets/files/txt.svg');
|
||||
case 'mp4':
|
||||
return require('../assets/files/video.svg');
|
||||
case 'xml':
|
||||
return require('../assets/files/xml.svg');
|
||||
case 'zip':
|
||||
case 'tar':
|
||||
case 'gz':
|
||||
return require('../assets/files/zip.svg');
|
||||
default:
|
||||
return require('../assets/files/unknown.svg');
|
||||
}
|
||||
}
|
||||
function asset(name: string): string {
|
||||
const key = `../assets/${name}`;
|
||||
const found = assets[key];
|
||||
if (!found) throw new Error(`missing icon asset: ${key}`);
|
||||
return found;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
const FOLDER_ICONS: Record<string, string> = {
|
||||
games: 'games',
|
||||
images: 'images',
|
||||
photos: 'images',
|
||||
music: 'music',
|
||||
sound: 'music',
|
||||
sounds: 'music',
|
||||
script: 'scripts',
|
||||
scripts: 'scripts',
|
||||
program: 'scripts',
|
||||
programs: 'scripts',
|
||||
template: 'templates',
|
||||
templates: 'templates',
|
||||
video: 'video',
|
||||
videos: 'video',
|
||||
};
|
||||
|
||||
const EXT_ICONS: Record<string, string> = {
|
||||
bin: 'binary',
|
||||
ical: 'calendar', ics: 'calendar', ifb: 'calendar', icalendar: 'calendar',
|
||||
cfg: 'cfg',
|
||||
vcf: 'contact',
|
||||
csv: 'csv', log: 'csv', logs: 'csv',
|
||||
sql: 'db', db: 'db', sqlite: 'db',
|
||||
epub: 'epub',
|
||||
eot: 'font', otf: 'font', ttf: 'font', woff: 'font', woff2: 'font',
|
||||
png: 'image', bmp: 'image', jpg: 'image', jpeg: 'image', jpe: 'image',
|
||||
jif: 'image', jfif: 'image', jfi: 'image', jp2: 'image', j2k: 'image',
|
||||
jpf: 'image', jpx: 'image', jpm: 'image', mj2: 'image', gif: 'image',
|
||||
tiff: 'image', tif: 'image', ppm: 'image', pgm: 'image', pbm: 'image',
|
||||
pnm: 'image', webp: 'image', heif: 'image', heifs: 'image', heic: 'image',
|
||||
heics: 'image', avci: 'image', avcs: 'image', avif: 'image', avifs: 'image',
|
||||
ico: 'image', icon: 'image', icns: 'image', pcx: 'image', pgf: 'image',
|
||||
tga: 'image', psd: 'image', xcf: 'image',
|
||||
js: 'js', mjs: 'js',
|
||||
json: 'json', json5: 'json',
|
||||
lua: 'lua',
|
||||
pdf: 'pdf',
|
||||
py: 'python',
|
||||
rom: 'rom', '89u': 'rom', smc: 'rom', sfc: 'rom', srm: 'rom', img: 'rom',
|
||||
svg: 'svg', svgz: 'svg',
|
||||
txt: 'txt',
|
||||
mp4: 'video',
|
||||
xml: 'xml',
|
||||
zip: 'zip', tar: 'zip', gz: 'zip',
|
||||
};
|
||||
|
||||
const name = computed(() => props.path.split('/').pop() as string);
|
||||
|
||||
const ext = computed(() => {
|
||||
const parts = name.value.split('.');
|
||||
if (parts[parts.length - 1] === 'tns') parts.pop();
|
||||
if (parts.length < 2) return '';
|
||||
return parts.pop() as string;
|
||||
});
|
||||
|
||||
const icon = computed(() => {
|
||||
if (props.dir) {
|
||||
const key = FOLDER_ICONS[name.value.trim().toLowerCase()] ?? 'folder';
|
||||
return asset(`folders/${key}.svg`);
|
||||
}
|
||||
if (name.value === 'ndless_resources.tns') return asset('files/resources.svg');
|
||||
if (name.value.startsWith('ndless_installer')) return asset('files/installer.svg');
|
||||
return asset(`files/${EXT_ICONS[ext.value.toLowerCase()] ?? 'unknown'}.svg`);
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -10,55 +10,59 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {Component, Prop, Vue, Watch} from 'vue-property-decorator';
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch} from 'vue';
|
||||
import type {FileInfo} from './devices';
|
||||
import FileIcon from "./FileIcon.vue";
|
||||
import FileIcon from './FileIcon.vue';
|
||||
|
||||
@Component({
|
||||
components: {FileIcon}
|
||||
})
|
||||
export default class FileView extends Vue {
|
||||
@Prop({type: Array, default: () => ([])}) private files!: FileInfo[];
|
||||
@Prop({type: Boolean, default: false}) private showHidden!: boolean;
|
||||
selected: number[] = [];
|
||||
const props = withDefaults(defineProps<{
|
||||
files: FileInfo[];
|
||||
showHidden?: boolean;
|
||||
}>(), {
|
||||
files: () => [],
|
||||
showHidden: false,
|
||||
});
|
||||
|
||||
@Watch('selected', {deep: true, immediate: true})
|
||||
onSelect(selected: number[]) {
|
||||
this.$emit('select', selected.map(file => this.filteredFiles[file]));
|
||||
const emit = defineEmits<{
|
||||
nav: [path: string];
|
||||
select: [files: FileInfo[]];
|
||||
}>();
|
||||
|
||||
const selected = ref<number[]>([]);
|
||||
|
||||
const filteredFiles = computed(() => {
|
||||
if (props.showHidden) return props.files;
|
||||
return props.files.filter(file => file.isDir || file.path.endsWith('.tns'));
|
||||
});
|
||||
|
||||
watch(selected, (sel) => {
|
||||
emit('select', sel.map(file => filteredFiles.value[file]));
|
||||
}, {deep: true, immediate: true});
|
||||
|
||||
watch(() => props.files, () => {
|
||||
selected.value = [];
|
||||
});
|
||||
|
||||
function formatPath({path, isDir}: FileInfo) {
|
||||
const name = path.split('/').pop() as string;
|
||||
if (props.showHidden || isDir) return name;
|
||||
return name.substring(0, name.length - 4);
|
||||
}
|
||||
|
||||
function xorSelection(i: number) {
|
||||
if (selected.value.includes(i)) selected.value = selected.value.filter(num => num !== i);
|
||||
else selected.value.push(i);
|
||||
}
|
||||
|
||||
function shiftSelection(item: number) {
|
||||
const lastSelected = selected.value[selected.value.length - 1];
|
||||
if (lastSelected === undefined) {
|
||||
selected.value.push(item);
|
||||
return;
|
||||
}
|
||||
|
||||
@Watch('files')
|
||||
onFileChange() {
|
||||
this.selected = [];
|
||||
}
|
||||
|
||||
get filteredFiles() {
|
||||
if (this.showHidden) return this.files;
|
||||
return this.files.filter(file => file.isDir || file.path.endsWith('.tns'));
|
||||
}
|
||||
|
||||
formatPath({path, isDir}: FileInfo) {
|
||||
const name = path.split('/').pop() as string;
|
||||
if (this.showHidden || isDir) return name;
|
||||
return name.substring(0, name.length - 4);
|
||||
}
|
||||
|
||||
xorSelection(i: number) {
|
||||
if (this.selected.includes(i)) this.selected = this.selected.filter(num => num !== i);
|
||||
else this.selected.push(i);
|
||||
}
|
||||
|
||||
shiftSelection(item: number) {
|
||||
const lastSelected = this.selected[this.selected.length-1];
|
||||
if(lastSelected === undefined) {
|
||||
this.selected.push(item);
|
||||
return;
|
||||
}
|
||||
const [lower, upper] = item > lastSelected ? [lastSelected, item] : [item, lastSelected];
|
||||
for(let i = lower; i <= upper; i++) {
|
||||
if (!this.selected.includes(i))this.selected.push(i);
|
||||
}
|
||||
const [lower, upper] = item > lastSelected ? [lastSelected, item] : [item, lastSelected];
|
||||
for (let i = lower; i <= upper; i++) {
|
||||
if (!selected.value.includes(i)) selected.value.push(i);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -59,9 +59,20 @@ export interface GenericDevices {
|
|||
move(dev: DevId | string, src: string, dest: string): Promise<void>;
|
||||
}
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
// 3. Declare augmentation for Vue
|
||||
interface Vue {
|
||||
import type { InjectionKey } from 'vue';
|
||||
|
||||
/**
|
||||
* n-link-core is consumed by more than one host (the Tauri desktop app and
|
||||
* the WebUSB web app), each of which supplies its own GenericDevices
|
||||
* implementation. Core components must therefore never import a concrete
|
||||
* store — they inject this key. The host provides it at startup.
|
||||
*/
|
||||
export const DEVICES_KEY = Symbol('devices') as InjectionKey<GenericDevices>;
|
||||
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
$devices: GenericDevices;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
{
|
||||
"name": "n-link-core",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"element-ui": "^2.13.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-property-decorator": "^9.0.2"
|
||||
"peerDependencies": {
|
||||
"element-plus": "^2.14.3",
|
||||
"vue": "^3.5.40"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.4.0",
|
||||
"@typescript-eslint/parser": "^4.4.0",
|
||||
"@vue/eslint-config-typescript": "^5.1.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"tailwindcss": "^1.8.9"
|
||||
"tailwindcss": "^3.4.19",
|
||||
"@tailwindcss/forms": "^0.5.11"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
n-link-core/shims-tsx.d.ts
vendored
|
|
@ -1,13 +0,0 @@
|
|||
import Vue, { VNode } from 'vue'
|
||||
|
||||
declare global {
|
||||
namespace JSX {
|
||||
// tslint:disable no-empty-interface
|
||||
interface Element extends VNode {}
|
||||
// tslint:disable no-empty-interface
|
||||
interface ElementClass extends Vue {}
|
||||
interface IntrinsicElements {
|
||||
[elem: string]: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
54
n-link-core/shims-vue.d.ts
vendored
|
|
@ -1,54 +0,0 @@
|
|||
declare module '*.vue' {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
||||
|
||||
import Vue, { PluginFunction } from 'vue';
|
||||
|
||||
interface IAsyncComputedOptions {
|
||||
errorHandler?: (error: string[]) => void;
|
||||
useRawError?: boolean;
|
||||
default?: any;
|
||||
}
|
||||
|
||||
export default class AsyncComputed {
|
||||
constructor(options?: IAsyncComputedOptions);
|
||||
static install: PluginFunction<never>;
|
||||
static version: string;
|
||||
}
|
||||
|
||||
type AsyncComputedGetter<T> = () => Promise<T> | T;
|
||||
export interface IAsyncComputedProperty<T> {
|
||||
default?: T | (() => T);
|
||||
get: AsyncComputedGetter<T>;
|
||||
watch?: () => void;
|
||||
shouldUpdate?: () => boolean;
|
||||
lazy?: boolean;
|
||||
}
|
||||
|
||||
interface IAsyncComputedProperties {
|
||||
[K: string]: AsyncComputedGetter<any> | IAsyncComputedProperty<any>;
|
||||
}
|
||||
|
||||
declare module 'vue/types/options' {
|
||||
interface ComponentOptions<V extends Vue> {
|
||||
// @ts-ignore
|
||||
asyncComputed?: IAsyncComputedProperties;
|
||||
}
|
||||
}
|
||||
|
||||
interface IASyncComputedState {
|
||||
state: 'updating' | 'success' | 'error';
|
||||
updating: boolean;
|
||||
success: boolean;
|
||||
error: boolean;
|
||||
exception: Error | null;
|
||||
update: () => void;
|
||||
}
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
// tslint:disable-next-line:interface-name
|
||||
interface Vue {
|
||||
$asyncComputed: { [K: string]: IASyncComputedState };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
module.exports = {
|
||||
future: {
|
||||
purgeLayersByDefault: true,
|
||||
removeDeprecatedGapUtilities: true,
|
||||
},
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: [
|
||||
|
|
@ -51,17 +47,5 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
},
|
||||
variants: {},
|
||||
plugins: [require('@tailwindcss/custom-forms')],
|
||||
purge: process.env.npm_package_name === 'web' && {
|
||||
enabled: process.env.NODE_ENV === 'production',
|
||||
content: [
|
||||
'components/**/*.vue',
|
||||
'layouts/**/*.vue',
|
||||
'pages/**/*.vue',
|
||||
'plugins/**/*.js',
|
||||
'nuxt.config.js',
|
||||
'../n-link-core/components/**/*.vue'
|
||||
]
|
||||
},
|
||||
plugins: [require('@tailwindcss/forms')],
|
||||
};
|
||||
|
|
|
|||
775
nlink-cli/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,775 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler32"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "array_iterator"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "015eb97c25bb20230c9fdf1533b4cee74b27924cd462f74b0d6fc011aaf196de"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adc2ab4d5a16117f9029e9a6b5e4e79f4c67f6519bc134210d4d4a04ba31f41b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9db17aec586697a93219b19726b5b68307eba92898c34b170857343fe67c99d"
|
||||
dependencies = [
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b009b6744c1445efd7244084e25e498636412effb6760b55067553baa925cc7"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.23.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7baab56125e25686df467fe470785512329883aab42696d661247aca2a2896e4"
|
||||
dependencies = [
|
||||
"console",
|
||||
"lazy_static",
|
||||
"number_prefix",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libflate"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18"
|
||||
dependencies = [
|
||||
"adler32",
|
||||
"crc32fast",
|
||||
"libflate_lz77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libflate_lz77"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf"
|
||||
dependencies = [
|
||||
"rle-decode-fast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libnspire"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6428b72ef81134d34d077748639ae280cf8b5b3314ae99fa8ff1e34cbc1d64b"
|
||||
dependencies = [
|
||||
"array_iterator",
|
||||
"displaydoc",
|
||||
"image",
|
||||
"libnspire-sys",
|
||||
"libusb1-sys",
|
||||
"rusb",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libnspire-sys"
|
||||
version = "0.3.4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"globwalk",
|
||||
"libusb1-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libusb1-sys"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be241693102a24766d0b8526c8988771edac2842630d7e730f8e9fbc014f3703"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libflate",
|
||||
"pkg-config",
|
||||
"tar",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "nlink-cli"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"indicatif",
|
||||
"libnspire",
|
||||
"rusb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rle-decode-fast"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
|
||||
|
||||
[[package]]
|
||||
name = "rusb"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f32cd45962594f9f8cd4547b5757132715600e2c8840aa9ccd3d1a9ed6fdc6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libusb1-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
22
nlink-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "nlink-cli"
|
||||
version = "0.1.6"
|
||||
description = "Standalone CLI for linking to the TI-Nspire (incl. CX II), extracted from n-link"
|
||||
edition = "2018"
|
||||
|
||||
[[bin]]
|
||||
name = "n-link-cli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
libnspire = "0.2.2"
|
||||
rusb = "0.6.4"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
indicatif = "0.15"
|
||||
# Link the system libusb (install libusb-1.0-0-dev). The vendored build in
|
||||
# libusb1-sys 0.4.x is broken on this toolchain, so we use the distro library.
|
||||
|
||||
# Local patched libnspire-sys: raises the CX II NNSE handshake retry limit
|
||||
# (10 -> 100) so the address<->time handshake has time to complete.
|
||||
[patch.crates-io]
|
||||
libnspire-sys = { path = "../vendor/libnspire-sys" }
|
||||
51
nlink-cli/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# nlink-cli — standalone TI-Nspire CX II link CLI (Linux)
|
||||
|
||||
A GUI-free command-line build extracted from [n-link](https://github.com/lights0123/n-link),
|
||||
for transferring files to/from a TI-Nspire CX II (CAS) over USB on Linux.
|
||||
|
||||
Built because:
|
||||
- **TiLP2** (distro `libticables` 1.3.6) does not support the CX II (USB PID `e022`) — its
|
||||
probe reports "no devices found".
|
||||
- The **prebuilt n-link 0.1.6 `.deb`** won't run on Ubuntu 26.04 (needs the retired
|
||||
`libssl.so.1.1` + WebKit2GTK 4.0). n-link is a Tauri/WebKit app.
|
||||
|
||||
This CLI reuses only n-link's self-contained `cli.rs` (upload/download/ls/mkdir/rmdir/
|
||||
copy/move/upload-os), dropping Tauri/WebKit entirely.
|
||||
|
||||
## Build
|
||||
```bash
|
||||
# one-time system deps
|
||||
sudo apt-get install -y libusb-1.0-0-dev pkg-config
|
||||
# Rust toolchain via rustup, then:
|
||||
cargo build --release
|
||||
# binary: target/release/n-link-cli
|
||||
```
|
||||
`Cargo.toml` uses a local `[patch.crates-io]` override of `libnspire-sys`
|
||||
(`../vendor/libnspire-sys`) whose CX II NNSE handshake retry limit is raised from 10.
|
||||
|
||||
## Run
|
||||
```bash
|
||||
./target/release/n-link-cli ls /
|
||||
./target/release/n-link-cli download "/MyFile.tns" ./
|
||||
./target/release/n-link-cli upload ./MyFile.tns "/"
|
||||
./target/release/n-link-cli --help
|
||||
```
|
||||
|
||||
## System setup required (Linux)
|
||||
1. **udev access** for the CX II (`/etc/udev/rules.d/70-ti-nspire.rules`):
|
||||
```
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="0451", ENV{ID_MM_DEVICE_IGNORE}="1"
|
||||
```
|
||||
(plus group/`uaccess` for PID `e022`) — then `udevadm control --reload-rules && udevadm trigger`.
|
||||
2. **ModemManager** grabs the interface on plug; the `ID_MM_DEVICE_IGNORE` rule above
|
||||
tells it to leave TI devices alone. Restart it after adding the rule:
|
||||
`sudo systemctl start ModemManager`.
|
||||
|
||||
## Known limitation
|
||||
libnspire's CX II ("NavNet SE") protocol support has been **frozen upstream since 2020**
|
||||
(`lights0123/libnspire`, last commit 2020-09-30; crates.io `libnspire-sys` 0.3.4 is the newest).
|
||||
Against **current (2026) CX II firmware** the USB link and handshake work — real filenames
|
||||
are transferred — but **multi-packet stream transfers are unreliable** and long operations
|
||||
(e.g. listing a folder with many entries) can fail mid-transfer with `Busy`. Short transfers
|
||||
are more likely to complete. This is a library/firmware-era mismatch, not a local
|
||||
configuration problem.
|
||||
7
nlink-cli/src/NOTICE.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
This program contains parts of other programs licensed under the GPL version
|
||||
3.0. They are as follows:
|
||||
|
||||
- libnspire, available for download at
|
||||
https://github.com/lights0123/libnspire-rs
|
||||
- Flat Remix, originally at https://github.com/daniruiz/flat-remix, with
|
||||
parts extracted available at {}
|
||||
394
nlink-cli/src/cli.rs
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
use std::ffi::OsStr;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::{fs::File, path::Path};
|
||||
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use libnspire::{dir::EntryType, PID, PID_CX2, VID};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(author, about, version)]
|
||||
struct Opt {
|
||||
#[clap(subcommand)]
|
||||
cmd: Option<SubCommand>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum SubCommand {
|
||||
Upload(Upload),
|
||||
Download(Download),
|
||||
UploadOS(UploadOS),
|
||||
Copy(Copy),
|
||||
Move(Move),
|
||||
Mkdir(Mkdir),
|
||||
Rmdir(Rmdir),
|
||||
Ls(Ls),
|
||||
/// View license information
|
||||
License,
|
||||
}
|
||||
|
||||
/// Upload files to the calculator
|
||||
#[derive(Args, Debug)]
|
||||
struct Upload {
|
||||
/// Files to upload
|
||||
#[clap(required = true)]
|
||||
files: Vec<PathBuf>,
|
||||
/// Destination path
|
||||
dest: String,
|
||||
}
|
||||
|
||||
/// Download files from the calculator
|
||||
#[derive(Args, Debug)]
|
||||
struct Download {
|
||||
/// Files to download
|
||||
#[clap(required = true)]
|
||||
files: Vec<String>,
|
||||
/// Destination path
|
||||
#[clap(required = true)]
|
||||
dest: PathBuf,
|
||||
}
|
||||
|
||||
/// Upload and install a .tcc/.tco/.tcc2/.tco2/.tct2 OS file
|
||||
#[derive(Args, Debug)]
|
||||
struct UploadOS {
|
||||
/// Path to the OS file
|
||||
#[clap(required = true)]
|
||||
file: PathBuf,
|
||||
|
||||
/// Disables the file extension check
|
||||
#[clap(long)]
|
||||
no_check_os: bool,
|
||||
}
|
||||
|
||||
/// Copy a file to a different location
|
||||
#[derive(Args, Debug)]
|
||||
struct Copy {
|
||||
/// Path to file
|
||||
#[clap(required = true)]
|
||||
from_path: String,
|
||||
|
||||
/// Path to new location
|
||||
#[clap(required = true)]
|
||||
dist_path: String,
|
||||
}
|
||||
|
||||
/// Move a file or directory to a new location
|
||||
#[derive(Args, Debug)]
|
||||
struct Move {
|
||||
/// Path to file
|
||||
#[clap(required = true)]
|
||||
from_path: String,
|
||||
|
||||
/// Path to new location
|
||||
#[clap(required = true)]
|
||||
dist_path: String,
|
||||
}
|
||||
|
||||
/// Create a directory
|
||||
#[derive(Args, Debug)]
|
||||
struct Mkdir {
|
||||
/// Path to directory
|
||||
#[clap(required = true)]
|
||||
path: String,
|
||||
}
|
||||
|
||||
/// Delete a directory
|
||||
#[derive(Args, Debug)]
|
||||
struct Rmdir {
|
||||
/// Path to directory
|
||||
#[clap(required = true)]
|
||||
path: String,
|
||||
}
|
||||
|
||||
/// List the contents of a directory
|
||||
#[derive(Args, Debug)]
|
||||
struct Ls {
|
||||
/// Path to directory
|
||||
#[clap(required = true)]
|
||||
path: String,
|
||||
}
|
||||
|
||||
fn get_dev() -> Option<libnspire::Handle<rusb::GlobalContext>> {
|
||||
rusb::devices()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|dev| {
|
||||
let descriptor = match dev.device_descriptor() {
|
||||
Ok(d) => d,
|
||||
Err(_) => return false,
|
||||
};
|
||||
descriptor.vendor_id() == VID && matches!(descriptor.product_id(), PID | PID_CX2)
|
||||
})
|
||||
.map(|dev| libnspire::Handle::new(dev.open().unwrap()).unwrap())
|
||||
}
|
||||
|
||||
pub fn cwd() -> PathBuf {
|
||||
#[cfg(target_os = "linux")]
|
||||
if std::env::var_os("APPIMAGE").is_some() && std::env::var_os("APPDIR").is_some() {
|
||||
if let Some(cwd) = std::env::var_os("OWD") {
|
||||
return cwd.into();
|
||||
}
|
||||
};
|
||||
std::env::current_dir().expect("Couldn't get current directory")
|
||||
}
|
||||
|
||||
pub fn run() -> bool {
|
||||
let opt: Opt = Opt::parse();
|
||||
if let Some(cmd) = opt.cmd {
|
||||
match cmd {
|
||||
SubCommand::Upload(Upload { files, mut dest }) => {
|
||||
if let Some(handle) = get_dev() {
|
||||
for file in files {
|
||||
let mut buf = vec![];
|
||||
File::open(cwd().join(&file))
|
||||
.unwrap()
|
||||
.read_to_end(&mut buf)
|
||||
.unwrap();
|
||||
let name = file
|
||||
.file_name()
|
||||
.expect("Failed to get file name")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let bar = ProgressBar::new(buf.len() as u64);
|
||||
bar.set_style(ProgressStyle::default_bar().template("{spinner:.green} {msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})"));
|
||||
bar.set_message(&format!("Upload {}", name));
|
||||
bar.enable_steady_tick(100);
|
||||
if dest.ends_with('/') {
|
||||
dest.remove(dest.len() - 1);
|
||||
}
|
||||
let res = handle.write_file(&format!("{}/{}", dest, name), &buf, &mut |remaining| {
|
||||
bar.set_position((buf.len() - remaining) as u64)
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
bar.finish_with_message(&format!("Upload {}: Ok", dest));
|
||||
}
|
||||
Err(error) => {
|
||||
bar.abandon_with_message(&format!("Failed: {}", error));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Couldn't find any device");
|
||||
}
|
||||
}
|
||||
SubCommand::Download(Download { dest, files }) => {
|
||||
if let Some(handle) = get_dev() {
|
||||
for file in files {
|
||||
let attr = handle.file_attr(&file);
|
||||
match attr {
|
||||
Ok(attr) => {
|
||||
let path = Path::new(&file);
|
||||
let dest_path = Path::join(&dest, path.file_name().unwrap().to_str().unwrap());
|
||||
match File::create(dest_path) {
|
||||
Ok(mut dest_file) => {
|
||||
let mut buf = vec![0u8; attr.size() as usize];
|
||||
|
||||
let bar = ProgressBar::new(buf.len() as u64);
|
||||
bar.set_style(ProgressStyle::default_bar().template("{spinner:.green} {msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})"));
|
||||
bar.set_message(&format!(
|
||||
"Download {}",
|
||||
path.file_name().unwrap().to_str().unwrap()
|
||||
));
|
||||
bar.enable_steady_tick(100);
|
||||
|
||||
let len = buf.len();
|
||||
|
||||
let res = handle.read_file(&file, &mut buf, &mut |remaining| {
|
||||
bar.set_position((len - remaining) as u64);
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
bar.set_message("Writing file to disk");
|
||||
|
||||
match dest_file.write_all(&buf) {
|
||||
Ok(_) => {
|
||||
bar.finish_with_message("Transfer completed");
|
||||
}
|
||||
Err(error) => {
|
||||
bar.abandon_with_message(&format!(
|
||||
"Failed to write file to disk: {}",
|
||||
error
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
bar.abandon_with_message(&format!("Failed to transfer file: {}", error))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Failed to open destination file: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Failed to read file info: {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Couldn't find any device");
|
||||
}
|
||||
}
|
||||
SubCommand::UploadOS(UploadOS { file, no_check_os }) => {
|
||||
if let Some(handle) = get_dev() {
|
||||
let calc_info = handle.info().expect("Failed to obtain device info");
|
||||
|
||||
let file_ext = file
|
||||
.extension()
|
||||
.unwrap_or(OsStr::new(""))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let mut buf = vec![];
|
||||
let mut f = File::open(cwd().join(&file)).unwrap_or_else(|err| {
|
||||
eprintln!("Failed to open file: {}", err);
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
if format!(".{}", file_ext) != calc_info.os_extension {
|
||||
if no_check_os {
|
||||
eprintln!(
|
||||
"Warning: {} expects file of type {}",
|
||||
calc_info.name, calc_info.os_extension
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"Error: {} expects file of type {}",
|
||||
calc_info.name, calc_info.os_extension
|
||||
);
|
||||
eprintln!("Provide --no-check-os to bypass this check.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
f.read_to_end(&mut buf).unwrap();
|
||||
|
||||
let name = file
|
||||
.file_name()
|
||||
.expect("Failed to get file name")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let bar = ProgressBar::new(buf.len() as u64);
|
||||
bar.set_style(ProgressStyle::default_bar().template("{spinner:.green} {msg} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})"));
|
||||
bar.set_message(&format!("Upload OS {}", name));
|
||||
bar.enable_steady_tick(100);
|
||||
|
||||
let res = handle.send_os(&buf, &mut |remaining| {
|
||||
bar.set_position((buf.len() - remaining) as u64);
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
bar.finish();
|
||||
}
|
||||
Err(error) => {
|
||||
bar.abandon_with_message(&format!("OS Upload failed: {}", error));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Couldn't find any device");
|
||||
}
|
||||
}
|
||||
SubCommand::Copy(Copy {
|
||||
from_path,
|
||||
dist_path,
|
||||
}) => {
|
||||
if let Some(handle) = get_dev() {
|
||||
match handle.copy_file(&from_path, &dist_path) {
|
||||
Ok(_) => {
|
||||
println!("Copy {} => {}: Ok", from_path, dist_path);
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Failed to copy file or directory: {}", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Couldn't find any device");
|
||||
}
|
||||
}
|
||||
SubCommand::Move(Move {
|
||||
from_path,
|
||||
dist_path,
|
||||
}) => {
|
||||
if let Some(handle) = get_dev() {
|
||||
match handle.move_file(&from_path, &dist_path) {
|
||||
Ok(_) => {
|
||||
println!("Move {} => {}: Ok", from_path, dist_path);
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Failed to move file or directory: {}", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Couldn't find any device");
|
||||
}
|
||||
}
|
||||
SubCommand::Mkdir(Mkdir { path }) => {
|
||||
if let Some(handle) = get_dev() {
|
||||
match handle.create_dir(&path) {
|
||||
Ok(_) => {
|
||||
println!("Create {}: Ok", path);
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Failed to create directory: {}", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Couldn't find any device");
|
||||
}
|
||||
}
|
||||
SubCommand::Rmdir(Rmdir { path }) => {
|
||||
if let Some(handle) = get_dev() {
|
||||
match handle.delete_dir(&path) {
|
||||
Ok(_) => {
|
||||
println!("Remove {}: Ok", path);
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Failed to delete directory: {}", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Couldn't find any device");
|
||||
}
|
||||
}
|
||||
SubCommand::Ls(Ls { path }) => {
|
||||
if let Some(handle) = get_dev() {
|
||||
match handle.list_dir(&path) {
|
||||
Ok(dir_list) => {
|
||||
for item in dir_list.iter() {
|
||||
println!(
|
||||
"{}{}",
|
||||
item.name().to_str().unwrap(),
|
||||
if item.entry_type() == EntryType::Directory {
|
||||
"/"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Failed to list directory: {}", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Couldn't find any device");
|
||||
}
|
||||
}
|
||||
SubCommand::License => {
|
||||
println!("{}", include_str!("../../LICENSE"));
|
||||
println!(include_str!("NOTICE.txt"), env!("CARGO_PKG_REPOSITORY"));
|
||||
}
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
10
nlink-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
mod cli;
|
||||
|
||||
fn main() {
|
||||
// cli::run() parses args and performs the requested operation.
|
||||
// It returns false only when no subcommand was given.
|
||||
if !cli::run() {
|
||||
eprintln!("No command given. Run with --help to see available commands.");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
3260
package-lock.json
generated
Normal file
15
package.json
|
|
@ -1,14 +1,7 @@
|
|||
{
|
||||
"private": true,
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"desktop",
|
||||
"n-link-core",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"fork-ts-checker-webpack-plugin": "^5.2.0"
|
||||
}
|
||||
"workspaces": [
|
||||
"desktop",
|
||||
"n-link-core"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
1
vendor/libnspire-sys/.cargo-ok
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"v":1}
|
||||
5
vendor/libnspire-sys/.cargo_vcs_info.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"git": {
|
||||
"sha1": "efc388fc49ba82c1fac6b381ef1f55d42eba93dc"
|
||||
}
|
||||
}
|
||||
31
vendor/libnspire-sys/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies
|
||||
#
|
||||
# If you believe there's an error in this file please file an
|
||||
# issue against the rust-lang/cargo repository. If you're
|
||||
# editing this file be aware that the upstream Cargo.toml
|
||||
# will likely look very different (and much more reasonable)
|
||||
|
||||
[package]
|
||||
edition = "2018"
|
||||
name = "libnspire-sys"
|
||||
version = "0.3.4"
|
||||
authors = ["lights0123 <developer@lights0123.com>"]
|
||||
build = "build.rs"
|
||||
links = "nspire"
|
||||
description = "low-level FFI bindings to libnspire for USB interaction with TI Nspire calculators"
|
||||
readme = "README.md"
|
||||
license = "GPL-3.0"
|
||||
repository = "https://github.com/lights0123/libnspire-rs"
|
||||
[dependencies.libusb1-sys]
|
||||
version = "0.4.1"
|
||||
[build-dependencies.cc]
|
||||
version = "1.0"
|
||||
features = ["parallel"]
|
||||
|
||||
[build-dependencies.globwalk]
|
||||
version = "0.7"
|
||||
21
vendor/libnspire-sys/Cargo.toml.orig
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "libnspire-sys"
|
||||
description = "low-level FFI bindings to libnspire for USB interaction with TI Nspire calculators"
|
||||
version = "0.3.4"
|
||||
authors = ["lights0123 <developer@lights0123.com>"]
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
links = "nspire"
|
||||
license = "GPL-3.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/lights0123/libnspire-rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
libusb1-sys = "0.4.1"
|
||||
|
||||
[build-dependencies]
|
||||
cc = { version = "1.0", features = ["parallel"] }
|
||||
globwalk = "0.7"
|
||||
# bindgen = "0.55.1"
|
||||
11
vendor/libnspire-sys/README.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# libnspire-sys
|
||||
[](https://crates.io/crates/libnspire-sys)
|
||||
[](https://docs.rs/libnspire-sys)
|
||||
|
||||
Low-level bindings to [libnspire] for USB interaction with TI Nspire calculators.
|
||||
|
||||
## License
|
||||
|
||||
WARNING: this crate is under the GPL-3.0, as that is what [libnspire] is under.
|
||||
|
||||
[libnspire]: https://github.com/Vogtinator/libnspire
|
||||
46
vendor/libnspire-sys/build.rs
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use globwalk::DirEntry;
|
||||
|
||||
fn main() {
|
||||
let files = globwalk::GlobWalkerBuilder::from_patterns("libnspire/src", &["*.{c,cpp}"])
|
||||
.build()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(DirEntry::into_path)
|
||||
.inspect(|path| println!("cargo:rerun-if-changed={}", path.display()));
|
||||
let mut cfg = cc::Build::new();
|
||||
if let Some(include) = std::env::var_os("DEP_USB_1.0_INCLUDE") {
|
||||
cfg.include(include);
|
||||
}
|
||||
if std::env::var_os("NSPIRE_DEBUG").is_some() {
|
||||
cfg.define("DEBUG", None);
|
||||
}
|
||||
cfg.files(files)
|
||||
.include("libnspire/src")
|
||||
.include("libnspire/src/services")
|
||||
.include("libnspire/src/api")
|
||||
.compile("nspire");
|
||||
|
||||
// let bindings = bindgen::Builder::default()
|
||||
// // The input header we would like to generate
|
||||
// // bindings for.
|
||||
// .header("libnspire/src/api/nspire.h")
|
||||
// // Tell cargo to invalidate the built crate whenever any of the
|
||||
// // included header files changed.
|
||||
// .parse_callbacks(Box::new(bindgen::CargoCallbacks))
|
||||
// .whitelist_function("(nspire.*|free)")
|
||||
// .whitelist_type("nspire.*")
|
||||
// .whitelist_var("nspire.*")
|
||||
// .whitelist_type("NSPIRE.*")
|
||||
// .whitelist_var("NSPIRE.*")
|
||||
// .size_t_is_usize(true)
|
||||
// // Finish the builder and generate the bindings.
|
||||
// .generate()
|
||||
// // Unwrap the Result and panic on failure.
|
||||
// .expect("Unable to generate bindings");
|
||||
//
|
||||
// // Write the bindings to the $OUT_DIR/bindings.rs file.
|
||||
// bindings
|
||||
// .write_to_file("src/bindings.rs")
|
||||
// .expect("Couldn't write bindings!");
|
||||
}
|
||||
6
vendor/libnspire-sys/libnspire/AUTHORS
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
Daniel Tang
|
||||
===========
|
||||
|
||||
Main developer
|
||||
|
||||
tangrs@tangrs.id.au
|
||||
674
vendor/libnspire-sys/libnspire/COPYING
vendored
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
27
vendor/libnspire-sys/libnspire/README
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
==============================
|
||||
Libnspire
|
||||
==============================
|
||||
|
||||
This is a library for communicating with TI-Nspire calculators. It was created as a lightweight alternative to the Nspire-specific functions in TiLP (http://lpg.ticalc.org/prj_tilp/index.html). The goal is to have a small library with a simple API and minimal dependancies.
|
||||
|
||||
At the moment, it is still in need of polishing up and documentation needs writing.
|
||||
|
||||
Dependancies
|
||||
==========
|
||||
|
||||
You will need:
|
||||
|
||||
* libusb-1.0
|
||||
* pkg-config
|
||||
|
||||
to build this library.
|
||||
|
||||
Installing
|
||||
==========
|
||||
|
||||
To install:
|
||||
|
||||
./configure
|
||||
make
|
||||
make install
|
||||
|
||||
99
vendor/libnspire-sys/libnspire/src/api/devinfo.h
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
This file is part of libnspire.
|
||||
|
||||
libnspire is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libnspire is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NSP_DEVINFO_H
|
||||
#define NSP_DEVINFO_H
|
||||
|
||||
#include <inttypes.h>
|
||||
#include "handle.h"
|
||||
|
||||
enum nspire_battery {
|
||||
NSPIRE_BATT_POWERED = 0x00,
|
||||
NSPIRE_BATT_LOW = 0xF1,
|
||||
NSPIRE_BATT_OK = 0x7F,
|
||||
NSPIRE_BATT_UNKNOWN = 0xFF
|
||||
};
|
||||
|
||||
enum nspire_version_index {
|
||||
NSPIRE_VER_OS,
|
||||
NSPIRE_VER_BOOT1,
|
||||
NSPIRE_VER_BOOT2,
|
||||
|
||||
/* Reserved */
|
||||
NSPIRE_VER_MAXNUM
|
||||
};
|
||||
|
||||
enum nspire_type {
|
||||
NSPIRE_CAS = 0x0E,
|
||||
NSPIRE_NONCAS = 0x1E,
|
||||
NSPIRE_CASCX = 0x0F,
|
||||
NSPIRE_NONCASCX = 0x1F
|
||||
};
|
||||
|
||||
enum nspire_runlevel {
|
||||
NSPIRE_RUNLEVEL_RECOVERY = 1,
|
||||
NSPIRE_RUNLEVEL_OS = 2
|
||||
};
|
||||
|
||||
struct nspire_devinfo {
|
||||
/* Flash storage */
|
||||
struct {
|
||||
uint64_t free, total;
|
||||
} storage;
|
||||
|
||||
/* Memory */
|
||||
struct {
|
||||
uint64_t free, total;
|
||||
} ram;
|
||||
|
||||
/* Versions */
|
||||
struct {
|
||||
uint8_t major, minor;
|
||||
uint16_t build;
|
||||
} versions[NSPIRE_VER_MAXNUM];
|
||||
enum nspire_type hw_type;
|
||||
|
||||
/* Power */
|
||||
struct {
|
||||
enum nspire_battery status;
|
||||
uint8_t is_charging;
|
||||
} batt;
|
||||
uint8_t clock_speed;
|
||||
|
||||
/* LCD */
|
||||
struct {
|
||||
uint16_t width, height;
|
||||
uint8_t bbp, sample_mode;
|
||||
} lcd;
|
||||
|
||||
/* File extensions */
|
||||
struct {
|
||||
char file[8];
|
||||
char os[8];
|
||||
} extensions;
|
||||
|
||||
/* ID */
|
||||
char device_name[20];
|
||||
char electronic_id[28];
|
||||
|
||||
/* Misc */
|
||||
enum nspire_runlevel runlevel;
|
||||
};
|
||||
|
||||
int nspire_device_info(nspire_handle_t *handle, struct nspire_devinfo *i);
|
||||
|
||||
#endif
|
||||
50
vendor/libnspire-sys/libnspire/src/api/dir.h
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
This file is part of libnspire.
|
||||
|
||||
libnspire is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libnspire is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NSP_DIR_H
|
||||
#define NSP_DIR_H
|
||||
|
||||
#include "file.h"
|
||||
#include "handle.h"
|
||||
|
||||
enum nspire_dir_type {
|
||||
NSPIRE_FILE = 0,
|
||||
NSPIRE_DIR = 1
|
||||
};
|
||||
|
||||
struct nspire_dir_item {
|
||||
char name[240];
|
||||
uint64_t size, date;
|
||||
enum nspire_dir_type type;
|
||||
};
|
||||
|
||||
struct nspire_dir_info {
|
||||
uint64_t num;
|
||||
struct nspire_dir_item items[];
|
||||
};
|
||||
|
||||
int nspire_dirlist(nspire_handle_t *, const char *, struct nspire_dir_info **);
|
||||
void nspire_dirlist_free(struct nspire_dir_info *d);
|
||||
int nspire_dir_create(nspire_handle_t *handle, const char *path);
|
||||
int nspire_dir_delete(nspire_handle_t *handle, const char *path);
|
||||
|
||||
int nspire_attr(nspire_handle_t *, const char *, struct nspire_dir_item *);
|
||||
|
||||
#define nspire_dir_move nspire_file_move
|
||||
#define nspire_dir_rename nspire_file_move
|
||||
|
||||
#endif
|
||||
53
vendor/libnspire-sys/libnspire/src/api/error.h
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
This file is part of libnspire.
|
||||
|
||||
libnspire is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libnspire is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NSP_ERROR_H
|
||||
#define NSP_ERROR_H
|
||||
|
||||
enum {
|
||||
NSPIRE_ERR_SUCCESS,
|
||||
|
||||
/* Generic */
|
||||
NSPIRE_ERR_TIMEOUT,
|
||||
NSPIRE_ERR_NOMEM,
|
||||
NSPIRE_ERR_INVALID,
|
||||
|
||||
/* USB */
|
||||
NSPIRE_ERR_LIBUSB,
|
||||
NSPIRE_ERR_NODEVICE,
|
||||
|
||||
/* Packet */
|
||||
NSPIRE_ERR_INVALPKT,
|
||||
NSPIRE_ERR_NACK,
|
||||
|
||||
/* Service */
|
||||
NSPIRE_ERR_BUSY,
|
||||
|
||||
/* File/Dir services */
|
||||
NSPIRE_ERR_EXISTS,
|
||||
NSPIRE_ERR_NONEXIST,
|
||||
|
||||
/* OS install */
|
||||
NSPIRE_ERR_OSFAILED,
|
||||
|
||||
/* Number of errors */
|
||||
NSPIRE_ERR_MAX
|
||||
};
|
||||
|
||||
const char *nspire_strerror(int error);
|
||||
|
||||
#endif
|
||||
35
vendor/libnspire-sys/libnspire/src/api/file.h
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
This file is part of libnspire.
|
||||
|
||||
libnspire is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libnspire is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NSP_FILE_H
|
||||
#define NSP_FILE_H
|
||||
|
||||
#include <string.h>
|
||||
#include "handle.h"
|
||||
|
||||
typedef void (*nspire_callback)(size_t, void*);
|
||||
int nspire_file_write(nspire_handle_t *, const char *, void*, size_t, nspire_callback cb, void *cb_data);
|
||||
int nspire_file_read(nspire_handle_t *handle, const char *path,
|
||||
void* data, size_t size, size_t *read_bytes, nspire_callback cb, void *cb_data);
|
||||
int nspire_file_move(nspire_handle_t *handle, const char *src, const char *dst);
|
||||
int nspire_file_copy(nspire_handle_t *handle, const char *src, const char *dst);
|
||||
int nspire_file_delete(nspire_handle_t *handle, const char *path);
|
||||
|
||||
#define nspire_file_touch(h,p) nspire_file_write((h), (p), NULL, 0);
|
||||
#define nspire_file_rename nspire_file_move
|
||||
|
||||
#endif
|
||||
29
vendor/libnspire-sys/libnspire/src/api/handle.h
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
This file is part of libnspire.
|
||||
|
||||
libnspire is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libnspire is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NSP_HANDLE_H
|
||||
#define NSP_HANDLE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct nspire_handle nspire_handle_t;
|
||||
typedef struct libusb_device_handle libusb_device_handle;
|
||||
|
||||
int nspire_init(nspire_handle_t **ptr, libusb_device_handle *dev, bool is_cx2);
|
||||
void nspire_free(nspire_handle_t *ptr);
|
||||
|
||||
#endif
|
||||
30
vendor/libnspire-sys/libnspire/src/api/nspire.h
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
This file is part of libnspire.
|
||||
|
||||
libnspire is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libnspire is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NSP_NSPIRE_H
|
||||
#define NSP_NSPIRE_H
|
||||
|
||||
#include "usb.h"
|
||||
#include "devinfo.h"
|
||||
#include "dir.h"
|
||||
#include "error.h"
|
||||
#include "file.h"
|
||||
#include "handle.h"
|
||||
#include "os.h"
|
||||
#include "screenshot.h"
|
||||
|
||||
#endif
|
||||
27
vendor/libnspire-sys/libnspire/src/api/os.h
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
This file is part of libnspire.
|
||||
|
||||
libnspire is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libnspire is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NSP_OS_H
|
||||
#define NSP_OS_H
|
||||
|
||||
#include <string.h>
|
||||
#include "handle.h"
|
||||
|
||||
typedef void (*nspire_callback)(size_t, void*);
|
||||
int nspire_os_send(nspire_handle_t *handle, void* data, size_t size, nspire_callback cb, void *cb_data);
|
||||
|
||||
#endif
|
||||
33
vendor/libnspire-sys/libnspire/src/api/screenshot.h
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
This file is part of libnspire.
|
||||
|
||||
libnspire is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libnspire is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NSP_SCREENSHOT_H
|
||||
#define NSP_SCREENSHOT_H
|
||||
|
||||
#include <inttypes.h>
|
||||
#include "handle.h"
|
||||
|
||||
struct nspire_image {
|
||||
uint16_t width, height;
|
||||
uint8_t bbp;
|
||||
|
||||
unsigned char data[];
|
||||
};
|
||||
|
||||
int nspire_screenshot(nspire_handle_t *handle, struct nspire_image **ptr);
|
||||
|
||||
#endif
|
||||
481
vendor/libnspire-sys/libnspire/src/cx2.cpp
vendored
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
/*
|
||||
* This file is part of libnspire.
|
||||
*
|
||||
* libnspire is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* libnspire is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include "endianconv.h"
|
||||
#ifdef _WIN32
|
||||
#include <sys/timeb.h>
|
||||
#include <sys/types.h>
|
||||
#include <winsock2.h>
|
||||
|
||||
int gettimeofday(struct timeval *t, void *timezone) {
|
||||
struct _timeb timebuffer;
|
||||
_ftime( &timebuffer );
|
||||
t->tv_sec=timebuffer.time;
|
||||
t->tv_usec=1000*timebuffer.millitm;
|
||||
return 0;
|
||||
}
|
||||
#define PACK( __Declaration__ ) __pragma( pack(push, 1) ) __Declaration__ __pragma( pack(pop))
|
||||
#else
|
||||
#include <sys/time.h>
|
||||
#define PACK( __Declaration__ ) __Declaration__ __attribute__((__packed__))
|
||||
#endif
|
||||
|
||||
#include <libusb.h>
|
||||
|
||||
#include "cx2.h"
|
||||
#include "error.h"
|
||||
#include "packet.h"
|
||||
// Windows...
|
||||
#undef min
|
||||
|
||||
enum Address {
|
||||
AddrAll = 0xFF,
|
||||
AddrMe = 0xFE,
|
||||
AddrCalc = 0x01
|
||||
};
|
||||
|
||||
enum Service {
|
||||
AddrReqService = 0x01,
|
||||
TimeService = 0x02,
|
||||
EchoService = 0x03,
|
||||
StreamService = 0x04,
|
||||
TransmitService = 0x05,
|
||||
LoopbackService = 0x06,
|
||||
StatsService = 0x07,
|
||||
UnknownService = 0x08,
|
||||
AckFlag = 0x80
|
||||
};
|
||||
|
||||
// Big endian!
|
||||
PACK(struct NNSEMessage {
|
||||
uint8_t misc; // Unused?
|
||||
uint8_t service; // Service number. If bit 7 set, an ACK
|
||||
uint8_t src; // Address of the source
|
||||
uint8_t dest; // Address of the destination
|
||||
uint8_t unknown; // No idea
|
||||
uint8_t reqAck; // 0x1: Whether an ack is expected, 0x9: Not the first try
|
||||
uint16_t length; // Length of the packet, including this header
|
||||
uint16_t seqno; // Sequence number. Increases by one for every non-ACK packet.
|
||||
uint16_t csum; // Checksum. Inverse of the 16bit modular sum with carry added.
|
||||
});
|
||||
|
||||
PACK(struct NNSEMessage_AddrReq {
|
||||
NNSEMessage hdr;
|
||||
uint8_t code; // 00
|
||||
uint8_t clientID[64];
|
||||
});
|
||||
|
||||
PACK(struct NNSEMessage_AddrResp {
|
||||
NNSEMessage hdr;
|
||||
uint8_t addr;
|
||||
});
|
||||
|
||||
PACK(struct NNSEMessage_UnkResp {
|
||||
NNSEMessage hdr;
|
||||
uint8_t noidea[2]; // 80 03
|
||||
});
|
||||
|
||||
PACK(struct NNSEMessage_TimeReq {
|
||||
NNSEMessage hdr;
|
||||
uint8_t code;
|
||||
});
|
||||
|
||||
PACK(struct NNSEMessage_TimeResp {
|
||||
NNSEMessage hdr;
|
||||
uint8_t noidea; // 80
|
||||
uint32_t sec;
|
||||
uint64_t frac;
|
||||
uint32_t frac2;
|
||||
});
|
||||
|
||||
static uint8_t *getPacketData(const NNSEMessage *c) {
|
||||
return ((uint8_t *)c) + sizeof(NNSEMessage);
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
static void dumpPacket(const NNSEMessage *message)
|
||||
{
|
||||
printf("Misc: \t%02x\n", message->misc);
|
||||
printf("Service:\t%02x\n", message->service);
|
||||
printf("Dest: \t%02x\n", message->dest);
|
||||
printf("Src: \t%02x\n", message->src);
|
||||
printf("Unknown:\t%02x\n", message->unknown);
|
||||
printf("ReqAck: \t%02x\n", message->reqAck);
|
||||
printf("Length: \t%04x\n", ntohs(message->length));
|
||||
printf("SeqNo: \t%04x\n", ntohs(message->seqno));
|
||||
printf("Csum: \t%04x\n", ntohs(message->csum));
|
||||
|
||||
auto datalen = ntohs(message->length) - sizeof(NNSEMessage);
|
||||
for(int i = 0; i < datalen; ++i)
|
||||
printf("%02x ", getPacketData(message)[i]);
|
||||
|
||||
printf("\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
static uint16_t compute_checksum(const uint8_t *data, uint32_t size)
|
||||
{
|
||||
uint32_t acc = 0;
|
||||
|
||||
if (size > 0)
|
||||
{
|
||||
for (uint32_t i = 0; i < size - 1; i += 2)
|
||||
{
|
||||
uint16_t cur = (((uint16_t)data[i]) << 8) | data[i + 1];
|
||||
acc += cur;
|
||||
}
|
||||
|
||||
if (size & 1)
|
||||
acc += (((uint16_t)data[size - 1]) << 8);
|
||||
}
|
||||
|
||||
while (acc >> 16)
|
||||
acc = (acc >> 16) + uint16_t(acc);
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
static bool readPacket(libusb_device_handle *handle, NNSEMessage *message, int maxlen)
|
||||
{
|
||||
if(maxlen < sizeof(NNSEMessage))
|
||||
return false;
|
||||
|
||||
int transferred = 0;
|
||||
memset(message, 0, sizeof(NNSEMessage));
|
||||
int r = libusb_bulk_transfer(handle, 0x81, reinterpret_cast<unsigned char*>(message), maxlen, &transferred, 60000);
|
||||
|
||||
if(r < 0
|
||||
|| transferred < sizeof(NNSEMessage))
|
||||
return false;
|
||||
|
||||
const auto completeLength = ntohs(message->length);
|
||||
|
||||
if(completeLength < sizeof(NNSEMessage)
|
||||
|| completeLength > maxlen)
|
||||
return false;
|
||||
|
||||
uint8_t *data = reinterpret_cast<uint8_t*>(message) + transferred;
|
||||
auto remainingLength = completeLength - transferred;
|
||||
while(remainingLength > 0)
|
||||
{
|
||||
r = libusb_bulk_transfer(handle, 0x81, data, remainingLength, &transferred, 1000);
|
||||
if(r < 0)
|
||||
return false;
|
||||
|
||||
data += transferred;
|
||||
remainingLength -= transferred;
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
printf("Got packet:\n");
|
||||
dumpPacket(message);
|
||||
#endif
|
||||
|
||||
if(compute_checksum(reinterpret_cast<uint8_t*>(message), transferred) != 0xFFFF)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool writePacket(libusb_device_handle *handle, NNSEMessage *message)
|
||||
{
|
||||
auto length = ntohs(message->length);
|
||||
|
||||
message->csum = 0;
|
||||
message->csum = htons(compute_checksum(reinterpret_cast<uint8_t*>(message), length) ^ 0xFFFF);
|
||||
|
||||
if(compute_checksum(reinterpret_cast<uint8_t*>(message), length) != 0xFFFF)
|
||||
return false;
|
||||
|
||||
#ifdef DEBUG
|
||||
printf("Sending packet:\n");
|
||||
dumpPacket(message);
|
||||
#endif
|
||||
|
||||
int transferred = 0;
|
||||
int r = libusb_bulk_transfer(handle, 0x01, reinterpret_cast<unsigned char*>(message), length, &transferred, 1000);
|
||||
if(r < 0
|
||||
|| length != transferred)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint16_t nextSeqno()
|
||||
{
|
||||
static uint16_t seqno = 0;
|
||||
return seqno++;
|
||||
}
|
||||
|
||||
template <typename T> bool sendMessage(libusb_device_handle *handle, T &message)
|
||||
{
|
||||
message.hdr.src = AddrMe;
|
||||
message.hdr.dest = AddrCalc;
|
||||
message.hdr.length = htons(sizeof(T));
|
||||
message.hdr.seqno = htons(nextSeqno());
|
||||
|
||||
return writePacket(handle, &message.hdr);
|
||||
}
|
||||
|
||||
template <typename T> T* messageCast(NNSEMessage *message)
|
||||
{
|
||||
if(ntohs(message->length) < sizeof(T))
|
||||
return nullptr;
|
||||
|
||||
return reinterpret_cast<T*>(message);
|
||||
}
|
||||
|
||||
static void handlePacket(struct nspire_handle *nsp_handle, NNSEMessage *message, uint8_t **streamdata = nullptr, int *streamsize = nullptr)
|
||||
{
|
||||
auto *handle = nsp_handle->device.dev;
|
||||
|
||||
if(message->dest != AddrMe && message->dest != AddrAll)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
printf("Not for me?\n");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if(message->service & AckFlag)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
printf("Got ack for %02x\n", ntohs(message->seqno));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if(message->reqAck & 1)
|
||||
{
|
||||
NNSEMessage ack = {};
|
||||
ack.misc = message->misc;
|
||||
ack.service = uint8_t(message->service | AckFlag);
|
||||
ack.src = message->dest;
|
||||
ack.dest = message->src;
|
||||
ack.unknown = message->unknown;
|
||||
ack.reqAck = uint8_t(message->reqAck & ~1);
|
||||
ack.length = htons(sizeof(NNSEMessage));
|
||||
ack.seqno = message->seqno;
|
||||
|
||||
if(!writePacket(handle, &ack))
|
||||
printf("Failed to ack\n");
|
||||
}
|
||||
|
||||
switch(message->service & ~AckFlag)
|
||||
{
|
||||
case AddrReqService:
|
||||
{
|
||||
const NNSEMessage_AddrReq *req = messageCast<NNSEMessage_AddrReq>(message);
|
||||
if(!req || req->code != 0)
|
||||
goto drop;
|
||||
|
||||
#ifdef DEBUG
|
||||
printf("Got request from client %s (product id %c%c)\n", &req->clientID[12], req->clientID[10], req->clientID[11]);
|
||||
#endif
|
||||
/* Sending this somehow introduces issues like the time request not
|
||||
arriving or the calc responding with yet another address request.
|
||||
// Address release request. Not sure how that works.
|
||||
NNSEMessage_AddrResp resp{};
|
||||
resp.hdr.service = message->service;
|
||||
resp.addr = AddrCalc;
|
||||
|
||||
if(!sendMessage(resp))
|
||||
printf("Failed to send message\n");
|
||||
*/
|
||||
|
||||
NNSEMessage_AddrResp resp2 = {};
|
||||
resp2.hdr.service = message->service;
|
||||
resp2.addr = 0x80; // No idea
|
||||
|
||||
// In some cases on HW and in Firebird always after reconnecting
|
||||
// it ignores the first packet for some reason. So just send it
|
||||
// twice (the seqno doesn't really matter at this point), if it
|
||||
// receives both it'll ignore the second one.
|
||||
if(!sendMessage(handle, resp2) || !sendMessage(handle, resp2))
|
||||
printf("Failed to send message\n");
|
||||
|
||||
break;
|
||||
}
|
||||
case TimeService:
|
||||
{
|
||||
const NNSEMessage_TimeReq *req = messageCast<NNSEMessage_TimeReq>(message);
|
||||
if(!req || req->code != 0)
|
||||
goto drop;
|
||||
|
||||
#ifdef DEBUG
|
||||
printf("Got time request\n");
|
||||
#endif
|
||||
|
||||
struct timeval val;
|
||||
gettimeofday(&val, nullptr);
|
||||
|
||||
NNSEMessage_TimeResp resp = {};
|
||||
resp.hdr.service = message->service;
|
||||
resp.noidea = 0x80;
|
||||
resp.sec = htonl(uint32_t(val.tv_sec));
|
||||
resp.frac = 0;
|
||||
|
||||
if(!sendMessage(handle, resp))
|
||||
printf("Failed to send message\n");
|
||||
|
||||
nsp_handle->cx2_handshake_complete = true;
|
||||
break;
|
||||
}
|
||||
case UnknownService:
|
||||
{
|
||||
if(ntohs(message->length) != sizeof(NNSEMessage) + 1 || getPacketData(message)[0] != 0x01)
|
||||
goto drop;
|
||||
|
||||
#ifdef DEBUG
|
||||
printf("Got packet for unknown service\n");
|
||||
#endif
|
||||
|
||||
NNSEMessage_UnkResp resp = {};
|
||||
resp.hdr.service = message->service;
|
||||
resp.noidea[0] = 0x81;
|
||||
resp.noidea[1] = 0x03;
|
||||
|
||||
if(!sendMessage(handle, resp))
|
||||
printf("Failed to send message\n");
|
||||
|
||||
break;
|
||||
}
|
||||
case StreamService:
|
||||
{
|
||||
if(streamdata)
|
||||
*streamdata = getPacketData(message);
|
||||
if(streamsize)
|
||||
*streamsize = ntohs(message->length) - sizeof(NNSEMessage);
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
printf("Unhandled service %02x\n", message->service & ~AckFlag);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
drop:
|
||||
printf("Ignoring packet.\n");
|
||||
}
|
||||
|
||||
static bool assureReady(struct nspire_handle *nsp_handle)
|
||||
{
|
||||
if(nsp_handle->cx2_handshake_complete)
|
||||
return true;
|
||||
|
||||
auto *handle = nsp_handle->device.dev;
|
||||
|
||||
const int maxlen = sizeof(NNSEMessage) + 1472;
|
||||
NNSEMessage * const message = reinterpret_cast<NNSEMessage*>(malloc(maxlen));
|
||||
for(int i = 30; i-- && !nsp_handle->cx2_handshake_complete;)
|
||||
{
|
||||
if(!readPacket(handle, message, maxlen))
|
||||
continue;
|
||||
|
||||
handlePacket(nsp_handle, message);
|
||||
}
|
||||
free(message);
|
||||
|
||||
return nsp_handle->cx2_handshake_complete;
|
||||
}
|
||||
|
||||
int packet_send_cx2(struct nspire_handle *nsp_handle, char *data, int size)
|
||||
{
|
||||
if(!assureReady(nsp_handle))
|
||||
return -NSPIRE_ERR_BUSY;
|
||||
|
||||
auto *handle = nsp_handle->device.dev;
|
||||
|
||||
int len = sizeof(NNSEMessage) + size;
|
||||
NNSEMessage *msg = reinterpret_cast<NNSEMessage*>(malloc(len));
|
||||
|
||||
msg->service = StreamService;
|
||||
msg->src = AddrMe;
|
||||
msg->dest = AddrCalc;
|
||||
msg->reqAck = 1;
|
||||
msg->length = htons(len);
|
||||
msg->seqno = htons(nextSeqno());
|
||||
|
||||
memcpy(getPacketData(msg), data, size);
|
||||
|
||||
int ret = -NSPIRE_ERR_SUCCESS;
|
||||
if(!writePacket(handle, msg))
|
||||
ret = -NSPIRE_ERR_BUSY;
|
||||
else
|
||||
{
|
||||
const int maxlen = sizeof(NNSEMessage) + 1472;
|
||||
NNSEMessage * const message = reinterpret_cast<NNSEMessage*>(malloc(maxlen));
|
||||
|
||||
bool acked = false;
|
||||
for(int i = 40; i-- && !ret && !acked;)
|
||||
{
|
||||
if(!readPacket(handle, message, maxlen))
|
||||
continue;
|
||||
|
||||
handlePacket(nsp_handle, message);
|
||||
|
||||
if(message->dest == AddrMe
|
||||
&& message->service == (StreamService | AckFlag)
|
||||
&& message->seqno == msg->seqno)
|
||||
acked = true;
|
||||
}
|
||||
|
||||
if(!acked)
|
||||
ret = -NSPIRE_ERR_BUSY;
|
||||
|
||||
free(message);
|
||||
}
|
||||
|
||||
free(msg);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int packet_recv_cx2(struct nspire_handle *nsp_handle, char *data, int size)
|
||||
{
|
||||
if(!assureReady(nsp_handle))
|
||||
return -NSPIRE_ERR_BUSY;
|
||||
|
||||
auto *handle = nsp_handle->device.dev;
|
||||
|
||||
const int maxlen = sizeof(NNSEMessage) + 1472;
|
||||
NNSEMessage * const message = reinterpret_cast<NNSEMessage*>(malloc(maxlen));
|
||||
|
||||
uint8_t *streamdata = nullptr;
|
||||
int streamsize = 0;
|
||||
for(int i = 40; i-- && !streamdata;)
|
||||
{
|
||||
if(!readPacket(handle, message, maxlen))
|
||||
continue;
|
||||
|
||||
handlePacket(nsp_handle, message, &streamdata, &streamsize);
|
||||
}
|
||||
|
||||
if(streamdata)
|
||||
memcpy(data, streamdata, std::min(size, streamsize));
|
||||
|
||||
free(message);
|
||||
|
||||
return streamdata ? -NSPIRE_ERR_SUCCESS : -NSPIRE_ERR_INVALPKT;
|
||||
}
|
||||
43
vendor/libnspire-sys/libnspire/src/cx2.h
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* This file is part of libnspire.
|
||||
*
|
||||
* libnspire is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* libnspire is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef CX2_H
|
||||
#define CX2_H
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#endif
|
||||
#include <libusb.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct nspire_handle;
|
||||
|
||||
// Receive a NavNet packet wrapped in the NavNet SE protocol.
|
||||
// Takes care of the handshake and other NNSE stuff like acking.
|
||||
int packet_recv_cx2(struct nspire_handle *handle, char *data, int size);
|
||||
// Send a NavNet packet wrapped in the NavNet SE protocol and wait for an ack.
|
||||
// Takes care of the handshake and other NNSE stuff like acking.
|
||||
int packet_send_cx2(struct nspire_handle *handle, char *data, int size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // CX2_H
|
||||
383
vendor/libnspire-sys/libnspire/src/data.c
vendored
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
/*
|
||||
This file is part of libnspire.
|
||||
|
||||
libnspire is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libnspire is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
#include "endianconv.h"
|
||||
#include "error.h"
|
||||
#include "packet.h"
|
||||
#include "handle.h"
|
||||
|
||||
static int handle_unknown(nspire_handle_t *handle, struct packet p) {
|
||||
int ret;
|
||||
|
||||
switch (p.dst_sid) {
|
||||
case 0x40de: /* Disconnection */
|
||||
ret = packet_ack(handle, p);
|
||||
break;
|
||||
default:
|
||||
ret = packet_nack(handle, p);
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int data_write_special(nspire_handle_t *handle, void *ptr, size_t len,
|
||||
void (*packet_callback)(struct packet *p)) {
|
||||
int ret;
|
||||
struct packet p = packet_new(handle);
|
||||
|
||||
if(len < 0xFF) {
|
||||
memcpy(p.data, ptr, len);
|
||||
p.data_size = len;
|
||||
} else if(len <= packet_max_datasize(handle)) {
|
||||
memcpy(p.bigdata, ptr, len);
|
||||
p.bigdatasize = dcpu32(len);
|
||||
p.data_size = 0xFF;
|
||||
} else
|
||||
return NSPIRE_ERR_NOMEM;
|
||||
|
||||
if (packet_callback)
|
||||
packet_callback(&p);
|
||||
|
||||
if ((ret = packet_send(handle, p)))
|
||||
return ret;
|
||||
|
||||
// Acks are handled a level lower only, in packet_send
|
||||
if (handle->is_cx2)
|
||||
return NSPIRE_ERR_SUCCESS;
|
||||
|
||||
/*
|
||||
* Wait for an ACK packet while also NACKing spurious packets (like
|
||||
* the LOGIN request).
|
||||
*
|
||||
* Legitimate packets that need ACKing will be resent by the device
|
||||
* ready for reading in the next data_read call
|
||||
*/
|
||||
while (1) {
|
||||
if ( (ret = packet_recv(handle, &p)) )
|
||||
return ret;
|
||||
|
||||
if (p.dst_sid == handle->host_sid) {
|
||||
if ((p.src_sid == 0xFF || p.src_sid == 0xFE)) {
|
||||
handle->seq++;
|
||||
if (!handle->seq) handle->seq++;
|
||||
return NSPIRE_ERR_SUCCESS;
|
||||
} else
|
||||
if (p.src_sid == 0xD3) {
|
||||
return -NSPIRE_ERR_INVALPKT;
|
||||
}
|
||||
} else {
|
||||
handle_unknown(handle, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int data_write(nspire_handle_t *handle, void *ptr, size_t len) {
|
||||
return data_write_special(handle, ptr, len, NULL);
|
||||
}
|
||||
|
||||
int data_read(nspire_handle_t *handle, void *ptr, size_t maxlen, size_t *actual) {
|
||||
int ret;
|
||||
size_t len;
|
||||
struct packet p;
|
||||
|
||||
while (1) {
|
||||
if ( (ret = packet_recv(handle, &p)) )
|
||||
return ret;
|
||||
|
||||
if (p.dst_sid == handle->host_sid) {
|
||||
// Acks are handled a level lower only, in packet_recv
|
||||
if(handle->is_cx2)
|
||||
break;
|
||||
|
||||
if ( (ret = packet_ack(handle, p)) )
|
||||
return ret;
|
||||
break;
|
||||
} else {
|
||||
handle_unknown(handle, p);
|
||||
}
|
||||
|
||||
if (ret)
|
||||
return ret;
|
||||
}
|
||||
|
||||
len = (maxlen < packet_datasize(&p)) ? maxlen : packet_datasize(&p);
|
||||
memcpy(ptr, packet_dataptr(&p), len);
|
||||
|
||||
if(actual)
|
||||
*actual = len;
|
||||
|
||||
return NSPIRE_ERR_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
Format string:
|
||||
b 8 bit integer
|
||||
h 16 bit integer
|
||||
w 32 bit integer
|
||||
l 64 bit integer
|
||||
0 expect zero
|
||||
s null terminated string len >= 8
|
||||
S null terminated string
|
||||
*/
|
||||
|
||||
int data_scan(const char *format, const void *buffer, size_t len, ...) {
|
||||
int ret;
|
||||
const uint8_t *ptr = buffer, *end = ptr + len;
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, len);
|
||||
while (ptr < end && *format) {
|
||||
len = end - ptr;
|
||||
switch (*format) {
|
||||
case 'b':
|
||||
{
|
||||
uint8_t *in = va_arg(ap, uint8_t*);
|
||||
if (in) *in = *ptr;
|
||||
ptr++;
|
||||
break;
|
||||
}
|
||||
case 'h':
|
||||
{
|
||||
uint16_t *in = va_arg(ap, uint16_t*);
|
||||
if (len < 2) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
if (in) {
|
||||
*in = ptr[0]<<8 | ptr[1];
|
||||
}
|
||||
ptr+=2;
|
||||
break;
|
||||
|
||||
}
|
||||
case 'w':
|
||||
{
|
||||
uint32_t *in = va_arg(ap, uint32_t*);
|
||||
if (len < 4) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
if (in) {
|
||||
*in = ptr[0]<<24 |
|
||||
ptr[1]<<16 |
|
||||
ptr[2]<<8 |
|
||||
ptr[3];
|
||||
}
|
||||
ptr+=4;
|
||||
break;
|
||||
|
||||
}
|
||||
case 'l':
|
||||
{
|
||||
uint64_t *in = va_arg(ap, uint64_t*);
|
||||
if (len < 8) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
if (in) {
|
||||
uint32_t low, high;
|
||||
high = ptr[0]<<24 |
|
||||
ptr[1]<<16 |
|
||||
ptr[2]<<8 |
|
||||
ptr[3];
|
||||
low = ptr[4]<<24 |
|
||||
ptr[5]<<16 |
|
||||
ptr[6]<<8 |
|
||||
ptr[7];
|
||||
*in = ((uint64_t)high<<32) | low;
|
||||
|
||||
}
|
||||
ptr+=8;
|
||||
break;
|
||||
}
|
||||
case 's':
|
||||
{
|
||||
char **in = va_arg(ap, char **);
|
||||
uint8_t *next = memchr(ptr, '\0', len);
|
||||
|
||||
if (!next) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
*in = (char*)ptr;
|
||||
|
||||
/* Strings are padded with zeros until len >= 8 */
|
||||
/* Take this into account */
|
||||
if (next - ptr < 8) {
|
||||
ptr = next + (8 - (next - ptr));
|
||||
} else {
|
||||
ptr = next;
|
||||
}
|
||||
ptr++;
|
||||
break;
|
||||
}
|
||||
case 'S':
|
||||
{
|
||||
char **in = va_arg(ap, char **);
|
||||
uint8_t *next = memchr(ptr, '\0', len);
|
||||
|
||||
if (!next) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
*in = (char*)ptr;
|
||||
|
||||
ptr = next + 1;
|
||||
break;
|
||||
}
|
||||
case '0':
|
||||
if (*ptr != 0) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
ptr++;
|
||||
break;
|
||||
default:
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
format++;
|
||||
}
|
||||
|
||||
ret = NSPIRE_ERR_SUCCESS;
|
||||
end:
|
||||
va_end(ap);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int data_build(const char *format, void *buffer,
|
||||
size_t len, size_t *wrote_bytes, ...) {
|
||||
int ret;
|
||||
uint8_t *ptr = buffer, *end = ptr + len;
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, wrote_bytes);
|
||||
while (ptr < end && *format) {
|
||||
len = end - ptr;
|
||||
switch (*format) {
|
||||
case 'b':
|
||||
{
|
||||
uint8_t in = va_arg(ap, unsigned int);
|
||||
*ptr = in;
|
||||
ptr++;
|
||||
break;
|
||||
}
|
||||
case 'h':
|
||||
{
|
||||
uint16_t in = va_arg(ap, unsigned int);
|
||||
if (len < 2) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
ptr[0] = (in>>8) & 0xFF;
|
||||
ptr[1] = (in) & 0xFF;
|
||||
ptr+=2;
|
||||
break;
|
||||
|
||||
}
|
||||
case 'w':
|
||||
{
|
||||
uint32_t in = va_arg(ap, uint32_t);
|
||||
if (len < 4) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
ptr[0] = (in>>24) & 0xFF;
|
||||
ptr[1] = (in>>16) & 0xFF;
|
||||
ptr[2] = (in>>8) & 0xFF;
|
||||
ptr[3] = (in) & 0xFF;
|
||||
ptr+=4;
|
||||
break;
|
||||
|
||||
}
|
||||
case 'l':
|
||||
{
|
||||
uint64_t in = va_arg(ap, uint64_t);
|
||||
if (len < 8) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
ptr[0] = (in>>56) & 0xFF;
|
||||
ptr[1] = (in>>48) & 0xFF;
|
||||
ptr[2] = (in>>40) & 0xFF;
|
||||
ptr[3] = (in>>32) & 0xFF;
|
||||
ptr[4] = (in>>24) & 0xFF;
|
||||
ptr[5] = (in>>16) & 0xFF;
|
||||
ptr[6] = (in>>8) & 0xFF;
|
||||
ptr[7] = (in) & 0xFF;
|
||||
ptr+=8;
|
||||
break;
|
||||
}
|
||||
case 's':
|
||||
{
|
||||
char *in = va_arg(ap, char *);
|
||||
size_t strl = strlen(in);
|
||||
|
||||
if (len <= strl || len <= 9) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Strings are padded with zeros until len (including the final 0) >= 9 */
|
||||
/* Take this into account */
|
||||
memset(ptr, 0, 9);
|
||||
strncpy((char*)ptr, in, len);
|
||||
|
||||
if (strl < 8) {
|
||||
ptr += 8;
|
||||
} else {
|
||||
ptr += strl;
|
||||
}
|
||||
*ptr++ = '\0';
|
||||
break;
|
||||
}
|
||||
case 'S':
|
||||
{
|
||||
char *in = va_arg(ap, char *);
|
||||
size_t strl = strlen(in);
|
||||
|
||||
if (len <= strl) {
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
|
||||
strncpy((char*)ptr, in, len);
|
||||
ptr += strl;
|
||||
*ptr++ = '\0';
|
||||
break;
|
||||
}
|
||||
case '0':
|
||||
*ptr++ = '\0';
|
||||
break;
|
||||
default:
|
||||
ret = -NSPIRE_ERR_INVALID;
|
||||
goto end;
|
||||
}
|
||||
format++;
|
||||
}
|
||||
|
||||
if (wrote_bytes)
|
||||
*wrote_bytes = ptr - (uint8_t*)buffer;
|
||||
|
||||
ret = NSPIRE_ERR_SUCCESS;
|
||||
end:
|
||||
va_end(ap);
|
||||
return ret;
|
||||
}
|
||||
43
vendor/libnspire-sys/libnspire/src/data.h
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
This file is part of libnspire.
|
||||
|
||||
libnspire is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
libnspire is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with libnspire. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef _DATA_H
|
||||
#define _DATA_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "handle.h"
|
||||
#include "endianconv.h"
|
||||
#include "packet.h"
|
||||
|
||||
int data_write_special(nspire_handle_t*, void*, size_t,
|
||||
void (*)(struct packet *));
|
||||
|
||||
int data_write(nspire_handle_t *handle, void *ptr, size_t maxlen);
|
||||
int data_read(nspire_handle_t *handle, void *ptr, size_t maxlen, size_t *actual);
|
||||
int data_build(const char *, void *, size_t, size_t *, ...);
|
||||
int data_scan(const char *, const void *, size_t, ...);
|
||||
|
||||
static inline int data_write8(nspire_handle_t *handle, uint8_t x) {
|
||||
return data_write(handle, &x, 1);
|
||||
}
|
||||
|
||||
static inline int data_write16(nspire_handle_t *handle, uint16_t x) {
|
||||
x = dcpu16(x);
|
||||
return data_write(handle, &x, 2);
|
||||
}
|
||||
|
||||
#endif
|
||||