@dennisrongo/skills 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -16
- package/package.json +1 -1
- package/skills/nextjs-app-router/SKILL.md +228 -175
- package/skills/nextjs-app-router/references/anti-patterns.md +163 -99
- package/skills/nextjs-app-router/references/folder-layout.md +79 -46
- package/skills/nextjs-app-router/references/good-patterns.md +233 -99
- package/skills/nextjs-app-router/references/templates/api-base.md +24 -21
- package/skills/nextjs-app-router/references/templates/auth-slice.md +82 -78
- package/skills/nextjs-app-router/references/templates/ci-and-hooks.md +58 -6
- package/skills/nextjs-app-router/references/templates/db-client.md +48 -0
- package/skills/nextjs-app-router/references/templates/env-and-utils.md +90 -23
- package/skills/nextjs-app-router/references/templates/feature-slice.md +110 -47
- package/skills/nextjs-app-router/references/templates/form-with-zod.md +23 -15
- package/skills/nextjs-app-router/references/templates/middleware.md +69 -59
- package/skills/nextjs-app-router/references/templates/next-config.md +4 -14
- package/skills/nextjs-app-router/references/templates/nextauth-config.md +178 -0
- package/skills/nextjs-app-router/references/templates/package.md +35 -5
- package/skills/nextjs-app-router/references/templates/prisma-schema.md +162 -0
- package/skills/nextjs-app-router/references/templates/providers-and-store.md +35 -21
- package/skills/nextjs-app-router/references/templates/root-layout.md +99 -20
- package/skills/nextjs-app-router/references/templates/route-group-layouts.md +46 -6
- package/skills/nextjs-app-router/references/templates/route-handler.md +168 -0
- package/skills/nextjs-app-router/references/templates/testing.md +105 -31
- package/skills/tauri-2-app/SKILL.md +381 -0
- package/skills/tauri-2-app/references/anti-patterns.md +434 -0
- package/skills/tauri-2-app/references/folder-layout.md +161 -0
- package/skills/tauri-2-app/references/good-patterns.md +477 -0
- package/skills/tauri-2-app/references/templates/build-rs.md +51 -0
- package/skills/tauri-2-app/references/templates/capabilities.md +68 -0
- package/skills/tauri-2-app/references/templates/cargo-toml.md +118 -0
- package/skills/tauri-2-app/references/templates/ci-workflow.md +99 -0
- package/skills/tauri-2-app/references/templates/encryption-mod.md +228 -0
- package/skills/tauri-2-app/references/templates/error-mod.md +126 -0
- package/skills/tauri-2-app/references/templates/lib-rs.md +98 -0
- package/skills/tauri-2-app/references/templates/macos-plist.md +89 -0
- package/skills/tauri-2-app/references/templates/main-rs.md +21 -0
- package/skills/tauri-2-app/references/templates/platform-traits.md +217 -0
- package/skills/tauri-2-app/references/templates/storage-mod.md +172 -0
- package/skills/tauri-2-app/references/templates/tauri-conf.md +136 -0
- package/skills/tauri-2-app/references/templates/use-tauri-command.md +194 -0
- package/skills/tauri-2-app/references/templates/vite-and-tsconfig.md +189 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# Good Patterns
|
|
2
|
+
|
|
3
|
+
The patterns below survived real production use. Each one has a failure mode it prevents — the rationale matters more than the rule.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
### Thin frontend, rich Rust backend
|
|
8
|
+
|
|
9
|
+
All audio, file I/O, system permissions, OS-specific behavior, and long-running computation lives in Rust. The frontend's only job is to invoke commands and render state.
|
|
10
|
+
|
|
11
|
+
**Why:** Tauri's selling point over Electron is performance and OS integration. Pushing work into the frontend reintroduces the same memory and startup costs you came to Tauri to avoid. It also makes the frontend untestable without a running Tauri instance.
|
|
12
|
+
|
|
13
|
+
**How to spot violations:**
|
|
14
|
+
- `import { open } from '@tauri-apps/plugin-fs'` in component code reading large files
|
|
15
|
+
- Frontend code parsing/processing audio buffers
|
|
16
|
+
- `setInterval` polling driving state that could be event-driven from Rust via `app.emit("...", payload)`
|
|
17
|
+
|
|
18
|
+
### Modular `src-tauri/src/`
|
|
19
|
+
|
|
20
|
+
One module per concern (`commands/`, `state/`, `storage/`, `platform/`, `settings/`, `error/`, plus per-feature folders). `lib.rs` re-exports a public API for tests and registers commands; `main.rs` is the tiny shim that calls `pub fn run()`.
|
|
21
|
+
|
|
22
|
+
**Why:** A monolithic `lib.rs` past ~1500 lines is unmaintainable. Splitting by feature lets `cargo test --test <feature>` target one slice and surfaces accidental cross-cutting dependencies.
|
|
23
|
+
|
|
24
|
+
### Trait-based platform abstractions
|
|
25
|
+
|
|
26
|
+
`platform/traits.rs` defines `PermissionChecker`, `FileOpener`, `TextInserter`, etc. `platform/macos.rs|windows.rs|linux.rs` implement them gated with `#[cfg(target_os = "...")]`. Wrappers in `platform/wrappers.rs` implement `Send + Sync` and are managed in Tauri State.
|
|
27
|
+
|
|
28
|
+
**Why:** Without this, `cfg!(target_os = "...")` checks scatter through command bodies, untestable and tedious to audit. The trait gives one place to swap behavior per OS and a clean seam for mocking in tests.
|
|
29
|
+
|
|
30
|
+
```rust
|
|
31
|
+
// platform/traits.rs
|
|
32
|
+
pub trait PermissionChecker: Send + Sync {
|
|
33
|
+
fn check_microphone(&self) -> bool;
|
|
34
|
+
fn check_accessibility(&self) -> bool;
|
|
35
|
+
fn open_system_settings(&self, kind: &str) -> Result<(), String>;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```rust
|
|
40
|
+
// platform/wrappers.rs
|
|
41
|
+
pub struct PlatformPermissionChecker {
|
|
42
|
+
#[cfg(target_os = "macos")] inner: crate::platform::MacOsPlatform,
|
|
43
|
+
#[cfg(target_os = "windows")] inner: crate::platform::WindowsPlatform,
|
|
44
|
+
#[cfg(target_os = "linux")] inner: crate::platform::LinuxPlatform,
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Tauri runtime
|
|
49
|
+
|
|
50
|
+
### Single-instance plugin first
|
|
51
|
+
|
|
52
|
+
```rust
|
|
53
|
+
tauri::Builder::default()
|
|
54
|
+
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
|
55
|
+
if let Some(window) = app.get_webview_window("main") {
|
|
56
|
+
let _ = window.show();
|
|
57
|
+
let _ = window.set_focus();
|
|
58
|
+
let _ = window.unminimize();
|
|
59
|
+
}
|
|
60
|
+
}))
|
|
61
|
+
.plugin(tauri_plugin_opener::init())
|
|
62
|
+
// ...
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Why:** Without single-instance, double-clicking the launcher spawns a second process that competes for the global hotkey, the audio device, the lock file, and the WebSocket. The plugin must register **first** so the second instance is intercepted before any other plugin initializes resources.
|
|
66
|
+
|
|
67
|
+
### `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]` in `main.rs`
|
|
68
|
+
|
|
69
|
+
```rust
|
|
70
|
+
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
71
|
+
|
|
72
|
+
#[cfg(target_os = "macos")]
|
|
73
|
+
embed_plist::embed_info_plist!("../Info.plist");
|
|
74
|
+
|
|
75
|
+
fn main() { {{app_name}}_lib::run() }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Why:** Without it, Windows release builds open a console window behind the Tauri window. With it, debug builds still get a console (useful for `println!` while developing) and release builds are clean.
|
|
79
|
+
|
|
80
|
+
`embed_plist` ensures macOS standalone binaries (not run via `.app` bundle) still carry their plist.
|
|
81
|
+
|
|
82
|
+
### Show window FIRST in `.setup()`
|
|
83
|
+
|
|
84
|
+
```rust
|
|
85
|
+
.setup(|app| {
|
|
86
|
+
let main_window = app.get_webview_window("main").unwrap();
|
|
87
|
+
// (window is already visible by default — don't .hide() it)
|
|
88
|
+
|
|
89
|
+
// Reset any stuck state from a previous run (force-quit / crash)
|
|
90
|
+
crate::cancellation::force_clear_transcription_handle();
|
|
91
|
+
|
|
92
|
+
// Now do heavy initialization on a background thread
|
|
93
|
+
let handle = app.handle().clone();
|
|
94
|
+
std::thread::spawn(move || {
|
|
95
|
+
// model load, audio device enumeration, etc.
|
|
96
|
+
// emit events back: handle.emit("init-progress", ...);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
Ok(())
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Why:** Users perceive a Tauri app as broken if the window doesn't appear within ~500ms. Synchronous setup work (model loading, device enumeration, network calls) blocks the window from drawing.
|
|
104
|
+
|
|
105
|
+
### Reset state on startup
|
|
106
|
+
|
|
107
|
+
```rust
|
|
108
|
+
// In .setup(), before any "ready" event:
|
|
109
|
+
let was_in_progress = crate::cancellation::has_ongoing_transcription();
|
|
110
|
+
crate::cancellation::force_clear_transcription_handle();
|
|
111
|
+
if was_in_progress {
|
|
112
|
+
tracing::warn!("Cleared stuck state from previous session (app likely force-quit)");
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Why:** Crashes and force-quits leave `static` / global state in odd places. Treating startup as recovery makes the app robust to ungraceful shutdowns.
|
|
117
|
+
|
|
118
|
+
## Commands
|
|
119
|
+
|
|
120
|
+
### `#[tauri::command]` shape
|
|
121
|
+
|
|
122
|
+
```rust
|
|
123
|
+
#[tauri::command]
|
|
124
|
+
pub async fn save_user_setting(
|
|
125
|
+
state: State<'_, std::sync::Mutex<AppState>>,
|
|
126
|
+
app_handle: AppHandle,
|
|
127
|
+
key: String,
|
|
128
|
+
value: String,
|
|
129
|
+
) -> Result<(), String> {
|
|
130
|
+
// Hold the lock for the minimum time
|
|
131
|
+
let mut settings = {
|
|
132
|
+
let guard = state.lock().map_err(|e| e.to_string())?;
|
|
133
|
+
guard.settings.clone().ok_or("settings not loaded")?
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
settings.set(&key, &value);
|
|
137
|
+
|
|
138
|
+
// Persist outside the lock
|
|
139
|
+
crate::settings::save_settings(&app_handle, &settings)?;
|
|
140
|
+
|
|
141
|
+
// Re-take the lock to commit the in-memory copy
|
|
142
|
+
state.lock().map_err(|e| e.to_string())?.settings = Some(settings);
|
|
143
|
+
|
|
144
|
+
Ok(())
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Why:** Holding `std::sync::Mutex` across `.await` deadlocks the Tauri runtime. Clone what you need, drop the guard, do the work, re-take the guard to commit.
|
|
149
|
+
|
|
150
|
+
### `spawn_blocking` for sync work inside async commands
|
|
151
|
+
|
|
152
|
+
```rust
|
|
153
|
+
#[tauri::command]
|
|
154
|
+
pub async fn open_file_dialog(app: AppHandle) -> Result<Option<String>, String> {
|
|
155
|
+
let app_handle = app.clone();
|
|
156
|
+
let path = tokio::task::spawn_blocking(move || {
|
|
157
|
+
app_handle
|
|
158
|
+
.dialog()
|
|
159
|
+
.file()
|
|
160
|
+
.add_filter("Audio", &["mp3", "wav"])
|
|
161
|
+
.blocking_pick_file()
|
|
162
|
+
})
|
|
163
|
+
.await
|
|
164
|
+
.map_err(|e| format!("Join error: {}", e))?;
|
|
165
|
+
|
|
166
|
+
Ok(path.map(|fp| fp.to_string()))
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Why:** `blocking_pick_file()` blocks the calling thread. Awaiting it inside an `async fn` stalls Tauri's IPC executor and any other in-flight command queues up.
|
|
171
|
+
|
|
172
|
+
### Error conversion at the IPC boundary
|
|
173
|
+
|
|
174
|
+
```rust
|
|
175
|
+
// error/mod.rs
|
|
176
|
+
#[macro_export]
|
|
177
|
+
macro_rules! into_string_err {
|
|
178
|
+
($expr:expr) => { $expr.map_err(|e| e.to_string()) };
|
|
179
|
+
($expr:expr, $msg:expr) => { $expr.map_err(|e| format!("{}: {}", $msg, e)) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
pub trait ResultExt<T, E>: Sized {
|
|
183
|
+
fn into_string(self) -> Result<T, String>;
|
|
184
|
+
fn into_string_msg(self, msg: &str) -> Result<T, String>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
impl<T, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
|
|
188
|
+
fn into_string(self) -> Result<T, String> { self.map_err(|e| e.to_string()) }
|
|
189
|
+
fn into_string_msg(self, msg: &str) -> Result<T, String> {
|
|
190
|
+
self.map_err(|e| format!("{}: {}", msg, e))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```rust
|
|
196
|
+
// commands/<domain>.rs
|
|
197
|
+
use crate::error::ResultExt;
|
|
198
|
+
|
|
199
|
+
#[tauri::command]
|
|
200
|
+
pub fn write_file(path: String, content: String) -> Result<(), String> {
|
|
201
|
+
std::fs::write(&path, content).into_string_msg("Failed to write file")
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Why:** Tauri commands must return `Result<T, String>` because the error crosses an IPC boundary. Internal APIs use `thiserror` enums for type safety. The macro / trait keeps the conversion one-line at the boundary.
|
|
206
|
+
|
|
207
|
+
## State & storage
|
|
208
|
+
|
|
209
|
+
### Shared `AppState` managed in a `Mutex`
|
|
210
|
+
|
|
211
|
+
```rust
|
|
212
|
+
// state/mod.rs
|
|
213
|
+
pub struct AppState {
|
|
214
|
+
pub app_handle: Option<AppHandle>,
|
|
215
|
+
pub settings: Option<Settings>,
|
|
216
|
+
// domain-specific managers
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// lib.rs, inside .setup():
|
|
220
|
+
let state = std::sync::Mutex::new(AppState::new());
|
|
221
|
+
app.manage(state);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Why:** Tauri's `State<T>` is a managed singleton accessible from every command. Using `std::sync::Mutex` (not `tokio::sync::Mutex`) is fine **as long as** you never hold the lock across an `.await` — which is the pattern enforced above.
|
|
225
|
+
|
|
226
|
+
### Settings with serde camelCase + migrations
|
|
227
|
+
|
|
228
|
+
```rust
|
|
229
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
230
|
+
#[serde(rename_all = "camelCase", default)]
|
|
231
|
+
pub struct Settings {
|
|
232
|
+
pub default_microphone: Option<String>,
|
|
233
|
+
pub hands_free_mode: bool,
|
|
234
|
+
pub local_whisper_model: String,
|
|
235
|
+
// ...
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Why:**
|
|
240
|
+
- `rename_all = "camelCase"` matches frontend naming, no manual key remapping.
|
|
241
|
+
- `default` on the struct makes every field optional — old settings files load with sensible fallbacks for new fields.
|
|
242
|
+
- Add migration logic in a `deserialize_settings(&Value)` pass for non-trivial shape changes (e.g. `"English"` → `"en"`).
|
|
243
|
+
|
|
244
|
+
### Shared storage utilities
|
|
245
|
+
|
|
246
|
+
```rust
|
|
247
|
+
// storage/mod.rs
|
|
248
|
+
pub fn get_app_data_dir(app: &AppHandle) -> Result<PathBuf, String> {
|
|
249
|
+
let dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
|
250
|
+
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
|
251
|
+
Ok(dir)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
pub fn load_json<T: serde::de::DeserializeOwned>(
|
|
255
|
+
app: &AppHandle, filename: &str,
|
|
256
|
+
) -> Vec<T> { /* read or return empty */ }
|
|
257
|
+
|
|
258
|
+
pub fn save_json<T: serde::Serialize>(
|
|
259
|
+
app: &AppHandle, filename: &str, entries: &[T],
|
|
260
|
+
) -> Result<(), String> { /* serialize + write */ }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Why:** Every persistent feature (settings, history, error log) needs the same primitives. Centralizing them ensures one consistent failure mode (empty on missing file, log + empty on parse error) and one place to add cache/locking later.
|
|
264
|
+
|
|
265
|
+
## Security
|
|
266
|
+
|
|
267
|
+
### Encrypted secrets at rest
|
|
268
|
+
|
|
269
|
+
```rust
|
|
270
|
+
// encryption/mod.rs
|
|
271
|
+
#[derive(Serialize, Deserialize)]
|
|
272
|
+
pub struct EncryptedApiKey {
|
|
273
|
+
pub ciphertext: String, // base64 AES-256-GCM ciphertext
|
|
274
|
+
pub nonce: String, // base64 12-byte nonce
|
|
275
|
+
pub salt: String, // base64 Argon2id salt
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
pub fn encrypt_api_key(plaintext: &str) -> Result<EncryptedApiKey, String> { /* ... */ }
|
|
279
|
+
pub fn decrypt_api_key(enc: &EncryptedApiKey) -> Result<String, String> { /* ... */ }
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The key is derived from a hardware-bound machine ID:
|
|
283
|
+
- macOS: `IOPlatformUUID` via `ioreg -rd1 -c IOPlatformExpertDevice`
|
|
284
|
+
- Windows: `wmic csproduct get uuid` (use `creation_flags(CREATE_NO_WINDOW)` to avoid a flashing console)
|
|
285
|
+
- Linux: `/etc/machine-id` or the `machine-uid` crate
|
|
286
|
+
|
|
287
|
+
**Why:** Plaintext API keys in `settings.json` is a known liability. AES-GCM gives authenticated encryption; Argon2id is memory-hard against GPU brute-force; hardware-binding means a stolen `settings.json` file alone isn't useful — the attacker needs the machine too.
|
|
288
|
+
|
|
289
|
+
Note: this is **at-rest** protection, not perfect security. A privileged process on the same machine can still decrypt. For higher assurance, integrate the OS keychain (macOS Keychain via `security-framework`, Windows DPAPI, Linux Secret Service).
|
|
290
|
+
|
|
291
|
+
### Granular capability scopes
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
// capabilities/default.json
|
|
295
|
+
{
|
|
296
|
+
"$schema": "../gen/schemas/desktop-schema.json",
|
|
297
|
+
"identifier": "default",
|
|
298
|
+
"description": "Capability for the main window",
|
|
299
|
+
"windows": ["main"],
|
|
300
|
+
"permissions": [
|
|
301
|
+
"core:default",
|
|
302
|
+
"opener:default",
|
|
303
|
+
"opener:allow-open-url",
|
|
304
|
+
"dialog:allow-save",
|
|
305
|
+
"dialog:allow-open",
|
|
306
|
+
"fs:allow-write-file",
|
|
307
|
+
"fs:allow-read-file",
|
|
308
|
+
"notification:default",
|
|
309
|
+
"notification:allow-show",
|
|
310
|
+
"global-shortcut:allow-register",
|
|
311
|
+
"global-shortcut:allow-unregister",
|
|
312
|
+
"process:allow-restart",
|
|
313
|
+
"updater:default"
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Why:** Listing exactly what the app uses (not blanket `"*"` or `"core:*"`) means the security audit surface is the capability file. If a plugin update adds new permissions, your app doesn't silently inherit them.
|
|
319
|
+
|
|
320
|
+
## Cargo
|
|
321
|
+
|
|
322
|
+
### Release profile
|
|
323
|
+
|
|
324
|
+
```toml
|
|
325
|
+
[profile.release]
|
|
326
|
+
lto = "fat"
|
|
327
|
+
codegen-units = 1
|
|
328
|
+
opt-level = 3
|
|
329
|
+
strip = true
|
|
330
|
+
panic = "abort"
|
|
331
|
+
|
|
332
|
+
[profile.dev]
|
|
333
|
+
opt-level = 1 # Faster dev builds while keeping some optimization
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Why:**
|
|
337
|
+
- `lto = "fat"` + `codegen-units = 1` = full whole-program optimization, smaller and faster binaries.
|
|
338
|
+
- `strip = true` removes symbols (~30% smaller binary).
|
|
339
|
+
- `panic = "abort"` skips unwinding code (smaller binary; no panic recovery, but Tauri apps generally restart on panic anyway).
|
|
340
|
+
- `opt-level = 1` in dev keeps debug builds 2-3× faster than the default `opt-level = 0` while staying fast to compile.
|
|
341
|
+
|
|
342
|
+
### Target-specific dependencies
|
|
343
|
+
|
|
344
|
+
```toml
|
|
345
|
+
[target.'cfg(target_os = "macos")'.dependencies]
|
|
346
|
+
security-framework = "3.0"
|
|
347
|
+
cocoa = "0.25"
|
|
348
|
+
objc = "0.2"
|
|
349
|
+
|
|
350
|
+
[target.'cfg(target_os = "windows")'.dependencies]
|
|
351
|
+
windows-sys = { version = "0.59", features = [
|
|
352
|
+
"Win32_System_ProcessStatus",
|
|
353
|
+
"Win32_Foundation",
|
|
354
|
+
"Win32_UI_Input_KeyboardAndMouse",
|
|
355
|
+
] }
|
|
356
|
+
|
|
357
|
+
[target.'cfg(target_os = "linux")'.dependencies]
|
|
358
|
+
machine-uid = "0.2"
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Why:** Platform crates don't compile on the wrong platform. Putting them under `[target.'cfg(...)']` makes the default `cargo build` portable. Enable only the `windows-sys` features the code actually uses — pulling all features in adds minutes to compile time.
|
|
362
|
+
|
|
363
|
+
## Frontend
|
|
364
|
+
|
|
365
|
+
### Typed `useTauriCommand` hook
|
|
366
|
+
|
|
367
|
+
```ts
|
|
368
|
+
// hooks/useTauriCommand.ts
|
|
369
|
+
export function useTauriCommand<T = unknown>(options: UseCommandOptions<T>): CommandState<T> {
|
|
370
|
+
const [data, setData] = useState<T | null>(null);
|
|
371
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
372
|
+
const [error, setError] = useState<string | null>(null);
|
|
373
|
+
|
|
374
|
+
const execute = useCallback(async (args?: Record<string, unknown>) => {
|
|
375
|
+
setIsLoading(true);
|
|
376
|
+
setError(null);
|
|
377
|
+
try {
|
|
378
|
+
const result = await invoke<T>(options.command, args || options.args);
|
|
379
|
+
setData(result);
|
|
380
|
+
return result;
|
|
381
|
+
} catch (err) {
|
|
382
|
+
setError(String(err));
|
|
383
|
+
throw err;
|
|
384
|
+
} finally {
|
|
385
|
+
setIsLoading(false);
|
|
386
|
+
}
|
|
387
|
+
}, [options.command, options.args]);
|
|
388
|
+
|
|
389
|
+
return { data, isLoading, error, execute };
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Why:** Every component would otherwise reinvent loading/error wrappers around `invoke()`, with subtle differences. One hook = one place to add retries, debouncing, logging, telemetry. Plus type safety — `T` flows through.
|
|
394
|
+
|
|
395
|
+
### `isTauriReady()` guard
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
// tauriReady.ts
|
|
399
|
+
export function isTauriReady(): boolean {
|
|
400
|
+
return typeof window !== "undefined";
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Auto-loading hooks should call this before invoking. In Tauri 2 the IPC bridge is ready before React mounts, but a frontend that's ever previewed via plain `vite dev` (without Tauri) needs the guard.
|
|
405
|
+
|
|
406
|
+
### `vite.config.ts` tuned for Tauri
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
export default defineConfig({
|
|
410
|
+
base: './', // relative paths so bundled HTML loads from tauri://localhost
|
|
411
|
+
plugins: [react()],
|
|
412
|
+
clearScreen: false, // don't hide Rust errors
|
|
413
|
+
server: {
|
|
414
|
+
port: 5173,
|
|
415
|
+
strictPort: true, // fail if port is taken (Tauri expects it)
|
|
416
|
+
watch: { ignored: ['**/src-tauri/**'] }, // Cargo handles its own files
|
|
417
|
+
},
|
|
418
|
+
esbuild: { jsx: 'automatic' },
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Why:**
|
|
423
|
+
- `base: './'`: without it, asset URLs are absolute (`/assets/index.css`) and break inside the `tauri://localhost` context.
|
|
424
|
+
- `strictPort: true`: Tauri's `devUrl` points at 5173 — if Vite silently picks 5174, Tauri shows a blank window.
|
|
425
|
+
- Ignoring `src-tauri/` avoids HMR loops triggered by Cargo's `target/` writes.
|
|
426
|
+
- `clearScreen: false`: Vite's screen-clear hides important Rust compiler errors.
|
|
427
|
+
|
|
428
|
+
### TypeScript strict config
|
|
429
|
+
|
|
430
|
+
```json
|
|
431
|
+
{
|
|
432
|
+
"compilerOptions": {
|
|
433
|
+
"strict": true,
|
|
434
|
+
"noUnusedLocals": true,
|
|
435
|
+
"noUnusedParameters": true,
|
|
436
|
+
"noFallthroughCasesInSwitch": true,
|
|
437
|
+
"isolatedModules": true,
|
|
438
|
+
"allowImportingTsExtensions": true
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Why:** Strict mode catches real bugs (`undefined` access, implicit `any`). `noUnusedLocals`/`noUnusedParameters` flag the kind of "I forgot to wire this up" mistakes that pass code review.
|
|
444
|
+
|
|
445
|
+
## CI
|
|
446
|
+
|
|
447
|
+
### Cross-platform matrix
|
|
448
|
+
|
|
449
|
+
```yaml
|
|
450
|
+
# .github/workflows/unit-tests.yml
|
|
451
|
+
name: Unit Tests
|
|
452
|
+
on:
|
|
453
|
+
push: { branches: ['**'] }
|
|
454
|
+
pull_request:
|
|
455
|
+
|
|
456
|
+
jobs:
|
|
457
|
+
test:
|
|
458
|
+
strategy:
|
|
459
|
+
fail-fast: false
|
|
460
|
+
matrix:
|
|
461
|
+
platform: [macos-latest, windows-latest, ubuntu-latest]
|
|
462
|
+
runs-on: ${{ matrix.platform }}
|
|
463
|
+
steps:
|
|
464
|
+
- uses: actions/checkout@v4
|
|
465
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
466
|
+
- uses: actions/setup-node@v4
|
|
467
|
+
with: { node-version: '20', cache: 'npm' }
|
|
468
|
+
- run: npm ci
|
|
469
|
+
- name: Install Ubuntu deps
|
|
470
|
+
if: matrix.platform == 'ubuntu-latest'
|
|
471
|
+
run: |
|
|
472
|
+
sudo apt-get update
|
|
473
|
+
sudo apt-get install -y libgtk-3-dev libayatana-appindicator3-dev pkg-config
|
|
474
|
+
- run: cd src-tauri && cargo test
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Why:** Tauri's behavior diverges per OS (file pickers, permissions, hotkeys). A green test on macOS does not imply Windows works. `fail-fast: false` lets you see all three results.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# `src-tauri/build.rs`
|
|
2
|
+
|
|
3
|
+
Tauri's standard build script plus platform-specific framework links. Use `CARGO_CFG_TARGET_OS` (the **target** OS — survives cross-compilation), never the host OS.
|
|
4
|
+
|
|
5
|
+
```rust
|
|
6
|
+
fn main() {
|
|
7
|
+
tauri_build::build();
|
|
8
|
+
|
|
9
|
+
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
|
10
|
+
|
|
11
|
+
match target_os.as_str() {
|
|
12
|
+
"macos" => {
|
|
13
|
+
// Frameworks commonly needed for system integration:
|
|
14
|
+
// - Accelerate, Metal: GPU/ML acceleration
|
|
15
|
+
// - CoreGraphics, ApplicationServices: window placement, accessibility
|
|
16
|
+
// - Foundation: NS* types
|
|
17
|
+
println!("cargo:rustc-link-lib=framework=Foundation");
|
|
18
|
+
println!("cargo:rustc-link-lib=framework=ApplicationServices");
|
|
19
|
+
// Enable the others on demand — keep build.rs to what the code uses:
|
|
20
|
+
// println!("cargo:rustc-link-lib=framework=Accelerate");
|
|
21
|
+
// println!("cargo:rustc-link-lib=framework=Metal");
|
|
22
|
+
// println!("cargo:rustc-link-lib=framework=CoreGraphics");
|
|
23
|
+
}
|
|
24
|
+
"windows" => {
|
|
25
|
+
// Most Windows APIs come in via `windows-sys` features. Only declare libs
|
|
26
|
+
// here for low-level needs not covered by a `windows-sys` feature.
|
|
27
|
+
// println!("cargo:rustc-link-lib=user32");
|
|
28
|
+
}
|
|
29
|
+
"linux" => {
|
|
30
|
+
// pthread + libm are usually pulled in by the system, but be explicit.
|
|
31
|
+
println!("cargo:rustc-link-lib=pthread");
|
|
32
|
+
println!("cargo:rustc-link-lib=m");
|
|
33
|
+
}
|
|
34
|
+
_ => {}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Why `CARGO_CFG_TARGET_OS` (not `cfg!`)
|
|
40
|
+
|
|
41
|
+
`cfg!(target_os = "...")` evaluates at compile time of `build.rs` against the **host** OS. When cross-compiling (macOS host → Linux target, Windows host → ARM target), `cfg!(target_os = "linux")` is `false` on a macOS host even though the actual build target is Linux.
|
|
42
|
+
|
|
43
|
+
`CARGO_CFG_TARGET_OS` is the environment variable Cargo sets to the target. Always use it in `build.rs`.
|
|
44
|
+
|
|
45
|
+
## Don't over-link
|
|
46
|
+
|
|
47
|
+
Every `cargo:rustc-link-lib=framework=*` line adds a framework dependency to the binary, even if the code doesn't actually use it. This bloats binaries and can cause notarization warnings. Start with the minimum (`Foundation`, `ApplicationServices` on macOS; `pthread`, `m` on Linux) and add more only when a linker error proves you need them.
|
|
48
|
+
|
|
49
|
+
## Don't run external commands
|
|
50
|
+
|
|
51
|
+
`build.rs` runs on every developer's machine. Calling out to `make`, `cmake`, `python` etc. introduces dependencies that may not be installed. If a Rust crate needs C dependencies (`whisper.cpp`, `opencv`), check whether the crate already handles the build — almost all do. Only add custom build logic when you're authoring a crate from scratch.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# `src-tauri/capabilities/default.json`
|
|
2
|
+
|
|
3
|
+
Capabilities are Tauri 2's permission system. The frontend can only invoke a plugin command if a capability file grants that exact scope to the current window.
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"$schema": "../gen/schemas/desktop-schema.json",
|
|
8
|
+
"identifier": "default",
|
|
9
|
+
"description": "Capability for the main window",
|
|
10
|
+
"windows": ["main"],
|
|
11
|
+
"permissions": [
|
|
12
|
+
"core:default",
|
|
13
|
+
|
|
14
|
+
"opener:default",
|
|
15
|
+
"opener:allow-open-url",
|
|
16
|
+
|
|
17
|
+
"dialog:allow-save",
|
|
18
|
+
"dialog:allow-open",
|
|
19
|
+
|
|
20
|
+
"fs:allow-read-file",
|
|
21
|
+
"fs:allow-write-file",
|
|
22
|
+
|
|
23
|
+
"notification:default",
|
|
24
|
+
"notification:allow-show",
|
|
25
|
+
"notification:allow-is-permission-granted",
|
|
26
|
+
"notification:allow-request-permission",
|
|
27
|
+
|
|
28
|
+
"global-shortcut:allow-is-registered",
|
|
29
|
+
"global-shortcut:allow-register",
|
|
30
|
+
"global-shortcut:allow-unregister",
|
|
31
|
+
|
|
32
|
+
"process:allow-restart",
|
|
33
|
+
|
|
34
|
+
"updater:default"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Per-window capability files
|
|
40
|
+
|
|
41
|
+
If the app has multiple windows (overlay, settings popup, waveform), each one gets its own capability file scoped to just that window's needs. A transparent waveform overlay does not need `fs:allow-write-file` or `dialog:*` — it usually only emits events.
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
// capabilities/waveform.json
|
|
45
|
+
{
|
|
46
|
+
"$schema": "../gen/schemas/desktop-schema.json",
|
|
47
|
+
"identifier": "waveform",
|
|
48
|
+
"description": "Capability for the waveform overlay window",
|
|
49
|
+
"windows": ["waveform"],
|
|
50
|
+
"permissions": [
|
|
51
|
+
"core:default"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Why granular over blanket
|
|
57
|
+
|
|
58
|
+
- **Granular permissions = audit surface.** The capability file is the single document a security reviewer reads to learn "what can the frontend do?". `"core:default"` only is OK; `"fs:default"` without listing specific actions is suspect.
|
|
59
|
+
- **Plugin updates can widen `default`.** If a plugin's `default` permission set expands in a new version, your app silently inherits the new permissions. Listing exact actions pins the contract.
|
|
60
|
+
- **Frontend reflection breaks at the boundary.** A malicious or buggy frontend can call any command listed in the handler, but the capability file is checked before the call ever reaches Rust. Tight capabilities are a defense in depth.
|
|
61
|
+
|
|
62
|
+
## Reading the schema
|
|
63
|
+
|
|
64
|
+
The `$schema` reference points at `../gen/schemas/desktop-schema.json`, which Tauri generates on `cargo build`. Editors with JSON schema support (VS Code, JetBrains) will autocomplete valid permission names and show inline docs for each one. If the file is missing, run `cd src-tauri && cargo build` once to generate it.
|
|
65
|
+
|
|
66
|
+
## Don't ship `"core:*"` glob
|
|
67
|
+
|
|
68
|
+
`"core:*"` exists but matches every core permission, including ones the app doesn't use. If a future Tauri release adds a new core permission (e.g. clipboard read), your app silently gains it. Either list the specific `core:allow-*` permissions, or use `"core:default"` (which is curated and stable across versions).
|