use crate::value::Value;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Settings {
#[serde(default = "default_stack_soft_cap")]
pub stack_soft_cap: usize,
#[serde(default = "default_rational_digit_cap")]
pub rational_digit_cap: usize,
#[serde(default = "default_undo_depth")]
pub undo_depth: usize,
}
impl Default for Settings {
fn default() -> Self {
Self {
stack_soft_cap: default_stack_soft_cap(),
rational_digit_cap: default_rational_digit_cap(),
undo_depth: default_undo_depth(),
}
}
}
fn default_stack_soft_cap() -> usize {
10_000
}
fn default_rational_digit_cap() -> usize {
10_000
}
fn default_undo_depth() -> usize {
10
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct PersistedState {
#[serde(default = "default_schema_version")]
pub schema_version: u32,
#[serde(default, deserialize_with = "deserialize_stack")]
pub stack: Vec<Value>,
#[serde(default)]
pub settings: Settings,
}
fn default_schema_version() -> u32 {
1
}
impl Default for PersistedState {
fn default() -> Self {
Self {
schema_version: 1,
stack: Vec::new(),
settings: Settings::default(),
}
}
}
impl<'de> Deserialize<'de> for PersistedState {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct Raw {
#[serde(default = "default_schema_version")]
schema_version: u32,
#[serde(default)]
stack: Vec<toml::Value>,
#[serde(default)]
settings: Settings,
}
let raw = Raw::deserialize(deserializer)?;
let mut stack = Vec::new();
let mut skipped = 0usize;
for item in raw.stack {
use serde::de::IntoDeserializer;
match Value::deserialize(item.into_deserializer()) {
Ok(v) => stack.push(v),
Err(_) => {
skipped += 1;
}
}
}
if skipped > 0 {
eprintln!(
"warning: skipped {} persisted value(s) with unknown kind",
skipped
);
}
Ok(PersistedState {
schema_version: raw.schema_version,
stack,
settings: raw.settings,
})
}
}
pub fn state_path() -> PathBuf {
directories::ProjectDirs::from("", "", "pnc")
.map(|p| p.data_dir().join("state.toml"))
.unwrap_or_else(|| PathBuf::from("state.toml"))
}
pub fn load() -> Result<PersistedState, crate::error::PncError> {
load_from(&state_path())
}
pub fn load_from(path: &std::path::Path) -> Result<PersistedState, crate::error::PncError> {
if !path.exists() {
return Ok(PersistedState::default());
}
let text = fs::read_to_string(path)?;
let state: PersistedState =
toml::from_str(&text).map_err(|e| crate::error::PncError::Parse(e.to_string()))?;
if state.schema_version > 1 {
return Err(crate::error::PncError::UnsupportedSchemaVersion);
}
Ok(state)
}
pub fn save(state: &PersistedState) -> Result<(), crate::error::PncError> {
save_to(state, &state_path())
}
pub fn lock_path() -> std::path::PathBuf {
state_path().with_file_name("pnc.lock")
}
pub fn acquire_lock() -> Result<std::fs::File, crate::error::PncError> {
let path = lock_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let file = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&path)?;
use fs2::FileExt;
file.try_lock_exclusive().map_err(|_| {
crate::error::PncError::Io(std::io::Error::other("another pnc instance is running"))
})?;
Ok(file)
}
pub fn save_to(
state: &PersistedState,
path: &std::path::Path,
) -> Result<(), crate::error::PncError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
use std::io::Write;
let text =
toml::to_string_pretty(state).map_err(|e| crate::error::PncError::Parse(e.to_string()))?;
if path.exists() {
let backup = path.with_extension("bak");
let _ = fs::copy(path, &backup);
}
let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
fs::create_dir_all(parent)?;
let mut last_err = None;
for _ in 0..2 {
let mut tmp = match tempfile::NamedTempFile::new_in(parent) {
Ok(t) => t,
Err(e) => {
last_err = Some(crate::error::PncError::Io(e));
continue;
}
};
if tmp.write_all(text.as_bytes()).is_err()
|| tmp.flush().is_err()
|| tmp.as_file().sync_all().is_err()
{
last_err = Some(crate::error::PncError::Io(std::io::Error::other(
"failed to write state",
)));
continue;
}
match tmp.persist(path) {
Ok(_) => {
last_err = None;
break;
}
Err(e) => {
last_err = Some(crate::error::PncError::Io(e.error));
continue;
}
}
}
if let Some(e) = last_err {
return Err(e);
}
if let Some(parent) = path.parent() {
if let Ok(dir) = fs::File::open(parent) {
let _ = dir.sync_all();
}
}
Ok(())
}