use anyhow::Context;
use crossterm::event::KeyCode;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct AppConfig {
pub general: GeneralConfig,
pub ui: UiConfig,
pub keybindings: KeybindingsConfig,
pub smart_context: SmartContextConfig,
pub auto_key_setup: AutoKeySetupConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GeneralConfig {
pub refresh_interval: u64,
pub default_shell: String,
pub ssh_command: String,
pub max_concurrent_connections: usize,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
refresh_interval: 30,
default_shell: String::from("/bin/bash"),
ssh_command: String::from("ssh"),
max_concurrent_connections: 10,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct UiConfig {
pub theme: String,
pub show_ip: bool,
pub show_uptime: bool,
pub card_layout: String,
pub border_style: String,
}
impl UiConfig {
pub fn available_themes() -> &'static [&'static str] {
&["default", "dracula", "nord", "gruvbox"]
}
pub fn is_valid_theme(name: &str) -> bool {
Self::available_themes().contains(&name)
}
}
impl Default for UiConfig {
fn default() -> Self {
Self {
theme: String::from("default"),
show_ip: true,
show_uptime: true,
card_layout: String::from("grid"),
border_style: String::from("rounded"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct KeybindingsConfig {
pub quit: String,
pub search: String,
pub connect: String,
pub dashboard: String,
pub file_manager: String,
pub snippets: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SmartContextConfig {
pub enabled: bool,
pub scan_interval: u64,
}
impl Default for SmartContextConfig {
fn default() -> Self {
Self {
enabled: true,
scan_interval: 300, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AutoKeySetupConfig {
pub enabled: bool,
pub suggest_on_password_auth: bool,
pub disable_password_auth: bool,
pub key_type: String,
pub key_directory: String,
pub backup_sshd_config: bool,
pub confirm_before_disable: bool,
}
impl Default for AutoKeySetupConfig {
fn default() -> Self {
Self {
enabled: true,
suggest_on_password_auth: true,
disable_password_auth: true,
key_type: String::from("ed25519"),
key_directory: String::from("~/.ssh"),
backup_sshd_config: true,
confirm_before_disable: true,
}
}
}
impl Default for KeybindingsConfig {
fn default() -> Self {
Self {
quit: String::from("q"),
search: String::from("/"),
connect: String::from("Enter"),
dashboard: String::from("F1"),
file_manager: String::from("F2"),
snippets: String::from("F3"),
}
}
}
#[derive(Debug, Clone)]
pub struct ParsedKeybindings {
pub quit: KeyCode,
pub search: KeyCode,
pub connect: KeyCode,
pub dashboard: KeyCode,
pub file_manager: KeyCode,
pub snippets: KeyCode,
}
impl ParsedKeybindings {
pub fn from_config(cfg: &KeybindingsConfig) -> Self {
let defaults = KeybindingsConfig::default();
Self {
quit: parse_keycode(&cfg.quit)
.unwrap_or_else(|| parse_keycode(&defaults.quit).expect("default quit")),
search: parse_keycode(&cfg.search)
.unwrap_or_else(|| parse_keycode(&defaults.search).expect("default search")),
connect: parse_keycode(&cfg.connect)
.unwrap_or_else(|| parse_keycode(&defaults.connect).expect("default connect")),
dashboard: parse_keycode(&cfg.dashboard)
.unwrap_or_else(|| parse_keycode(&defaults.dashboard).expect("default dashboard")),
file_manager: parse_keycode(&cfg.file_manager).unwrap_or_else(|| {
parse_keycode(&defaults.file_manager).expect("default file_manager")
}),
snippets: parse_keycode(&cfg.snippets)
.unwrap_or_else(|| parse_keycode(&defaults.snippets).expect("default snippets")),
}
}
}
impl Default for ParsedKeybindings {
fn default() -> Self {
Self::from_config(&KeybindingsConfig::default())
}
}
pub fn parse_keycode(s: &str) -> Option<KeyCode> {
match s {
"Enter" => Some(KeyCode::Enter),
"Esc" | "Escape" => Some(KeyCode::Esc),
"Tab" => Some(KeyCode::Tab),
"Backspace" | "BS" => Some(KeyCode::Backspace),
"Delete" | "Del" => Some(KeyCode::Delete),
"Up" => Some(KeyCode::Up),
"Down" => Some(KeyCode::Down),
"Left" => Some(KeyCode::Left),
"Right" => Some(KeyCode::Right),
"Home" => Some(KeyCode::Home),
"End" => Some(KeyCode::End),
"PageUp" => Some(KeyCode::PageUp),
"PageDown" => Some(KeyCode::PageDown),
f if f.starts_with('F') || f.starts_with('f') => f[1..].parse::<u8>().ok().map(KeyCode::F),
c if c.chars().count() == 1 => c.chars().next().map(KeyCode::Char),
_ => None,
}
}
pub fn load_app_config(path: Option<&std::path::Path>) -> anyhow::Result<AppConfig> {
use crate::utils::platform;
let config_path = match path {
Some(p) => p.to_path_buf(),
None => match platform::app_config_path() {
Some(p) => p,
None => return Ok(AppConfig::default()),
},
};
if !config_path.exists() {
return Ok(AppConfig::default());
}
let content = std::fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config: {}", config_path.display()))?;
let config: AppConfig = toml::from_str(&content)
.with_context(|| format!("Failed to parse config: {}", config_path.display()))?;
Ok(config)
}
pub fn save_theme_to_config(theme_name: &str) -> anyhow::Result<()> {
use crate::utils::platform;
let config_path = match platform::app_config_path() {
Some(p) => p,
None => anyhow::bail!("Cannot determine config path for this platform"),
};
if let Some(parent) = config_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create config directory: {}", parent.display()))?;
}
let mut config = if config_path.exists() {
let content = std::fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read config: {}", config_path.display()))?;
toml::from_str::<AppConfig>(&content)
.with_context(|| format!("Failed to parse config: {}", config_path.display()))?
} else {
AppConfig::default()
};
config.ui.theme = theme_name.to_string();
let content = toml::to_string_pretty(&config).context("Failed to serialize config")?;
std::fs::write(&config_path, content)
.with_context(|| format!("Failed to write config: {}", config_path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_keybindings_parse() {
let _kb = ParsedKeybindings::default();
}
}