Skip to main content

idet_core/
options.rs

1//! Editing options both frontends read from one file, so that changing them
2//! in the window editor changes them for the terminal editor as well.
3
4use std::{fs, path::PathBuf};
5
6const DEFAULT_TAB_WIDTH: usize = 4;
7const MAX_TAB_WIDTH: usize = 16;
8
9/// The settings shared by every idet frontend, stored as one line per option
10/// under `$XDG_CONFIG_HOME/idet/options.conf`.
11///
12/// Editors read them at startup and write them back the moment one changes.
13/// Two editors running at once each keep their own copy, and the last one to
14/// change an option is the one whose value survives on disk.
15#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub struct Options {
17    /// Whether a line too long for the window continues on the next screen
18    /// row rather than running off the side. The text on disk is untouched
19    /// either way.
20    pub wrap: bool,
21    /// How many spaces one indentation step adds or removes.
22    pub tab_width: usize,
23}
24
25impl Default for Options {
26    fn default() -> Self {
27        Self {
28            wrap: true,
29            tab_width: DEFAULT_TAB_WIDTH,
30        }
31    }
32}
33
34impl std::fmt::Display for Options {
35    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        writeln!(formatter, "wrap {}", self.wrap)?;
37        writeln!(formatter, "tab-width {}", self.tab_width)
38    }
39}
40
41impl Options {
42    /// Reads the options file, falling back to the defaults for anything it
43    /// does not name and for a file that is missing or unreadable.
44    #[must_use]
45    pub fn load() -> Self {
46        let text = Self::path()
47            .and_then(|path| fs::read_to_string(path).ok())
48            .unwrap_or_default();
49        Self::parse(&text)
50    }
51
52    /// Writes the options back to their file, creating the directory if it is
53    /// not there yet.
54    ///
55    /// # Errors
56    ///
57    /// Fails when the config directory cannot be determined or written to.
58    pub fn save(&self) -> std::io::Result<()> {
59        let Some(path) = Self::path() else {
60            return Err(std::io::Error::other("no config directory"));
61        };
62        if let Some(parent) = path.parent() {
63            fs::create_dir_all(parent)?;
64        }
65        fs::write(path, self.to_string())
66    }
67
68    /// Where the options are stored, or `None` when the environment names
69    /// neither a config directory nor a home directory.
70    #[must_use]
71    pub fn path() -> Option<PathBuf> {
72        let base = std::env::var_os("XDG_CONFIG_HOME")
73            .map(PathBuf::from)
74            .filter(|path| path.is_absolute())
75            .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))?;
76        Some(base.join("idet").join("options.conf"))
77    }
78
79    /// One indentation step as the spaces it inserts.
80    #[must_use]
81    pub fn indent(&self) -> String {
82        " ".repeat(self.tab_width)
83    }
84
85    fn parse(text: &str) -> Self {
86        let mut options = Self::default();
87        for line in text.lines() {
88            let mut words = line.split_whitespace();
89            match (words.next(), words.next()) {
90                (Some("wrap"), Some(value)) => options.wrap = value == "true",
91                (Some("tab-width"), Some(value)) => {
92                    if let Ok(width) = value.parse::<usize>() {
93                        options.tab_width = width.clamp(1, MAX_TAB_WIDTH);
94                    }
95                }
96                _ => {}
97            }
98        }
99        options
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::Options;
106
107    #[test]
108    fn what_is_written_is_what_is_read_back() {
109        let options = Options {
110            wrap: false,
111            tab_width: 2,
112        };
113        assert_eq!(Options::parse(&options.to_string()), options);
114    }
115
116    #[test]
117    fn an_empty_or_broken_file_leaves_the_defaults_standing() {
118        assert_eq!(Options::parse(""), Options::default());
119        assert_eq!(
120            Options::parse("tab-width wide\nnonsense\n"),
121            Options::default()
122        );
123    }
124
125    #[test]
126    fn an_unusable_tab_width_is_pulled_into_range() {
127        assert_eq!(Options::parse("tab-width 0").tab_width, 1);
128        assert_eq!(Options::parse("tab-width 99").tab_width, 16);
129    }
130}