runa_tui/
config.rs

1//! Configuration options for runa
2//!
3//! This module defines all configuration options and deserializes them
4//! from the `runa.toml` using `serde`.
5//!
6//! Each config struct corresponds to a top-level key in the `runa.toml`.
7
8pub mod display;
9pub mod input;
10pub mod theme;
11
12pub use display::Display;
13pub use input::{Editor, Keys};
14pub use theme::Theme;
15
16use serde::Deserialize;
17use std::collections::HashSet;
18use std::ffi::OsString;
19use std::sync::Arc;
20use std::{fs, io, path::PathBuf};
21
22#[derive(Deserialize, Debug, Default)]
23#[serde(default)]
24pub struct RawConfig {
25    dirs_first: bool,
26    show_hidden: bool,
27    show_system: bool,
28    case_insensitive: bool,
29    always_show: Vec<String>,
30    display: Display,
31    theme: Theme,
32    editor: Editor,
33    keys: Keys,
34}
35
36#[derive(Debug)]
37pub struct Config {
38    dirs_first: bool,
39    show_hidden: bool,
40    show_system: bool,
41    case_insensitive: bool,
42    always_show: Arc<HashSet<OsString>>,
43    display: Display,
44    theme: Theme,
45    editor: Editor,
46    keys: Keys,
47}
48
49impl From<RawConfig> for Config {
50    fn from(raw: RawConfig) -> Self {
51        Self {
52            dirs_first: raw.dirs_first,
53            show_hidden: raw.show_hidden,
54            show_system: raw.show_system,
55            case_insensitive: raw.case_insensitive,
56            always_show: Arc::new(
57                raw.always_show
58                    .into_iter()
59                    .map(OsString::from)
60                    .collect::<HashSet<_>>(),
61            ),
62            display: raw.display,
63            theme: raw.theme,
64            editor: raw.editor,
65            keys: raw.keys,
66        }
67    }
68}
69
70impl Config {
71    pub fn load() -> Self {
72        let path = Self::default_path();
73
74        if !path.exists() {
75            eprintln!("No config file found at {:?}", path);
76            eprintln!(
77                "Tip: Run 'runa --init' or '--init-minimal' to generate a default configuration."
78            );
79            eprintln!("Starting with internal defaults...\n");
80            return Self::default();
81        }
82
83        match std::fs::read_to_string(&path) {
84            Ok(content) => match toml::from_str::<RawConfig>(&content) {
85                Ok(raw) => raw.into(),
86                Err(e) => {
87                    eprintln!("Error parsing config: {}", e);
88                    Self::default()
89                }
90            },
91            Err(_) => Self::default(),
92        }
93    }
94
95    // Getters
96
97    pub fn dirs_first(&self) -> bool {
98        self.dirs_first
99    }
100
101    pub fn show_hidden(&self) -> bool {
102        self.show_hidden
103    }
104
105    pub fn show_system(&self) -> bool {
106        self.show_system
107    }
108
109    pub fn case_insensitive(&self) -> bool {
110        self.case_insensitive
111    }
112
113    pub fn always_show(&self) -> &Arc<HashSet<OsString>> {
114        &self.always_show
115    }
116
117    pub fn display(&self) -> &Display {
118        &self.display
119    }
120
121    pub fn theme(&self) -> &Theme {
122        &self.theme
123    }
124
125    pub fn editor(&self) -> &Editor {
126        &self.editor
127    }
128
129    pub fn keys(&self) -> &Keys {
130        &self.keys
131    }
132
133    pub fn default_path() -> PathBuf {
134        if let Ok(path) = std::env::var("RUNA_CONFIG") {
135            return PathBuf::from(path);
136        }
137        if let Some(home) = dirs::home_dir() {
138            return home.join(".config/runa/runa.toml");
139        }
140        PathBuf::from("runa.toml")
141    }
142
143    pub fn generate_default(path: &PathBuf, minimal: bool) -> std::io::Result<()> {
144        if path.exists() {
145            return Err(io::Error::new(
146                io::ErrorKind::AlreadyExists,
147                format!("Config file already exists at {:?}", path),
148            ));
149        }
150        if let Some(parent) = path.parent() {
151            fs::create_dir_all(parent)?;
152        }
153        let full_toml = r##"# runa.toml - default configuration for runa
154
155# Note:
156# Commented values are the internal defaults of runa
157# Use hex codes (eg. "#333333") or terminal colors ("cyan")
158
159# General behavior
160dirs_first = true
161show_hidden = false
162# show_system = false
163case_insensitive = true
164# always_show = []
165
166[display]
167# selection_marker = true
168# dir_marker = true
169borders = "split"
170# titles = false
171separators = true
172parent = true
173preview = true
174preview_underline = true
175# preview_underline_color = false
176entry_padding = 1
177# scroll_padding = 5
178
179# [display.layout]
180# parent = 20
181# main = 40
182# preview = 40
183
184[theme]
185selection_icon = ""
186
187[theme.selection]
188# fg = "default"
189bg = "#333333"
190
191[theme.accent]
192fg = "#353536"
193# bg = "default"
194
195# [theme.entry]
196# fg = "default"
197# bg = "default"
198
199[theme.directory]
200fg = "blue"
201# bg = "default"
202
203# [theme.separator]
204# fg = "default"
205# bg = "default"
206
207# [theme.parent]
208# fg = "default"
209# bg = "default"
210# selection_fg = "default"
211# selection_bg = "default"
212
213# [theme.preview]
214# fg = "default"
215# bg = "default"
216# selection_fg = "default"
217# selection_bg = "default"
218
219# [theme.underline]
220# fg = "default"
221# bg = "default"
222
223[theme.path]
224fg = "magenta"
225# bg = "default"
226
227# [theme.marker]
228# icon = "*"
229# fg = "default"
230# bg = "default"
231
232# [theme.widget]
233# size = "medium"           # "small", "medium", "large" or [w ,h] or { w = 30, y = 30 }.
234# position = "center"       # "center", "top_left", "bottomright", or [x, y] (percent) or { x = 42, y = 80 }.
235# confirm_size = "large"
236
237# [theme.widget.color]
238# fg = "default"
239# bg = "default"
240
241# [theme.widget.border]
242# fg = "default"
243# bg = "default"
244
245[editor]
246# cmd = "nvim"
247
248# [keys]
249# open_file = ["Enter"]
250# go_up = ["k", "Up Arrow"]
251# go_down = ["j", "Down Arrow"]
252# go_parent = ["h", "Left Arrow", "Backspace"]
253# go_into_dir = ["l", "Right Arrow"]
254# quit = ["q", "Esc"]
255# delete = ["d"]
256# copy = ["y"]
257# paste = ["p"]
258# rename = ["r"]
259# create = ["n"]
260# create_directory = ["Shift+n"]
261# filter = ["f"]
262# toggle_marker = [" "]     # " " - indicates space bar
263"##;
264
265        let minimal_toml = r##"# runa.toml - minimal configuration
266# Only the essentials. The rest uses internal defaults.
267
268dirs_first = true
269show_hidden = false
270
271[display]
272borders = "split"
273entry_padding = 1
274
275[theme]
276selection_icon = ""
277
278[theme.selection]
279bg = "#333333"
280
281[theme.accent]
282fg = "#353536"
283
284[theme.directory]
285fg = "blue"
286
287[theme.path]
288fg = "magenta"
289
290[editor]
291# cmd = "nvim"
292"##;
293
294        let content = if minimal { minimal_toml } else { full_toml };
295
296        fs::write(path, content)?;
297        println!(
298            "{} Default config generated at {:?}",
299            if minimal { "Minimal" } else { "Full" },
300            path
301        );
302        Ok(())
303    }
304}
305
306impl Default for Config {
307    fn default() -> Self {
308        Config {
309            dirs_first: true,
310            show_hidden: false,
311            show_system: false,
312            case_insensitive: true,
313            always_show: Arc::new(HashSet::new()),
314            display: Display::default(),
315            theme: Theme::default(),
316            editor: Editor::default(),
317            keys: Keys::default(),
318        }
319    }
320}