MULTI-BOARD !

This commit is contained in:
Haapy
2026-05-15 11:57:12 +00:00
parent a831b22213
commit 66ea8b2b69
11 changed files with 746 additions and 94 deletions

View File

@@ -2,7 +2,10 @@ mod pty;
mod storage;
use pty::{pty_kill, pty_resize, pty_spawn, pty_write, PtyState};
use storage::{board_load, board_save};
use storage::{
board_create, board_delete, board_load, board_rename, board_save, boards_load_index,
boards_set_current,
};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -35,8 +38,13 @@ pub fn run() {
pty_write,
pty_resize,
pty_kill,
boards_load_index,
board_save,
board_load,
board_create,
board_rename,
board_delete,
boards_set_current,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -1,32 +1,119 @@
use std::fs;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tauri::{AppHandle, Manager};
use uuid::Uuid;
fn board_path(app: &AppHandle) -> Result<PathBuf, String> {
let dir = app
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct BoardMeta {
pub id: String,
pub name: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct BoardIndex {
pub version: u32,
pub current_board_id: String,
pub boards: Vec<BoardMeta>,
}
fn data_dir(app: &AppHandle) -> Result<PathBuf, String> {
let d = 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"))
fs::create_dir_all(&d).map_err(|e| format!("create_dir_all: {e}"))?;
Ok(d)
}
#[tauri::command]
pub fn board_save(app: AppHandle, state: Value) -> Result<(), String> {
let path = board_path(&app)?;
fn boards_dir(app: &AppHandle) -> Result<PathBuf, String> {
let d = data_dir(app)?.join("boards");
fs::create_dir_all(&d).map_err(|e| format!("create boards dir: {e}"))?;
Ok(d)
}
fn index_path(app: &AppHandle) -> Result<PathBuf, String> {
Ok(data_dir(app)?.join("boards.json"))
}
fn board_file(app: &AppHandle, id: &str) -> Result<PathBuf, String> {
if id.is_empty() || id.contains('/') || id.contains('\\') || id.contains("..") {
return Err(format!("invalid board id: {id:?}"));
}
Ok(boards_dir(app)?.join(format!("{id}.json")))
}
fn atomic_write(path: &Path, body: &str) -> Result<(), String> {
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}"))?;
fs::rename(&tmp, path).map_err(|e| format!("rename: {e}"))?;
Ok(())
}
fn empty_board_json() -> String {
serde_json::to_string_pretty(&serde_json::json!({
"version": 1,
"cards": [],
"viewport": { "x": 0, "y": 0, "scale": 1 },
"maxZ": 0,
}))
.unwrap()
}
fn write_index(app: &AppHandle, idx: &BoardIndex) -> Result<(), String> {
let body = serde_json::to_string_pretty(idx).map_err(|e| format!("serialize index: {e}"))?;
atomic_write(&index_path(app)?, &body)
}
fn read_index(app: &AppHandle) -> Result<BoardIndex, String> {
let path = index_path(app)?;
if !path.exists() {
// First run, or upgrading from the pre-multi-board single-file layout.
// Migrate the old board.json (if any) into boards/default.json.
let legacy = data_dir(app)?.join("board.json");
let default_id = "default".to_string();
let default_file = board_file(app, &default_id)?;
if legacy.exists() && !default_file.exists() {
let body = fs::read_to_string(&legacy).map_err(|e| format!("read legacy: {e}"))?;
atomic_write(&default_file, &body)?;
let _ = fs::remove_file(&legacy);
} else if !default_file.exists() {
atomic_write(&default_file, &empty_board_json())?;
}
let idx = BoardIndex {
version: 1,
current_board_id: default_id.clone(),
boards: vec![BoardMeta {
id: default_id,
name: "Default".to_string(),
}],
};
write_index(app, &idx)?;
return Ok(idx);
}
let body = fs::read_to_string(&path).map_err(|e| format!("read index: {e}"))?;
serde_json::from_str(&body).map_err(|e| format!("parse index: {e}"))
}
#[tauri::command]
pub fn board_load(app: AppHandle) -> Result<Option<Value>, String> {
let path = board_path(&app)?;
pub fn boards_load_index(app: AppHandle) -> Result<BoardIndex, String> {
read_index(&app)
}
#[tauri::command]
pub fn board_save(app: AppHandle, id: String, state: Value) -> Result<(), String> {
let path = board_file(&app, &id)?;
let body = serde_json::to_string_pretty(&state).map_err(|e| format!("serialize: {e}"))?;
atomic_write(&path, &body)
}
#[tauri::command]
pub fn board_load(app: AppHandle, id: String) -> Result<Option<Value>, String> {
let path = board_file(&app, &id)?;
if !path.exists() {
return Ok(None);
}
@@ -34,3 +121,58 @@ pub fn board_load(app: AppHandle) -> Result<Option<Value>, String> {
let v = serde_json::from_str(&body).map_err(|e| format!("parse: {e}"))?;
Ok(Some(v))
}
#[tauri::command]
pub fn board_create(app: AppHandle, name: String) -> Result<BoardMeta, String> {
let id = Uuid::new_v4().to_string();
let mut idx = read_index(&app)?;
let meta = BoardMeta {
id: id.clone(),
name,
};
idx.boards.push(meta.clone());
write_index(&app, &idx)?;
atomic_write(&board_file(&app, &id)?, &empty_board_json())?;
Ok(meta)
}
#[tauri::command]
pub fn board_rename(app: AppHandle, id: String, name: String) -> Result<(), String> {
let mut idx = read_index(&app)?;
let b = idx
.boards
.iter_mut()
.find(|b| b.id == id)
.ok_or_else(|| format!("board {id} not found"))?;
b.name = name;
write_index(&app, &idx)
}
#[tauri::command]
pub fn board_delete(app: AppHandle, id: String) -> Result<BoardIndex, String> {
let mut idx = read_index(&app)?;
if idx.boards.len() <= 1 {
return Err("can't delete the last board".to_string());
}
let before = idx.boards.len();
idx.boards.retain(|b| b.id != id);
if idx.boards.len() == before {
return Err(format!("board {id} not found"));
}
if idx.current_board_id == id {
idx.current_board_id = idx.boards[0].id.clone();
}
write_index(&app, &idx)?;
let _ = fs::remove_file(board_file(&app, &id)?);
Ok(idx)
}
#[tauri::command]
pub fn boards_set_current(app: AppHandle, id: String) -> Result<(), String> {
let mut idx = read_index(&app)?;
if !idx.boards.iter().any(|b| b.id == id) {
return Err(format!("board {id} not found"));
}
idx.current_board_id = id;
write_index(&app, &idx)
}