mod config;
mod error;
mod hypr;
mod scheduler;
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {
if cfg!(debug_assertions) {
eprintln!($($arg)*);
}
};
}
use tokio::io::AsyncBufReadExt;
use tokio::signal::unix::{SignalKind, signal};
use error::{Result, StutterError};
use scheduler::set_priority;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
log!("[stutter] starting...");
let mut cfg = config::load();
log!(
"[stutter] focused_nice={} default_nice={}",
cfg.focused_nice,
cfg.default_nice
);
let mut prev_pid: Option<u32> = None;
let mut line = String::new();
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;
let mut sighup = signal(SignalKind::hangup())?;
loop {
let mut reader = match hypr::connect_events().await {
Ok(r) => r,
Err(e) => {
eprintln!("[stutter] failed to connect to event socket: {e}, retrying in 3s");
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
continue;
}
};
log!("[stutter] connected to event socket");
loop {
line.clear();
tokio::select! {
res = reader.read_line(&mut line) => {
let n = match res {
Ok(n) => n,
Err(e) => {
log!("[stutter] socket error: {e}, reconnecting in 3s");
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
break;
}
};
if n == 0 {
log!("[stutter] event socket closed, reconnecting in 3s");
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
break;
}
let event = line.trim_end();
if event.starts_with("activewindow>>") {
match hypr::get_active_window_pid().await {
Ok(new_pid) => {
if let Some(p) = prev_pid {
if p != new_pid {
if let Err(e) = set_priority(p, cfg.default_nice) {
log!("[stutter] failed to reset priority for pid {p}: {e}");
}
}
}
match set_priority(new_pid, cfg.focused_nice) {
Ok(()) => log!("[stutter] pid {new_pid} → nice {}", cfg.focused_nice),
Err(e) => log!("[stutter] failed to boost pid {new_pid}: {e}"),
}
prev_pid = Some(new_pid);
}
Err(StutterError::NoActiveWindow) => {
}
Err(e) => {
log!("[stutter] failed to get active window: {e}");
}
}
}
}
() = async { tokio::select! { _ = sigterm.recv() => {}, _ = sigint.recv() => {} } } => {
log!("[stutter] received termination signal, exiting");
if let Some(p) = prev_pid {
let _ = set_priority(p, cfg.default_nice);
}
return Ok(());
}
_ = sighup.recv() => {
log!("[stutter] received SIGHUP, reloading config");
cfg = config::load();
log!(
"[stutter] reloaded config: focused_nice={} default_nice={}",
cfg.focused_nice,
cfg.default_nice
);
}
}
}
}
}