use std::{
env, fs,
path::{Path, PathBuf},
};
use anyhow::Context;
use egui::{Color32, hex_color};
use notify::Watcher;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc::{self, UnboundedReceiver};
use wgpu::Backends;
#[derive(Deserialize, Debug, Clone, Copy)]
#[serde(try_from = "String")]
pub struct ColorConfig(Color32);
impl Into<Color32> for ColorConfig {
fn into(self) -> Color32 {
self.0
}
}
impl Serialize for ColorConfig {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let [r, g, b, a] = self.0.to_array();
let hex = format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a);
serializer.serialize_str(&hex)
}
}
impl TryFrom<String> for ColorConfig {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match Color32::from_hex(&s) {
Ok(color) => Ok(ColorConfig(color)),
Err(err) => Err(format!("{:?}", err)),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[serde(default)]
pub struct WindowConfig {
pub padding: u32,
pub border_radius: f32,
pub background: ColorConfig,
pub gap: [u32; 2],
pub max_width: u32,
}
impl Default for WindowConfig {
fn default() -> Self {
Self {
padding: 10,
border_radius: 6.0,
background: ColorConfig(hex_color!("#222222ee")),
gap: [10, 10],
max_width: 50,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[serde(default)]
pub struct ItemConfig {
pub padding: u32,
pub border_radius: f32,
pub border_width: u32,
pub border_color: ColorConfig,
pub hover_border_color: ColorConfig,
pub active_border_color: ColorConfig,
pub background: ColorConfig,
pub hover_background: ColorConfig,
pub active_background: ColorConfig,
pub icon_size: u32,
pub text_color: ColorConfig,
pub gap: [u32; 2],
}
impl Default for ItemConfig {
fn default() -> Self {
Self {
padding: 7,
border_radius: 6.0,
border_width: 2,
border_color: ColorConfig(hex_color!("#eeeeee00")),
hover_border_color: ColorConfig(hex_color!("#6f6f6f77")),
active_border_color: ColorConfig(hex_color!("#ccccccff")),
background: ColorConfig(hex_color!("#11111100")),
hover_background: ColorConfig(hex_color!("#11111144")),
active_background: ColorConfig(hex_color!("#11111144")),
icon_size: 18,
text_color: ColorConfig(hex_color!("#bbbbbb")),
gap: [7, 5],
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub enum RenderBackend {
Default,
Vulkan,
Gl,
Software,
}
impl Default for RenderBackend {
fn default() -> Self {
RenderBackend::Software
}
}
impl Into<Backends> for RenderBackend {
fn into(self) -> Backends {
match self {
RenderBackend::Gl => Backends::GL,
RenderBackend::Vulkan => Backends::VULKAN,
_ => Backends::default(),
}
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub render_backend: RenderBackend,
pub window: WindowConfig,
pub item: ItemConfig,
}
pub enum ConfigEvent {
Updated,
}
pub struct ConfigHandle {
config: Config,
path: Option<PathBuf>,
event_rx: UnboundedReceiver<ConfigEvent>,
}
impl ConfigHandle {
fn get_config_dir() -> Option<PathBuf> {
if let Ok(config_path) = env::var("XDG_CONFIG_HOME") {
PathBuf::from(format!("{}/alttabway", config_path)).into()
} else if let Ok(home_path) = env::var("HOME") {
PathBuf::from(format!("{}/.config/alttabway", home_path)).into()
} else {
return None;
}
}
fn get_existing_config() -> anyhow::Result<(Config, PathBuf)> {
let config_dir = Self::get_config_dir().context("Config file location could not be determined (requires XDG_CONFIG_HOME or HOME env variable to be set)")?;
let config_file = Path::new(&config_dir).join("alttabway.toml");
if !config_file.exists() {
let _ = fs::create_dir_all(config_dir);
let config = Config::default();
let serialized_config = toml::to_string_pretty(&config).unwrap();
fs::write(&config_file, serialized_config)?;
return Ok((config, config_file));
}
let config_str = fs::read_to_string(&config_file)?;
Ok((toml::from_str(&config_str)?, config_file))
}
pub fn new() -> Self {
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (config, config_path) = match Self::get_existing_config() {
Ok(result) => result,
Err(err) => {
tracing::warn!("Error using config file: {}", err);
return ConfigHandle {
config: Config::default(),
path: None,
event_rx,
};
}
};
let path = Some(config_path.clone());
tokio::spawn(async move {
use notify::{Event, EventKind, event::ModifyKind};
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = match notify::recommended_watcher(tx) {
Ok(watcher) => watcher,
Err(err) => {
tracing::warn!("Failed to watch config file! {}", err);
return;
}
};
if watcher
.watch(&config_path, notify::RecursiveMode::NonRecursive)
.is_err()
{
return;
};
let (async_tx, mut async_rx) = tokio::sync::mpsc::unbounded_channel();
std::thread::spawn(move || {
while let Ok(val) = rx.recv() {
if async_tx.send(val).is_err() {
break;
}
}
});
loop {
tokio::select! {
Some(event) = async_rx.recv() => {
if let Ok(Event {
kind: EventKind::Modify(ModifyKind::Data(_)),
..
}) = event
{
if event_tx.send(ConfigEvent::Updated).is_err() {
break;
}
}
}
_ = event_tx.closed() => break
}
}
});
Self {
config,
event_rx,
path,
}
}
pub fn get_config(&self) -> &Config {
&self.config
}
pub async fn recv(&mut self) -> Option<ConfigEvent> {
while let Some(event) = self.event_rx.recv().await {
match event {
ConfigEvent::Updated => {
if let Some(path) = &self.path
&& let Ok(config_str) = fs::read_to_string(&path)
{
match toml::from_str::<Config>(&config_str) {
Ok(new_config) => {
self.config = new_config;
return Some(event);
}
Err(err) => tracing::warn!("Failed to parsed updated config: {}", err),
}
}
}
}
}
None
}
pub fn requires_monitor_width(&self) -> bool {
self.config.window.max_width <= 100
}
}