use crate::stack::DEFAULT_HISTORY_DEPTH;
use crate::value::Value;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DisplayMode {
#[default]
Auto,
Fix,
Sci,
Eng,
}
impl<'de> Deserialize<'de> for DisplayMode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"auto" => Ok(DisplayMode::Auto),
"fix" => Ok(DisplayMode::Fix),
"sci" => Ok(DisplayMode::Sci),
"eng" => Ok(DisplayMode::Eng),
_ => Ok(DisplayMode::Auto),
}
}
}
#[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_history_depth")]
pub history_depth: usize,
#[serde(default)]
pub display_mode: DisplayMode,
#[serde(default = "default_fix_decimals")]
pub fix_decimals: u8,
#[serde(default = "default_sci_digits")]
pub sci_digits: u8,
#[serde(default = "default_eng_digits")]
pub eng_digits: u8,
}
impl Default for Settings {
fn default() -> Self {
Self {
stack_soft_cap: default_stack_soft_cap(),
rational_digit_cap: default_rational_digit_cap(),
history_depth: default_history_depth(),
display_mode: DisplayMode::default(),
fix_decimals: default_fix_decimals(),
sci_digits: default_sci_digits(),
eng_digits: default_eng_digits(),
}
}
}
fn default_stack_soft_cap() -> usize {
10_000
}
fn default_rational_digit_cap() -> usize {
10_000
}
fn default_history_depth() -> usize {
DEFAULT_HISTORY_DEPTH
}
fn default_fix_decimals() -> u8 {
2
}
fn default_sci_digits() -> u8 {
4
}
fn default_eng_digits() -> u8 {
4
}
pub const CURRENT_SCHEMA_VERSION: u32 = 2;
#[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,
#[serde(default)]
pub migrated: bool,
}
fn default_schema_version() -> u32 {
1
}
impl Default for PersistedState {
fn default() -> Self {
Self {
schema_version: 1,
stack: Vec::new(),
settings: Settings::default(),
migrated: false,
}
}
}
impl<'de> Deserialize<'de> for PersistedState {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawV1 {
#[serde(default = "default_schema_version")]
schema_version: u32,
#[serde(default)]
stack: Vec<toml::Value>,
#[serde(default)]
settings: SettingsRaw,
#[serde(default)]
migrated: bool,
}
#[derive(Deserialize)]
struct SettingsRaw {
#[serde(default = "default_stack_soft_cap")]
stack_soft_cap: usize,
#[serde(default = "default_rational_digit_cap")]
rational_digit_cap: usize,
#[serde(default)]
history_depth: Option<usize>,
#[serde(default)]
undo_depth: Option<usize>,
#[serde(default)]
display_mode: Option<DisplayMode>,
#[serde(default)]
fix_decimals: Option<u8>,
#[serde(default)]
sci_digits: Option<u8>,
#[serde(default)]
eng_digits: Option<u8>,
}
impl Default for SettingsRaw {
fn default() -> Self {
Self {
stack_soft_cap: default_stack_soft_cap(),
rational_digit_cap: default_rational_digit_cap(),
history_depth: None,
undo_depth: None,
display_mode: None,
fix_decimals: None,
sci_digits: None,
eng_digits: None,
}
}
}
let raw = RawV1::deserialize(deserializer)?;
let version = raw.schema_version;
let is_v1 = version <= 1;
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(e) => {
skipped += 1;
tracing::debug!(error = %e, "skipped persisted value");
}
}
}
if skipped > 0 {
tracing::warn!(
skipped = skipped,
"skipped persisted value(s) with unknown kind"
);
}
let history_depth = if is_v1 {
raw.settings
.history_depth
.or(raw.settings.undo_depth)
.unwrap_or_else(default_history_depth)
} else {
raw.settings
.history_depth
.unwrap_or_else(default_history_depth)
};
let mut settings = Settings {
stack_soft_cap: raw.settings.stack_soft_cap,
rational_digit_cap: raw.settings.rational_digit_cap,
history_depth,
display_mode: raw.settings.display_mode.unwrap_or_default(),
fix_decimals: raw
.settings
.fix_decimals
.unwrap_or_else(default_fix_decimals),
sci_digits: raw.settings.sci_digits.unwrap_or_else(default_sci_digits),
eng_digits: raw.settings.eng_digits.unwrap_or_else(default_eng_digits),
};
if settings.history_depth > 1000 {
settings.history_depth = 1000;
}
let migrated = raw.migrated || is_v1;
let schema_version = if is_v1 {
CURRENT_SCHEMA_VERSION
} else {
version
};
Ok(PersistedState {
schema_version,
stack,
settings,
migrated,
})
}
}
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)?;
if text.is_empty() {
return Ok(PersistedState::default());
}
let toml_val: toml::Value =
toml::from_str(&text).map_err(|e| crate::error::PncError::Parse(e.to_string()))?;
if let Some(sv) = toml_val.get("schema_version") {
match sv {
toml::Value::Integer(n) => {
let v = *n as u32;
if v == 0 || v > CURRENT_SCHEMA_VERSION {
return Err(crate::error::PncError::UnsupportedSchemaVersion);
}
}
_ => {
return Err(crate::error::PncError::Parse(
"corrupt schema_version".to_string(),
));
}
}
}
let state: PersistedState =
toml::from_str(&text).map_err(|e| crate::error::PncError::Parse(e.to_string()))?;
let mut state = state;
if state.settings.history_depth > 1000 {
state.settings.history_depth = 1000;
}
Ok(state)
}
pub fn save(state: &PersistedState) -> Result<(), crate::error::PncError> {
save_to(state, &state_path())
}
pub fn lock_path() -> std::path::PathBuf {
for var in ["PNC_LOCK_PATH", "PNC_LOCK_FILE", "PNC_LOCK"] {
if let Ok(v) = std::env::var(var)
&& !v.is_empty()
{
return std::path::PathBuf::from(v);
}
}
state_path().with_file_name("pnc.lock")
}
pub fn acquire_lock() -> Result<std::fs::File, crate::error::PncError> {
acquire_lock_at(&lock_path())
}
pub fn acquire_lock_at(path: &std::path::Path) -> Result<std::fs::File, crate::error::PncError> {
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(format!(
"another pnc instance is running (lock: {}); set PNC_LOCK_PATH to override",
path.display()
)))
})?;
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)?;
}
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 tmp_path = path.with_file_name(format!(
"{}.tmp",
path.file_name()
.and_then(|s| s.to_str())
.unwrap_or("state.toml")
));
let write_result = (|| -> std::io::Result<()> {
use std::io::Write;
let mut f = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp_path)?;
f.write_all(text.as_bytes())?;
f.flush()?;
f.sync_all()?;
fs::rename(&tmp_path, path)?;
Ok(())
})();
match write_result {
Ok(()) => {
last_err = None;
break;
}
Err(e) => {
let _ = fs::remove_file(&tmp_path);
last_err = Some(crate::error::PncError::Io(e));
continue;
}
}
}
if let Some(e) = last_err {
return Err(e);
}
if let Some(parent) = path.parent()
&& let Ok(dir) = fs::File::open(parent)
{
let _ = dir.sync_all();
}
Ok(())
}