diff --git a/Cargo.lock b/Cargo.lock index e337d52..dc606dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,7 +653,7 @@ dependencies = [ [[package]] name = "sway-flash-indicator" -version = "0.1.0" +version = "0.1.1" dependencies = [ "directories", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index c911499..7385d8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sway-flash-indicator" -version = "0.1.0" +version = "0.1.1" edition = "2021" [dependencies] diff --git a/src/colour.rs b/src/colour.rs new file mode 100644 index 0000000..0304946 --- /dev/null +++ b/src/colour.rs @@ -0,0 +1,11 @@ +use lab::Lab; + +use crate::prelude::*; + +pub fn parse_hex(hex: &str) -> Res { + let hex = hex.strip_prefix('#').unwrap_or(hex); + let r = u8::from_str_radix(&hex[..2], 16).map_err(|_| Error::HexParse)?; + let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| Error::HexParse)?; + let b = u8::from_str_radix(&hex[4..], 16).map_err(|_| Error::HexParse)?; + Ok(Lab::from_rgb(&[r, g, b])) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..b8e72a4 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,68 @@ +use lab::Lab; +use serde::{Deserialize, Serialize}; + +use crate::prelude::*; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Config { + pub frames_delay: u32, + pub frames_anim: u32, + pub refresh_rate: u64, + #[serde(serialize_with = "lab_ser", deserialize_with = "lab_de")] + pub flash_colour: Lab, +} + +pub fn parse_config() -> Res<()> { + let dirs = directories::ProjectDirs::from("com", "alexjanka", "sway-flash-indicator") + .ok_or(Error::NoMatchingConfig)?; + + let config_dir = dirs.config_dir(); + if let Ok(false) = config_dir.try_exists() { + log::info!("config dir doesn't exist, creating"); + std::fs::create_dir_all(config_dir)?; + } + + let config_path = config_dir.join("config.toml"); + let config = if config_path.try_exists().is_ok_and(|v| v) { + let v = toml::from_str(&std::fs::read_to_string(&config_path)?)?; + log::info!("read config from {config_path:?}"); + v + } else { + let c = Config::default(); + let stringified = toml::to_string(&c)?; + std::fs::write(&config_path, stringified)?; + log::info!("wrote default to {config_path:?}"); + c + }; + + crate::CONFIG.set(config)?; + + Ok(()) +} + +impl Default for Config { + fn default() -> Self { + Self { + frames_delay: 10, + frames_anim: 20, + refresh_rate: 60, + flash_colour: Lab { + l: 53.2, + a: 80.1, + b: 67.2, + }, + } + } +} + +fn lab_ser(colour: &Lab, serializer: S) -> Result { + let [r, g, b] = colour.to_rgb(); + format!("#{r:02x}{g:02x}{b:02x}").serialize(serializer) +} + +fn lab_de<'a, D: serde::Deserializer<'a>>(deserializer: D) -> Result { + use serde::de::Error; + + let hex = String::deserialize(deserializer)?; + crate::colour::parse_hex(&hex).map_err(|_| D::Error::custom("couldn't parse hex")) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..696aea0 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,25 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("no matching config")] + NoMatchingConfig, + #[error("couldnt parse hex")] + HexParse, + #[error(transparent)] + Swayipc(#[from] swayipc_async::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("couldn't set cell")] + SetCell, + #[error(transparent)] + TomlDe(#[from] toml::de::Error), + #[error(transparent)] + TomlSer(#[from] toml::ser::Error), +} + +impl From> for Error { + fn from(_: tokio::sync::SetError) -> Self { + Self::SetCell + } +} + +pub type Res = Result; diff --git a/src/flash.rs b/src/flash.rs new file mode 100644 index 0000000..14fe262 --- /dev/null +++ b/src/flash.rs @@ -0,0 +1,48 @@ +use crate::prelude::*; + +pub async fn interpolate_task() -> Res<()> { + let mut connection = swayipc_async::Connection::new().await?; + + let to = crate::DEFAULT_BORDER.get().ok_or(Error::NoMatchingConfig)?; + let config = CONFIG.get().ok_or(Error::NoMatchingConfig)?; + let per_frame = 1.0 / config.frames_anim as f32; + let (d_l, d_a, d_b) = ( + (to.l - config.flash_colour.l) * per_frame, + (to.a - config.flash_colour.a) * per_frame, + (to.b - config.flash_colour.b) * per_frame, + ); + + let mut c = config.flash_colour; + connection.run_command(set_command(&c)?).await?; + + let dur_per_frame = std::time::Duration::from_secs_f64(1.0 / config.refresh_rate as f64); + + tokio::time::sleep(dur_per_frame * config.frames_delay).await; + for _ in 0..config.frames_anim { + c.l += d_l; + c.a += d_a; + c.b += d_b; + + connection.run_command(set_command(&c)?).await?; + + tokio::time::sleep(dur_per_frame).await; + } + connection + .run_command( + crate::DEFAULT_COLOURS + .get() + .ok_or(Error::NoMatchingConfig)?, + ) + .await?; + Ok(()) +} + +fn set_command(colour: &lab::Lab) -> Res { + let [r, g, b] = colour.to_rgb(); + Ok(format!( + "{} #{r:02x}{g:02x}{b:02x}", + crate::DEFAULT_COLOURS_NO_INDICATOR + .get() + .ok_or(Error::NoMatchingConfig)? + )) +} diff --git a/src/main.rs b/src/main.rs index 4c5f317..6b629ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,70 +1,20 @@ +use config::Config; use futures_util::StreamExt; use lab::Lab; -use serde::{Deserialize, Serialize}; -#[derive(thiserror::Error, Debug)] -enum Error { - #[error("no matching config")] - NoMatchingConfig, - #[error("couldnt parse hex")] - HexParse, - #[error(transparent)] - Swayipc(#[from] swayipc_async::Error), - #[error(transparent)] - Io(#[from] std::io::Error), - #[error("couldn't set cell")] - SetCell, - #[error(transparent)] - TomlDe(#[from] toml::de::Error), - #[error(transparent)] - TomlSer(#[from] toml::ser::Error), +mod colour; +mod config; +mod error; +mod flash; + +pub mod prelude { + pub use crate::error::*; + pub use crate::CONFIG; } -impl From> for Error { - fn from(_: tokio::sync::SetError) -> Self { - Self::SetCell - } -} +use prelude::*; -type Res = Result; - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -struct Config { - frames_delay: u32, - frames_anim: u32, - refresh_rate: u64, - #[serde(serialize_with = "lab_ser", deserialize_with = "lab_de")] - flash_colour: Lab, -} - -impl Default for Config { - fn default() -> Self { - Self { - frames_delay: 10, - frames_anim: 20, - refresh_rate: 60, - flash_colour: Lab { - l: 53.2, - a: 80.1, - b: 67.2, - }, - } - } -} - -fn lab_ser(colour: &Lab, serializer: S) -> Result { - let [r, g, b] = colour.to_rgb(); - format!("#{r:02x}{g:02x}{b:02x}").serialize(serializer) -} - -fn lab_de<'a, D: serde::Deserializer<'a>>(deserializer: D) -> Result { - use serde::de::Error; - - let hex = String::deserialize(deserializer)?; - parse_hex(&hex).map_err(|_| D::Error::custom("couldn't parse hex")) -} - -static CONFIG: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); +pub static CONFIG: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); static DEFAULT_COLOURS: tokio::sync::OnceCell = tokio::sync::OnceCell::const_new(); static DEFAULT_COLOURS_NO_INDICATOR: tokio::sync::OnceCell = @@ -78,7 +28,7 @@ async fn main() -> Res<()> { } pretty_env_logger::init(); - parse_config()?; + config::parse_config()?; let mut event_connection = swayipc_async::Connection::new().await?; @@ -99,7 +49,7 @@ async fn main() -> Res<()> { fut.abort(); } fut = Some(tokio::spawn(async { - if let Err(e) = interpolate_task().await { + if let Err(e) = flash::interpolate_task().await { log::warn!("error {e:?}"); } })); @@ -113,34 +63,6 @@ async fn main() -> Res<()> { Ok(()) } -fn parse_config() -> Res<()> { - let dirs = directories::ProjectDirs::from("com", "alexjanka", "sway-flash-indicator") - .ok_or(Error::NoMatchingConfig)?; - - let config_dir = dirs.config_dir(); - if let Ok(false) = config_dir.try_exists() { - log::info!("config dir doesn't exist, creating"); - std::fs::create_dir_all(config_dir)?; - } - - let config_path = config_dir.join("config.toml"); - let config = if config_path.try_exists().is_ok_and(|v| v) { - let v = toml::from_str(&std::fs::read_to_string(&config_path)?)?; - log::info!("read config from {config_path:?}"); - v - } else { - let c = Config::default(); - let stringified = toml::to_string(&c)?; - std::fs::write(&config_path, stringified)?; - log::info!("wrote default to {config_path:?}"); - c - }; - - CONFIG.set(config)?; - - Ok(()) -} - async fn get_sway_config(connection: &mut swayipc_async::Connection) -> Res<()> { let config = connection.get_config().await?; let default_colours = config @@ -154,7 +76,7 @@ async fn get_sway_config(connection: &mut swayipc_async::Connection) -> Res<()> .split_whitespace() .nth(2) .ok_or(Error::NoMatchingConfig)?; - DEFAULT_BORDER.set(parse_hex(default_border)?)?; + DEFAULT_BORDER.set(colour::parse_hex(default_border)?)?; DEFAULT_COLOURS.set(default_colours.to_string())?; let mut split = default_colours.split_whitespace().collect::>(); @@ -163,54 +85,3 @@ async fn get_sway_config(connection: &mut swayipc_async::Connection) -> Res<()> Ok(()) } - -fn parse_hex(hex: &str) -> Res { - let hex = hex.strip_prefix('#').unwrap_or(hex); - let r = u8::from_str_radix(&hex[..2], 16).map_err(|_| Error::HexParse)?; - let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| Error::HexParse)?; - let b = u8::from_str_radix(&hex[4..], 16).map_err(|_| Error::HexParse)?; - Ok(Lab::from_rgb(&[r, g, b])) -} - -async fn interpolate_task() -> Res<()> { - let mut connection = swayipc_async::Connection::new().await?; - - let to = DEFAULT_BORDER.get().ok_or(Error::NoMatchingConfig)?; - let config = CONFIG.get().ok_or(Error::NoMatchingConfig)?; - let per_frame = 1.0 / config.frames_anim as f32; - let (d_l, d_a, d_b) = ( - (to.l - config.flash_colour.l) * per_frame, - (to.a - config.flash_colour.a) * per_frame, - (to.b - config.flash_colour.b) * per_frame, - ); - - let mut c = config.flash_colour; - connection.run_command(set_command(&c)?).await?; - - let dur_per_frame = std::time::Duration::from_secs_f64(1.0 / config.refresh_rate as f64); - - tokio::time::sleep(dur_per_frame * config.frames_delay).await; - for _ in 0..config.frames_anim { - c.l += d_l; - c.a += d_a; - c.b += d_b; - - connection.run_command(set_command(&c)?).await?; - - tokio::time::sleep(dur_per_frame).await; - } - connection - .run_command(DEFAULT_COLOURS.get().ok_or(Error::NoMatchingConfig)?) - .await?; - Ok(()) -} - -fn set_command(colour: &Lab) -> Res { - let [r, g, b] = colour.to_rgb(); - Ok(format!( - "{} #{r:02x}{g:02x}{b:02x}", - DEFAULT_COLOURS_NO_INDICATOR - .get() - .ok_or(Error::NoMatchingConfig)? - )) -}