Skip to main content

quickterm/
config.rs

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}