From ba91e4e58cb796d76d58d2c10fe3247cbd06a71d Mon Sep 17 00:00:00 2001 From: mpuchstein Date: Thu, 6 Mar 2025 08:07:04 +0100 Subject: [PATCH] reached milestone for active window client --- hyprman/src/main.rs | 791 +++++++++++++++++++++++++++++--------------- 1 file changed, 532 insertions(+), 259 deletions(-) diff --git a/hyprman/src/main.rs b/hyprman/src/main.rs index 4254200..08aaf7c 100644 --- a/hyprman/src/main.rs +++ b/hyprman/src/main.rs @@ -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::{ collections::HashSet, env, @@ -10,218 +17,149 @@ use std::{ thread, 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)] #[serde(tag = "event", content = "data")] enum HyprlandEvent { - Workspace { workspace_name: String }, - WorkspaceV2 { workspace_id: u8, workspace_name: String }, - FocusedMon { monitor_name: String, workspace_name: String }, - FocusedMonV2 { monitor_name: String, workspace_id: u8 }, - ActiveWindow { window_class: String, window_title: String }, - ActiveWindowV2 { window_address: String }, - Fullscreen { status: u8 }, - MonitorRemoved { monitor_name: String }, - MonitorAdded { monitor_name: String }, - MonitorAddedV2 { monitor_id: u8, monitor_name: String, monitor_description: String }, - 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 }, - MoveIntoGroup { window_address: String }, - MoveOutOfGroup { window_address: String }, - IgnoreGroupLock { value: u8 }, - LockGroups { value: u8 }, + Workspace { + workspace_name: String, + }, + WorkspaceV2 { + workspace_id: u8, + workspace_name: String, + }, + FocusedMon { + monitor_name: String, + workspace_name: String, + }, + FocusedMonV2 { + monitor_name: String, + workspace_id: u8, + }, + ActiveWindow { + window_class: String, + window_title: String, + }, + ActiveWindowV2 { + window_address: String, + }, + Fullscreen { + status: u8, + }, + MonitorRemoved { + monitor_name: String, + }, + MonitorAdded { + monitor_name: String, + }, + MonitorAddedV2 { + monitor_id: u8, + monitor_name: String, + monitor_description: String, + }, + 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, + }, + MoveIntoGroup { + window_address: String, + }, + MoveOutOfGroup { + window_address: String, + }, + IgnoreGroupLock { + value: u8, + }, + LockGroups { + value: u8, + }, ConfigReloaded, - Pin { window_address: String, pin_state: u8 }, -} - -fn parse_event_line(line: &str) -> Result> { - 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::()?; - 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::()?; - 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::()?; - 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::()?; - 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::()?; - 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::()?; - 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::()?; - 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::()?; - 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::()?; - 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::()?; - 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::()?; - let owner = fields.next().ok_or("Missing owner")?.parse::()?; - 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::()?; - let window_addresses: Vec = 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::()?; - Ok(HyprlandEvent::IgnoreGroupLock { value }) - } - "lockgroups" => { - let value = data.parse::()?; - 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::()?; - Ok(HyprlandEvent::Pin { window_address, pin_state }) - } - _ => Err(format!("Unknown event type: {}", event_name).into()), - } + Pin { + window_address: String, + pin_state: u8, + }, } /// === 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> { + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + 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::()?; + let owner = fields.next().ok_or("Missing owner")?.parse::()?; + 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::()?; + let window_addresses: Vec = 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::()?; + Ok(HyprlandEvent::IgnoreGroupLock { value }) + } + "lockgroups" => { + let value = data.parse::()?; + 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::()?; + 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, + #[serde(skip_serializing_if = "Option::is_none")] + monitor_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + windows: Option, + #[serde(skip_serializing_if = "Option::is_none")] + has_fullscreen: Option, + #[serde(skip_serializing_if = "Option::is_none")] + last_window: Option, + #[serde(skip_serializing_if = "Option::is_none")] + last_window_title: Option, +} + +#[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, + tags: Vec, + swallowing: String, + #[serde(rename = "focusHistoryID")] + focus_history_id: i32, + inhibiting_idle: bool, +} + /// === Client Subscription Infrastructure === #[derive(Debug, Clone)] @@ -316,7 +564,8 @@ fn client_handler(stream: UnixStream, subscriptions: Arc return; } 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 } else { let filters: HashSet = subscription_line @@ -332,7 +581,10 @@ fn client_handler(stream: UnixStream, subscriptions: Arc { 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. @@ -401,9 +653,8 @@ fn hyprland_event_thread(subscriptions: Arc>>) { fn client_server_thread(client_socket_path: String, subscriptions: Arc>>) { // Remove existing socket file if present. let _ = fs::remove_file(&client_socket_path); - let listener = UnixListener::bind(&client_socket_path).unwrap_or_else(|e| { - panic!("Failed to bind client socket {}: {}", client_socket_path, e) - }); + let listener = UnixListener::bind(&client_socket_path) + .unwrap_or_else(|e| panic!("Failed to bind client socket {}: {}", client_socket_path, e)); info!("Client server listening on {}", client_socket_path); for stream in listener.incoming() { @@ -418,9 +669,8 @@ fn client_server_thread(client_socket_path: String, subscriptions: Arc UnixStream { - UnixStream::connect(socket_path).unwrap_or_else(|err| { - panic!("Could not connect to socket {}: {}", socket_path, err) - }) + UnixStream::connect(socket_path) + .unwrap_or_else(|err| panic!("Could not connect to socket {}: {}", socket_path, err)) } /// 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); 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); for line in reader.lines() { @@ -498,64 +751,84 @@ fn run_client(config: &Config, subscription: &str) { } } -fn run_activewindow_client(config: &Config) { - let hypr_rundir_path = get_hypr_rundir_path(); - info!("Using hypr runtime directory: {}", hypr_rundir_path); +/// Prints the active window as json - let socket1_path = format!("{}/.socket1.sock", hypr_rundir_path); - info!("Using hypr socket2 path: {}", socket1_path); - match UnixStream::connect(&config.client_socket_path) { +fn run_activewindow_client(config: &Config) { + let subscription_line = "activewindowv2,fullscreen,closewindow,movewindow,changefloatingmode,moveintogroup,moveoutofgroup,togglegroup,pin,windowtitle\n"; + info!("Using subscription line: {}", subscription_line); + let event_reader = match UnixStream::connect(&config.client_socket_path) { Ok(mut stream) => { - let subscription_line = "activewindowv2,openwindow,closewindow,movewindow\n"; if let Err(e) = stream.write_all(subscription_line.as_bytes()) { eprintln!("Failed to send subscription: {}", e); std::process::exit(1); } println!("Successfully connected to daemon."); - match UnixStream::connect(&socket1_path) { - 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); - } - - } + BufReader::new(stream) } Err(e) => { eprintln!("Failed to connect to daemon. Is it running? Error: {}", e); 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 { + 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 = 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 === fn stop_daemon() -> Result<(), Box> { // Compute pid file path from $XDG_RUNTIME_DIR/hyprman/ - let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR") - .expect("XDG_RUNTIME_DIR not set"); + let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set"); let hyprman_dir = format!("{}/hyprman", xdg_runtime_dir); let pid_file_path = format!("{}/hyprman.pid", hyprman_dir); let pid_str = fs::read_to_string(&pid_file_path)?; @@ -585,7 +858,9 @@ fn print_help() { println!(" -d, --daemon Run as daemon"); println!(" -r, --restart Restart 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"); } @@ -600,12 +875,10 @@ fn main() { env_logger::init(); // Ensure $XDG_RUNTIME_DIR/hyprman/ exists. - let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR") - .expect("XDG_RUNTIME_DIR not set"); + let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set"); let hyprman_dir = format!("{}/hyprman", xdg_runtime_dir); if fs::metadata(&hyprman_dir).is_err() { - fs::create_dir_all(&hyprman_dir) - .expect("Failed to create hyprman runtime directory"); + fs::create_dir_all(&hyprman_dir).expect("Failed to create hyprman runtime directory"); } // If the socket path from the config is relative, interpret it relative to hyprman_dir. if !config.client_socket_path.starts_with("/") {