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