What persists: every card (kind, position, size, title, note text, ptyId, z-order) + viewport pan/zoom + maxZ counter,

in a single board.json under app_data_dir() (~/.local/share/{appName}/board.json on Linux).

  Write pattern: tmp file + atomic rename, debounced 400ms. A crash mid-write can't corrupt the existing file; worst
  case you lose ~400ms of recent edits.

  Read pattern: load on app start, gate the Canvas render on the result. Missing file → default welcome board. Parse
  error → log and fall back to default (won't overwrite the bad file until the user makes changes; actually it will save
   over it after the next change — if you want to preserve a corrupt file for recovery that's a one-line tweak).

  Terminal caveat: live PTY processes die with the app session, so sanitize() in App.tsx clears every terminal card's
  ptyId on load. Each terminal card respawns a fresh shell on mount via the existing pty.spawn path. The card's
  position, size, and title are preserved.
This commit is contained in:
Haapy
2026-05-15 11:40:36 +00:00
parent 595666e94b
commit 092d27beab
13 changed files with 311 additions and 71 deletions

View File

@@ -1,6 +1,8 @@
mod pty;
mod storage;
use pty::{pty_kill, pty_resize, pty_spawn, pty_write, PtyState};
use storage::{board_load, board_save};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -33,6 +35,8 @@ pub fn run() {
pty_write,
pty_resize,
pty_kill,
board_save,
board_load,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

36
src-tauri/src/storage.rs Normal file
View File

@@ -0,0 +1,36 @@
use std::fs;
use std::path::PathBuf;
use serde_json::Value;
use tauri::{AppHandle, Manager};
fn board_path(app: &AppHandle) -> Result<PathBuf, String> {
let dir = app
.path()
.app_data_dir()
.map_err(|e| format!("app_data_dir: {e}"))?;
fs::create_dir_all(&dir).map_err(|e| format!("create_dir_all: {e}"))?;
Ok(dir.join("board.json"))
}
#[tauri::command]
pub fn board_save(app: AppHandle, state: Value) -> Result<(), String> {
let path = board_path(&app)?;
let tmp = path.with_extension("json.tmp");
let body = serde_json::to_string_pretty(&state).map_err(|e| e.to_string())?;
fs::write(&tmp, body).map_err(|e| format!("write tmp: {e}"))?;
// Atomic replace so a crash mid-write can't corrupt the existing file.
fs::rename(&tmp, &path).map_err(|e| format!("rename: {e}"))?;
Ok(())
}
#[tauri::command]
pub fn board_load(app: AppHandle) -> Result<Option<Value>, String> {
let path = board_path(&app)?;
if !path.exists() {
return Ok(None);
}
let body = fs::read_to_string(&path).map_err(|e| format!("read: {e}"))?;
let v = serde_json::from_str(&body).map_err(|e| format!("parse: {e}"))?;
Ok(Some(v))
}