1pub 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 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}