rura 1.6.0

Interactive TUI pipeline editor built for rapid iteration
use crate::app::CommandLinePlacement;
use crate::props::APP_NAME;
use log::info;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct StyleConfig {
    pub fg: Option<String>,
    pub bg: Option<String>,
    pub bold: Option<bool>,
    pub italic: Option<bool>,
    pub underlined: Option<bool>,
    pub dim: Option<bool>,
}

impl StyleConfig {
    fn fg(fg: &str) -> Self {
        StyleConfig {
            fg: Some(fg.to_string()),
            ..Default::default()
        }
    }

    fn bg(bg: &str) -> Self {
        StyleConfig {
            bg: Some(bg.to_string()),
            ..Default::default()
        }
    }

    fn fg_bg(fg: &str, bg: &str) -> Self {
        StyleConfig {
            fg: Some(fg.to_string()),
            bg: Some(bg.to_string()),
            ..Default::default()
        }
    }

    fn underlined(self) -> Self {
        StyleConfig {
            underlined: Some(true),
            ..self
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct ThemeConfig {
    pub cmd_regular: StyleConfig,
    pub cmd_regular_pipe: StyleConfig,
    pub cmd_regular_current: StyleConfig,
    pub cmd_highlight: StyleConfig,
    pub cmd_highlight_pipe: StyleConfig,
    pub cmd_highlight_current: StyleConfig,
    pub cmd_quoted: StyleConfig,
    pub cmd_invalid: StyleConfig,
    pub cmd_diff_base: StyleConfig,
    pub output_highlight: StyleConfig,
    pub output_highlight_current: StyleConfig,
    pub line_nums: StyleConfig,
    pub popup: StyleConfig,
    pub diff_addition: StyleConfig,
    pub diff_deletion: StyleConfig,
    pub diff_equal: StyleConfig,
}

impl Default for ThemeConfig {
    fn default() -> Self {
        ThemeConfig {
            cmd_regular: StyleConfig::default(),
            cmd_regular_pipe: StyleConfig::fg("green"),
            cmd_regular_current: StyleConfig::default(),
            cmd_highlight: StyleConfig::fg_bg("black", "yellow"),
            cmd_highlight_pipe: StyleConfig::bg("yellow"),
            cmd_highlight_current: StyleConfig::fg_bg("black", "yellow"),
            cmd_quoted: StyleConfig::fg("yellow"),
            cmd_invalid: StyleConfig::default(),
            cmd_diff_base: StyleConfig::default().underlined(),
            output_highlight: StyleConfig::fg_bg("white", "magenta"),
            output_highlight_current: StyleConfig::fg_bg("black", "yellow"),
            line_nums: StyleConfig::fg("magenta"),
            popup: StyleConfig::fg_bg("white", "blue"),
            diff_addition: StyleConfig::fg("green"),
            diff_deletion: StyleConfig::fg("red"),
            diff_equal: StyleConfig::default(),
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct KeyBindingsConfig {
    pub quit: Vec<String>,
    pub execute_full: Vec<String>,
    pub execute_until_current: Vec<String>,
    pub execute_until_prev: Vec<String>,
    pub reset_input: Vec<String>,
    pub scroll_down: Vec<String>,
    pub scroll_down_page: Vec<String>,
    pub scroll_up: Vec<String>,
    pub scroll_up_page: Vec<String>,
    pub scroll_left: Vec<String>,
    pub scroll_left_page: Vec<String>,
    pub scroll_right: Vec<String>,
    pub scroll_right_page: Vec<String>,
    pub toggle_wrap: Vec<String>,
    pub history_prev: Vec<String>,
    pub history_next: Vec<String>,
    pub subcommand_next: Vec<String>,
    pub subcommand_prev: Vec<String>,
    pub complete: Vec<String>,
    pub complete_prev: Vec<String>,
    pub search_next: Vec<String>,
    pub search_prev: Vec<String>,
    pub save_output: Vec<String>,
    pub save_command: Vec<String>,
    pub format_command: Vec<String>,
    pub subcommand_cut: Vec<String>,
    pub subcommand_copy: Vec<String>,
    pub subcommand_paste: Vec<String>,
    pub toggle_diff: Vec<String>,
    pub diff_base: Vec<String>,
    pub diff_base_stdin: Vec<String>,
    pub toggle_live: Vec<String>,
    pub toggle_live_until_cursor: Vec<String>,
}

impl Default for KeyBindingsConfig {
    fn default() -> Self {
        KeyBindingsConfig {
            quit: vec!["ctrl+c".into()],
            execute_full: vec!["enter".into()],
            execute_until_current: vec!["alt+\\".into()],
            execute_until_prev: vec!["alt+|".into()],
            reset_input: vec!["alt+i".into()],
            scroll_down: vec!["down".into(), "alt+j".into()],
            scroll_down_page: vec![
                "pagedown".into(),
                "ctrl+d".into(),
                "alt+down".into(),
                "alt+shift+j".into(),
            ],
            scroll_up: vec!["up".into(), "alt+k".into()],
            scroll_up_page: vec![
                "pageup".into(),
                "ctrl+u".into(),
                "alt+up".into(),
                "alt+shift+k".into(),
            ],
            scroll_left: vec!["alt+h".into()],
            scroll_left_page: vec!["shift+alt+h".into()],
            scroll_right: vec!["alt+l".into()],
            scroll_right_page: vec!["shift+alt+l".into()],
            toggle_wrap: vec!["alt+w".into()],
            history_prev: vec!["ctrl+p".into()],
            history_next: vec!["ctrl+n".into()],
            subcommand_next: vec!["alt+right".into()],
            subcommand_prev: vec!["alt+left".into()],
            complete: vec!["tab".into()],
            complete_prev: vec!["shift+tab".into(), "alt+tab".into()],
            search_next: vec!["f3".into(), "ctrl+f".into()],
            search_prev: vec!["f4".into(), "ctrl+b".into()],
            save_output: vec!["ctrl+s".into()],
            save_command: vec!["ctrl+alt+s".into()],
            format_command: vec!["alt+o".into()],
            subcommand_cut: vec!["alt+x".into()],
            subcommand_copy: vec!["alt+c".into()],
            subcommand_paste: vec!["alt+v".into()],
            toggle_diff: vec!["alt+d".into()],
            diff_base: vec!["alt+/".into()],
            diff_base_stdin: vec!["alt+?".into()],
            toggle_live_until_cursor: vec!["f11".into()],
            toggle_live: vec!["f12".into()],
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct Config {
    pub log_level: Option<String>,
    pub theme: ThemeConfig,
    pub keybindings: KeyBindingsConfig,
    pub command_line_placement: CommandLinePlacement,
    pub highlight_duration_ms: u64,
    pub debounce_duration_ms: u64,
    pub shell: Option<String>,
    pub no_cache: bool,
}

impl Default for Config {
    fn default() -> Self {
        Config {
            log_level: None,
            theme: ThemeConfig::default(),
            keybindings: KeyBindingsConfig::default(),
            command_line_placement: CommandLinePlacement::default(),
            highlight_duration_ms: 250,
            debounce_duration_ms: 500,
            shell: None,
            no_cache: false,
        }
    }
}

pub fn config_path() -> Option<PathBuf> {
    dirs::config_dir().map(|d| d.join(APP_NAME).join("config.toml"))
}

pub fn history_path() -> Option<PathBuf> {
    dirs::data_dir().map(|d| d.join(APP_NAME).join("history.txt"))
}

pub fn load_config(custom_path: Option<&str>) -> Config {
    if let Some(p) = custom_path {
        info!("Loading config from arg path: {}", p);
        let path = PathBuf::from(p);
        if !path.exists() {
            panic!("Config file not found: {}", path.display());
        }
        match fs::read_to_string(&path) {
            Ok(content) => toml::from_str(&content).unwrap_or_default(),
            Err(_) => panic!("Invalid config file: {}", path.display()),
        }
    } else if let Ok(env_path) = std::env::var("RURA_CONFIG") {
        info!("Loading config env path: {}", env_path);
        let path = PathBuf::from(env_path);
        if !path.exists() {
            panic!("Config file not found: {}", path.display());
        }
        match fs::read_to_string(&path) {
            Ok(content) => toml::from_str(&content).unwrap_or_default(),
            Err(_) => panic!("Invalid config file: {}", path.display()),
        }
    } else {
        info!(
            "Loading config from default path: {}",
            config_path().unwrap_or_default().to_string_lossy()
        );
        match config_path() {
            Some(path) => {
                if !path.exists() {
                    if let Some(parent) = path.parent() {
                        let _ = fs::create_dir_all(parent);
                    }
                    let default = Config::default();
                    if let Ok(content) = toml::to_string_pretty(&default) {
                        let _ = fs::write(&path, content);
                    }
                    return default;
                }
                match fs::read_to_string(&path) {
                    Ok(content) => toml::from_str(&content).unwrap_or_default(),
                    Err(_) => panic!("Invalid config file: {}", path.display()),
                }
            }
            None => return Config::default(),
        }
    }
}