use std::{
fs,
path::{Path, PathBuf}
};
use serde::{Deserialize, Serialize};
use crate::error::TwcError;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum OutputPreference {
#[default]
Table,
Json,
Quiet
}
#[cfg(feature = "tui")]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DashboardPrefs {
#[serde(default)]
pub hidden_widgets: Vec<String>,
#[serde(default = "default_list_width")]
pub list_width_pct: u16,
#[serde(default)]
pub hide_empty_tabs: bool
}
#[cfg(feature = "tui")]
const fn default_list_width() -> u16 {
40
}
#[cfg(feature = "tui")]
impl Default for DashboardPrefs {
fn default() -> Self {
Self {
hidden_widgets: Vec::new(),
list_width_pct: default_list_width(),
hide_empty_tabs: false
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Language {
#[default]
En,
Ru
}
impl Language {
#[must_use]
pub const fn locale(self) -> &'static str {
match self {
Self::En => "en",
Self::Ru => "ru"
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(default)]
pub language: Language,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub profiles: std::collections::HashMap<String, String>,
#[cfg(feature = "tui")]
#[serde(default)]
pub theme: crate::tui::themes::Theme,
#[serde(default, alias = "output")]
pub output: OutputPreference,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_region: Option<String>,
#[serde(default = "default_refresh_interval")]
pub refresh_interval: u64,
#[cfg(feature = "tui")]
#[serde(default)]
pub dashboard: DashboardPrefs
}
const fn default_refresh_interval() -> u64 {
5
}
impl Default for AppConfig {
fn default() -> Self {
Self {
token: None,
profiles: std::collections::HashMap::new(),
language: Language::default(),
#[cfg(feature = "tui")]
theme: crate::tui::themes::Theme::default(),
output: OutputPreference::Table,
default_region: None,
refresh_interval: 5,
#[cfg(feature = "tui")]
dashboard: DashboardPrefs::default()
}
}
}
impl AppConfig {
pub fn token_for(&self, profile: Option<&str>) -> Result<Option<String>, TwcError> {
profile.map_or_else(
|| Ok(self.token.clone()),
|name| {
self.profiles.get(name).cloned().map(Some).ok_or_else(|| {
TwcError::ConfigNotFound(format!("profile '{name}' not found in config"))
})
}
)
}
pub fn path() -> Result<PathBuf, TwcError> {
let dir = dirs::config_dir().ok_or_else(|| {
TwcError::ConfigNotFound("unable to determine config directory".to_string())
})?;
Ok(dir.join("twc-rs").join("config.toml"))
}
pub fn load() -> Result<Self, TwcError> {
let path = Self::path()?;
if !path.exists() {
let cfg = Self::default();
cfg.save()?;
return Ok(cfg);
}
let content = fs::read_to_string(&path)
.map_err(|e| TwcError::ConfigNotFound(format!("{}: {e}", path.display())))?;
let config: Self = toml::from_str(&content)?;
Ok(config)
}
pub fn save(&self) -> Result<(), TwcError> {
self.save_to(&Self::path()?)
}
pub fn save_to(&self, path: &Path) -> Result<(), TwcError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
TwcError::ConfigWrite(format!("failed to create dir {}: {e}", parent.display()))
})?;
restrict_dir_permissions(parent)?;
}
let content = toml::to_string_pretty(self)?;
write_private(path, content.as_bytes())?;
Ok(())
}
}
#[cfg(unix)]
fn write_private(path: &Path, content: &[u8]) -> Result<(), TwcError> {
use std::{
io::Write,
os::unix::fs::{OpenOptionsExt, PermissionsExt}
};
let mut file = fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(path)
.map_err(|e| TwcError::ConfigWrite(format!("failed to write {}: {e}", path.display())))?;
file.set_permissions(fs::Permissions::from_mode(0o600))
.map_err(|e| {
TwcError::ConfigWrite(format!(
"failed to set permissions on {}: {e}",
path.display()
))
})?;
file.write_all(content)
.map_err(|e| TwcError::ConfigWrite(format!("failed to write {}: {e}", path.display())))?;
Ok(())
}
#[cfg(not(unix))]
fn write_private(path: &Path, content: &[u8]) -> Result<(), TwcError> {
fs::write(path, content)
.map_err(|e| TwcError::ConfigWrite(format!("failed to write {}: {e}", path.display())))
}
#[cfg(unix)]
fn restrict_dir_permissions(dir: &Path) -> Result<(), TwcError> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(dir, fs::Permissions::from_mode(0o700)).map_err(|e| {
TwcError::ConfigWrite(format!(
"failed to set permissions on {}: {e}",
dir.display()
))
})
}
#[cfg(not(unix))]
fn restrict_dir_permissions(_dir: &Path) -> Result<(), TwcError> {
Ok(())
}
#[cfg(test)]
mod tests;