quickterm 0.1.0

A small drop-down terminal for Sway/i3 IPC
Documentation
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;

use serde::Deserialize;

use crate::error::QuicktermError;

pub const CONFIG_FILE_NAME: &str = "quickterm.json";
pub const LEGACY_CONFIG_FILE_NAME: &str = "i3-quickterm.json";
pub const DEFAULT_HISTORY_PATH: &str = "{$HOME}/.cache/quickterm.order";
pub const LEGACY_HISTORY_PATH: &str = "{$HOME}/.cache/i3-quickterm.order";

#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Position {
    Top,
    Bottom,
    Center,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Config {
    pub menu: String,
    pub term: String,
    pub history: Option<String>,
    pub ratio: f64,
    pub pos: Position,
    pub shells: BTreeMap<String, String>,
}

#[derive(Debug, Default, Deserialize)]
struct PartialConfig {
    menu: Option<String>,
    term: Option<String>,
    history: Option<Option<String>>,
    ratio: Option<f64>,
    pos: Option<Position>,
    shells: Option<BTreeMap<String, String>>,
}

pub fn default_config() -> Config {
    let mut shells = BTreeMap::new();
    shells.insert("haskell".to_string(), "ghci".to_string());
    shells.insert("js".to_string(), "node".to_string());
    shells.insert("python".to_string(), "ipython3 --no-banner".to_string());
    shells.insert("shell".to_string(), "{$SHELL}".to_string());

    Config {
        menu: "rofi -dmenu -p 'quickterm: ' -no-custom -auto-select".to_string(),
        term: "urxvt".to_string(),
        history: Some(DEFAULT_HISTORY_PATH.to_string()),
        ratio: 0.25,
        pos: Position::Top,
        shells,
    }
}

pub fn config_path_for(home: &str, xdg_config_dir: Option<&str>) -> String {
    match xdg_config_dir {
        Some(dir) => format!("{dir}/{CONFIG_FILE_NAME}"),
        None => format!("{home}/.config/{CONFIG_FILE_NAME}"),
    }
}

pub fn legacy_config_path_for(home: &str, xdg_config_dir: Option<&str>) -> String {
    match xdg_config_dir {
        Some(dir) => format!("{dir}/{LEGACY_CONFIG_FILE_NAME}"),
        None => format!("{home}/.config/{LEGACY_CONFIG_FILE_NAME}"),
    }
}

pub fn config_path_from_env() -> Result<PathBuf, QuicktermError> {
    let home = std::env::var("HOME")
        .map_err(|_| QuicktermError::InvalidConfig("HOME is not set".to_string()))?;
    let xdg = std::env::var("XDG_CONFIG_DIR").ok();
    let preferred = PathBuf::from(config_path_for(&home, xdg.as_deref()));
    if preferred.exists() {
        return Ok(preferred);
    }

    let legacy = PathBuf::from(legacy_config_path_for(&home, xdg.as_deref()));
    if legacy.exists() {
        return Ok(legacy);
    }

    Ok(preferred)
}

pub fn load_config() -> Result<Config, QuicktermError> {
    let path = config_path_from_env()?;
    if !path.exists() {
        return Ok(default_config());
    }

    let text = fs::read_to_string(path)?;
    let partial: PartialConfig = serde_json::from_str(&text)
        .map_err(|err| QuicktermError::InvalidConfig(err.to_string()))?;

    let mut config = default_config();
    if let Some(value) = partial.menu {
        config.menu = value;
    }
    if let Some(value) = partial.term {
        config.term = value;
    }
    if let Some(value) = partial.history {
        config.history = value;
    }
    if let Some(value) = partial.ratio {
        config.ratio = value;
    }
    if let Some(value) = partial.pos {
        config.pos = value;
    }
    if let Some(value) = partial.shells {
        config.shells = value;
    }

    Ok(config)
}