Compare commits

..

3 commits

Author SHA1 Message Date
Alex Janka 9f5ea80d52 0.2.0 - autotiling 2024-07-25 15:23:53 +10:00
Alex Janka 8bab54b524 rearrange as modules 2024-07-25 14:24:14 +10:00
Alex Janka a6ee8faf31 update errors 2024-07-25 14:16:10 +10:00
7 changed files with 202 additions and 149 deletions

3
Cargo.lock generated
View file

@ -653,7 +653,7 @@ dependencies = [
[[package]] [[package]]
name = "sway-flash-indicator" name = "sway-flash-indicator"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"directories", "directories",
"futures-util", "futures-util",
@ -662,6 +662,7 @@ dependencies = [
"pretty_env_logger", "pretty_env_logger",
"serde", "serde",
"swayipc-async", "swayipc-async",
"thiserror",
"tokio", "tokio",
"toml", "toml",
] ]

View file

@ -1,6 +1,6 @@
[package] [package]
name = "sway-flash-indicator" name = "sway-flash-indicator"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -13,3 +13,4 @@ lab = "0.11.0"
directories = "5.0.1" directories = "5.0.1"
toml = "0.8.15" toml = "0.8.15"
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
thiserror = "1.0.63"

11
src/colour.rs Normal file
View file

@ -0,0 +1,11 @@
use lab::Lab;
use crate::prelude::*;
pub fn parse_hex(hex: &str) -> Res<Lab> {
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]))
}

72
src/config.rs Normal file
View file

@ -0,0 +1,72 @@
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 autosplit_enabled: bool,
pub autosplit_ratio: f64,
}
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,
},
autosplit_enabled: true,
autosplit_ratio: 1.0,
}
}
}
fn lab_ser<S: serde::Serializer>(colour: &Lab, serializer: S) -> Result<S::Ok, S::Error> {
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<Lab, D::Error> {
use serde::de::Error;
let hex = String::deserialize(deserializer)?;
crate::colour::parse_hex(&hex).map_err(|_| D::Error::custom("couldn't parse hex"))
}

25
src/error.rs Normal file
View file

@ -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<T> From<tokio::sync::SetError<T>> for Error {
fn from(_: tokio::sync::SetError<T>) -> Self {
Self::SetCell
}
}
pub type Res<T> = Result<T, Error>;

48
src/flash.rs Normal file
View file

@ -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<String> {
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)?
))
}

View file

