1use std::collections::BTreeMap;
2use std::fs;
3use std::path::PathBuf;
4
5use serde::Deserialize;
6
7use crate::error::QuicktermError;
8
9pub const CONFIG_FILE_NAME: &str = "quickterm.json";
10pub const LEGACY_CONFIG_FILE_NAME: &str = "i3-quickterm.json";
11pub const DEFAULT_HISTORY_PATH: &str = "{$HOME}/.cache/quickterm.order";
12pub const LEGACY_HISTORY_PATH: &str = "{$HOME}/.cache/i3-quickterm.order";
13
14#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum Position {
17 Top,
18 Bottom,
19 Center,
20}
21
22#[derive(Debug, Clone, PartialEq, Deserialize)]
23pub struct Config {
24 pub menu: String,
25 pub term: String,
26 pub history: Option<String>,
27 pub ratio: f64,
28 pub pos: Position,
29 pub shells: BTreeMap<String, String>,
30}
31
32#[derive(Debug, Default, Deserialize)]
33struct PartialConfig {
34 menu: Option<String>,
35 term: Option<String>,
36 history: Option<Option<String>>,
37 ratio: Option<f64>,
38 pos: Option<Position>,
39 shells: Option<BTreeMap<String, String>>,
40}
41
42pub fn default_config() -> Config {
43 let mut shells = BTreeMap::new();
44 shells.insert("haskell".to_string(), "ghci".to_string());
45 shells.insert("js".to_string(), "node".to_string());
46 shells.insert("python".to_string(), "ipython3 --no-banner".to_string());
47 shells.insert("shell".to_string(), "{$SHELL}".to_string());
48
49 Config {
50 menu: "rofi -dmenu -p 'quickterm: ' -no-custom -auto-select".to_string(),
51 term: "urxvt".to_string(),
52 history: Some(DEFAULT_HISTORY_PATH.to_string()),
53 ratio: 0.25,
54 pos: Position::Top,
55 shells,
56 }
57}
58
59pub fn config_path_for(home: &str, xdg_config_dir: Option<&str>) -> String {
60 match xdg_config_dir {
61 Some(dir) => format!("{dir}/{CONFIG_FILE_NAME}"),
62 None => format!("{home}/.config/{CONFIG_FILE_NAME}"),
63 }
64}
65
66pub fn legacy_config_path_for(home: &str, xdg_config_dir: Option<&str>) -> String {
67 match xdg_config_dir {
68 Some(dir) => format!("{dir}/{LEGACY_CONFIG_FILE_NAME}"),
69 None => format!("{home}/.config/{LEGACY_CONFIG_FILE_NAME}"),
70 }
71}
72
73pub fn config_path_from_env() -> Result<PathBuf, QuicktermError> {
74 let home = std::env::var("HOME")
75 .map_err(|_| QuicktermError::InvalidConfig("HOME is not set".to_string()))?;
76 let xdg = std::env::var("XDG_CONFIG_DIR").ok();
77 let preferred = PathBuf::from(config_path_for(&home, xdg.as_deref()));
78 if preferred.exists() {
79 return Ok(preferred);
80 }
81
82 let legacy = PathBuf::from(legacy_config_path_for(&home, xdg.as_deref()));
83 if legacy.exists() {
84 return Ok(legacy);
85 }
86
87 Ok(preferred)
88}
89
90pub fn load_config() -> Result<Config, QuicktermError> {
91 let path = config_path_from_env()?;
92 if !path.exists() {
93 return Ok(default_config());
94 }
95
96 let text = fs::read_to_string(path)?;
97 let partial: PartialConfig = serde_json::from_str(&text)
98 .map_err(|err| QuicktermError::InvalidConfig(err.to_string()))?;
99
100 let mut config = default_config();
101 if let Some(value) = partial.menu {
102 config.menu = value;
103 }
104 if let Some(value) = partial.term {
105 config.term = value;
106 }
107 if let Some(value) = partial.history {
108 config.history = value;
109 }
110 if let Some(value) = partial.ratio {
111 config.ratio = value;
112 }
113 if let Some(value) = partial.pos {
114 config.pos = value;
115 }
116 if let Some(value) = partial.shells {
117 config.shells = value;
118 }
119
120 Ok(config)
121}