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