reached milestone for active window client

This commit is contained in:
2025-03-06 08:07:04 +01:00
parent ad8b9179bb
commit ba91e4e58c

View File

@@ -1,3 +1,10 @@
use daemonize::Daemonize;
use libc::wchar_t;
use log::{error, info};
use serde::{Deserialize, Serialize};
use signal_hook::{consts::TERM_SIGNALS, iterator::Signals};
use std::collections::HashMap;
use std::io::Read;
use std::{ use std::{
collections::HashSet, collections::HashSet,
env, env,
@@ -10,218 +17,149 @@ use std::{
thread, thread,
time::Duration, time::Duration,
}; };
use std::io::Read;
use daemonize::Daemonize;
use log::{error, info};
use serde::{Deserialize, Serialize};
use signal_hook::{consts::TERM_SIGNALS, iterator::Signals};
/// === Hyprland Event Types and Parsing === /// === Hyprland Event Types ===
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "event", content = "data")] #[serde(tag = "event", content = "data")]
enum HyprlandEvent { enum HyprlandEvent {
Workspace { workspace_name: String }, Workspace {
WorkspaceV2 { workspace_id: u8, workspace_name: String }, workspace_name: String,
FocusedMon { monitor_name: String, workspace_name: String }, },
FocusedMonV2 { monitor_name: String, workspace_id: u8 }, WorkspaceV2 {
ActiveWindow { window_class: String, window_title: String }, workspace_id: u8,
ActiveWindowV2 { window_address: String }, workspace_name: String,
Fullscreen { status: u8 }, },
MonitorRemoved { monitor_name: String }, FocusedMon {
MonitorAdded { monitor_name: String }, monitor_name: String,
MonitorAddedV2 { monitor_id: u8, monitor_name: String, monitor_description: String }, workspace_name: String,
CreateWorkspace { workspace_name: String }, },
CreateWorkspaceV2 { workspace_id: u8, workspace_name: String }, FocusedMonV2 {
DestroyWorkspace { workspace_name: String }, monitor_name: String,
DestroyWorkspaceV2 { workspace_id: u8, workspace_name: String }, workspace_id: u8,
MoveWorkspace { workspace_name: String, monitor_name: String }, },
MoveWorkspaceV2 { workspace_id: u8, workspace_name: String, monitor_name: String }, ActiveWindow {
RenameWorkspace { workspace_id: u8, new_name: String }, window_class: String,
ActiveSpecial { workspace_name: String, monitor_name: String }, window_title: String,
ActiveLayout { keyboard_name: String, layout_name: String }, },
OpenWindow { window_address: String, workspace_name: String, window_class: String, window_title: String }, ActiveWindowV2 {
CloseWindow { window_address: String }, window_address: String,
MoveWindow { window_address: String, workspace_name: String }, },
MoveWindowV2 { window_address: String, workspace_id: u8, workspace_name: String }, Fullscreen {
OpenLayer { namespace: String }, status: u8,
CloseLayer { namespace: String }, },
Submap { submap_name: String }, MonitorRemoved {
ChangeFloatingMode { window_address: String, floating: u8 }, monitor_name: String,
Urgent { window_address: String }, },
Screencast { state: u8, owner: u8 }, MonitorAdded {
WindowTitle { window_address: String }, monitor_name: String,
WindowTitleV2 { window_address: String, window_title: String }, },
ToggleGroup { toggle_status: u8, window_addresses: Vec<String> }, MonitorAddedV2 {
MoveIntoGroup { window_address: String }, monitor_id: u8,
MoveOutOfGroup { window_address: String }, monitor_name: String,
IgnoreGroupLock { value: u8 }, monitor_description: String,
LockGroups { value: u8 }, },
CreateWorkspace {
workspace_name: String,
},
CreateWorkspaceV2 {
workspace_id: u8,
workspace_name: String,
},
DestroyWorkspace {
workspace_name: String,
},
DestroyWorkspaceV2 {
workspace_id: u8,
workspace_name: String,
},
MoveWorkspace {
workspace_name: String,
monitor_name: String,
},
MoveWorkspaceV2 {
workspace_id: u8,
workspace_name: String,
monitor_name: String,
},
RenameWorkspace {
workspace_id: u8,
new_name: String,
},
ActiveSpecial {
workspace_name: String,
monitor_name: String,
},
ActiveLayout {
keyboard_name: String,
layout_name: String,
},
OpenWindow {
window_address: String,
workspace_name: String,
window_class: String,
window_title: String,
},
CloseWindow {
window_address: String,
},
MoveWindow {
window_address: String,
workspace_name: String,
},
MoveWindowV2 {
window_address: String,
workspace_id: u8,
workspace_name: String,
},
OpenLayer {
namespace: String,
},
CloseLayer {
namespace: String,
},
Submap {
submap_name: String,
},
ChangeFloatingMode {
window_address: String,
floating: u8,
},
Urgent {
window_address: String,
},
Screencast {
state: u8,
owner: u8,
},
WindowTitle {
window_address: String,
},
WindowTitleV2 {
window_address: String,
window_title: String,
},
ToggleGroup {
toggle_status: u8,
window_addresses: Vec<String>,
},
MoveIntoGroup {
window_address: String,
},
MoveOutOfGroup {
window_address: String,
},
IgnoreGroupLock {
value: u8,
},
LockGroups {
value: u8,
},
ConfigReloaded, ConfigReloaded,
Pin { window_address: String, pin_state: u8 }, Pin {
} window_address: String,
pin_state: u8,
fn parse_event_line(line: &str) -> Result<HyprlandEvent, Box<dyn Error>> { },
let line = line.trim();
let mut parts = line.split(">>");
let event_name = parts.next().ok_or("Missing event name")?;
let data = parts.next().unwrap_or("").trim();
match event_name {
"workspace" => Ok(HyprlandEvent::Workspace { workspace_name: data.to_string() }),
"workspacev2" => {
let mut fields = data.split(',');
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::WorkspaceV2 { workspace_id, workspace_name })
}
"focusedmon" => {
let mut fields = data.split(',');
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::FocusedMon { monitor_name, workspace_name })
}
"focusedmonv2" => {
let mut fields = data.split(',');
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
Ok(HyprlandEvent::FocusedMonV2 { monitor_name, workspace_id })
}
"activewindow" => {
let mut fields = data.split(',');
let window_class = fields.next().ok_or("Missing window_class")?.to_string();
let window_title = fields.next().ok_or("Missing window_title")?.to_string();
Ok(HyprlandEvent::ActiveWindow { window_class, window_title })
}
"activewindowv2" => Ok(HyprlandEvent::ActiveWindowV2 { window_address: data.to_string() }),
"fullscreen" => {
let status = data.parse::<u8>()?;
Ok(HyprlandEvent::Fullscreen { status })
}
"monitorremoved" => Ok(HyprlandEvent::MonitorRemoved { monitor_name: data.to_string() }),
"monitoradded" => Ok(HyprlandEvent::MonitorAdded { monitor_name: data.to_string() }),
"monitoraddedv2" => {
let mut fields = data.split(',');
let monitor_id = fields.next().ok_or("Missing monitor_id")?.parse::<u8>()?;
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
let monitor_description = fields.next().ok_or("Missing monitor_description")?.to_string();
Ok(HyprlandEvent::MonitorAddedV2 { monitor_id, monitor_name, monitor_description })
}
"createworkspace" => Ok(HyprlandEvent::CreateWorkspace { workspace_name: data.to_string() }),
"createworkspacev2" => {
let mut fields = data.split(',');
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::CreateWorkspaceV2 { workspace_id, workspace_name })
}
"destroyworkspace" => Ok(HyprlandEvent::DestroyWorkspace { workspace_name: data.to_string() }),
"destroyworkspacev2" => {
let mut fields = data.split(',');
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::DestroyWorkspaceV2 { workspace_id, workspace_name })
}
"moveworkspace" => {
let mut fields = data.split(',');
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
Ok(HyprlandEvent::MoveWorkspace { workspace_name, monitor_name })
}
"moveworkspacev2" => {
let mut fields = data.split(',');
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
Ok(HyprlandEvent::MoveWorkspaceV2 { workspace_id, workspace_name, monitor_name })
}
"renameworkspace" => {
let mut fields = data.split(',');
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let new_name = fields.next().ok_or("Missing new_name")?.to_string();
Ok(HyprlandEvent::RenameWorkspace { workspace_id, new_name })
}
"activespecial" => {
let mut fields = data.split(',');
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
Ok(HyprlandEvent::ActiveSpecial { workspace_name, monitor_name })
}
"activelayout" => {
let mut fields = data.split(',');
let keyboard_name = fields.next().ok_or("Missing keyboard_name")?.to_string();
let layout_name = fields.next().ok_or("Missing layout_name")?.to_string();
Ok(HyprlandEvent::ActiveLayout { keyboard_name, layout_name })
}
"openwindow" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
let window_class = fields.next().ok_or("Missing window_class")?.to_string();
let window_title = fields.next().ok_or("Missing window_title")?.to_string();
Ok(HyprlandEvent::OpenWindow { window_address, workspace_name, window_class, window_title })
}
"closewindow" => Ok(HyprlandEvent::CloseWindow { window_address: data.to_string() }),
"movewindow" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::MoveWindow { window_address, workspace_name })
}
"movewindowv2" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::MoveWindowV2 { window_address, workspace_id, workspace_name })
}
"openlayer" => Ok(HyprlandEvent::OpenLayer { namespace: data.to_string() }),
"closelayer" => Ok(HyprlandEvent::CloseLayer { namespace: data.to_string() }),
"submap" => Ok(HyprlandEvent::Submap { submap_name: data.to_string() }),
"changefloatingmode" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let floating = fields.next().ok_or("Missing floating")?.parse::<u8>()?;
Ok(HyprlandEvent::ChangeFloatingMode { window_address, floating })
}
"urgent" => Ok(HyprlandEvent::Urgent { window_address: data.to_string() }),
"screencast" => {
let mut fields = data.split(',');
let state = fields.next().ok_or("Missing state")?.parse::<u8>()?;
let owner = fields.next().ok_or("Missing owner")?.parse::<u8>()?;
Ok(HyprlandEvent::Screencast { state, owner })
}
"windowtitle" => Ok(HyprlandEvent::WindowTitle { window_address: data.to_string() }),
"windowtitlev2" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let window_title = fields.next().ok_or("Missing window_title")?.to_string();
Ok(HyprlandEvent::WindowTitleV2 { window_address, window_title })
}
"togglegroup" => {
let mut fields = data.split(',');
let toggle_status = fields.next().ok_or("Missing toggle_status")?.parse::<u8>()?;
let window_addresses: Vec<String> = fields.map(|s| s.to_string()).collect();
Ok(HyprlandEvent::ToggleGroup { toggle_status, window_addresses })
}
"moveintogroup" => Ok(HyprlandEvent::MoveIntoGroup { window_address: data.to_string() }),
"moveoutofgroup" => Ok(HyprlandEvent::MoveOutOfGroup { window_address: data.to_string() }),
"ignoregrouplock" => {
let value = data.parse::<u8>()?;
Ok(HyprlandEvent::IgnoreGroupLock { value })
}
"lockgroups" => {
let value = data.parse::<u8>()?;
Ok(HyprlandEvent::LockGroups { value })
}
"configreloaded" => Ok(HyprlandEvent::ConfigReloaded),
"pin" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let pin_state = fields.next().ok_or("Missing pin_state")?.parse::<u8>()?;
Ok(HyprlandEvent::Pin { window_address, pin_state })
}
_ => Err(format!("Unknown event type: {}", event_name).into()),
}
} }
/// === Utility: Extract event type string for filtering === /// === Utility: Extract event type string for filtering ===
@@ -269,6 +207,316 @@ fn event_type(event: &HyprlandEvent) -> &'static str {
} }
} }
/// === Hyprland Events parsing ===
fn parse_event_line(line: &str) -> Result<HyprlandEvent, Box<dyn Error>> {
let line = line.trim();
let mut parts = line.split(">>");
let event_name = parts.next().ok_or("Missing event name")?;
let data = parts.next().unwrap_or("").trim();
match event_name {
"workspace" => Ok(HyprlandEvent::Workspace {
workspace_name: data.to_string(),
}),
"workspacev2" => {
let mut fields = data.split(',');
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::WorkspaceV2 {
workspace_id,
workspace_name,
})
}
"focusedmon" => {
let mut fields = data.split(',');
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::FocusedMon {
monitor_name,
workspace_name,
})
}
"focusedmonv2" => {
let mut fields = data.split(',');
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
Ok(HyprlandEvent::FocusedMonV2 {
monitor_name,
workspace_id,
})
}
"activewindow" => {
let mut fields = data.split(',');
let window_class = fields.next().ok_or("Missing window_class")?.to_string();
let window_title = fields.next().ok_or("Missing window_title")?.to_string();
Ok(HyprlandEvent::ActiveWindow {
window_class,
window_title,
})
}
"activewindowv2" => Ok(HyprlandEvent::ActiveWindowV2 {
window_address: data.to_string(),
}),
"fullscreen" => {
let status = data.parse::<u8>()?;
Ok(HyprlandEvent::Fullscreen { status })
}
"monitorremoved" => Ok(HyprlandEvent::MonitorRemoved {
monitor_name: data.to_string(),
}),
"monitoradded" => Ok(HyprlandEvent::MonitorAdded {
monitor_name: data.to_string(),
}),
"monitoraddedv2" => {
let mut fields = data.split(',');
let monitor_id = fields.next().ok_or("Missing monitor_id")?.parse::<u8>()?;
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
let monitor_description = fields
.next()
.ok_or("Missing monitor_description")?
.to_string();
Ok(HyprlandEvent::MonitorAddedV2 {
monitor_id,
monitor_name,
monitor_description,
})
}
"createworkspace" => Ok(HyprlandEvent::CreateWorkspace {
workspace_name: data.to_string(),
}),
"createworkspacev2" => {
let mut fields = data.split(',');
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::CreateWorkspaceV2 {
workspace_id,
workspace_name,
})
}
"destroyworkspace" => Ok(HyprlandEvent::DestroyWorkspace {
workspace_name: data.to_string(),
}),
"destroyworkspacev2" => {
let mut fields = data.split(',');
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::DestroyWorkspaceV2 {
workspace_id,
workspace_name,
})
}
"moveworkspace" => {
let mut fields = data.split(',');
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
Ok(HyprlandEvent::MoveWorkspace {
workspace_name,
monitor_name,
})
}
"moveworkspacev2" => {
let mut fields = data.split(',');
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
Ok(HyprlandEvent::MoveWorkspaceV2 {
workspace_id,
workspace_name,
monitor_name,
})
}
"renameworkspace" => {
let mut fields = data.split(',');
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let new_name = fields.next().ok_or("Missing new_name")?.to_string();
Ok(HyprlandEvent::RenameWorkspace {
workspace_id,
new_name,
})
}
"activespecial" => {
let mut fields = data.split(',');
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
Ok(HyprlandEvent::ActiveSpecial {
workspace_name,
monitor_name,
})
}
"activelayout" => {
let mut fields = data.split(',');
let keyboard_name = fields.next().ok_or("Missing keyboard_name")?.to_string();
let layout_name = fields.next().ok_or("Missing layout_name")?.to_string();
Ok(HyprlandEvent::ActiveLayout {
keyboard_name,
layout_name,
})
}
"openwindow" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
let window_class = fields.next().ok_or("Missing window_class")?.to_string();
let window_title = fields.next().ok_or("Missing window_title")?.to_string();
Ok(HyprlandEvent::OpenWindow {
window_address,
workspace_name,
window_class,
window_title,
})
}
"closewindow" => Ok(HyprlandEvent::CloseWindow {
window_address: data.to_string(),
}),
"movewindow" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::MoveWindow {
window_address,
workspace_name,
})
}
"movewindowv2" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
Ok(HyprlandEvent::MoveWindowV2 {
window_address,
workspace_id,
workspace_name,
})
}
"openlayer" => Ok(HyprlandEvent::OpenLayer {
namespace: data.to_string(),
}),
"closelayer" => Ok(HyprlandEvent::CloseLayer {
namespace: data.to_string(),
}),
"submap" => Ok(HyprlandEvent::Submap {
submap_name: data.to_string(),
}),
"changefloatingmode" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let floating = fields.next().ok_or("Missing floating")?.parse::<u8>()?;
Ok(HyprlandEvent::ChangeFloatingMode {
window_address,
floating,
})
}
"urgent" => Ok(HyprlandEvent::Urgent {
window_address: data.to_string(),
}),
"screencast" => {
let mut fields = data.split(',');
let state = fields.next().ok_or("Missing state")?.parse::<u8>()?;
let owner = fields.next().ok_or("Missing owner")?.parse::<u8>()?;
Ok(HyprlandEvent::Screencast { state, owner })
}
"windowtitle" => Ok(HyprlandEvent::WindowTitle {
window_address: data.to_string(),
}),
"windowtitlev2" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let window_title = fields.next().ok_or("Missing window_title")?.to_string();
Ok(HyprlandEvent::WindowTitleV2 {
window_address,
window_title,
})
}
"togglegroup" => {
let mut fields = data.split(',');
let toggle_status = fields
.next()
.ok_or("Missing toggle_status")?
.parse::<u8>()?;
let window_addresses: Vec<String> = fields.map(|s| s.to_string()).collect();
Ok(HyprlandEvent::ToggleGroup {
toggle_status,
window_addresses,
})
}
"moveintogroup" => Ok(HyprlandEvent::MoveIntoGroup {
window_address: data.to_string(),
}),
"moveoutofgroup" => Ok(HyprlandEvent::MoveOutOfGroup {
window_address: data.to_string(),
}),
"ignoregrouplock" => {
let value = data.parse::<u8>()?;
Ok(HyprlandEvent::IgnoreGroupLock { value })
}
"lockgroups" => {
let value = data.parse::<u8>()?;
Ok(HyprlandEvent::LockGroups { value })
}
"configreloaded" => Ok(HyprlandEvent::ConfigReloaded),
"pin" => {
let mut fields = data.split(',');
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
let pin_state = fields.next().ok_or("Missing pin_state")?.parse::<u8>()?;
Ok(HyprlandEvent::Pin {
window_address,
pin_state,
})
}
_ => Err(format!("Unknown event type: {}", event_name).into()),
}
}
/// === Structs for Interaction with Socket1
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Workspace {
id: u32,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
monitor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
monitor_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
windows: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
has_fullscreen: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
last_window: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
last_window_title: Option<String>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Client {
address: String,
mapped: bool,
hidden: bool,
at: (i32, i32),
size: (i32, i32),
workspace: Workspace,
floating: bool,
pseudo: bool,
monitor: u8,
class: String,
title: String,
initial_class: String,
initial_title: String,
pid: u32,
xwayland: bool,
pinned: bool,
fullscreen: i32,
fullscreen_client: i32,
grouped: Vec<String>,
tags: Vec<String>,
swallowing: String,
#[serde(rename = "focusHistoryID")]
focus_history_id: i32,
inhibiting_idle: bool,
}
/// === Client Subscription Infrastructure === /// === Client Subscription Infrastructure ===
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -316,7 +564,8 @@ fn client_handler(stream: UnixStream, subscriptions: Arc<Mutex<Vec<ClientHandle>
return; return;
} }
let subscription_line = subscription_line.trim(); let subscription_line = subscription_line.trim();
let subscription = if subscription_line.is_empty() || subscription_line.to_lowercase() == "all" { let subscription = if subscription_line.is_empty() || subscription_line.to_lowercase() == "all"
{
Subscription::All Subscription::All
} else { } else {
let filters: HashSet<String> = subscription_line let filters: HashSet<String> = subscription_line
@@ -332,7 +581,10 @@ fn client_handler(stream: UnixStream, subscriptions: Arc<Mutex<Vec<ClientHandle>
{ {
let mut subs = subscriptions.lock().unwrap(); let mut subs = subscriptions.lock().unwrap();
subs.push(ClientHandle { sender: tx, subscription }); subs.push(ClientHandle {
sender: tx,
subscription,
});
} }
// Loop and write events to the client. // Loop and write events to the client.
@@ -401,9 +653,8 @@ fn hyprland_event_thread(subscriptions: Arc<Mutex<Vec<ClientHandle>>>) {
fn client_server_thread(client_socket_path: String, subscriptions: Arc<Mutex<Vec<ClientHandle>>>) { fn client_server_thread(client_socket_path: String, subscriptions: Arc<Mutex<Vec<ClientHandle>>>) {
// Remove existing socket file if present. // Remove existing socket file if present.
let _ = fs::remove_file(&client_socket_path); let _ = fs::remove_file(&client_socket_path);
let listener = UnixListener::bind(&client_socket_path).unwrap_or_else(|e| { let listener = UnixListener::bind(&client_socket_path)
panic!("Failed to bind client socket {}: {}", client_socket_path, e) .unwrap_or_else(|e| panic!("Failed to bind client socket {}: {}", client_socket_path, e));
});
info!("Client server listening on {}", client_socket_path); info!("Client server listening on {}", client_socket_path);
for stream in listener.incoming() { for stream in listener.incoming() {
@@ -418,9 +669,8 @@ fn client_server_thread(client_socket_path: String, subscriptions: Arc<Mutex<Vec
} }
fn create_socket(socket_path: &str) -> UnixStream { fn create_socket(socket_path: &str) -> UnixStream {
UnixStream::connect(socket_path).unwrap_or_else(|err| { UnixStream::connect(socket_path)
panic!("Could not connect to socket {}: {}", socket_path, err) .unwrap_or_else(|err| panic!("Could not connect to socket {}: {}", socket_path, err))
})
} }
/// The main daemon functionality: spawn threads, handle signals, etc. /// The main daemon functionality: spawn threads, handle signals, etc.
@@ -478,7 +728,10 @@ fn run_client(config: &Config, subscription: &str) {
eprintln!("Failed to send subscription: {}", e); eprintln!("Failed to send subscription: {}", e);
std::process::exit(1); std::process::exit(1);
} }
println!("Subscribed to '{}' events. Waiting for events...", subscription); println!(
"Subscribed to '{}' events. Waiting for events...",
subscription
);
let reader = BufReader::new(stream); let reader = BufReader::new(stream);
for line in reader.lines() { for line in reader.lines() {
@@ -498,64 +751,84 @@ fn run_client(config: &Config, subscription: &str) {
} }
} }
fn run_activewindow_client(config: &Config) { /// Prints the active window as json
let hypr_rundir_path = get_hypr_rundir_path();
info!("Using hypr runtime directory: {}", hypr_rundir_path);
let socket1_path = format!("{}/.socket1.sock", hypr_rundir_path); fn run_activewindow_client(config: &Config) {
info!("Using hypr socket2 path: {}", socket1_path); let subscription_line = "activewindowv2,fullscreen,closewindow,movewindow,changefloatingmode,moveintogroup,moveoutofgroup,togglegroup,pin,windowtitle\n";
match UnixStream::connect(&config.client_socket_path) { info!("Using subscription line: {}", subscription_line);
let event_reader = match UnixStream::connect(&config.client_socket_path) {
Ok(mut stream) => { Ok(mut stream) => {
let subscription_line = "activewindowv2,openwindow,closewindow,movewindow\n";
if let Err(e) = stream.write_all(subscription_line.as_bytes()) { if let Err(e) = stream.write_all(subscription_line.as_bytes()) {
eprintln!("Failed to send subscription: {}", e); eprintln!("Failed to send subscription: {}", e);
std::process::exit(1); std::process::exit(1);
} }
println!("Successfully connected to daemon."); println!("Successfully connected to daemon.");
match UnixStream::connect(&socket1_path) { BufReader::new(stream)
Ok(mut stream1) => {
let query = "activewindow";
let reader = BufReader::new(stream);
for line in reader.lines() {
match line {
Ok(msg) => {
println!("{}", msg);
if let Err(e) = stream1.write_all(query.as_bytes()) {
eprintln!("Failed to send query: {}", e);
std::process::exit(1);
}
println!("Successfully sent query to daemon.");
let mut response = String::new();
stream1.read_to_string(&mut response).unwrap();
println!("{}", response);
},
Err(e) => {
eprintln!("Error reading from daemon: {}", e);
break;
}
}
}
}
Err(e) => {
eprintln!("Failed to connect to socket1: {}", e);
std::process::exit(1);
}
}
} }
Err(e) => { Err(e) => {
eprintln!("Failed to connect to daemon. Is it running? Error: {}", e); eprintln!("Failed to connect to daemon. Is it running? Error: {}", e);
std::process::exit(1); std::process::exit(1);
} }
};
let mut clients = query_socket(QueryModes::Clients);
for event_line in event_reader.lines() {
let event: HyprlandEvent =
serde_json::from_str(&event_line.unwrap()).expect("Failed to parse event");
match event {
HyprlandEvent::ActiveWindowV2 { window_address } => {
if let Some(client) = clients.get(&format!("0x{}", window_address)) {
println!("{:#?}", serde_json::to_string(&client).unwrap());
} else {
clients = query_socket(QueryModes::Clients);
if let Some(client) = clients.get(&format!("0x{}", window_address)) {
println!("{}", serde_json::to_string(&client).unwrap());
} else {
eprintln!("Failed to find window address {}", window_address);
std::process::exit(1);
} }
}
}
_ => {
clients = query_socket(QueryModes::Clients);
}
}
}
}
enum QueryModes {
Clients,
Workspaces,
}
/// === Helper functions for clients that also query socket1 ===
fn query_socket(mode: QueryModes) -> HashMap<String, Client> {
let hypr_rundir_path = get_hypr_rundir_path();
info!("Using hypr runtime directory: {}", hypr_rundir_path);
let socket_path = format!("{}/.socket.sock", hypr_rundir_path);
info!("Using hypr socket1 path: {}", socket_path);
let query = match mode {
QueryModes::Clients => "j/clients",
QueryModes::Workspaces => "j/workspaces",
};
info!("Using query: {}", query);
let mut stream = create_socket(&socket_path);
stream.write_all(query.as_bytes()).unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).unwrap();
let clients: Vec<Client> = serde_json::from_str(&response).expect("Failed to parse response");
info!("Successfully queried {} active clients", clients.len());
stream.flush().expect("Failed to flush stream");
clients
.into_iter()
.map(|c| (c.address.clone(), c))
.collect()
} }
/// === Daemon Control Functions === /// === Daemon Control Functions ===
fn stop_daemon() -> Result<(), Box<dyn Error>> { fn stop_daemon() -> Result<(), Box<dyn Error>> {
// Compute pid file path from $XDG_RUNTIME_DIR/hyprman/ // Compute pid file path from $XDG_RUNTIME_DIR/hyprman/
let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR") let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
.expect("XDG_RUNTIME_DIR not set");
let hyprman_dir = format!("{}/hyprman", xdg_runtime_dir); let hyprman_dir = format!("{}/hyprman", xdg_runtime_dir);
let pid_file_path = format!("{}/hyprman.pid", hyprman_dir); let pid_file_path = format!("{}/hyprman.pid", hyprman_dir);
let pid_str = fs::read_to_string(&pid_file_path)?; let pid_str = fs::read_to_string(&pid_file_path)?;
@@ -585,7 +858,9 @@ fn print_help() {
println!(" -d, --daemon Run as daemon"); println!(" -d, --daemon Run as daemon");
println!(" -r, --restart Restart the daemon"); println!(" -r, --restart Restart the daemon");
println!(" -k, --kill Stop the daemon"); println!(" -k, --kill Stop the daemon");
println!(" -f, --filter [event] Run in client mode, subscribing to [event] (default: all)"); println!(
" -f, --filter [event] Run in client mode, subscribing to [event] (default: all)"
);
println!(" -h, --help Show this help message"); println!(" -h, --help Show this help message");
} }
@@ -600,12 +875,10 @@ fn main() {
env_logger::init(); env_logger::init();
// Ensure $XDG_RUNTIME_DIR/hyprman/ exists. // Ensure $XDG_RUNTIME_DIR/hyprman/ exists.
let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR") let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set");
.expect("XDG_RUNTIME_DIR not set");
let hyprman_dir = format!("{}/hyprman", xdg_runtime_dir); let hyprman_dir = format!("{}/hyprman", xdg_runtime_dir);
if fs::metadata(&hyprman_dir).is_err() { if fs::metadata(&hyprman_dir).is_err() {
fs::create_dir_all(&hyprman_dir) fs::create_dir_all(&hyprman_dir).expect("Failed to create hyprman runtime directory");
.expect("Failed to create hyprman runtime directory");
} }
// If the socket path from the config is relative, interpret it relative to hyprman_dir. // If the socket path from the config is relative, interpret it relative to hyprman_dir.
if !config.client_socket_path.starts_with("/") { if !config.client_socket_path.starts_with("/") {