use crate::root::{Root, RootInit};
use crate::socket::socket_handler;
use crate::util;
use crate::util::check_new_version;
use crate::wm::{configure_wm, configure_wm_initial};
use anyhow::{Context, bail};
use async_channel::{Receiver, Sender};
use config_lib::Config;
use core_lib::listener::{
hyprshell_config_block, hyprshell_config_listener, hyprshell_css_listener,
};
use core_lib::transfer::ExternalTransferType;
use core_lib::{WarnWithDetails, notify, notify_resident, notify_warn};
use exec_lib::listener::{hyprland_config_listener, monitor_listener};
use relm4::RelmApp;
use relm4::adw::gio;
use relm4::adw::gtk::gdk::Display;
use relm4::adw::gtk::{
Application, CssProvider, STYLE_PROVIDER_PRIORITY_USER, glib,
style_context_add_provider_for_display,
};
use std::any::Any;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
use std::{env, process, thread};
use tracing::{debug, debug_span, error, info, trace, warn};
pub fn start(
config_file: PathBuf,
css_path: PathBuf,
data_dir: PathBuf,
cache_dir: PathBuf,
) -> anyhow::Result<()> {
let config_file = Rc::new(config_file);
let css_path = Rc::new(css_path);
let data_dir = Rc::new(data_dir);
let cache_dir = Rc::new(cache_dir);
util::preactivate(&cache_dir).context("Failed to preactivate GTK")?;
match check_new_version(&cache_dir) {
Err(err) => {
debug!("Unable to compare previous to current version.\n{err:?}");
}
Ok((Ordering::Greater, messages)) => {
notify(
&format!(
"Hyprshell was updated to a new version ({})",
env!("CARGO_PKG_VERSION")
),
Duration::from_secs(5),
);
thread::sleep(Duration::from_millis(500));
for info in messages {
notify_resident(&info, Duration::from_secs(10));
}
}
Ok((Ordering::Less, _)) => {
notify_warn(
"Hyprshell was downgraded, downgrading config must be done manually if needed",
);
}
Ok((Ordering::Equal, _)) => {
debug!("Hyprshell version did not change");
}
}
let (external_event_sender, external_event_receiver) = async_channel::unbounded();
if env::var_os("HYPRSHELL_NO_LISTENERS").is_none() {
register_event_restarter(
config_file.clone(),
css_path.clone(),
external_event_sender.clone(),
);
}
thread::spawn(move || {
socket_handler(external_event_sender);
});
configure_wm_initial();
let wayland_socket_index = env::var("WAYLAND_DISPLAY")
.ok()
.and_then(|s| s.split('-').next_back()?.parse::<i32>().ok())
.unwrap_or(1);
let id = format!(
"{}-{}-{}",
core_lib::APPLICATION_ID,
wayland_socket_index,
if cfg!(debug_assertions) { "-test" } else { "" }
);
trace!("Application id: {}", id);
let relm = RelmApp::new(&id)
.visible_on_activate(false)
.with_args(vec![]);
debug!("Application created");
relm.run::<Root>(RootInit {
config_file,
css_path,
data_dir,
cache_dir,
external_event_receiver: external_event_receiver.clone(),
});
Ok(())
}
pub fn register_event_restarter(
config_file: Rc<PathBuf>,
css_path: Rc<PathBuf>,
event_sender: Sender<ExternalTransferType>,
) {
let (restart_sender, restart_receiver) = async_channel::unbounded();
let debounce_delay = env::var("HYPRSHELL_RELOAD_DEBOUNCE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(2000);
let debounce_timer = Rc::new(RefCell::new(None::<glib::SourceId>));
glib::spawn_future_local(async move {
loop {
let cause = restart_receiver.recv().await.unwrap_or_default();
debug!("Received restart request ({cause}), starting debounce timer");
if let Some(timer_id) = debounce_timer.borrow_mut().take() {
timer_id.remove();
trace!("Cancelled previous debounce timer");
}
let event_sender_clone = event_sender.clone();
let debounce_timer_clone = debounce_timer.clone();
let timer_id =
glib::timeout_add_local_once(Duration::from_millis(debounce_delay), move || {
trace!("Debounce timer expired, triggering restart ({cause})");
*debounce_timer_clone.borrow_mut() = None;
let event_sender_inner = event_sender_clone.clone();
glib::spawn_future_local(async move {
info!("Restarting gui ({cause})");
event_sender_inner
.send(ExternalTransferType::Reload)
.await
.warn_details("unable to send restart");
});
});
*debounce_timer.borrow_mut() = Some(timer_id);
}
});
let delay = env::var("HYPRSHELL_RELOAD_DELAY")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(1000);
glib::timeout_add_local_once(Duration::from_millis(delay), move || {
setup_restart_listener(&config_file, &css_path, &restart_sender);
});
}
static WATCHERS: OnceLock<Mutex<Vec<Box<dyn Any + Send>>>> = OnceLock::new();
fn setup_restart_listener(config_file: &Path, css_path: &Path, restart_tx: &Sender<&'static str>) {
let tx = restart_tx.clone();
if let Ok(watcher) = hyprshell_config_listener(config_file, move |mess| {
let _ = tx.send_blocking(mess);
}) {
WATCHERS
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("Failed to lock watchers")
.push(Box::new(watcher));
}
let tx = restart_tx.clone();
if let Ok(watcher) = hyprshell_css_listener(css_path, move |mess| {
let _ = tx.send_blocking(mess);
}) {
WATCHERS
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("Failed to lock watchers")
.push(Box::new(watcher));
}
let tx = restart_tx.clone();
glib::spawn_future_local(async move {
monitor_listener(move |mess| {
let _ = tx.send_blocking(mess);
})
.await;
});
let tx = restart_tx.clone();
glib::spawn_future_local(async move {
hyprland_config_listener(move |mess| {
let _ = tx.send_blocking(mess);
})
.await;
});
}