use crate::cli::{ColorOption, ConfigAction, FormatOption}; use crate::error::{LsPlusError, Result}; use dirs; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub display: DisplayConfig,
pub behavior: BehaviorConfig,
pub icons: IconConfig,
pub colors: ColorConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayConfig {
pub default_format: FormatOption,
pub color_mode: ColorOption,
pub show_icons: bool,
pub human_readable_sizes: bool,
pub directories_first: bool,
pub show_hidden: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorConfig {
pub follow_symlinks: bool,
pub git_integration: bool,
pub max_depth: Option<usize>,
pub sort_by_time: bool,
pub reverse_sort: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IconConfig {
pub enable_icons: bool,
pub icon_theme: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorConfig {
pub file_colors: bool,
pub size_colors: bool,
pub time_colors: bool,
pub permission_colors: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
display: DisplayConfig {
default_format: FormatOption::Default, color_mode: ColorOption::Auto, show_icons: false, human_readable_sizes: true, directories_first: true, show_hidden: false, },
behavior: BehaviorConfig {
follow_symlinks: false, git_integration: false, max_depth: None, sort_by_time: false, reverse_sort: false, },
icons: IconConfig {
enable_icons: false, icon_theme: "default".to_string(), },
colors: ColorConfig {
file_colors: true, size_colors: true, time_colors: true, permission_colors: true, },
}
}
}
impl Config {
pub fn config_dir() -> Result<PathBuf> {
dirs::config_dir()
.map(|dir| dir.join("ls-plus")) .ok_or_else(|| LsPlusError::system_error("Could not determine config directory"))
}
pub fn config_file() -> Result<PathBuf> {
Ok(Self::config_dir()?.join("config.toml"))
}
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)
.map_err(|e| LsPlusError::system_error(format!("Failed to read config file: {e}")))?;
let config: Config = toml::from_str(&content)
.map_err(|e| LsPlusError::invalid_config(format!("Invalid config format: {e}")))?;
Ok(config)
}
pub fn save(&self) -> Result<()> {
let config_dir = Self::config_dir()?;
let config_file = Self::config_file()?;
if !config_dir.exists() {
fs::create_dir_all(&config_dir).map_err(|e| {
LsPlusError::system_error(format!("Failed to create config directory: {e}"))
})?;
}
let content = toml::to_string_pretty(self)
.map_err(|e| LsPlusError::system_error(format!("Failed to serialize config: {e}")))?;
fs::write(&config_file, content)
.map_err(|e| LsPlusError::system_error(format!("Failed to write config file: {e}")))?;
Ok(())
}
pub fn set_value(&mut self, key: &str, value: &str) -> Result<()> {
match key {
"display.default_format" => {
self.display.default_format = match value {
"default" => FormatOption::Default, "long" => FormatOption::Long, "tree" => FormatOption::Tree, "grid" => FormatOption::Grid, "json" => FormatOption::Json, _ => return Err(LsPlusError::invalid_config("Invalid format option")),
};
}
"display.color_mode" => {
self.display.color_mode = match value {
"always" => ColorOption::Always, "never" => ColorOption::Never, "auto" => ColorOption::Auto, _ => return Err(LsPlusError::invalid_config("Invalid color option")),
};
}
"display.show_icons" => {
self.display.show_icons = value
.parse()
.map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
}
"display.human_readable_sizes" => {
self.display.human_readable_sizes = value
.parse()
.map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
}
"display.directories_first" => {
self.display.directories_first = value
.parse()
.map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
}
"display.show_hidden" => {
self.display.show_hidden = value
.parse()
.map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
}
"behavior.follow_symlinks" => {
self.behavior.follow_symlinks = value
.parse()
.map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
}
"behavior.git_integration" => {
self.behavior.git_integration = value
.parse()
.map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
}
"behavior.sort_by_time" => {
self.behavior.sort_by_time = value
.parse()
.map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
}
"behavior.reverse_sort" => {
self.behavior.reverse_sort = value
.parse()
.map_err(|_| LsPlusError::invalid_config("Invalid boolean value"))?;
}
_ => {
return Err(LsPlusError::invalid_config(format!(
"Unknown config key: {key}"
)))
}
}
Ok(())
}
}
pub async fn handle_config_command(action: ConfigAction) -> Result<()> {
match action {
ConfigAction::Show => {
let config = Config::load()?;
let config_str = toml::to_string_pretty(&config).map_err(|e| {
LsPlusError::system_error(format!("Failed to serialize config: {e}"))
})?;
println!("{config_str}");
}
ConfigAction::Set { key, value } => {
let mut config = Config::load()?;
config.set_value(&key, &value)?;
config.save()?;
println!("Configuration updated: {key} = {value}");
}
ConfigAction::Reset => {
let config = Config::default();
config.save()?;
println!("Configuration reset to defaults");
}
ConfigAction::Edit => {
let config_file = Config::config_file()?;
if !config_file.exists() {
let default_config = Config::default();
default_config.save()?;
}
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
let status = std::process::Command::new(&editor)
.arg(&config_file)
.status()
.map_err(|e| LsPlusError::system_error(format!("Failed to open editor: {e}")))?;
if !status.success() {
return Err(LsPlusError::system_error("Editor exited with error"));
}
}
}
Ok(())
}