Compare commits
No commits in common. "9f5ea80d526b5c06380a99d32241ff5008f69177" and "2a25fca8745ef2dce855183c618186413870d178" have entirely different histories.
9f5ea80d52
...
2a25fca874
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -653,7 +653,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sway-flash-indicator"
|
name = "sway-flash-indicator"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"directories",
|
"directories",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
@ -662,7 +662,6 @@ dependencies = [
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"serde",
|
"serde",
|
||||||
"swayipc-async",
|
"swayipc-async",
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "sway-flash-indicator"
|
name = "sway-flash-indicator"
|
||||||
version = "0.2.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -13,4 +13,3 @@ 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"
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
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]))
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
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
25
src/error.rs
|
@ -1,25 +0,0 @@
|
||||||
#[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
48
src/flash.rs
|
@ -1,48 +0,0 @@
|
||||||
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)?
|
|
||||||
))
|
|
||||||
}
|
|
189
src/main.rs
189
src/main.rs
|
@ -1,20 +1,66 @@
|
||||||
use config::Config;
|
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use lab::Lab;
|
use lab::Lab;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
mod colour;
|
#[derive(Debug)]
|
||||||
mod config;
|
struct NoMatchingConfig;
|
||||||
mod error;
|
|
||||||
mod flash;
|
|
||||||
|
|
||||||
pub mod prelude {
|
impl std::fmt::Display for NoMatchingConfig {
|
||||||
pub use crate::error::*;
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
pub use crate::CONFIG;
|
write!(f, "no config for client.focused")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use prelude::*;
|
impl std::error::Error for NoMatchingConfig {}
|
||||||
|
|
||||||
pub static CONFIG: tokio::sync::OnceCell<Config> = tokio::sync::OnceCell::const_new();
|
#[derive(Debug)]
|
||||||
|
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> =
|
||||||
|
@ -22,24 +68,20 @@ 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() -> Res<()> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
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();
|
||||||
|
|
||||||
config::parse_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([
|
.subscribe([swayipc_async::EventType::Binding])
|
||||||
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;
|
||||||
|
@ -53,32 +95,13 @@ async fn main() -> Res<()> {
|
||||||
fut.abort();
|
fut.abort();
|
||||||
}
|
}
|
||||||
fut = Some(tokio::spawn(async {
|
fut = Some(tokio::spawn(async {
|
||||||
if let Err(e) = flash::interpolate_task().await {
|
if let Err(e) = interpolate_task().await {
|
||||||
log::warn!("error {e:?}");
|
log::warn!("error {e:?}");
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
swayipc_async::Event::Window(window)
|
_ => unreachable!(),
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,20 +109,50 @@ async fn main() -> Res<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_sway_config(connection: &mut swayipc_async::Connection) -> Res<()> {
|
fn parse_config() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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(Error::NoMatchingConfig)?
|
.ok_or(NoMatchingConfig)?
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
let default_border = default_colours
|
let default_border = default_colours
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.nth(2)
|
.nth(2)
|
||||||
.ok_or(Error::NoMatchingConfig)?;
|
.ok_or(NoMatchingConfig)?;
|
||||||
DEFAULT_BORDER.set(colour::parse_hex(default_border)?)?;
|
DEFAULT_BORDER.set(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<_>>();
|
||||||
|
@ -108,3 +161,55 @@ async fn get_sway_config(connection: &mut swayipc_async::Connection) -> Res<()>
|
||||||
|
|
||||||
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)?
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue