ommp.git

ommp

~ Oh My Music Player — a standalone terminal music player built with Rust and ratatui
git clone https://github.com/debuman/ommp.git

src/app/persist.rs (3417B)

      1 use serde::{Deserialize, Serialize};
      2 use std::fs;
      3 use std::path::PathBuf;
      4 
      5 /// Every field defaults, so a state file written by an older build — or one
      6 /// missing a key — restores what it can instead of dropping the whole session.
      7 #[derive(Serialize, Deserialize)]
      8 pub struct SavedState {
      9     #[serde(default = "default_volume")]
     10     pub volume: f32,
     11     #[serde(default)]
     12     pub shuffle: bool,
     13     #[serde(default = "default_repeat")]
     14     pub repeat: String,
     15     #[serde(default = "default_pane_widths")]
     16     pub pane_widths: [u16; 3],
     17     #[serde(default)]
     18     pub playlists: Vec<SavedPlaylist>,
     19     #[serde(default = "default_info_view")]
     20     pub info_view: String,
     21     #[serde(default = "default_right_split")]
     22     pub right_split: u16,
     23 }
     24 
     25 fn default_volume() -> f32 {
     26     0.5
     27 }
     28 
     29 fn default_repeat() -> String {
     30     "Off".to_string()
     31 }
     32 
     33 pub const DEFAULT_PANE_WIDTHS: [u16; 3] = [20, 60, 20];
     34 
     35 fn default_pane_widths() -> [u16; 3] {
     36     DEFAULT_PANE_WIDTHS
     37 }
     38 
     39 fn default_info_view() -> String {
     40     "Clock".to_string()
     41 }
     42 
     43 fn default_right_split() -> u16 {
     44     50
     45 }
     46 
     47 #[derive(Serialize, Deserialize)]
     48 pub struct SavedPlaylist {
     49     pub name: String,
     50     pub tracks: Vec<PathBuf>,
     51 }
     52 
     53 /// Pane widths must each clear the 10% minimum the resize code assumes and sum
     54 /// to exactly 100; anything else makes `LayoutAreas` produce degenerate rects
     55 /// and the border-drag math underflow. Reject rather than try to repair.
     56 pub fn sane_pane_widths(w: [u16; 3]) -> [u16; 3] {
     57     if w.iter().all(|&p| p >= 10) && w.iter().sum::<u16>() == 100 {
     58         w
     59     } else {
     60         DEFAULT_PANE_WIDTHS
     61     }
     62 }
     63 
     64 fn state_path() -> PathBuf {
     65     let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
     66     PathBuf::from(home).join(".config/ommp/state.json")
     67 }
     68 
     69 pub fn save(state: &SavedState) -> anyhow::Result<()> {
     70     let path = state_path();
     71     if let Some(parent) = path.parent() {
     72         fs::create_dir_all(parent)?;
     73     }
     74     let json = serde_json::to_string_pretty(state)?;
     75     // Write-then-rename: a crash or full disk mid-write leaves the previous
     76     // state file intact instead of a truncated one that parses to nothing.
     77     let tmp = path.with_extension("json.tmp");
     78     fs::write(&tmp, json)?;
     79     fs::rename(&tmp, &path)?;
     80     Ok(())
     81 }
     82 
     83 pub fn load() -> Option<SavedState> {
     84     let path = state_path();
     85     let data = fs::read_to_string(path).ok()?;
     86     serde_json::from_str(&data).ok()
     87 }
     88 
     89 #[cfg(test)]
     90 mod tests {
     91     use super::*;
     92 
     93     #[test]
     94     fn pane_widths_reject_anything_layout_cannot_use() {
     95         assert_eq!(sane_pane_widths([20, 60, 20]), [20, 60, 20]);
     96         assert_eq!(sane_pane_widths([10, 80, 10]), [10, 80, 10]);
     97         // Below the 10% floor the resize math underflows.
     98         assert_eq!(sane_pane_widths([10, 5, 85]), DEFAULT_PANE_WIDTHS);
     99         // Sums that are not 100 leave the dashboard with a gap or overflow.
    100         assert_eq!(sane_pane_widths([20, 20, 20]), DEFAULT_PANE_WIDTHS);
    101         assert_eq!(sane_pane_widths([0, 0, 0]), DEFAULT_PANE_WIDTHS);
    102     }
    103 
    104     #[test]
    105     fn missing_fields_fall_back_instead_of_dropping_the_file() {
    106         let saved: SavedState = serde_json::from_str(r#"{"volume":0.25}"#).unwrap();
    107         assert_eq!(saved.volume, 0.25);
    108         assert_eq!(saved.repeat, "Off");
    109         assert_eq!(saved.pane_widths, DEFAULT_PANE_WIDTHS);
    110         assert_eq!(saved.right_split, 50);
    111         assert!(saved.playlists.is_empty());
    112     }
    113 }