1use crate::stack::DEFAULT_HISTORY_DEPTH;
2use crate::value::Value;
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
8#[serde(rename_all = "lowercase")]
9pub enum DisplayMode {
10 #[default]
11 Auto,
12 Fix,
13 Sci,
14 Eng,
15}
16
17impl<'de> Deserialize<'de> for DisplayMode {
18 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
19 where
20 D: serde::Deserializer<'de>,
21 {
22 let s = String::deserialize(deserializer)?;
23 match s.to_lowercase().as_str() {
24 "auto" => Ok(DisplayMode::Auto),
25 "fix" => Ok(DisplayMode::Fix),
26 "sci" => Ok(DisplayMode::Sci),
27 "eng" => Ok(DisplayMode::Eng),
28 _ => Ok(DisplayMode::Auto),
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35pub struct Settings {
36 #[serde(default = "default_stack_soft_cap")]
37 pub stack_soft_cap: usize,
38 #[serde(default = "default_rational_digit_cap")]
39 pub rational_digit_cap: usize,
40 #[serde(default = "default_history_depth")]
41 pub history_depth: usize,
42 #[serde(default)]
43 pub display_mode: DisplayMode,
44 #[serde(default = "default_fix_decimals")]
45 pub fix_decimals: u8,
46 #[serde(default = "default_sci_digits")]
47 pub sci_digits: u8,
48 #[serde(default = "default_eng_digits")]
49 pub eng_digits: u8,
50}
51
52impl Default for Settings {
53 fn default() -> Self {
54 Self {
55 stack_soft_cap: default_stack_soft_cap(),
56 rational_digit_cap: default_rational_digit_cap(),
57 history_depth: default_history_depth(),
58 display_mode: DisplayMode::default(),
59 fix_decimals: default_fix_decimals(),
60 sci_digits: default_sci_digits(),
61 eng_digits: default_eng_digits(),
62 }
63 }
64}
65
66fn default_stack_soft_cap() -> usize {
67 10_000
68}
69
70fn default_rational_digit_cap() -> usize {
71 10_000
72}
73
74fn default_history_depth() -> usize {
75 DEFAULT_HISTORY_DEPTH
76}
77
78fn default_fix_decimals() -> u8 {
79 2
80}
81
82fn default_sci_digits() -> u8 {
83 4
84}
85
86fn default_eng_digits() -> u8 {
87 4
88}
89
90pub const CURRENT_SCHEMA_VERSION: u32 = 2;
91
92#[derive(Debug, Clone, PartialEq, Serialize)]
94pub struct PersistedState {
95 #[serde(default = "default_schema_version")]
96 pub schema_version: u32,
97 #[serde(default, deserialize_with = "deserialize_stack")]
98 pub stack: Vec<Value>,
99 #[serde(default)]
100 pub settings: Settings,
101 #[serde(default)]
102 pub migrated: bool,
103}
104
105fn default_schema_version() -> u32 {
106 1
107}
108
109impl Default for PersistedState {
110 fn default() -> Self {
111 Self {
112 schema_version: 1,
113 stack: Vec::new(),
114 settings: Settings::default(),
115 migrated: false,
116 }
117 }
118}
119
120impl<'de> Deserialize<'de> for PersistedState {
121 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
122 where
123 D: serde::Deserializer<'de>,
124 {
125 #[derive(Deserialize)]
126 struct RawV1 {
127 #[serde(default = "default_schema_version")]
128 schema_version: u32,
129 #[serde(default)]
130 stack: Vec<toml::Value>,
131 #[serde(default)]
132 settings: SettingsRaw,
133 #[serde(default)]
134 migrated: bool,
135 }
136
137 #[derive(Deserialize)]
138 struct SettingsRaw {
139 #[serde(default = "default_stack_soft_cap")]
140 stack_soft_cap: usize,
141 #[serde(default = "default_rational_digit_cap")]
142 rational_digit_cap: usize,
143 #[serde(default)]
144 history_depth: Option<usize>,
145 #[serde(default)]
146 undo_depth: Option<usize>,
147 #[serde(default)]
148 display_mode: Option<DisplayMode>,
149 #[serde(default)]
150 fix_decimals: Option<u8>,
151 #[serde(default)]
152 sci_digits: Option<u8>,
153 #[serde(default)]
154 eng_digits: Option<u8>,
155 }
156
157 impl Default for SettingsRaw {
158 fn default() -> Self {
159 Self {
160 stack_soft_cap: default_stack_soft_cap(),
161 rational_digit_cap: default_rational_digit_cap(),
162 history_depth: None,
163 undo_depth: None,
164 display_mode: None,
165 fix_decimals: None,
166 sci_digits: None,
167 eng_digits: None,
168 }
169 }
170 }
171
172 let raw = RawV1::deserialize(deserializer)?;
173
174 let version = raw.schema_version;
175 let is_v1 = version <= 1; let mut stack = Vec::new();
178 let mut skipped = 0usize;
179 for item in raw.stack {
180 use serde::de::IntoDeserializer;
181 match Value::deserialize(item.into_deserializer()) {
182 Ok(v) => stack.push(v),
183 Err(e) => {
184 skipped += 1;
185 tracing::debug!(error = %e, "skipped persisted value");
186 }
187 }
188 }
189 if skipped > 0 {
190 tracing::warn!(
191 skipped = skipped,
192 "skipped persisted value(s) with unknown kind"
193 );
194 }
195
196 let history_depth = if is_v1 {
198 raw.settings
200 .history_depth
201 .or(raw.settings.undo_depth)
202 .unwrap_or_else(default_history_depth)
203 } else {
204 raw.settings
206 .history_depth
207 .unwrap_or_else(default_history_depth)
208 };
209
210 let mut settings = Settings {
211 stack_soft_cap: raw.settings.stack_soft_cap,
212 rational_digit_cap: raw.settings.rational_digit_cap,
213 history_depth,
214 display_mode: raw.settings.display_mode.unwrap_or_default(),
215 fix_decimals: raw
216 .settings
217 .fix_decimals
218 .unwrap_or_else(default_fix_decimals),
219 sci_digits: raw.settings.sci_digits.unwrap_or_else(default_sci_digits),
220 eng_digits: raw.settings.eng_digits.unwrap_or_else(default_eng_digits),
221 };
222
223 if settings.history_depth > 1000 {
225 settings.history_depth = 1000;
226 }
227
228 let migrated = raw.migrated || is_v1;
229
230 let schema_version = if is_v1 {
231 CURRENT_SCHEMA_VERSION
232 } else {
233 version
234 };
235
236 Ok(PersistedState {
237 schema_version,
238 stack,
239 settings,
240 migrated,
241 })
242 }
243}
244
245pub fn state_path() -> PathBuf {
246 directories::ProjectDirs::from("", "", "pnc")
247 .map(|p| p.data_dir().join("state.toml"))
248 .unwrap_or_else(|| PathBuf::from("state.toml"))
249}
250
251pub fn load() -> Result<PersistedState, crate::error::PncError> {
252 load_from(&state_path())
253}
254
255pub fn load_from(path: &std::path::Path) -> Result<PersistedState, crate::error::PncError> {
256 if !path.exists() {
257 return Ok(PersistedState::default());
258 }
259 let text = fs::read_to_string(path)?;
260 if text.is_empty() {
261 return Ok(PersistedState::default());
262 }
263 let toml_val: toml::Value =
265 toml::from_str(&text).map_err(|e| crate::error::PncError::Parse(e.to_string()))?;
266
267 if let Some(sv) = toml_val.get("schema_version") {
269 match sv {
270 toml::Value::Integer(n) => {
271 let v = *n as u32;
272 if v == 0 || v > CURRENT_SCHEMA_VERSION {
273 return Err(crate::error::PncError::UnsupportedSchemaVersion);
274 }
275 }
276 _ => {
277 return Err(crate::error::PncError::Parse(
279 "corrupt schema_version".to_string(),
280 ));
281 }
282 }
283 }
284
285 let state: PersistedState =
286 toml::from_str(&text).map_err(|e| crate::error::PncError::Parse(e.to_string()))?;
287
288 let mut state = state;
290 if state.settings.history_depth > 1000 {
291 state.settings.history_depth = 1000;
292 }
293
294 Ok(state)
295}
296
297pub fn save(state: &PersistedState) -> Result<(), crate::error::PncError> {
298 save_to(state, &state_path())
299}
300
301pub fn lock_path() -> std::path::PathBuf {
302 for var in ["PNC_LOCK_PATH", "PNC_LOCK_FILE", "PNC_LOCK"] {
304 if let Ok(v) = std::env::var(var)
305 && !v.is_empty()
306 {
307 return std::path::PathBuf::from(v);
308 }
309 }
310 state_path().with_file_name("pnc.lock")
311}
312
313pub fn acquire_lock() -> Result<std::fs::File, crate::error::PncError> {
314 acquire_lock_at(&lock_path())
315}
316
317pub fn acquire_lock_at(path: &std::path::Path) -> Result<std::fs::File, crate::error::PncError> {
318 if let Some(parent) = path.parent() {
319 fs::create_dir_all(parent)?;
320 }
321 let file = fs::OpenOptions::new()
322 .create(true)
323 .write(true)
324 .truncate(false)
325 .open(path)?;
326 use fs2::FileExt;
327 file.try_lock_exclusive().map_err(|_| {
328 crate::error::PncError::Io(std::io::Error::other(format!(
329 "another pnc instance is running (lock: {}); set PNC_LOCK_PATH to override",
330 path.display()
331 )))
332 })?;
333 Ok(file)
334}
335
336pub fn save_to(
337 state: &PersistedState,
338 path: &std::path::Path,
339) -> Result<(), crate::error::PncError> {
340 if let Some(parent) = path.parent() {
341 fs::create_dir_all(parent)?;
342 }
343 let text =
344 toml::to_string_pretty(state).map_err(|e| crate::error::PncError::Parse(e.to_string()))?;
345 if path.exists() {
346 let backup = path.with_extension("bak");
347 let _ = fs::copy(path, &backup);
348 }
349 let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
350 fs::create_dir_all(parent)?;
351
352 let mut last_err = None;
356 for _ in 0..2 {
357 let tmp_path = path.with_file_name(format!(
358 "{}.tmp",
359 path.file_name()
360 .and_then(|s| s.to_str())
361 .unwrap_or("state.toml")
362 ));
363 let write_result = (|| -> std::io::Result<()> {
364 use std::io::Write;
365 let mut f = fs::OpenOptions::new()
366 .create(true)
367 .write(true)
368 .truncate(true)
369 .open(&tmp_path)?;
370 f.write_all(text.as_bytes())?;
371 f.flush()?;
372 f.sync_all()?;
373 fs::rename(&tmp_path, path)?;
374 Ok(())
375 })();
376 match write_result {
377 Ok(()) => {
378 last_err = None;
379 break;
380 }
381 Err(e) => {
382 let _ = fs::remove_file(&tmp_path);
383 last_err = Some(crate::error::PncError::Io(e));
384 continue;
385 }
386 }
387 }
388 if let Some(e) = last_err {
389 return Err(e);
390 }
391 if let Some(parent) = path.parent()
393 && let Ok(dir) = fs::File::open(parent)
394 {
395 let _ = dir.sync_all();
396 }
397 Ok(())
398}