use std::{
env,
fmt::{self, Display, Formatter},
path::PathBuf,
sync::{LazyLock, OnceLock},
};
use color_eyre::eyre::Result;
use config::{Config, Environment, File};
use glob::glob;
use rat_theme3::{DarkTheme, Palette, SalsaTheme, ShellTheme};
use ratatui::style::Color;
use serde::{Deserialize, Serialize};
use crate::logging::{PROJECT_NAME, project_directory};
pub static THEME: OnceLock<&'static str> = OnceLock::new();
#[inline(always)]
fn init_theme(name: Box<str>) {
let static_str: &'static str = Box::leak(name);
THEME.set(static_str).unwrap();
}
#[inline(always)]
fn current_theme() -> &'static str {
THEME.get().unwrap()
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub(crate) struct Settings {
#[serde(default)]
pub(crate) debug: bool,
#[serde(default)]
pub(crate) silent: bool,
#[serde(default)]
pub(crate) theme: Theme,
}
pub static CONFIG_FOLDER: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
env::var(format!("{}_CONFIG", &*PROJECT_NAME))
.ok()
.map(PathBuf::from)
});
pub(crate) fn get_config_dir() -> PathBuf {
if let Some(s) = CONFIG_FOLDER.clone() {
s
} else if let Some(proj_dirs) = project_directory() {
proj_dirs.config_local_dir().to_path_buf()
} else {
PathBuf::from(".").join(".config")
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy, Default)]
#[allow(unused)]
pub enum DefaultTheme {
#[default]
ImperialDark,
RadiumDark,
TundraDark,
OceanDark,
MonochromeDark,
BlackWhiteDark,
Base16Dark,
Base16RelaxDark,
MonekaiDark,
SolarizedDark,
OxoCarbonDark,
RustDark,
VSCodeDark,
ImperialShell,
RadiumShell,
TundraShell,
OceanShell,
MonochromeShell,
BlackWhiteShell,
Base16Shell,
Base16RelaxShell,
MonekaiShell,
SolarizedShell,
OxoCarbonShell,
RustShell,
VSCodeShell,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum Theme {
Custom(Box<CustomTheme>),
Default(DefaultTheme),
}
impl Default for Theme {
fn default() -> Self {
Self::Default(DefaultTheme::default())
}
}
impl Display for DefaultTheme {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
type Theme = DefaultTheme;
let text = match self {
Theme::ImperialDark => "Imperial Dark",
Theme::RadiumDark => "Radium Dark",
Theme::TundraDark => "Tundra Dark",
Theme::OceanDark => "Ocean Dark",
Theme::MonochromeDark => "Monochrome Dark",
Theme::BlackWhiteDark => "Black & White Dark",
Theme::Base16Dark => "Base16 Dark",
Theme::Base16RelaxDark => "Base16 Relax Dark",
Theme::MonekaiDark => "Monekai Dark",
Theme::SolarizedDark => "Solarized Dark",
Theme::OxoCarbonDark => "OxoCarbon Dark",
Theme::RustDark => "Rust Dark",
Theme::VSCodeDark => "VSCode Dark",
Theme::ImperialShell => "Imperial Shell",
Theme::RadiumShell => "Radium Shell",
Theme::TundraShell => "Tundra Shell",
Theme::OceanShell => "Ocean Shell",
Theme::MonochromeShell => "Monochrome Shell",
Theme::BlackWhiteShell => "Black & White Shell",
Theme::Base16Shell => "Base16 Shell",
Theme::Base16RelaxShell => "Base16 Relax Shell",
Theme::MonekaiShell => "Monekai Shell",
Theme::SolarizedShell => "Solarized Shell",
Theme::OxoCarbonShell => "OxoCarbon Shell",
Theme::RustShell => "Rust Shell",
Theme::VSCodeShell => "VSCode Shell",
};
write!(f, "{}", text)
}
}
impl Settings {
pub(crate) fn new() -> Result<Self> {
let s = Config::builder()
.add_source(
glob(
&get_config_dir()
.join("config")
.join("*")
.display()
.to_string(),
)?
.map(|p| File::from(p.expect("Failed to read config file")))
.collect::<Vec<_>>(),
)
.add_source(
Environment::with_prefix(&PROJECT_NAME)
.separator("__")
.prefix_separator("_"),
);
let s = s.build()?;
Ok(s.try_deserialize().unwrap_or_default())
}
pub(crate) fn get_theme(&self) -> &Theme {
&self.theme
}
}
pub(crate) struct LoggingConfig {
silent: bool,
debug: bool,
}
impl LoggingConfig {
pub(crate) fn get_level(&self) -> tracing::Level {
if self.debug {
tracing::Level::DEBUG
} else if self.silent {
tracing::Level::ERROR
} else {
tracing::Level::INFO
}
}
}
impl From<&Settings> for LoggingConfig {
fn from(settings: &Settings) -> Self {
Self {
silent: settings.silent,
debug: settings.debug,
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub(crate) struct CustomTheme {
pub(crate) name: Box<str>,
pub(crate) palette: CustomPalette,
pub(crate) type_: ThemeType,
}
#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub enum ThemeType {
#[default]
Dark,
Shell,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct CustomPalette {
pub name: Box<str>,
pub text_light: Color,
pub text_bright: Color,
pub text_dark: Color,
pub text_black: Color,
pub white: [Color; 8],
pub black: [Color; 8],
pub gray: [Color; 8],
pub red: [Color; 8],
pub orange: [Color; 8],
pub yellow: [Color; 8],
pub limegreen: [Color; 8],
pub green: [Color; 8],
pub bluegreen: [Color; 8],
pub cyan: [Color; 8],
pub blue: [Color; 8],
pub deepblue: [Color; 8],
pub purple: [Color; 8],
pub magenta: [Color; 8],
pub redpink: [Color; 8],
pub primary: [Color; 8],
pub secondary: [Color; 8],
}
impl From<CustomPalette> for Palette {
fn from(palette: CustomPalette) -> Self {
init_theme(palette.name);
Self {
name: current_theme(),
text_light: palette.text_light,
text_bright: palette.text_bright,
text_dark: palette.text_dark,
text_black: palette.text_black,
white: palette.white,
black: palette.black,
gray: palette.gray,
red: palette.red,
orange: palette.orange,
yellow: palette.yellow,
limegreen: palette.limegreen,
green: palette.green,
bluegreen: palette.bluegreen,
cyan: palette.cyan,
blue: palette.blue,
deepblue: palette.deepblue,
purple: palette.purple,
magenta: palette.magenta,
redpink: palette.redpink,
primary: palette.primary,
secondary: palette.secondary,
}
}
}
impl From<CustomTheme> for Box<dyn SalsaTheme> {
fn from(theme: CustomTheme) -> Self {
let p = theme.palette.into();
match theme.type_ {
ThemeType::Dark => Box::new(DarkTheme::new(&theme.name, p)),
ThemeType::Shell => Box::new(ShellTheme::new(&theme.name, p)),
}
}
}
impl From<Box<CustomTheme>> for Box<dyn SalsaTheme> {
fn from(theme: Box<CustomTheme>) -> Self {
let p = theme.palette.into();
match theme.type_ {
ThemeType::Dark => Box::new(DarkTheme::new(&theme.name, p)),
ThemeType::Shell => Box::new(ShellTheme::new(&theme.name, p)),
}
}
}
impl From<Box<dyn SalsaTheme>> for CustomTheme {
fn from(theme: Box<dyn SalsaTheme>) -> Self {
let p = theme.palette();
if theme.name().contains("Shell") {
CustomTheme {
name: Box::from("Shell"),
palette: CustomPalette {
name: Box::from(p.name),
text_light: p.text_light,
text_bright: p.text_bright,
text_dark: p.text_dark,
text_black: p.text_black,
white: p.white,
black: p.black,
gray: p.gray,
red: p.red,
orange: p.orange,
yellow: p.yellow,
limegreen: p.limegreen,
green: p.green,
bluegreen: p.bluegreen,
cyan: p.cyan,
blue: p.blue,
deepblue: p.deepblue,
purple: p.purple,
magenta: p.magenta,
redpink: p.redpink,
primary: p.primary,
secondary: p.secondary,
},
type_: ThemeType::Shell,
}
} else {
CustomTheme {
name: Box::from("Dark"),
palette: CustomPalette {
name: Box::from(p.name),
text_light: p.text_light,
text_bright: p.text_bright,
text_dark: p.text_dark,
text_black: p.text_black,
white: p.white,
black: p.black,
gray: p.gray,
red: p.red,
orange: p.orange,
yellow: p.yellow,
limegreen: p.limegreen,
green: p.green,
bluegreen: p.bluegreen,
cyan: p.cyan,
blue: p.blue,
deepblue: p.deepblue,
purple: p.purple,
magenta: p.magenta,
redpink: p.redpink,
primary: p.primary,
secondary: p.secondary,
},
type_: ThemeType::Dark,
}
}
}
}
pub fn install_manpages() -> Result<()> {
use std::{env, fs, path::PathBuf};
#[cfg(target_os = "windows")]
{
eprintln!("Manpage installation is not supported on Windows.");
return Ok(());
}
#[cfg(not(target_os = "windows"))]
{
let prefix = env::var("PREFIX").unwrap_or("/usr/local".to_string());
let man1_dir = PathBuf::from(&prefix).join("share/man/man1");
let man5_dir = PathBuf::from(&prefix).join("share/man/man5");
fs::create_dir_all(&man1_dir)?;
fs::create_dir_all(&man5_dir)?;
let man1_file = format!("{}.1", &*PROJECT_NAME).to_lowercase();
let man5_file = format!("{}.5", &*PROJECT_NAME).to_lowercase();
let man1_contents = include_str!("../man/filessh.1");
let man5_contents = include_str!("../man/filessh.5");
fs::write(man1_dir.join(&man1_file), man1_contents)?;
fs::write(man5_dir.join(&man5_file), man5_contents)?;
println!("Installed manpages:");
println!(" {}/share/man/man1/{}", prefix, man1_file);
println!(" {}/share/man/man5/{}", prefix, man5_file);
Ok(())
}
}
pub fn init_config() -> Result<()> {
use std::fs;
println!("Generating default config file...");
let config_dir = get_config_dir().join("config");
if !config_dir.exists() {
fs::create_dir_all(&config_dir)?;
let config = Settings::new()?;
let toml = toml::to_string_pretty(&config)?;
fs::write(config_dir.join("default.toml"), toml)?;
println!(
"Config file generated at {}",
config_dir.join("default.toml").display()
);
} else {
println!("Config file already exists");
}
Ok(())
}