1use std::{fs, path::PathBuf};
5
6const DEFAULT_TAB_WIDTH: usize = 4;
7const MAX_TAB_WIDTH: usize = 16;
8
9#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub struct Options {
17 pub wrap: bool,
21 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 #[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 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 #[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 #[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}