use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::OnceLock;
static CONFIG: OnceLock<ShellConfig> = OnceLock::new();
pub fn get_config() -> &'static ShellConfig {
CONFIG.get_or_init(|| ShellConfig::load().unwrap_or_default())
}
pub fn reload_config() -> Result<ShellConfig> {
ShellConfig::load()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ShellConfig {
pub shell: ShellSettings,
pub colors: ColorConfig,
pub prompt: PromptConfig,
pub ai: AiConfig,
pub history: HistoryConfig,
pub editor: EditorConfig,
pub keybindings: KeybindingsConfig,
#[serde(default)]
pub aliases: HashMap<String, String>,
#[serde(default)]
pub env: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ShellSettings {
pub show_banner: bool,
pub show_tips: bool,
pub default_directory: String,
pub vi_mode: bool,
pub auto_cd: bool,
pub glob_expansion: bool,
pub command_correction: bool,
pub bell_style: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ColorConfig {
pub enabled: bool,
pub theme: String,
pub force: bool,
pub true_color: bool,
pub custom: CustomColors,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CustomColors {
pub number: String,
pub string: String,
pub boolean: String,
pub keyword: String,
pub punctuation: String,
pub key: String,
pub uri: String,
pub error: String,
pub warning: String,
pub success: String,
pub dim: String,
pub comment: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PromptConfig {
pub format: String,
pub continuation: String,
pub right: String,
pub show_git: bool,
pub show_time: bool,
pub time_threshold_ms: u64,
pub transient: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AiConfig {
pub default_provider: String,
pub default_model: String,
pub suggestions: bool,
pub max_tokens: u32,
pub temperature: f32,
pub streaming: bool,
pub allowed_tools: Vec<String>,
pub blocked_tools: Vec<String>,
pub max_agent_steps: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct HistoryConfig {
pub enabled: bool,
pub max_size: usize,
pub ignore_duplicates: bool,
pub ignore_space: bool,
pub ignore_patterns: Vec<String>,
pub share: bool,
pub timestamps: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct EditorConfig {
pub external: String,
pub tab_width: u8,
pub syntax_highlighting: bool,
pub line_numbers: bool,
pub auto_indent: bool,
pub bracket_matching: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct KeybindingsConfig {
pub mode: String,
#[serde(default)]
pub custom: HashMap<String, String>,
}
impl Default for ShellConfig {
fn default() -> Self {
Self {
shell: ShellSettings::default(),
colors: ColorConfig::default(),
prompt: PromptConfig::default(),
ai: AiConfig::default(),
history: HistoryConfig::default(),
editor: EditorConfig::default(),
keybindings: KeybindingsConfig::default(),
aliases: HashMap::new(),
env: HashMap::new(),
}
}
}
impl Default for ShellSettings {
fn default() -> Self {
Self {
show_banner: true,
show_tips: true,
default_directory: String::new(),
vi_mode: false,
auto_cd: false,
glob_expansion: true,
command_correction: true,
bell_style: "none".to_string(),
}
}
}
impl Default for ColorConfig {
fn default() -> Self {
Self {
enabled: true,
theme: "catppuccin".to_string(),
force: false,
true_color: true,
custom: CustomColors::default(),
}
}
}
impl Default for CustomColors {
fn default() -> Self {
Self {
number: "#a6e3a1".to_string(), string: "#a6e3a1".to_string(), boolean: "#cba6f7".to_string(), keyword: "#cba6f7".to_string(), punctuation: "#89b4fa".to_string(), key: "#89dceb".to_string(), uri: "#f9e2af".to_string(), error: "#f38ba8".to_string(), warning: "#fab387".to_string(), success: "#a6e3a1".to_string(), dim: "#6c7086".to_string(), comment: "#6c7086".to_string(), }
}
}
impl Default for PromptConfig {
fn default() -> Self {
Self {
format: "{symbol}".to_string(), continuation: "… ".to_string(),
right: String::new(),
show_git: true,
show_time: true,
time_threshold_ms: 1000,
transient: false,
}
}
}
impl Default for AiConfig {
fn default() -> Self {
Self {
default_provider: "openai".to_string(),
default_model: "gpt-4o-mini".to_string(),
suggestions: false,
max_tokens: 4096,
temperature: 0.7,
streaming: true,
allowed_tools: Vec::new(),
blocked_tools: Vec::new(),
max_agent_steps: 10,
}
}
}
impl Default for HistoryConfig {
fn default() -> Self {
Self {
enabled: true,
max_size: 10000,
ignore_duplicates: true,
ignore_space: true,
ignore_patterns: vec![r"^exit$".to_string(), r"^quit$".to_string()],
share: false,
timestamps: true,
}
}
}
impl Default for EditorConfig {
fn default() -> Self {
Self {
external: std::env::var("EDITOR").unwrap_or_else(|_| {
if cfg!(windows) {
"notepad".to_string()
} else {
"vi".to_string()
}
}),
tab_width: 4,
syntax_highlighting: true,
line_numbers: true,
auto_indent: true,
bracket_matching: true,
}
}
}
impl Default for KeybindingsConfig {
fn default() -> Self {
Self {
mode: "emacs".to_string(),
custom: HashMap::new(),
}
}
}
impl ShellConfig {
pub fn config_dir() -> PathBuf {
if let Ok(path) = std::env::var("AETHER_CONFIG_HOME") {
return PathBuf::from(path);
}
dirs::config_dir()
.map(|p| p.join("aether"))
.unwrap_or_else(|| {
dirs::home_dir()
.map(|h| h.join(".config").join("aether"))
.unwrap_or_else(|| PathBuf::from(".config/aether"))
})
}
pub fn data_dir() -> PathBuf {
if let Ok(path) = std::env::var("AETHER_DATA_HOME") {
return PathBuf::from(path);
}
dirs::data_dir()
.map(|p| p.join("aether"))
.unwrap_or_else(|| {
dirs::home_dir()
.map(|h| h.join(".local").join("share").join("aether"))
.unwrap_or_else(|| PathBuf::from(".local/share/aether"))
})
}
pub fn cache_dir() -> PathBuf {
if let Ok(path) = std::env::var("AETHER_CACHE_HOME") {
return PathBuf::from(path);
}
dirs::cache_dir()
.map(|p| p.join("aether"))
.unwrap_or_else(|| {
dirs::home_dir()
.map(|h| h.join(".cache").join("aether"))
.unwrap_or_else(|| PathBuf::from(".cache/aether"))
})
}
pub fn config_file() -> PathBuf {
if let Ok(path) = std::env::var("AETHER_CONFIG") {
return PathBuf::from(path);
}
let xdg_config = Self::config_dir().join("config.toml");
if xdg_config.exists() {
return xdg_config;
}
if let Some(home) = dirs::home_dir() {
let legacy = home.join(".aetherrc");
if legacy.exists() {
return legacy;
}
}
xdg_config
}
pub fn init_script() -> PathBuf {
Self::config_dir().join("init.ae")
}
pub fn history_file() -> PathBuf {
Self::data_dir().join("history")
}
pub fn plugins_dir() -> PathBuf {
Self::data_dir().join("plugins")
}
pub fn themes_dir() -> PathBuf {
Self::config_dir().join("themes")
}
pub fn load() -> Result<Self> {
let config_file = Self::config_file();
if !config_file.exists() {
return Ok(Self::default());
}
let content = fs::read_to_string(&config_file)
.with_context(|| format!("Failed to read config file: {:?}", config_file))?;
if config_file
.extension()
.map(|e| e == "toml")
.unwrap_or(false)
|| content.trim().starts_with('[')
|| content.contains(" = ")
{
toml::from_str(&content)
.with_context(|| format!("Failed to parse config file: {:?}", config_file))
} else {
Self::parse_legacy(&content)
}
}
pub fn save(&self) -> Result<()> {
let config_dir = Self::config_dir();
fs::create_dir_all(&config_dir)?;
let config_file = config_dir.join("config.toml");
let content = toml::to_string_pretty(self)?;
fs::write(&config_file, content)?;
Ok(())
}
pub fn init_dirs() -> Result<()> {
fs::create_dir_all(Self::config_dir())?;
fs::create_dir_all(Self::data_dir())?;
fs::create_dir_all(Self::cache_dir())?;
fs::create_dir_all(Self::plugins_dir())?;
fs::create_dir_all(Self::themes_dir())?;
Ok(())
}
pub fn generate_default_config() -> String {
r##"# AetherShell Configuration
# Location: ~/.config/aether/config.toml
# Documentation: https://github.com/nervosys/AetherShell#configuration
[shell]
# Show welcome banner on startup
show_banner = true
# Show helpful tips
show_tips = true
# Default directory (empty = current directory)
default_directory = ""
# Enable vi editing mode (false = emacs mode)
vi_mode = false
# Treat directory names as cd commands
auto_cd = false
# Enable glob pattern expansion
glob_expansion = true
# Suggest corrections for typos
command_correction = true
# Bell style: "none", "visible", "audible"
bell_style = "none"
[colors]
# Enable colored output
enabled = true
# Theme: "catppuccin", "monokai", "dracula", "nord", "gruvbox", "solarized", "custom"
theme = "catppuccin"
# Force colors even when output is not a TTY
force = false
# Enable 24-bit true color
true_color = true
# Custom colors (only used when theme = "custom")
[colors.custom]
number = "#a6e3a1"
string = "#a6e3a1"
boolean = "#cba6f7"
keyword = "#cba6f7"
punctuation = "#89b4fa"
key = "#89dceb"
uri = "#f9e2af"
error = "#f38ba8"
warning = "#fab387"
success = "#a6e3a1"
dim = "#6c7086"
comment = "#6c7086"
[prompt]
# Prompt format - placeholders: {symbol}, {cwd}, {user}, {host}, {git_branch}, {time}, {status}
format = "{symbol}"
# Continuation prompt for multi-line input
continuation = "... "
# Right-side prompt (optional)
right = ""
# Show git branch in prompt
show_git = true
# Show execution time for commands
show_time = true
# Only show time if command took longer than this (ms)
time_threshold_ms = 1000
# Clear previous prompts when entering new command
transient = false
[ai]
# Default AI provider
default_provider = "openai"
# Default model
default_model = "gpt-4o-mini"
# Enable AI-powered suggestions
suggestions = false
# Maximum tokens for responses
max_tokens = 4096
# Temperature (0.0 = deterministic, 2.0 = creative)
temperature = 0.7
# Stream responses as they generate
streaming = true
# Tools agents are allowed to use (empty = all)
allowed_tools = []
# Tools agents are blocked from using
blocked_tools = []
# Maximum steps before agent timeout
max_agent_steps = 10
[history]
# Enable command history
enabled = true
# Maximum history entries
max_size = 10000
# Don't save duplicate consecutive commands
ignore_duplicates = true
# Ignore commands starting with a space
ignore_space = true
# Regex patterns to ignore
ignore_patterns = ["^exit$", "^quit$"]
# Share history between shell sessions
share = false
# Save timestamps with history
timestamps = true
[editor]
# External editor command
external = ""
# Tab display width
tab_width = 4
# Enable syntax highlighting
syntax_highlighting = true
# Show line numbers
line_numbers = true
# Auto-indent new lines
auto_indent = true
# Highlight matching brackets
bracket_matching = true
[keybindings]
# Mode: "emacs" or "vi"
mode = "emacs"
[keybindings.custom]
# Custom keybindings (key = action)
# Example: ctrl-r = "history_search"
[aliases]
# Command aliases
# Example: ll = "ls -la"
[env]
# Environment variables to set on startup
# Example: EDITOR = "nvim"
"##
.to_string()
}
fn parse_legacy(content: &str) -> Result<Self> {
let mut config = Self::default();
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim().trim_matches('"').trim_matches('\'');
match key {
"color" | "colors" => config.colors.enabled = value.parse().unwrap_or(true),
"theme" => config.colors.theme = value.to_string(),
"vi_mode" | "vi-mode" => config.shell.vi_mode = value.parse().unwrap_or(false),
"auto_cd" | "auto-cd" => config.shell.auto_cd = value.parse().unwrap_or(false),
"banner" | "show_banner" => {
config.shell.show_banner = value.parse().unwrap_or(true)
}
"history_size" | "history-size" => {
config.history.max_size = value.parse().unwrap_or(10000)
}
"editor" | "EDITOR" => config.editor.external = value.to_string(),
"ai_model" | "model" => config.ai.default_model = value.to_string(),
"ai_provider" | "provider" => config.ai.default_provider = value.to_string(),
_ => {} }
}
}
Ok(config)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Theme {
Catppuccin,
CatppuccinLatte,
Monokai,
Dracula,
Nord,
Gruvbox,
GruvboxLight,
Solarized,
SolarizedLight,
TokyoNight,
TokyoNightStorm,
TokyoNightLight,
OneDark,
OneLight,
Material,
MaterialOcean,
Palenight,
Ayu,
AyuLight,
AyuMirage,
Synthwave84,
Cyberpunk,
Everforest,
EverforestLight,
Kanagawa,
RosePine,
RosePineMoon,
RosePineDawn,
Nightfox,
Dawnfox,
Github,
GithubLight,
Cobalt2,
Horizon,
Spacegray,
Atom,
Sublime,
VsCode,
Custom,
}
impl Theme {
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().replace(['-', '_'], "").as_str() {
"catppuccin" | "catppuccinmocha" => Theme::Catppuccin,
"catppuccinlatte" => Theme::CatppuccinLatte,
"monokai" | "monokaiclassic" => Theme::Monokai,
"dracula" => Theme::Dracula,
"nord" => Theme::Nord,
"gruvbox" | "gruvboxdark" => Theme::Gruvbox,
"gruvboxlight" => Theme::GruvboxLight,
"solarized" | "solarizeddark" => Theme::Solarized,
"solarizedlight" => Theme::SolarizedLight,
"tokyonight" | "tokyonightnight" => Theme::TokyoNight,
"tokyonightstorm" => Theme::TokyoNightStorm,
"tokyonightlight" | "tokyonightday" => Theme::TokyoNightLight,
"onedark" | "atomonedark" => Theme::OneDark,
"onelight" | "atomonelight" => Theme::OneLight,
"material" | "materialdark" => Theme::Material,
"materialocean" => Theme::MaterialOcean,
"palenight" | "materialpalenight" => Theme::Palenight,
"ayu" | "ayudark" => Theme::Ayu,
"ayulight" => Theme::AyuLight,
"ayumirage" => Theme::AyuMirage,
"synthwave" | "synthwave84" => Theme::Synthwave84,
"cyberpunk" | "neon" => Theme::Cyberpunk,
"everforest" | "everforestdark" => Theme::Everforest,
"everforestlight" => Theme::EverforestLight,
"kanagawa" | "kanagawawave" => Theme::Kanagawa,
"rosepine" | "rosepinedark" => Theme::RosePine,
"rosepinemoon" => Theme::RosePineMoon,
"rosepinedawn" => Theme::RosePineDawn,
"nightfox" => Theme::Nightfox,
"dawnfox" => Theme::Dawnfox,
"github" | "githubdark" => Theme::Github,
"githublight" => Theme::GithubLight,
"cobalt" | "cobalt2" => Theme::Cobalt2,
"horizon" => Theme::Horizon,
"spacegray" => Theme::Spacegray,
"atom" => Theme::Atom,
"sublime" | "sublimetext" => Theme::Sublime,
"vscode" | "vscodedark" | "darkplus" => Theme::VsCode,
"custom" => Theme::Custom,
_ => Theme::Catppuccin, }
}
pub fn list() -> Vec<&'static str> {
vec![
"catppuccin",
"catppuccin-latte",
"monokai",
"dracula",
"nord",
"gruvbox",
"gruvbox-light",
"solarized",
"solarized-light",
"tokyo-night",
"tokyo-night-storm",
"tokyo-night-light",
"one-dark",
"one-light",
"material",
"material-ocean",
"palenight",
"ayu",
"ayu-light",
"ayu-mirage",
"synthwave84",
"cyberpunk",
"everforest",
"everforest-light",
"kanagawa",
"rose-pine",
"rose-pine-moon",
"rose-pine-dawn",
"nightfox",
"dawnfox",
"github",
"github-light",
"cobalt2",
"horizon",
"spacegray",
"atom",
"sublime",
"vscode",
]
}
pub fn colors(&self) -> CustomColors {
match self {
Theme::Catppuccin => CustomColors::default(),
Theme::CatppuccinLatte => CustomColors {
number: "#40a02b".to_string(), string: "#40a02b".to_string(), boolean: "#8839ef".to_string(), keyword: "#8839ef".to_string(), punctuation: "#1e66f5".to_string(), key: "#04a5e5".to_string(), uri: "#df8e1d".to_string(), error: "#d20f39".to_string(), warning: "#fe640b".to_string(), success: "#40a02b".to_string(), dim: "#9ca0b0".to_string(), comment: "#9ca0b0".to_string(),
},
Theme::Monokai => CustomColors {
number: "#ae81ff".to_string(),
string: "#e6db74".to_string(),
boolean: "#ae81ff".to_string(),
keyword: "#f92672".to_string(),
punctuation: "#f8f8f2".to_string(),
key: "#66d9ef".to_string(),
uri: "#e6db74".to_string(),
error: "#f92672".to_string(),
warning: "#fd971f".to_string(),
success: "#a6e22e".to_string(),
dim: "#75715e".to_string(),
comment: "#75715e".to_string(),
},
Theme::Dracula => CustomColors {
number: "#bd93f9".to_string(),
string: "#f1fa8c".to_string(),
boolean: "#bd93f9".to_string(),
keyword: "#ff79c6".to_string(),
punctuation: "#f8f8f2".to_string(),
key: "#8be9fd".to_string(),
uri: "#f1fa8c".to_string(),
error: "#ff5555".to_string(),
warning: "#ffb86c".to_string(),
success: "#50fa7b".to_string(),
dim: "#6272a4".to_string(),
comment: "#6272a4".to_string(),
},
Theme::Nord => CustomColors {
number: "#b48ead".to_string(),
string: "#a3be8c".to_string(),
boolean: "#b48ead".to_string(),
keyword: "#81a1c1".to_string(),
punctuation: "#d8dee9".to_string(),
key: "#88c0d0".to_string(),
uri: "#ebcb8b".to_string(),
error: "#bf616a".to_string(),
warning: "#d08770".to_string(),
success: "#a3be8c".to_string(),
dim: "#4c566a".to_string(),
comment: "#4c566a".to_string(),
},
Theme::Gruvbox => CustomColors {
number: "#d3869b".to_string(),
string: "#b8bb26".to_string(),
boolean: "#d3869b".to_string(),
keyword: "#fb4934".to_string(),
punctuation: "#ebdbb2".to_string(),
key: "#83a598".to_string(),
uri: "#fabd2f".to_string(),
error: "#fb4934".to_string(),
warning: "#fe8019".to_string(),
success: "#b8bb26".to_string(),
dim: "#928374".to_string(),
comment: "#928374".to_string(),
},
Theme::GruvboxLight => CustomColors {
number: "#8f3f71".to_string(),
string: "#79740e".to_string(),
boolean: "#8f3f71".to_string(),
keyword: "#9d0006".to_string(),
punctuation: "#3c3836".to_string(),
key: "#076678".to_string(),
uri: "#b57614".to_string(),
error: "#9d0006".to_string(),
warning: "#af3a03".to_string(),
success: "#79740e".to_string(),
dim: "#928374".to_string(),
comment: "#928374".to_string(),
},
Theme::Solarized => CustomColors {
number: "#d33682".to_string(),
string: "#2aa198".to_string(),
boolean: "#d33682".to_string(),
keyword: "#859900".to_string(),
punctuation: "#839496".to_string(),
key: "#268bd2".to_string(),
uri: "#b58900".to_string(),
error: "#dc322f".to_string(),
warning: "#cb4b16".to_string(),
success: "#859900".to_string(),
dim: "#586e75".to_string(),
comment: "#586e75".to_string(),
},
Theme::SolarizedLight => CustomColors {
number: "#d33682".to_string(),
string: "#2aa198".to_string(),
boolean: "#d33682".to_string(),
keyword: "#859900".to_string(),
punctuation: "#657b83".to_string(),
key: "#268bd2".to_string(),
uri: "#b58900".to_string(),
error: "#dc322f".to_string(),
warning: "#cb4b16".to_string(),
success: "#859900".to_string(),
dim: "#93a1a1".to_string(),
comment: "#93a1a1".to_string(),
},
Theme::TokyoNight => CustomColors {
number: "#ff9e64".to_string(),
string: "#9ece6a".to_string(),
boolean: "#ff9e64".to_string(),
keyword: "#bb9af7".to_string(),
punctuation: "#c0caf5".to_string(),
key: "#7dcfff".to_string(),
uri: "#e0af68".to_string(),
error: "#f7768e".to_string(),
warning: "#e0af68".to_string(),
success: "#9ece6a".to_string(),
dim: "#565f89".to_string(),
comment: "#565f89".to_string(),
},
Theme::TokyoNightStorm => CustomColors {
number: "#ff9e64".to_string(),
string: "#9ece6a".to_string(),
boolean: "#ff9e64".to_string(),
keyword: "#bb9af7".to_string(),
punctuation: "#a9b1d6".to_string(),
key: "#7dcfff".to_string(),
uri: "#e0af68".to_string(),
error: "#f7768e".to_string(),
warning: "#e0af68".to_string(),
success: "#9ece6a".to_string(),
dim: "#565f89".to_string(),
comment: "#565f89".to_string(),
},
Theme::TokyoNightLight => CustomColors {
number: "#965027".to_string(),
string: "#485e30".to_string(),
boolean: "#965027".to_string(),
keyword: "#7847bd".to_string(),
punctuation: "#343b58".to_string(),
key: "#0f4b6e".to_string(),
uri: "#8c6c3e".to_string(),
error: "#8c4351".to_string(),
warning: "#8c6c3e".to_string(),
success: "#485e30".to_string(),
dim: "#9699a3".to_string(),
comment: "#9699a3".to_string(),
},
Theme::OneDark => CustomColors {
number: "#d19a66".to_string(),
string: "#98c379".to_string(),
boolean: "#d19a66".to_string(),
keyword: "#c678dd".to_string(),
punctuation: "#abb2bf".to_string(),
key: "#56b6c2".to_string(),
uri: "#e5c07b".to_string(),
error: "#e06c75".to_string(),
warning: "#e5c07b".to_string(),
success: "#98c379".to_string(),
dim: "#5c6370".to_string(),
comment: "#5c6370".to_string(),
},
Theme::OneLight => CustomColors {
number: "#986801".to_string(),
string: "#50a14f".to_string(),
boolean: "#986801".to_string(),
keyword: "#a626a4".to_string(),
punctuation: "#383a42".to_string(),
key: "#0184bc".to_string(),
uri: "#c18401".to_string(),
error: "#e45649".to_string(),
warning: "#c18401".to_string(),
success: "#50a14f".to_string(),
dim: "#a0a1a7".to_string(),
comment: "#a0a1a7".to_string(),
},
Theme::Material => CustomColors {
number: "#f78c6c".to_string(),
string: "#c3e88d".to_string(),
boolean: "#f78c6c".to_string(),
keyword: "#c792ea".to_string(),
punctuation: "#eeffff".to_string(),
key: "#89ddff".to_string(),
uri: "#ffcb6b".to_string(),
error: "#ff5370".to_string(),
warning: "#ffcb6b".to_string(),
success: "#c3e88d".to_string(),
dim: "#546e7a".to_string(),
comment: "#546e7a".to_string(),
},
Theme::MaterialOcean => CustomColors {
number: "#f78c6c".to_string(),
string: "#c3e88d".to_string(),
boolean: "#f78c6c".to_string(),
keyword: "#c792ea".to_string(),
punctuation: "#a6accd".to_string(),
key: "#89ddff".to_string(),
uri: "#ffcb6b".to_string(),
error: "#ff5370".to_string(),
warning: "#ffcb6b".to_string(),
success: "#c3e88d".to_string(),
dim: "#464b5d".to_string(),
comment: "#464b5d".to_string(),
},
Theme::Palenight => CustomColors {
number: "#f78c6c".to_string(),
string: "#c3e88d".to_string(),
boolean: "#f78c6c".to_string(),
keyword: "#c792ea".to_string(),
punctuation: "#a6accd".to_string(),
key: "#82aaff".to_string(),
uri: "#ffcb6b".to_string(),
error: "#ff5370".to_string(),
warning: "#ffcb6b".to_string(),
success: "#c3e88d".to_string(),
dim: "#676e95".to_string(),
comment: "#676e95".to_string(),
},
Theme::Ayu => CustomColors {
number: "#e6b450".to_string(),
string: "#aad94c".to_string(),
boolean: "#e6b450".to_string(),
keyword: "#ff8f40".to_string(),
punctuation: "#bfbdb6".to_string(),
key: "#59c2ff".to_string(),
uri: "#ffb454".to_string(),
error: "#d95757".to_string(),
warning: "#ffb454".to_string(),
success: "#aad94c".to_string(),
dim: "#636a72".to_string(),
comment: "#636a72".to_string(),
},
Theme::AyuLight => CustomColors {
number: "#ff9940".to_string(),
string: "#86b300".to_string(),
boolean: "#ff9940".to_string(),
keyword: "#fa8d3e".to_string(),
punctuation: "#5c6166".to_string(),
key: "#399ee6".to_string(),
uri: "#f2ae49".to_string(),
error: "#e65050".to_string(),
warning: "#f2ae49".to_string(),
success: "#86b300".to_string(),
dim: "#8a9199".to_string(),
comment: "#8a9199".to_string(),
},
Theme::AyuMirage => CustomColors {
number: "#ffcc66".to_string(),
string: "#d5ff80".to_string(),
boolean: "#ffcc66".to_string(),
keyword: "#ffae57".to_string(),
punctuation: "#cbccc6".to_string(),
key: "#73d0ff".to_string(),
uri: "#ffd580".to_string(),
error: "#ff6666".to_string(),
warning: "#ffd580".to_string(),
success: "#d5ff80".to_string(),
dim: "#5c6773".to_string(),
comment: "#5c6773".to_string(),
},
Theme::Synthwave84 => CustomColors {
number: "#f97e72".to_string(),
string: "#ff8b39".to_string(),
boolean: "#f97e72".to_string(),
keyword: "#fede5d".to_string(),
punctuation: "#ffffff".to_string(),
key: "#36f9f6".to_string(),
uri: "#ff7edb".to_string(),
error: "#fe4450".to_string(),
warning: "#fede5d".to_string(),
success: "#72f1b8".to_string(),
dim: "#848bbd".to_string(),
comment: "#848bbd".to_string(),
},
Theme::Cyberpunk => CustomColors {
number: "#ff00ff".to_string(), string: "#00ffff".to_string(), boolean: "#ff00ff".to_string(),
keyword: "#ffff00".to_string(), punctuation: "#ffffff".to_string(),
key: "#00ff00".to_string(), uri: "#ff69b4".to_string(), error: "#ff0000".to_string(), warning: "#ffa500".to_string(), success: "#00ff00".to_string(),
dim: "#808080".to_string(),
comment: "#808080".to_string(),
},
Theme::Everforest => CustomColors {
number: "#d699b6".to_string(),
string: "#a7c080".to_string(),
boolean: "#d699b6".to_string(),
keyword: "#e67e80".to_string(),
punctuation: "#d3c6aa".to_string(),
key: "#7fbbb3".to_string(),
uri: "#dbbc7f".to_string(),
error: "#e67e80".to_string(),
warning: "#e69875".to_string(),
success: "#a7c080".to_string(),
dim: "#859289".to_string(),
comment: "#859289".to_string(),
},
Theme::EverforestLight => CustomColors {
number: "#df69ba".to_string(),
string: "#8da101".to_string(),
boolean: "#df69ba".to_string(),
keyword: "#f85552".to_string(),
punctuation: "#5c6a72".to_string(),
key: "#35a77c".to_string(),
uri: "#dfa000".to_string(),
error: "#f85552".to_string(),
warning: "#f57d26".to_string(),
success: "#8da101".to_string(),
dim: "#939f91".to_string(),
comment: "#939f91".to_string(),
},
Theme::Kanagawa => CustomColors {
number: "#d27e99".to_string(),
string: "#98bb6c".to_string(),
boolean: "#d27e99".to_string(),
keyword: "#957fb8".to_string(),
punctuation: "#dcd7ba".to_string(),
key: "#7e9cd8".to_string(),
uri: "#e6c384".to_string(),
error: "#c34043".to_string(),
warning: "#ff9e3b".to_string(),
success: "#98bb6c".to_string(),
dim: "#727169".to_string(),
comment: "#727169".to_string(),
},
Theme::RosePine => CustomColors {
number: "#ebbcba".to_string(),
string: "#f6c177".to_string(),
boolean: "#ebbcba".to_string(),
keyword: "#c4a7e7".to_string(),
punctuation: "#e0def4".to_string(),
key: "#9ccfd8".to_string(),
uri: "#f6c177".to_string(),
error: "#eb6f92".to_string(),
warning: "#f6c177".to_string(),
success: "#31748f".to_string(),
dim: "#6e6a86".to_string(),
comment: "#6e6a86".to_string(),
},
Theme::RosePineMoon => CustomColors {
number: "#ea9a97".to_string(),
string: "#f6c177".to_string(),
boolean: "#ea9a97".to_string(),
keyword: "#c4a7e7".to_string(),
punctuation: "#e0def4".to_string(),
key: "#9ccfd8".to_string(),
uri: "#f6c177".to_string(),
error: "#eb6f92".to_string(),
warning: "#f6c177".to_string(),
success: "#3e8fb0".to_string(),
dim: "#6e6a86".to_string(),
comment: "#6e6a86".to_string(),
},
Theme::RosePineDawn => CustomColors {
number: "#d7827e".to_string(),
string: "#ea9d34".to_string(),
boolean: "#d7827e".to_string(),
keyword: "#907aa9".to_string(),
punctuation: "#575279".to_string(),
key: "#56949f".to_string(),
uri: "#ea9d34".to_string(),
error: "#b4637a".to_string(),
warning: "#ea9d34".to_string(),
success: "#286983".to_string(),
dim: "#9893a5".to_string(),
comment: "#9893a5".to_string(),
},
Theme::Nightfox => CustomColors {
number: "#f4a261".to_string(),
string: "#81b29a".to_string(),
boolean: "#f4a261".to_string(),
keyword: "#9d79d6".to_string(),
punctuation: "#cdcecf".to_string(),
key: "#63cdcf".to_string(),
uri: "#dbc074".to_string(),
error: "#c94f6d".to_string(),
warning: "#dbc074".to_string(),
success: "#81b29a".to_string(),
dim: "#738091".to_string(),
comment: "#738091".to_string(),
},
Theme::Dawnfox => CustomColors {
number: "#b95d76".to_string(),
string: "#618774".to_string(),
boolean: "#b95d76".to_string(),
keyword: "#806e9c".to_string(),
punctuation: "#575279".to_string(),
key: "#597b8c".to_string(),
uri: "#b79a3e".to_string(),
error: "#9d4059".to_string(),
warning: "#b79a3e".to_string(),
success: "#618774".to_string(),
dim: "#898b93".to_string(),
comment: "#898b93".to_string(),
},
Theme::Github => CustomColors {
number: "#79c0ff".to_string(),
string: "#a5d6ff".to_string(),
boolean: "#79c0ff".to_string(),
keyword: "#ff7b72".to_string(),
punctuation: "#c9d1d9".to_string(),
key: "#7ee787".to_string(),
uri: "#a5d6ff".to_string(),
error: "#ff7b72".to_string(),
warning: "#d29922".to_string(),
success: "#7ee787".to_string(),
dim: "#8b949e".to_string(),
comment: "#8b949e".to_string(),
},
Theme::GithubLight => CustomColors {
number: "#0550ae".to_string(),
string: "#0a3069".to_string(),
boolean: "#0550ae".to_string(),
keyword: "#cf222e".to_string(),
punctuation: "#24292f".to_string(),
key: "#116329".to_string(),
uri: "#0a3069".to_string(),
error: "#cf222e".to_string(),
warning: "#9a6700".to_string(),
success: "#116329".to_string(),
dim: "#6e7781".to_string(),
comment: "#6e7781".to_string(),
},
Theme::Cobalt2 => CustomColors {
number: "#ff628c".to_string(),
string: "#a5ff90".to_string(),
boolean: "#ff628c".to_string(),
keyword: "#ff9d00".to_string(),
punctuation: "#ffffff".to_string(),
key: "#9effff".to_string(),
uri: "#ffc600".to_string(),
error: "#ff628c".to_string(),
warning: "#ffc600".to_string(),
success: "#a5ff90".to_string(),
dim: "#0088ff".to_string(),
comment: "#0088ff".to_string(),
},
Theme::Horizon => CustomColors {
number: "#f09383".to_string(),
string: "#fab795".to_string(),
boolean: "#f09383".to_string(),
keyword: "#ee64ae".to_string(),
punctuation: "#e0e0e0".to_string(),
key: "#25b0bc".to_string(),
uri: "#fac29a".to_string(),
error: "#e95678".to_string(),
warning: "#fab795".to_string(),
success: "#29d398".to_string(),
dim: "#6c6f93".to_string(),
comment: "#6c6f93".to_string(),
},
Theme::Spacegray => CustomColors {
number: "#a78cfa".to_string(),
string: "#99ffc4".to_string(),
boolean: "#a78cfa".to_string(),
keyword: "#ff6e6e".to_string(),
punctuation: "#ffffff".to_string(),
key: "#6eb4ff".to_string(),
uri: "#ffffa5".to_string(),
error: "#ff6e6e".to_string(),
warning: "#ffffa5".to_string(),
success: "#99ffc4".to_string(),
dim: "#767b8c".to_string(),
comment: "#767b8c".to_string(),
},
Theme::Atom => CustomColors {
number: "#d19a66".to_string(),
string: "#98c379".to_string(),
boolean: "#d19a66".to_string(),
keyword: "#c678dd".to_string(),
punctuation: "#abb2bf".to_string(),
key: "#61afef".to_string(),
uri: "#e5c07b".to_string(),
error: "#e06c75".to_string(),
warning: "#e5c07b".to_string(),
success: "#98c379".to_string(),
dim: "#5c6370".to_string(),
comment: "#5c6370".to_string(),
},
Theme::Sublime => CustomColors {
number: "#f9ae58".to_string(),
string: "#99c794".to_string(),
boolean: "#f9ae58".to_string(),
keyword: "#c695c6".to_string(),
punctuation: "#d8dee9".to_string(),
key: "#6699cc".to_string(),
uri: "#fac761".to_string(),
error: "#ec5f66".to_string(),
warning: "#fac761".to_string(),
success: "#99c794".to_string(),
dim: "#a6acb9".to_string(),
comment: "#a6acb9".to_string(),
},
Theme::VsCode => CustomColors {
number: "#b5cea8".to_string(),
string: "#ce9178".to_string(),
boolean: "#569cd6".to_string(),
keyword: "#c586c0".to_string(),
punctuation: "#d4d4d4".to_string(),
key: "#9cdcfe".to_string(),
uri: "#ce9178".to_string(),
error: "#f14c4c".to_string(),
warning: "#cca700".to_string(),
success: "#89d185".to_string(),
dim: "#6a9955".to_string(),
comment: "#6a9955".to_string(),
},
Theme::Custom => CustomColors::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ShellConfig::default();
assert!(config.colors.enabled);
assert_eq!(config.colors.theme, "catppuccin");
assert!(config.history.enabled);
assert_eq!(config.history.max_size, 10000);
}
#[test]
fn test_theme_parsing() {
assert_eq!(Theme::from_str("catppuccin"), Theme::Catppuccin);
assert_eq!(Theme::from_str("DRACULA"), Theme::Dracula);
assert_eq!(Theme::from_str("unknown"), Theme::Catppuccin);
assert_eq!(Theme::from_str("tokyo-night"), Theme::TokyoNight);
assert_eq!(Theme::from_str("tokyo_night"), Theme::TokyoNight);
assert_eq!(Theme::from_str("tokyonight"), Theme::TokyoNight);
assert_eq!(Theme::from_str("one-dark"), Theme::OneDark);
assert_eq!(Theme::from_str("onedark"), Theme::OneDark);
assert_eq!(Theme::from_str("rose-pine"), Theme::RosePine);
assert_eq!(Theme::from_str("rosepine"), Theme::RosePine);
assert_eq!(Theme::from_str("kanagawa"), Theme::Kanagawa);
assert_eq!(Theme::from_str("material-ocean"), Theme::MaterialOcean);
assert_eq!(Theme::from_str("synthwave84"), Theme::Synthwave84);
assert_eq!(Theme::from_str("everforest"), Theme::Everforest);
assert_eq!(Theme::from_str("gruvbox-light"), Theme::GruvboxLight);
assert_eq!(Theme::from_str("catppuccin-latte"), Theme::CatppuccinLatte);
assert_eq!(Theme::from_str("github"), Theme::Github);
assert_eq!(Theme::from_str("vscode"), Theme::VsCode);
assert_eq!(Theme::from_str("cobalt2"), Theme::Cobalt2);
assert_eq!(Theme::from_str("nightfox"), Theme::Nightfox);
let themes = Theme::list();
assert!(themes.len() >= 38);
assert!(themes.contains(&"catppuccin"));
assert!(themes.contains(&"tokyo-night"));
assert!(themes.contains(&"rose-pine"));
}
#[test]
fn test_toml_roundtrip() {
let config = ShellConfig::default();
let toml_str = toml::to_string(&config).unwrap();
let parsed: ShellConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(config.colors.theme, parsed.colors.theme);
}
#[test]
fn test_legacy_parsing() {
let legacy = r#"
color = true
theme = monokai
vi_mode = true
history_size = 5000
"#;
let config = ShellConfig::parse_legacy(legacy).unwrap();
assert!(config.colors.enabled);
assert_eq!(config.colors.theme, "monokai");
assert!(config.shell.vi_mode);
assert_eq!(config.history.max_size, 5000);
}
#[test]
fn test_generate_default_config() {
let default_config = ShellConfig::generate_default_config();
assert!(default_config.contains("[shell]"));
assert!(default_config.contains("[colors]"));
assert!(default_config.contains("[prompt]"));
}
}