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