Add PDF state persistence
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::config::{GlobalConfig, ProjectConfig};
|
||||
use crate::config::{GlobalConfig, ProjectConfig, SessionConfig};
|
||||
use crate::state::AppState;
|
||||
use tauri::State;
|
||||
|
||||
/// Load the global config from `~/.config/brittle/config.toml`.
|
||||
/// Returns the default config if the file does not yet exist.
|
||||
@@ -80,6 +82,32 @@ pub fn get_layout_config() -> Result<crate::config::LayoutConfig, String> {
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Return the session state for the currently open project.
|
||||
///
|
||||
/// Returns `Default` if no session has been saved yet.
|
||||
/// Errors if no repository is open.
|
||||
#[tauri::command]
|
||||
pub fn get_session(state: State<AppState>) -> Result<SessionConfig, String> {
|
||||
let guard = state.brittle.lock().map_err(|_| "lock poisoned".to_string())?;
|
||||
let brittle = guard.as_ref().ok_or_else(|| "no repository open".to_string())?;
|
||||
ProjectConfig::load(brittle.repository_root())
|
||||
.map(|c| c.session)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Persist the session state for the currently open project.
|
||||
///
|
||||
/// Errors if no repository is open.
|
||||
#[tauri::command]
|
||||
pub fn save_session(state: State<AppState>, session: SessionConfig) -> Result<(), String> {
|
||||
let guard = state.brittle.lock().map_err(|_| "lock poisoned".to_string())?;
|
||||
let brittle = guard.as_ref().ok_or_else(|| "no repository open".to_string())?;
|
||||
let root = brittle.repository_root();
|
||||
let mut config = ProjectConfig::load(root).map_err(|e| e.to_string())?;
|
||||
config.session = session;
|
||||
config.save(root).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Return the user's keybinding overrides from the global config.
|
||||
///
|
||||
/// Map keys are action names in snake_case (e.g. `"tab_next"`); values are
|
||||
|
||||
@@ -10,7 +10,7 @@ mod global;
|
||||
mod project;
|
||||
|
||||
pub use global::GlobalConfig;
|
||||
pub use project::ProjectConfig;
|
||||
pub use project::{PdfTabEntry, PdfTabState, ProjectConfig, SessionConfig};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -104,8 +104,8 @@ pub struct MergedConfig {
|
||||
pub appearance: AppearanceConfig,
|
||||
pub layout: LayoutConfig,
|
||||
pub keybindings: KeybindingsConfig,
|
||||
/// Reference IDs of tabs that should be restored on launch.
|
||||
pub open_tabs: Vec<String>,
|
||||
/// Tabs that should be restored on launch.
|
||||
pub open_tabs: Vec<PdfTabEntry>,
|
||||
}
|
||||
|
||||
impl MergedConfig {
|
||||
@@ -157,7 +157,7 @@ pub enum ConfigError {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::project::SessionConfig;
|
||||
use crate::config::project::{PdfTabEntry, SessionConfig};
|
||||
|
||||
// ── AppearanceConfig ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -273,12 +273,18 @@ mod tests {
|
||||
let global = GlobalConfig::default();
|
||||
let project = ProjectConfig {
|
||||
session: SessionConfig {
|
||||
open_tabs: vec!["tab-a".to_string(), "tab-b".to_string()],
|
||||
open_tabs: vec![
|
||||
PdfTabEntry { ref_id: "tab-a".to_string(), title: "a2024".to_string() },
|
||||
PdfTabEntry { ref_id: "tab-b".to_string(), title: "b2024".to_string() },
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let merged = MergedConfig::merge(&global, Some(&project));
|
||||
assert_eq!(merged.open_tabs, ["tab-a", "tab-b"]);
|
||||
assert_eq!(merged.open_tabs.len(), 2);
|
||||
assert_eq!(merged.open_tabs[0].ref_id, "tab-a");
|
||||
assert_eq!(merged.open_tabs[1].ref_id, "tab-b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
//! Per-project configuration.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{AppearanceOverride, ConfigError};
|
||||
|
||||
/// Reference IDs of tabs to restore when the project is next opened.
|
||||
/// Persisted zoom and scroll state for a single PDF tab.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct PdfTabState {
|
||||
pub zoom: f64,
|
||||
pub scroll_top: f64,
|
||||
}
|
||||
|
||||
/// A PDF tab to restore on next launch.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct PdfTabEntry {
|
||||
pub ref_id: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
/// Session state restored when a project is next opened.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct SessionConfig {
|
||||
pub open_tabs: Vec<String>,
|
||||
/// PDF tabs to reopen, in order (Library tab is implicit).
|
||||
pub open_tabs: Vec<PdfTabEntry>,
|
||||
/// Index into [Library, ...open_tabs] of the tab that was active.
|
||||
pub active_tab: usize,
|
||||
/// Per-reference zoom and scroll position, keyed by ref_id.
|
||||
pub pdf_states: HashMap<String, PdfTabState>,
|
||||
}
|
||||
|
||||
/// Per-project configuration, stored at `{repo}/.brittle/config.toml`.
|
||||
@@ -77,6 +98,8 @@ mod tests {
|
||||
let cfg = ProjectConfig::default();
|
||||
assert!(cfg.appearance.is_none());
|
||||
assert!(cfg.session.open_tabs.is_empty());
|
||||
assert_eq!(cfg.session.active_tab, 0);
|
||||
assert!(cfg.session.pdf_states.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -87,7 +110,13 @@ mod tests {
|
||||
font_size: Some(16),
|
||||
}),
|
||||
session: SessionConfig {
|
||||
open_tabs: vec!["abc-123".to_string()],
|
||||
open_tabs: vec![PdfTabEntry {
|
||||
ref_id: "abc-123".to_string(),
|
||||
title: "smith2024".to_string(),
|
||||
}],
|
||||
active_tab: 1,
|
||||
pdf_states: [("abc-123".to_string(), PdfTabState { zoom: 1.5, scroll_top: 320.0 })]
|
||||
.into_iter().collect(),
|
||||
},
|
||||
};
|
||||
let s = toml::to_string_pretty(&cfg).unwrap();
|
||||
@@ -130,7 +159,8 @@ mod tests {
|
||||
font_size: None,
|
||||
}),
|
||||
session: SessionConfig {
|
||||
open_tabs: vec!["ref-1".to_string()],
|
||||
open_tabs: vec![PdfTabEntry { ref_id: "ref-1".to_string(), title: "ref2024".to_string() }],
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
original.save_to(&path).unwrap();
|
||||
@@ -146,7 +176,8 @@ mod tests {
|
||||
|
||||
let original = ProjectConfig {
|
||||
session: SessionConfig {
|
||||
open_tabs: vec!["x".to_string()],
|
||||
open_tabs: vec![PdfTabEntry { ref_id: "x".to_string(), title: "x2024".to_string() }],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -32,6 +32,8 @@ pub fn run() {
|
||||
commands::config::get_keybindings,
|
||||
commands::config::get_layout_config,
|
||||
commands::config::get_last_project,
|
||||
commands::config::get_session,
|
||||
commands::config::save_session,
|
||||
// repository
|
||||
commands::repository::create_repository,
|
||||
commands::repository::open_repository,
|
||||
|
||||
Reference in New Issue
Block a user