use crate::keybinds::configure_wm;
use crate::receive_handle::event_handler;
use crate::socket::socket_handler;
use crate::util;
use crate::util::check_new_version;
use anyhow::Context;
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::TransferType;
use core_lib::{WarnWithDetails, notify, notify_resident, notify_warn};
use exec_lib::listener::{hyprland_config_listener, monitor_listener};
use launcher_lib::{LauncherData, create_windows_overview_launcher_window};
use relm4::adw::gtk::gdk::Display;
use relm4::adw::gtk::prelude::*;
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};
use windows_lib::{
WindowsOverviewData, WindowsSwitchData, create_windows_overview_window,
create_windows_switch_window,
};
pub fn start(
config_file: PathBuf,
css_path: PathBuf,
data_dir: PathBuf,
cache_dir: PathBuf,
hyprland_version: &semver::Version,
) -> 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().context("Failed to preactivate GTK and reload icons")?;
exec_lib::reload_hyprland_config()
.context("Failed to reload hyprland config")
.warn_details("unable to reload hyprland config");
let (event_sender, event_receiver) = async_channel::unbounded();
if env::var_os("HYPRSHELL_NO_LISTENERS").is_none() {
register_event_restarter(config_file.clone(), css_path.clone(), event_sender.clone());
}
let event_sender_2 = event_sender.clone();
thread::spawn(move || {
socket_handler(&event_sender_2);
});
let wayland_socket_index = env::var("WAYLAND_DISPLAY")
.ok()
.and_then(|s| s.split('-').next_back()?.parse::<i32>().ok())
.unwrap_or(1);
let mut inc = 0;
info!("Starting gui loop");
loop {
inc += 1;
let id = format!(
"{}-{}-{}{}",
core_lib::APPLICATION_ID,
wayland_socket_index,
inc,
if cfg!(debug_assertions) { "-test" } else { "" }
);
trace!("Application id: {}", id);
let application = Application::builder().application_id(id).build();
debug!("Application created");
let config_file = config_file.clone();
let css_path = css_path.clone();
let data_dir = data_dir.clone();
let cache_dir = cache_dir.clone();
let event_sender = event_sender.clone();
let event_receiver = event_receiver.clone();
let hyprland_version = hyprland_version.clone();
application.connect_activate(move |app| {
activate(
app,
&config_file,
&css_path,
&data_dir,
&cache_dir,
event_sender.clone(),
event_receiver.clone(),
&hyprland_version.clone(),
);
});
let exit = application.run_with_args::<String>(&[]);
debug!("Application exited with code {exit:?}");
}
}
pub struct Globals {
pub windows: Option<WindowsGlobal>,
pub app: Application,
}
#[derive(Debug, Default)]
pub struct WindowsGlobal {
pub overview: Option<(WindowsOverviewData, LauncherData)>,
pub switch: Option<WindowsSwitchData>,
}
#[allow(clippy::cognitive_complexity)]
#[allow(clippy::too_many_arguments)]
fn activate(
app: &Application,
config_file: &Path,
css_path: &Path,
data_dir: &Path,
cache_dir: &Path,
event_sender: Sender<TransferType>,
event_receiver: Receiver<TransferType>,
hyprland_version: &semver::Version,
) {
let _span = debug_span!("activate").entered();
apply_css(css_path).warn_details("Failed to apply CSS");
exec_lib::set_follow_mouse_default().warn_details("Failed to set set_remain_focused default");
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 is up to date");
}
}
let config = match config_lib::load_and_migrate_config(config_file, true) {
Ok(config) => config,
Err(err) => {
notify_warn(&format!("Failed to load config: {err:?}"));
if let Err(err) = hyprshell_config_block(config_file) {
error!("Failed to block config: {err:?}",);
process::exit(1);
}
info!("Trying to rerun application after config reload");
return; }
};
if config.windows.is_none()
|| matches!(&config.windows, Some(windows) if windows.overview.is_none() && windows.switch.is_none())
{
notify_warn("Nothing is enabled in the config");
if let Err(err) = hyprshell_config_block(config_file) {
error!("Failed to block config: {err:?}",);
process::exit(1);
}
info!("Trying to rerun application after config reload");
return; }
if let Err(err) = configure_wm(&config, hyprland_version) {
notify_warn(&format!("Failed to configure wm: {err:?}"));
if let Err(err) = hyprshell_config_block(config_file) {
error!("Failed to block config: {err:?}");
process::exit(1);
}
info!("Trying to rerun application after config reload");
return; }
let globals = match create_windows(app, &config, data_dir, event_sender.clone()) {
Ok(data) => data,
Err(err) => {
notify_warn(&format!("Failed to create windows: {err:?}"));
if let Err(err) = hyprshell_config_block(config_file) {
error!("Failed to block config: {err:?}");
process::exit(1);
}
info!("Trying to rerun application after config reload");
return; }
};
glib::spawn_future_local(async move {
event_handler(globals, event_receiver, event_sender).await;
info!("Application exited, restarting");
});
info!("Application initialized");
}
fn create_windows(
app: &Application,
config: &Config,
data_dir: &Path,
event_sender: Sender<TransferType>,
) -> anyhow::Result<Globals> {
let mut global = Globals {
windows: None,
app: app.clone(),
};
if let Some(windows) = &config.windows {
let mut windows_data = WindowsGlobal::default();
if let Some(overview) = &windows.overview {
let overview_data = create_windows_overview_window(app, overview, windows)
.context("failed to create overview window")?;
let launcher_data = create_windows_overview_launcher_window(
app,
&overview.launcher,
data_dir,
&event_sender,
)
.context("failed to create launcher window")?;
windows_data.overview = Some((overview_data, launcher_data));
} else {
debug!("Windows overview disabled");
}
if let Some(switch) = &windows.switch {
let switch_data = create_windows_switch_window(app, switch, windows, event_sender)
.context("failed to create switch window")?;
windows_data.switch = Some(switch_data);
} else {
debug!("Windows switch disabled");
}
global.windows = Some(windows_data);
} else {
debug!("Windows disabled");
}
Ok(global)
}
fn apply_css(custom_css: &Path) -> anyhow::Result<()> {
let provider_app = CssProvider::new();
provider_app.load_from_string(include_str!("default_styles.css"));
style_context_add_provider_for_display(
&Display::default().context("Could not connect to a display.")?,
&provider_app,
STYLE_PROVIDER_PRIORITY_USER,
);
windows_lib::get_css()?;
launcher_lib::get_css()?;
if custom_css.exists() {
debug!("Loading custom css file {custom_css:?}");
let provider_user = CssProvider::new();
provider_user.load_from_path(custom_css);
style_context_add_provider_for_display(
&Display::default().context("Could not connect to a display.")?,
&provider_user,
STYLE_PROVIDER_PRIORITY_USER,
);
} else {
debug!("Custom css file {custom_css:?} does not exist");
}
Ok(())
}
pub fn register_event_restarter(
config_file: Rc<PathBuf>,
css_path: Rc<PathBuf>,
event_sender: Sender<TransferType>,
) {
let delay = env::var("HYPRSHELL_RELOAD_TIMEOUT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(1500);
let (restart_sender, restart_receiver) = async_channel::unbounded();
glib::timeout_add_local_once(Duration::from_millis(delay), move || {
setup_restart_listener(&config_file, &css_path, &restart_sender);
});
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(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(TransferType::Restart)
.await
.warn_details("unable to send restart");
});
});
*debounce_timer.borrow_mut() = Some(timer_id);
}
});
}
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;
});
}