use crate::linux::core::DaemonCore;
use log::error;
use log::info;
use std::{env, process::Command, sync::Arc};
use tokio::sync::Mutex;
use wayclip_core::models::error::WayclipError;
use wayclip_core::models::input::keyboard::WayclipKeyCombo;
use wayclip_global_hotkey::GlobalHotKeyEvent;
use wayclip_global_hotkey::HotKeyState;
use wayclip_global_hotkey::{GlobalHotKeyManager, hotkey::HotKey};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum SessionType {
X11,
Wayland,
#[default]
Unknown,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum DesktopEnvironmentType {
Hyprland,
Gnome,
Sway,
Kde,
#[default]
Unknown,
}
pub struct DesktopEnvironmentManager {
pub daemon: Arc<Mutex<DaemonCore>>,
pub desktop: DesktopEnvironmentType,
pub trigger_combo: WayclipKeyCombo,
}
impl DesktopEnvironmentManager {
pub fn new(
daemon: Arc<Mutex<DaemonCore>>,
trigger_combo: WayclipKeyCombo,
) -> Result<Self, WayclipError> {
let (desktop, _) = Self::get_env_session()?;
Ok(Self {
daemon,
desktop,
trigger_combo,
})
}
pub fn get_env_session() -> Result<(DesktopEnvironmentType, SessionType), WayclipError> {
let session_env = env::var("XDG_SESSION_TYPE")?;
let session = match session_env.to_lowercase().as_str() {
"wayland" => SessionType::Wayland,
"x11" | "xorg" => SessionType::X11,
_ => SessionType::Unknown,
};
let desktop_env = env::var("XDG_CURRENT_DESKTOP")?;
let desktop = match desktop_env.to_lowercase().as_str() {
"hyprland" => DesktopEnvironmentType::Hyprland,
"gnome" => DesktopEnvironmentType::Gnome,
"sway" => DesktopEnvironmentType::Sway,
"kde" => DesktopEnvironmentType::Kde,
_ => DesktopEnvironmentType::Gnome,
};
Ok((desktop, session))
}
fn run_command(mut cmd: Command, desc: &str) -> Result<(), WayclipError> {
let output = cmd
.output()
.map_err(|e| WayclipError::CLI(format!("Failed to execute '{desc}': {e}").into()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let err_msg = if !stderr.is_empty() {
stderr
} else if !stdout.is_empty() {
stdout
} else {
format!("process exited with code {:?}", output.status.code())
};
return Err(WayclipError::CLI(
format!("Command '{desc}' failed: {err_msg}").into(),
));
}
Ok(())
}
fn get_trigger_command_string(&self) -> Result<String, WayclipError> {
let env = env::current_exe()?;
let parent = env
.parent()
.ok_or_else(|| WayclipError::NotFound("No parent found".into()))?;
let process_path = parent.join("cli");
let process_string = process_path
.to_str()
.ok_or_else(|| WayclipError::Validation("Could not convert to str".into()))?
.to_string();
Ok(format!("{process_string} daemon save"))
}
pub fn remove_global_hotkey(&self) -> Result<(), WayclipError> {
let manager = GlobalHotKeyManager::new()?;
let hotkey = HotKey::new(
Some(self.trigger_combo.key_modifiers.clone().into()),
self.trigger_combo.key_code.clone().into(),
);
manager.unregister(hotkey)?;
Ok(())
}
pub fn setup_global_hotkey(&self) -> Result<(), WayclipError> {
let manager = GlobalHotKeyManager::new()?;
let hotkey = HotKey::new(
Some(self.trigger_combo.key_modifiers.clone().into()),
self.trigger_combo.key_code.clone().into(),
);
manager.register(hotkey)?;
log::debug!("Registered a shortcut");
let daemon_clone = self.daemon.clone();
tokio::task::spawn_blocking(move || {
let _manager = manager;
while let Ok(event) = GlobalHotKeyEvent::receiver().recv() {
if event.id() == hotkey.id() && event.state() == HotKeyState::Released {
log::debug!("Hotkey triggered");
let daemon_clone = Arc::clone(&daemon_clone);
tokio::spawn(async move {
if let Err(e) = DaemonCore::save_clip(daemon_clone, None).await {
error!("Error saving clip via shortcut: {e}")
}
});
}
}
});
Ok(())
}
pub fn create_auto_bind(&self) -> Result<(), WayclipError> {
match self.desktop {
DesktopEnvironmentType::Hyprland => {
let bind_string = self.trigger_combo.clone().to_string().replace("+", ", ");
let full_string = format!(
"{}, exec, {}",
bind_string,
self.get_trigger_command_string()?
);
let mut cmd = Command::new("hyprctl");
cmd.arg("keyword").arg("bind").arg(&full_string);
Self::run_command(cmd, "hyprctl keyword bind")?;
}
DesktopEnvironmentType::Sway => {
let bind_string = self.trigger_combo.to_string();
let trigger_cmd = self.get_trigger_command_string()?;
let mut cmd = Command::new("swaymsg");
cmd.arg("bindsym")
.arg(&bind_string)
.arg("exec")
.arg(&trigger_cmd);
Self::run_command(cmd, "swaymsg bindsym")?;
}
DesktopEnvironmentType::Gnome | DesktopEnvironmentType::Kde => {
self.setup_global_hotkey()?
}
_ => info!(
"No auto bind setup available for your desktop environment. Please bind {} to {}",
self.trigger_combo,
self.get_trigger_command_string()?
),
}
Ok(())
}
pub fn remove_auto_bind(&self) -> Result<(), WayclipError> {
match self.desktop {
DesktopEnvironmentType::Hyprland => {
let bind_string = self.trigger_combo.clone().to_string().replace("+", ", ");
let mut cmd = Command::new("hyprctl");
cmd.arg("keyword").arg("unbind").arg(&bind_string);
Self::run_command(cmd, "hyprctl keyword unbind")?;
}
DesktopEnvironmentType::Sway => {
let bind_string = self.trigger_combo.to_string();
let mut cmd = Command::new("swaymsg");
cmd.arg("unbindsym").arg(&bind_string);
Self::run_command(cmd, "swaymsg unbindsym")?;
}
DesktopEnvironmentType::Gnome | DesktopEnvironmentType::Kde => {
self.remove_global_hotkey()?
}
_ => info!("No auto bind removal available for your desktop environment",),
}
Ok(())
}
}
impl Drop for DesktopEnvironmentManager {
fn drop(&mut self) {
if let Err(e) = self.remove_auto_bind() {
log::warn!("Failed to unbind hotkeys on drop: {e:?}");
}
}
}