@ -1,66 +1,20 @@
use config::Config;
use futures_util::StreamExt; use futures_util::StreamExt;
use lab::Lab; use lab::Lab;
use serde::{Deserialize, Serialize};
#[derive(Debug)] mod colour;
struct NoMatchingConfig; mod config;
mod error;
mod flash;
impl std::fmt::Display for NoMatchingConfig { pub mod prelude {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { pub use crate::error::*;
write!(f, "no config for client.focused") pub use crate::CONFIG;
}
} }
impl std::error::Error for NoMatchingConfig {} use prelude::*;
#[derive(Debug)] pub static CONFIG: tokio::sync::OnceCell<Config> = tokio::sync::OnceCell::const_new();
struct HexParse;
impl std::fmt::Display for HexParse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid hex colour")
}
}
impl std::error::Error for HexParse {}
#[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<S: serde::Serializer>(colour: &Lab, serializer: S) -> Result<S::Ok, S::Error> {
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<Lab, D::Error> {
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<Config> = tokio::sync::OnceCell::const_new();
static DEFAULT_COLOURS: tokio::sync::OnceCell<String> = tokio::sync::OnceCell::const_new(); static DEFAULT_COLOURS: tokio::sync::OnceCell<String> = tokio::sync::OnceCell::const_new();
static DEFAULT_COLOURS_NO_INDICATOR: tokio::sync::OnceCell<String> = static DEFAULT_COLOURS_NO_INDICATOR: tokio::sync::OnceCell<String> =
@ -68,20 +22,24 @@ static DEFAULT_COLOURS_NO_INDICATOR: tokio::sync::OnceCell<String> =
static DEFAULT_BORDER: tokio::sync::OnceCell<Lab> = tokio::sync::OnceCell::const_new(); static DEFAULT_BORDER: tokio::sync::OnceCell<Lab> = tokio::sync::OnceCell::const_new();
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Res<()> {
if std::env::var_os("RUST_LOG").is_none() { if std::env::var_os("RUST_LOG").is_none() {
std::env::set_var("RUST_LOG", "info"); std::env::set_var("RUST_LOG", "info");
} }
pretty_env_logger::init(); pretty_env_logger::init();
parse_config()?; config::parse_config()?;
let mut event_connection = swayipc_async::Connection::new().await?; let mut event_connection = swayipc_async::Connection::new().await?;
let mut autosplit_connection = swayipc_async::Connection::new().await?;
get_sway_config(&mut event_connection).await?; get_sway_config(&mut event_connection).await?;
let mut events = event_connection let mut events = event_connection
.subscribe([swayipc_async::EventType::Binding]) .subscribe([
swayipc_async::EventType::Binding,
swayipc_async::EventType::Window,
])
.await?; .await?;
let mut fut: Option<tokio::task::JoinHandle<()>> = None; let mut fut: Option<tokio::task::JoinHandle<()>> = None;
@ -95,13 +53,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
fut.abort(); fut.abort();
} }
fut = Some(tokio::spawn(async { fut = Some(tokio::spawn(async {
if let Err(e) = interpolate_task().await { if let Err(e) = flash::interpolate_task().await {
log::warn!("error {e:?}"); log::warn!("error {e:?}");
} }
})); }));
} }
} }
_ => unreachable!(), swayipc_async::Event::Window(window)
// TODO: change on window closed also
// the node we're given is the one that closes
if window.change != swayipc_async::WindowChange::Mark =>
{
let node = window.container;
let (width, height) = (node.window_rect.width, node.window_rect.height);
let ratio = (width as f64) / (height as f64);
let autosplit_ratio =
CONFIG.get().ok_or(Error::NoMatchingConfig)?.autosplit_ratio;
if let Err(e) = if ratio > autosplit_ratio {
autosplit_connection.run_command("splith").await
} else {
autosplit_connection.run_command("splitv").await
} {
log::warn!("error {e:?} setting split");
}
}
_ => {}
} }
} }
} }
@ -109,50 +86,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
fn parse_config() -> Result<(), Box<dyn std::error::Error>> { async fn get_sway_config(connection: &mut swayipc_async::Connection) -> Res<()> {
let dirs = directories::ProjectDirs::from("com", "alexjanka", "sway-flash-indicator")
.ok_or(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,
) -> Result<(), Box<dyn std::error::Error>> {
let config = connection.get_config().await?; let config = connection.get_config().await?;
let default_colours = config let default_colours = config
.config .config
.split('\n') .split('\n')
.find(|v| v.contains("client.focused")) .find(|v| v.contains("client.focused"))
.ok_or(NoMatchingConfig)? .ok_or(Error::NoMatchingConfig)?
.trim(); .trim();
let default_border = default_colours let default_border = default_colours
.split_whitespace() .split_whitespace()
.nth(2) .nth(2)
.ok_or(NoMatchingConfig)?; .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())?; DEFAULT_COLOURS.set(default_colours.to_string())?;
let mut split = default_colours.split_whitespace().collect::<Vec<_>>(); let mut split = default_colours.split_whitespace().collect::<Vec<_>>();
@ -161,55 +108,3 @@ async fn get_sway_config(
Ok(()) Ok(())
} }
fn parse_hex(hex: &str) -> Result<Lab, HexParse> {
let hex = hex.strip_prefix('#').unwrap_or(hex);
let r = u8::from_str_radix(&hex[..2], 16).map_err(|_| HexParse)?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| HexParse)?;
let b = u8::from_str_radix(&hex[4..], 16).map_err(|_| HexParse)?;
Ok(Lab::from_rgb(&[r, g, b]))
}
async fn interpolate_task() -> Result<(), Box<dyn std::error::Error>> {
let mut connection = swayipc_async::Connection::new().await?;
let to = DEFAULT_BORDER.get().ok_or(NoMatchingConfig)?;
let config = CONFIG.get().ok_or(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;
{
let command = set_command(&c)?;
connection.run_command(command).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;
{
let command = set_command(&c)?;
connection.run_command(command).await?;
}
tokio::time::sleep(dur_per_frame).await;
}
connection
.run_command(DEFAULT_COLOURS.get().ok_or(NoMatchingConfig)?)
.await?;
Ok(())
}
fn set_command(colour: &Lab) -> Result<String, Box<dyn std::error::Error>> {
let [r, g, b] = colour.to_rgb();
Ok(format!(
"{} #{r:02x}{g:02x}{b:02x}",
DEFAULT_COLOURS_NO_INDICATOR.get().ok_or(NoMatchingConfig)?
))
}