made the manager daemonizable and added client mode
This commit is contained in:
@@ -6,3 +6,9 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
log = "0.4.26"
|
||||||
|
signal-hook = "0.3.17"
|
||||||
|
toml = "0.8.20"
|
||||||
|
env_logger = "0.11.6"
|
||||||
|
daemonize = "0.5.0"
|
||||||
|
libc = "0.2.170"
|
@@ -1,344 +1,133 @@
|
|||||||
use std::io::{BufRead, BufReader};
|
use std::{
|
||||||
use std::os::unix::net::UnixStream;
|
collections::HashSet,
|
||||||
use std::{env, thread};
|
env,
|
||||||
use std::error::Error;
|
error::Error,
|
||||||
use serde::{Serialize, Deserialize};
|
fs,
|
||||||
|
io::{BufRead, BufReader, BufWriter, Write},
|
||||||
|
os::unix::net::{UnixListener, UnixStream},
|
||||||
|
process::Command,
|
||||||
|
sync::{mpsc, Arc, Mutex},
|
||||||
|
thread,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
/// Represents a Hyprland event emitted from a UnixStream.
|
use daemonize::Daemonize;
|
||||||
/// Each variant corresponds to a specific event type with its associated data.
|
use log::{error, info};
|
||||||
/// The enum is annotated for JSON (de)serialization with serde.
|
use serde::{Deserialize, Serialize};
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
use signal_hook::{consts::TERM_SIGNALS, iterator::Signals};
|
||||||
|
|
||||||
|
/// === Hyprland Event Types and Parsing ===
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[serde(tag = "event", content = "data")]
|
#[serde(tag = "event", content = "data")]
|
||||||
enum HyprlandEvent {
|
enum HyprlandEvent {
|
||||||
/// Emitted on workspace change.
|
Workspace { workspace_name: String },
|
||||||
/// Triggered ONLY when a user explicitly requests a workspace change (not due to mouse movements).
|
WorkspaceV2 { workspace_id: u8, workspace_name: String },
|
||||||
/// Data: workspace_name.
|
FocusedMon { monitor_name: String, workspace_name: String },
|
||||||
Workspace {
|
FocusedMonV2 { monitor_name: String, workspace_id: u8 },
|
||||||
workspace_name: String,
|
ActiveWindow { window_class: String, window_title: String },
|
||||||
},
|
ActiveWindowV2 { window_address: String },
|
||||||
/// Emitted on workspace change (v2).
|
Fullscreen { status: u8 },
|
||||||
/// Triggered ONLY when a user explicitly requests a workspace change.
|
MonitorRemoved { monitor_name: String },
|
||||||
/// Data: workspace_id, workspace_name.
|
MonitorAdded { monitor_name: String },
|
||||||
WorkspaceV2 {
|
MonitorAddedV2 { monitor_id: u8, monitor_name: String, monitor_description: String },
|
||||||
workspace_id: u8,
|
CreateWorkspace { workspace_name: String },
|
||||||
workspace_name: String,
|
CreateWorkspaceV2 { workspace_id: u8, workspace_name: String },
|
||||||
},
|
DestroyWorkspace { workspace_name: String },
|
||||||
/// Emitted when the active monitor changes.
|
DestroyWorkspaceV2 { workspace_id: u8, workspace_name: String },
|
||||||
/// Data: monitor_name, workspace_name.
|
MoveWorkspace { workspace_name: String, monitor_name: String },
|
||||||
FocusedMon {
|
MoveWorkspaceV2 { workspace_id: u8, workspace_name: String, monitor_name: String },
|
||||||
monitor_name: String,
|
RenameWorkspace { workspace_id: u8, new_name: String },
|
||||||
workspace_name: String,
|
ActiveSpecial { workspace_name: String, monitor_name: String },
|
||||||
},
|
ActiveLayout { keyboard_name: String, layout_name: String },
|
||||||
/// Emitted when the active monitor changes (v2).
|
OpenWindow { window_address: String, workspace_name: String, window_class: String, window_title: String },
|
||||||
/// Data: monitor_name, workspace_id.
|
CloseWindow { window_address: String },
|
||||||
FocusedMonV2 {
|
MoveWindow { window_address: String, workspace_name: String },
|
||||||
monitor_name: String,
|
MoveWindowV2 { window_address: String, workspace_id: u8, workspace_name: String },
|
||||||
workspace_id: u8,
|
OpenLayer { namespace: String },
|
||||||
},
|
CloseLayer { namespace: String },
|
||||||
/// Emitted on active window change.
|
Submap { submap_name: String },
|
||||||
/// Data: window_class, window_title.
|
ChangeFloatingMode { window_address: String, floating: u8 },
|
||||||
ActiveWindow {
|
Urgent { window_address: String },
|
||||||
window_class: String,
|
Screencast { state: u8, owner: u8 },
|
||||||
window_title: String,
|
WindowTitle { window_address: String },
|
||||||
},
|
WindowTitleV2 { window_address: String, window_title: String },
|
||||||
/// Emitted on active window change (v2).
|
ToggleGroup { toggle_status: u8, window_addresses: Vec<String> },
|
||||||
/// Data: window_address.
|
MoveIntoGroup { window_address: String },
|
||||||
ActiveWindowV2 {
|
MoveOutOfGroup { window_address: String },
|
||||||
window_address: String,
|
IgnoreGroupLock { value: u8 },
|
||||||
},
|
LockGroups { value: u8 },
|
||||||
/// Emitted when a window enters or exits fullscreen mode.
|
|
||||||
/// Data: status (0 for exit fullscreen, 1 for enter fullscreen).
|
|
||||||
Fullscreen {
|
|
||||||
status: u8,
|
|
||||||
},
|
|
||||||
/// Emitted when a monitor is removed (disconnected).
|
|
||||||
/// Data: monitor_name.
|
|
||||||
MonitorRemoved {
|
|
||||||
monitor_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a monitor is added (connected).
|
|
||||||
/// Data: monitor_name.
|
|
||||||
MonitorAdded {
|
|
||||||
monitor_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a monitor is added (connected) (v2).
|
|
||||||
/// Data: monitor_id, monitor_name, monitor_description.
|
|
||||||
MonitorAddedV2 {
|
|
||||||
monitor_id: u8,
|
|
||||||
monitor_name: String,
|
|
||||||
monitor_description: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a workspace is created.
|
|
||||||
/// Data: workspace_name.
|
|
||||||
CreateWorkspace {
|
|
||||||
workspace_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a workspace is created (v2).
|
|
||||||
/// Data: workspace_id, workspace_name.
|
|
||||||
CreateWorkspaceV2 {
|
|
||||||
workspace_id: u8,
|
|
||||||
workspace_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a workspace is destroyed.
|
|
||||||
/// Data: workspace_name.
|
|
||||||
DestroyWorkspace {
|
|
||||||
workspace_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a workspace is destroyed (v2).
|
|
||||||
/// Data: workspace_id, workspace_name.
|
|
||||||
DestroyWorkspaceV2 {
|
|
||||||
workspace_id: u8,
|
|
||||||
workspace_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a workspace is moved to a different monitor.
|
|
||||||
/// Data: workspace_name, monitor_name.
|
|
||||||
MoveWorkspace {
|
|
||||||
workspace_name: String,
|
|
||||||
monitor_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a workspace is moved to a different monitor (v2).
|
|
||||||
/// Data: workspace_id, workspace_name, monitor_name.
|
|
||||||
MoveWorkspaceV2 {
|
|
||||||
workspace_id: u8,
|
|
||||||
workspace_name: String,
|
|
||||||
monitor_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a workspace is renamed.
|
|
||||||
/// Data: workspace_id, new_name.
|
|
||||||
RenameWorkspace {
|
|
||||||
workspace_id: u8,
|
|
||||||
new_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when the special workspace opened in a monitor changes.
|
|
||||||
/// Data: workspace_name, monitor_name.
|
|
||||||
ActiveSpecial {
|
|
||||||
workspace_name: String,
|
|
||||||
monitor_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when the layout of the active keyboard changes.
|
|
||||||
/// Data: keyboard_name, layout_name.
|
|
||||||
ActiveLayout {
|
|
||||||
keyboard_name: String,
|
|
||||||
layout_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a window is opened.
|
|
||||||
/// Data: window_address, workspace_name, window_class, window_title.
|
|
||||||
OpenWindow {
|
|
||||||
window_address: String,
|
|
||||||
workspace_name: String,
|
|
||||||
window_class: String,
|
|
||||||
window_title: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a window is closed.
|
|
||||||
/// Data: window_address.
|
|
||||||
CloseWindow {
|
|
||||||
window_address: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a window is moved to a workspace.
|
|
||||||
/// Data: window_address, workspace_name.
|
|
||||||
MoveWindow {
|
|
||||||
window_address: String,
|
|
||||||
workspace_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a window is moved to a workspace (v2).
|
|
||||||
/// Data: window_address, workspace_id, workspace_name.
|
|
||||||
MoveWindowV2 {
|
|
||||||
window_address: String,
|
|
||||||
workspace_id: u8,
|
|
||||||
workspace_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a layerSurface is mapped.
|
|
||||||
/// Data: namespace.
|
|
||||||
OpenLayer {
|
|
||||||
namespace: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a layerSurface is unmapped.
|
|
||||||
/// Data: namespace.
|
|
||||||
CloseLayer {
|
|
||||||
namespace: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a keybind submap changes.
|
|
||||||
/// Data: submap_name.
|
|
||||||
Submap {
|
|
||||||
submap_name: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a window changes its floating mode.
|
|
||||||
/// Data: window_address, floating (0 for non‑floating, 1 for floating).
|
|
||||||
ChangeFloatingMode {
|
|
||||||
window_address: String,
|
|
||||||
floating: u8,
|
|
||||||
},
|
|
||||||
/// Emitted when a window requests an urgent state.
|
|
||||||
/// Data: window_address.
|
|
||||||
Urgent {
|
|
||||||
window_address: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a screencast state changes.
|
|
||||||
/// Data: state (0 or 1), owner (0 for monitor share, 1 for window share).
|
|
||||||
Screencast {
|
|
||||||
state: u8,
|
|
||||||
owner: u8,
|
|
||||||
},
|
|
||||||
/// Emitted when a window title changes.
|
|
||||||
/// Data: window_address.
|
|
||||||
WindowTitle {
|
|
||||||
window_address: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a window title changes (v2).
|
|
||||||
/// Data: window_address, window_title.
|
|
||||||
WindowTitleV2 {
|
|
||||||
window_address: String,
|
|
||||||
window_title: String,
|
|
||||||
},
|
|
||||||
/// Emitted when the togglegroup command is used.
|
|
||||||
/// Data: toggle_status (0 means group destroyed, 1 means group exists),
|
|
||||||
/// and window_addresses (one or more window addresses).
|
|
||||||
ToggleGroup {
|
|
||||||
toggle_status: u8,
|
|
||||||
window_addresses: Vec<String>,
|
|
||||||
},
|
|
||||||
/// Emitted when a window is merged into a group.
|
|
||||||
/// Data: window_address.
|
|
||||||
MoveIntoGroup {
|
|
||||||
window_address: String,
|
|
||||||
},
|
|
||||||
/// Emitted when a window is removed from a group.
|
|
||||||
/// Data: window_address.
|
|
||||||
MoveOutOfGroup {
|
|
||||||
window_address: String,
|
|
||||||
},
|
|
||||||
/// Emitted when ignoregrouplock is toggled.
|
|
||||||
/// Data: value (0 or 1).
|
|
||||||
IgnoreGroupLock {
|
|
||||||
value: u8,
|
|
||||||
},
|
|
||||||
/// Emitted when lockgroups is toggled.
|
|
||||||
/// Data: value (0 or 1).
|
|
||||||
LockGroups {
|
|
||||||
value: u8,
|
|
||||||
},
|
|
||||||
/// Emitted when the configuration is done reloading.
|
|
||||||
/// No data.
|
|
||||||
ConfigReloaded,
|
ConfigReloaded,
|
||||||
/// Emitted when a window is pinned or unpinned.
|
Pin { window_address: String, pin_state: u8 },
|
||||||
/// Data: window_address, pin_state (0 or 1).
|
|
||||||
Pin {
|
|
||||||
window_address: String,
|
|
||||||
pin_state: u8,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a single event line from the Hyprland stream into a `HyprlandEvent`.
|
|
||||||
///
|
|
||||||
/// The expected format is:
|
|
||||||
///
|
|
||||||
/// EVENT>>DATA\n
|
|
||||||
///
|
|
||||||
/// where DATA may be a single value or a comma‑separated list of fields.
|
|
||||||
/// For example:
|
|
||||||
/// - `"workspace>>Development"`
|
|
||||||
/// - `"workspacev2>>2,Development"`
|
|
||||||
///
|
|
||||||
/// Returns an error if the event type is unknown or if required fields are missing/cannot be parsed.
|
|
||||||
fn parse_event_line(line: &str) -> Result<HyprlandEvent, Box<dyn Error>> {
|
fn parse_event_line(line: &str) -> Result<HyprlandEvent, Box<dyn Error>> {
|
||||||
// Trim whitespace and any newline characters.
|
|
||||||
let line = line.trim();
|
let line = line.trim();
|
||||||
// Split into event name and data using ">>" as delimiter.
|
|
||||||
let mut parts = line.split(">>");
|
let mut parts = line.split(">>");
|
||||||
let event_name = parts.next().ok_or("Missing event name")?;
|
let event_name = parts.next().ok_or("Missing event name")?;
|
||||||
let data = parts.next().unwrap_or("").trim();
|
let data = parts.next().unwrap_or("").trim();
|
||||||
|
|
||||||
match event_name {
|
match event_name {
|
||||||
"workspace" => {
|
"workspace" => Ok(HyprlandEvent::Workspace { workspace_name: data.to_string() }),
|
||||||
// Data: WORKSPACENAME
|
|
||||||
Ok(HyprlandEvent::Workspace {
|
|
||||||
workspace_name: data.to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"workspacev2" => {
|
"workspacev2" => {
|
||||||
// Data: WORKSPACEID,WORKSPACENAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
|
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 workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
||||||
Ok(HyprlandEvent::WorkspaceV2 { workspace_id, workspace_name })
|
Ok(HyprlandEvent::WorkspaceV2 { workspace_id, workspace_name })
|
||||||
}
|
}
|
||||||
"focusedmon" => {
|
"focusedmon" => {
|
||||||
// Data: MONITORNAME,WORKSPACENAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
|
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
|
||||||
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
||||||
Ok(HyprlandEvent::FocusedMon { monitor_name, workspace_name })
|
Ok(HyprlandEvent::FocusedMon { monitor_name, workspace_name })
|
||||||
}
|
}
|
||||||
"focusedmonv2" => {
|
"focusedmonv2" => {
|
||||||
// Data: MONITORNAME,WORKSPACEID
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
|
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
|
||||||
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
|
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
|
||||||
Ok(HyprlandEvent::FocusedMonV2 { monitor_name, workspace_id })
|
Ok(HyprlandEvent::FocusedMonV2 { monitor_name, workspace_id })
|
||||||
}
|
}
|
||||||
"activewindow" => {
|
"activewindow" => {
|
||||||
// Data: WINDOWCLASS,WINDOWTITLE
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let window_class = fields.next().ok_or("Missing window_class")?.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();
|
let window_title = fields.next().ok_or("Missing window_title")?.to_string();
|
||||||
Ok(HyprlandEvent::ActiveWindow { window_class, window_title })
|
Ok(HyprlandEvent::ActiveWindow { window_class, window_title })
|
||||||
}
|
}
|
||||||
"activewindowv2" => {
|
"activewindowv2" => Ok(HyprlandEvent::ActiveWindowV2 { window_address: data.to_string() }),
|
||||||
// Data: WINDOWADDRESS
|
|
||||||
Ok(HyprlandEvent::ActiveWindowV2 { window_address: data.to_string() })
|
|
||||||
}
|
|
||||||
"fullscreen" => {
|
"fullscreen" => {
|
||||||
// Data: 0/1
|
|
||||||
let status = data.parse::<u8>()?;
|
let status = data.parse::<u8>()?;
|
||||||
Ok(HyprlandEvent::Fullscreen { status })
|
Ok(HyprlandEvent::Fullscreen { status })
|
||||||
}
|
}
|
||||||
"monitorremoved" => {
|
"monitorremoved" => Ok(HyprlandEvent::MonitorRemoved { monitor_name: data.to_string() }),
|
||||||
// Data: MONITORNAME
|
"monitoradded" => Ok(HyprlandEvent::MonitorAdded { monitor_name: data.to_string() }),
|
||||||
Ok(HyprlandEvent::MonitorRemoved { monitor_name: data.to_string() })
|
|
||||||
}
|
|
||||||
"monitoradded" => {
|
|
||||||
// Data: MONITORNAME
|
|
||||||
Ok(HyprlandEvent::MonitorAdded { monitor_name: data.to_string() })
|
|
||||||
}
|
|
||||||
"monitoraddedv2" => {
|
"monitoraddedv2" => {
|
||||||
// Data: MONITORID,MONITORNAME,MONITORDESCRIPTION
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let monitor_id = fields.next().ok_or("Missing monitor_id")?.parse::<u8>()?;
|
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_name = fields.next().ok_or("Missing monitor_name")?.to_string();
|
||||||
let monitor_description = fields.next().ok_or("Missing monitor_description")?.to_string();
|
let monitor_description = fields.next().ok_or("Missing monitor_description")?.to_string();
|
||||||
Ok(HyprlandEvent::MonitorAddedV2 { monitor_id, monitor_name, monitor_description })
|
Ok(HyprlandEvent::MonitorAddedV2 { monitor_id, monitor_name, monitor_description })
|
||||||
}
|
}
|
||||||
"createworkspace" => {
|
"createworkspace" => Ok(HyprlandEvent::CreateWorkspace { workspace_name: data.to_string() }),
|
||||||
// Data: WORKSPACENAME
|
|
||||||
Ok(HyprlandEvent::CreateWorkspace { workspace_name: data.to_string() })
|
|
||||||
}
|
|
||||||
"createworkspacev2" => {
|
"createworkspacev2" => {
|
||||||
// Data: WORKSPACEID,WORKSPACENAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
|
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 workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
||||||
Ok(HyprlandEvent::CreateWorkspaceV2 { workspace_id, workspace_name })
|
Ok(HyprlandEvent::CreateWorkspaceV2 { workspace_id, workspace_name })
|
||||||
}
|
}
|
||||||
"destroyworkspace" => {
|
"destroyworkspace" => Ok(HyprlandEvent::DestroyWorkspace { workspace_name: data.to_string() }),
|
||||||
// Data: WORKSPACENAME
|
|
||||||
Ok(HyprlandEvent::DestroyWorkspace { workspace_name: data.to_string() })
|
|
||||||
}
|
|
||||||
"destroyworkspacev2" => {
|
"destroyworkspacev2" => {
|
||||||
// Data: WORKSPACEID,WORKSPACENAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
|
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 workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
||||||
Ok(HyprlandEvent::DestroyWorkspaceV2 { workspace_id, workspace_name })
|
Ok(HyprlandEvent::DestroyWorkspaceV2 { workspace_id, workspace_name })
|
||||||
}
|
}
|
||||||
"moveworkspace" => {
|
"moveworkspace" => {
|
||||||
// Data: WORKSPACENAME,MONITORNAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
||||||
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
|
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
|
||||||
Ok(HyprlandEvent::MoveWorkspace { workspace_name, monitor_name })
|
Ok(HyprlandEvent::MoveWorkspace { workspace_name, monitor_name })
|
||||||
}
|
}
|
||||||
"moveworkspacev2" => {
|
"moveworkspacev2" => {
|
||||||
// Data: WORKSPACEID,WORKSPACENAME,MONITORNAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
|
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 workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
||||||
@@ -346,28 +135,24 @@ fn parse_event_line(line: &str) -> Result<HyprlandEvent, Box<dyn Error>> {
|
|||||||
Ok(HyprlandEvent::MoveWorkspaceV2 { workspace_id, workspace_name, monitor_name })
|
Ok(HyprlandEvent::MoveWorkspaceV2 { workspace_id, workspace_name, monitor_name })
|
||||||
}
|
}
|
||||||
"renameworkspace" => {
|
"renameworkspace" => {
|
||||||
// Data: WORKSPACEID,NEWNAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
|
let workspace_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
|
||||||
let new_name = fields.next().ok_or("Missing new_name")?.to_string();
|
let new_name = fields.next().ok_or("Missing new_name")?.to_string();
|
||||||
Ok(HyprlandEvent::RenameWorkspace { workspace_id, new_name })
|
Ok(HyprlandEvent::RenameWorkspace { workspace_id, new_name })
|
||||||
}
|
}
|
||||||
"activespecial" => {
|
"activespecial" => {
|
||||||
// Data: WORKSPACENAME,MONITORNAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
||||||
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
|
let monitor_name = fields.next().ok_or("Missing monitor_name")?.to_string();
|
||||||
Ok(HyprlandEvent::ActiveSpecial { workspace_name, monitor_name })
|
Ok(HyprlandEvent::ActiveSpecial { workspace_name, monitor_name })
|
||||||
}
|
}
|
||||||
"activelayout" => {
|
"activelayout" => {
|
||||||
// Data: KEYBOARDNAME,LAYOUTNAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let keyboard_name = fields.next().ok_or("Missing keyboard_name")?.to_string();
|
let keyboard_name = fields.next().ok_or("Missing keyboard_name")?.to_string();
|
||||||
let layout_name = fields.next().ok_or("Missing layout_name")?.to_string();
|
let layout_name = fields.next().ok_or("Missing layout_name")?.to_string();
|
||||||
Ok(HyprlandEvent::ActiveLayout { keyboard_name, layout_name })
|
Ok(HyprlandEvent::ActiveLayout { keyboard_name, layout_name })
|
||||||
}
|
}
|
||||||
"openwindow" => {
|
"openwindow" => {
|
||||||
// Data: WINDOWADDRESS,WORKSPACENAME,WINDOWCLASS,WINDOWTITLE
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
|
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 workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
||||||
@@ -375,97 +160,61 @@ fn parse_event_line(line: &str) -> Result<HyprlandEvent, Box<dyn Error>> {
|
|||||||
let window_title = fields.next().ok_or("Missing window_title")?.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 })
|
Ok(HyprlandEvent::OpenWindow { window_address, workspace_name, window_class, window_title })
|
||||||
}
|
}
|
||||||
"closewindow" => {
|
"closewindow" => Ok(HyprlandEvent::CloseWindow { window_address: data.to_string() }),
|
||||||
// Data: WINDOWADDRESS
|
|
||||||
Ok(HyprlandEvent::CloseWindow { window_address: data.to_string() })
|
|
||||||
}
|
|
||||||
"movewindow" => {
|
"movewindow" => {
|
||||||
// Data: WINDOWADDRESS,WORKSPACENAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
|
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 workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
||||||
Ok(HyprlandEvent::MoveWindow { window_address, workspace_name })
|
Ok(HyprlandEvent::MoveWindow { window_address, workspace_name })
|
||||||
}
|
}
|
||||||
"movewindowv2" => {
|
"movewindowv2" => {
|
||||||
// Data: WINDOWADDRESS,WORKSPACEID,WORKSPACENAME
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
|
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_id = fields.next().ok_or("Missing workspace_id")?.parse::<u8>()?;
|
||||||
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
let workspace_name = fields.next().ok_or("Missing workspace_name")?.to_string();
|
||||||
Ok(HyprlandEvent::MoveWindowV2 { window_address, workspace_id, workspace_name })
|
Ok(HyprlandEvent::MoveWindowV2 { window_address, workspace_id, workspace_name })
|
||||||
}
|
}
|
||||||
"openlayer" => {
|
"openlayer" => Ok(HyprlandEvent::OpenLayer { namespace: data.to_string() }),
|
||||||
// Data: NAMESPACE
|
"closelayer" => Ok(HyprlandEvent::CloseLayer { namespace: data.to_string() }),
|
||||||
Ok(HyprlandEvent::OpenLayer { namespace: data.to_string() })
|
"submap" => Ok(HyprlandEvent::Submap { submap_name: data.to_string() }),
|
||||||
}
|
|
||||||
"closelayer" => {
|
|
||||||
// Data: NAMESPACE
|
|
||||||
Ok(HyprlandEvent::CloseLayer { namespace: data.to_string() })
|
|
||||||
}
|
|
||||||
"submap" => {
|
|
||||||
// Data: SUBMAPNAME
|
|
||||||
Ok(HyprlandEvent::Submap { submap_name: data.to_string() })
|
|
||||||
}
|
|
||||||
"changefloatingmode" => {
|
"changefloatingmode" => {
|
||||||
// Data: WINDOWADDRESS,FLOATING
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
|
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
|
||||||
let floating = fields.next().ok_or("Missing floating")?.parse::<u8>()?;
|
let floating = fields.next().ok_or("Missing floating")?.parse::<u8>()?;
|
||||||
Ok(HyprlandEvent::ChangeFloatingMode { window_address, floating })
|
Ok(HyprlandEvent::ChangeFloatingMode { window_address, floating })
|
||||||
}
|
}
|
||||||
"urgent" => {
|
"urgent" => Ok(HyprlandEvent::Urgent { window_address: data.to_string() }),
|
||||||
// Data: WINDOWADDRESS
|
|
||||||
Ok(HyprlandEvent::Urgent { window_address: data.to_string() })
|
|
||||||
}
|
|
||||||
"screencast" => {
|
"screencast" => {
|
||||||
// Data: STATE,OWNER
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let state = fields.next().ok_or("Missing state")?.parse::<u8>()?;
|
let state = fields.next().ok_or("Missing state")?.parse::<u8>()?;
|
||||||
let owner = fields.next().ok_or("Missing owner")?.parse::<u8>()?;
|
let owner = fields.next().ok_or("Missing owner")?.parse::<u8>()?;
|
||||||
Ok(HyprlandEvent::Screencast { state, owner })
|
Ok(HyprlandEvent::Screencast { state, owner })
|
||||||
}
|
}
|
||||||
"windowtitle" => {
|
"windowtitle" => Ok(HyprlandEvent::WindowTitle { window_address: data.to_string() }),
|
||||||
// Data: WINDOWADDRESS
|
|
||||||
Ok(HyprlandEvent::WindowTitle { window_address: data.to_string() })
|
|
||||||
}
|
|
||||||
"windowtitlev2" => {
|
"windowtitlev2" => {
|
||||||
// Data: WINDOWADDRESS,WINDOWTITLE
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
|
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
|
||||||
let window_title = fields.next().ok_or("Missing window_title")?.to_string();
|
let window_title = fields.next().ok_or("Missing window_title")?.to_string();
|
||||||
Ok(HyprlandEvent::WindowTitleV2 { window_address, window_title })
|
Ok(HyprlandEvent::WindowTitleV2 { window_address, window_title })
|
||||||
}
|
}
|
||||||
"togglegroup" => {
|
"togglegroup" => {
|
||||||
// Data: TOGGLE_STATUS,WINDOWADDRESS(ES)
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let toggle_status = fields.next().ok_or("Missing toggle_status")?.parse::<u8>()?;
|
let toggle_status = fields.next().ok_or("Missing toggle_status")?.parse::<u8>()?;
|
||||||
let window_addresses: Vec<String> = fields.map(|s| s.to_string()).collect();
|
let window_addresses: Vec<String> = fields.map(|s| s.to_string()).collect();
|
||||||
Ok(HyprlandEvent::ToggleGroup { toggle_status, window_addresses })
|
Ok(HyprlandEvent::ToggleGroup { toggle_status, window_addresses })
|
||||||
}
|
}
|
||||||
"moveintogroup" => {
|
"moveintogroup" => Ok(HyprlandEvent::MoveIntoGroup { window_address: data.to_string() }),
|
||||||
// Data: WINDOWADDRESS
|
"moveoutofgroup" => Ok(HyprlandEvent::MoveOutOfGroup { window_address: data.to_string() }),
|
||||||
Ok(HyprlandEvent::MoveIntoGroup { window_address: data.to_string() })
|
|
||||||
}
|
|
||||||
"moveoutofgroup" => {
|
|
||||||
// Data: WINDOWADDRESS
|
|
||||||
Ok(HyprlandEvent::MoveOutOfGroup { window_address: data.to_string() })
|
|
||||||
}
|
|
||||||
"ignoregrouplock" => {
|
"ignoregrouplock" => {
|
||||||
// Data: VALUE
|
|
||||||
let value = data.parse::<u8>()?;
|
let value = data.parse::<u8>()?;
|
||||||
Ok(HyprlandEvent::IgnoreGroupLock { value })
|
Ok(HyprlandEvent::IgnoreGroupLock { value })
|
||||||
}
|
}
|
||||||
"lockgroups" => {
|
"lockgroups" => {
|
||||||
// Data: VALUE
|
|
||||||
let value = data.parse::<u8>()?;
|
let value = data.parse::<u8>()?;
|
||||||
Ok(HyprlandEvent::LockGroups { value })
|
Ok(HyprlandEvent::LockGroups { value })
|
||||||
}
|
}
|
||||||
"configreloaded" => {
|
"configreloaded" => Ok(HyprlandEvent::ConfigReloaded),
|
||||||
// No data.
|
|
||||||
Ok(HyprlandEvent::ConfigReloaded)
|
|
||||||
}
|
|
||||||
"pin" => {
|
"pin" => {
|
||||||
// Data: WINDOWADDRESS,PINSTATE
|
|
||||||
let mut fields = data.split(',');
|
let mut fields = data.split(',');
|
||||||
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
|
let window_address = fields.next().ok_or("Missing window_address")?.to_string();
|
||||||
let pin_state = fields.next().ok_or("Missing pin_state")?.parse::<u8>()?;
|
let pin_state = fields.next().ok_or("Missing pin_state")?.parse::<u8>()?;
|
||||||
@@ -474,51 +223,400 @@ fn parse_event_line(line: &str) -> Result<HyprlandEvent, Box<dyn Error>> {
|
|||||||
_ => Err(format!("Unknown event type: {}", event_name).into()),
|
_ => Err(format!("Unknown event type: {}", event_name).into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Helper to fetch environment variables or panic with a clear message.
|
|
||||||
fn get_env_var(var: &str) -> String {
|
/// === Utility: Extract event type string for filtering ===
|
||||||
env::var(var).unwrap_or_else(|_| panic!("Environment variable {} is not set", var))
|
|
||||||
|
fn event_type(event: &HyprlandEvent) -> &'static str {
|
||||||
|
match event {
|
||||||
|
HyprlandEvent::Workspace { .. } => "workspace",
|
||||||
|
HyprlandEvent::WorkspaceV2 { .. } => "workspacev2",
|
||||||
|
HyprlandEvent::FocusedMon { .. } => "focusedmon",
|
||||||
|
HyprlandEvent::FocusedMonV2 { .. } => "focusedmonv2",
|
||||||
|
HyprlandEvent::ActiveWindow { .. } => "activewindow",
|
||||||
|
HyprlandEvent::ActiveWindowV2 { .. } => "activewindowv2",
|
||||||
|
HyprlandEvent::Fullscreen { .. } => "fullscreen",
|
||||||
|
HyprlandEvent::MonitorRemoved { .. } => "monitorremoved",
|
||||||
|
HyprlandEvent::MonitorAdded { .. } => "monitoradded",
|
||||||
|
HyprlandEvent::MonitorAddedV2 { .. } => "monitoraddedv2",
|
||||||
|
HyprlandEvent::CreateWorkspace { .. } => "createworkspace",
|
||||||
|
HyprlandEvent::CreateWorkspaceV2 { .. } => "createworkspacev2",
|
||||||
|
HyprlandEvent::DestroyWorkspace { .. } => "destroyworkspace",
|
||||||
|
HyprlandEvent::DestroyWorkspaceV2 { .. } => "destroyworkspacev2",
|
||||||
|
HyprlandEvent::MoveWorkspace { .. } => "moveworkspace",
|
||||||
|
HyprlandEvent::MoveWorkspaceV2 { .. } => "moveworkspacev2",
|
||||||
|
HyprlandEvent::RenameWorkspace { .. } => "renameworkspace",
|
||||||
|
HyprlandEvent::ActiveSpecial { .. } => "activespecial",
|
||||||
|
HyprlandEvent::ActiveLayout { .. } => "activelayout",
|
||||||
|
HyprlandEvent::OpenWindow { .. } => "openwindow",
|
||||||
|
HyprlandEvent::CloseWindow { .. } => "closewindow",
|
||||||
|
HyprlandEvent::MoveWindow { .. } => "movewindow",
|
||||||
|
HyprlandEvent::MoveWindowV2 { .. } => "movewindowv2",
|
||||||
|
HyprlandEvent::OpenLayer { .. } => "openlayer",
|
||||||
|
HyprlandEvent::CloseLayer { .. } => "closelayer",
|
||||||
|
HyprlandEvent::Submap { .. } => "submap",
|
||||||
|
HyprlandEvent::ChangeFloatingMode { .. } => "changefloatingmode",
|
||||||
|
HyprlandEvent::Urgent { .. } => "urgent",
|
||||||
|
HyprlandEvent::Screencast { .. } => "screencast",
|
||||||
|
HyprlandEvent::WindowTitle { .. } => "windowtitle",
|
||||||
|
HyprlandEvent::WindowTitleV2 { .. } => "windowtitlev2",
|
||||||
|
HyprlandEvent::ToggleGroup { .. } => "togglegroup",
|
||||||
|
HyprlandEvent::MoveIntoGroup { .. } => "moveintogroup",
|
||||||
|
HyprlandEvent::MoveOutOfGroup { .. } => "moveoutofgroup",
|
||||||
|
HyprlandEvent::IgnoreGroupLock { .. } => "ignoregrouplock",
|
||||||
|
HyprlandEvent::LockGroups { .. } => "lockgroups",
|
||||||
|
HyprlandEvent::ConfigReloaded => "configreloaded",
|
||||||
|
HyprlandEvent::Pin { .. } => "pin",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// === Client Subscription Infrastructure ===
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Subscription {
|
||||||
|
All,
|
||||||
|
Filtered(HashSet<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ClientHandle {
|
||||||
|
sender: mpsc::Sender<HyprlandEvent>,
|
||||||
|
subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// === Configuration Loading ===
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Config {
|
||||||
|
// Socket path where clients connect to receive events.
|
||||||
|
// If relative, it will be interpreted relative to $XDG_RUNTIME_DIR/hyprman/
|
||||||
|
client_socket_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_config(path: &str) -> Config {
|
||||||
|
let content = fs::read_to_string(path).expect("Failed to read config file");
|
||||||
|
toml::from_str(&content).expect("Failed to parse config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// === Daemon Mode Functions ===
|
||||||
|
|
||||||
|
fn client_handler(stream: UnixStream, subscriptions: Arc<Mutex<Vec<ClientHandle>>>) {
|
||||||
|
let mut reader = BufReader::new(stream.try_clone().expect("Failed to clone stream"));
|
||||||
|
let mut writer = BufWriter::new(stream);
|
||||||
|
// Read a line from the client to get subscription preferences.
|
||||||
|
let mut subscription_line = String::new();
|
||||||
|
if let Err(e) = reader.read_line(&mut subscription_line) {
|
||||||
|
error!("Failed to read subscription from client: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let subscription_line = subscription_line.trim();
|
||||||
|
let subscription = if subscription_line.is_empty() || subscription_line.to_lowercase() == "all" {
|
||||||
|
Subscription::All
|
||||||
|
} else {
|
||||||
|
let filters: HashSet<String> = subscription_line
|
||||||
|
.split(',')
|
||||||
|
.map(|s| s.trim().to_lowercase())
|
||||||
|
.collect();
|
||||||
|
Subscription::Filtered(filters)
|
||||||
|
};
|
||||||
|
info!("Client subscribed to: {:?}", subscription);
|
||||||
|
|
||||||
|
// Create a channel for sending events to this client.
|
||||||
|
let (tx, rx) = mpsc::channel::<HyprlandEvent>();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut subs = subscriptions.lock().unwrap();
|
||||||
|
subs.push(ClientHandle { sender: tx, subscription });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop and write events to the client.
|
||||||
|
loop {
|
||||||
|
match rx.recv() {
|
||||||
|
Ok(event) => {
|
||||||
|
let json = serde_json::to_string(&event).unwrap();
|
||||||
|
if let Err(e) = writeln!(writer, "{}", json) {
|
||||||
|
error!("Failed to write to client: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Err(e) = writer.flush() {
|
||||||
|
error!("Failed to flush writer: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Channel error: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hyprland_event_thread(subscriptions: Arc<Mutex<Vec<ClientHandle>>>) {
|
||||||
|
let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR")
|
||||||
|
.unwrap_or_else(|_| panic!("Environment variable XDG_RUNTIME_DIR is not set"));
|
||||||
|
let hypr_instance_signature = env::var("HYPRLAND_INSTANCE_SIGNATURE")
|
||||||
|
.unwrap_or_else(|_| panic!("Environment variable HYPRLAND_INSTANCE_SIGNATURE is not set"));
|
||||||
|
let hypr_rundir_path = format!("{}/hypr/{}", xdg_runtime_dir, hypr_instance_signature);
|
||||||
|
info!("Using hypr runtime directory: {}", hypr_rundir_path);
|
||||||
|
|
||||||
|
let socket2_path = format!("{}/.socket2.sock", hypr_rundir_path);
|
||||||
|
info!("Using hypr socket2 path: {}", socket2_path);
|
||||||
|
let socket2 = create_socket(&socket2_path);
|
||||||
|
let reader = BufReader::new(socket2);
|
||||||
|
|
||||||
|
for line in reader.lines() {
|
||||||
|
match line {
|
||||||
|
Ok(line_content) => {
|
||||||
|
match parse_event_line(&line_content) {
|
||||||
|
Ok(event) => {
|
||||||
|
let event_name = event_type(&event);
|
||||||
|
let json = serde_json::to_string(&event).unwrap();
|
||||||
|
info!("Received event: {}", json);
|
||||||
|
let mut subs = subscriptions.lock().unwrap();
|
||||||
|
// Dispatch events to matching clients.
|
||||||
|
subs.retain(|client| {
|
||||||
|
let send_result = match &client.subscription {
|
||||||
|
Subscription::All => client.sender.send(event.clone()),
|
||||||
|
Subscription::Filtered(filters) => {
|
||||||
|
if filters.contains(&event_name.to_string()) {
|
||||||
|
client.sender.send(event.clone())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
send_result.is_ok()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => error!("Error parsing event '{}': {}", line_content, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => error!("Error reading line: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn client_server_thread(client_socket_path: String, subscriptions: Arc<Mutex<Vec<ClientHandle>>>) {
|
||||||
|
// 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)
|
||||||
|
});
|
||||||
|
info!("Client server listening on {}", client_socket_path);
|
||||||
|
|
||||||
|
for stream in listener.incoming() {
|
||||||
|
match stream {
|
||||||
|
Ok(stream) => {
|
||||||
|
let subs = subscriptions.clone();
|
||||||
|
thread::spawn(move || client_handler(stream, subs));
|
||||||
|
}
|
||||||
|
Err(e) => error!("Failed to accept client connection: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a UnixStream from a given socket path.
|
|
||||||
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).unwrap_or_else(|err| {
|
||||||
panic!("Could not connect to socket {}: {}", socket_path, err)
|
panic!("Could not connect to socket {}: {}", socket_path, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles a single event string: parses it and prints its JSON representation.
|
/// The main daemon functionality: spawn threads, handle signals, etc.
|
||||||
fn handle_event(event_str: String) {
|
fn run_daemon(config: Config) {
|
||||||
match parse_event_line(&event_str) {
|
// Global subscription registry.
|
||||||
Ok(event) => {
|
let subscriptions = Arc::new(Mutex::new(Vec::<ClientHandle>::new()));
|
||||||
let json = serde_json::to_string(&event).unwrap();
|
|
||||||
println!("{}", json);
|
// Setup signal handling for graceful shutdown.
|
||||||
|
let mut signals = Signals::new(TERM_SIGNALS).expect("Unable to setup signal handling");
|
||||||
|
let signals_handle = signals.handle();
|
||||||
|
let shutdown_flag = Arc::new(Mutex::new(false));
|
||||||
|
{
|
||||||
|
let shutdown_flag = shutdown_flag.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
for signal in signals.forever() {
|
||||||
|
info!("Received termination signal: {}", signal);
|
||||||
|
*shutdown_flag.lock().unwrap() = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => eprintln!("Error parsing event '{}': {}", event_str, e),
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn thread to read and dispatch Hyprland events.
|
||||||
|
let subs_clone = subscriptions.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
hyprland_event_thread(subs_clone);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spawn thread to accept client connections.
|
||||||
|
let client_socket_path = config.client_socket_path;
|
||||||
|
let subs_clone = subscriptions.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
client_server_thread(client_socket_path, subs_clone);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Main thread waits for shutdown.
|
||||||
|
loop {
|
||||||
|
if *shutdown_flag.lock().unwrap() {
|
||||||
|
info!("Shutting down daemon");
|
||||||
|
signals_handle.close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// === Client Mode Function ===
|
||||||
|
/// Accepts a subscription filter (e.g. "all" or "activewindow")
|
||||||
|
fn run_client(config: &Config, subscription: &str) {
|
||||||
|
match UnixStream::connect(&config.client_socket_path) {
|
||||||
|
Ok(mut stream) => {
|
||||||
|
// Send subscription preferences.
|
||||||
|
let subscription_line = format!("{}\n", subscription);
|
||||||
|
if let Err(e) = stream.write_all(subscription_line.as_bytes()) {
|
||||||
|
eprintln!("Failed to send subscription: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
println!("Subscribed to '{}' events. Waiting for events...", subscription);
|
||||||
|
|
||||||
|
let reader = BufReader::new(stream);
|
||||||
|
for line in reader.lines() {
|
||||||
|
match line {
|
||||||
|
Ok(msg) => println!("{}", msg),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error reading from daemon: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to connect to daemon. Is it running? Error: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// === Daemon Control Functions ===
|
||||||
|
|
||||||
|
fn stop_daemon() -> Result<(), Box<dyn Error>> {
|
||||||
|
// 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 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)?;
|
||||||
|
let pid: i32 = pid_str.trim().parse()?;
|
||||||
|
unsafe {
|
||||||
|
if libc::kill(pid, libc::SIGTERM) != 0 {
|
||||||
|
return Err(format!("Failed to kill process {}", pid).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs::remove_file(&pid_file_path)?;
|
||||||
|
println!("Daemon stopped.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restart_daemon() -> Result<(), Box<dyn Error>> {
|
||||||
|
stop_daemon()?;
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
let current_exe = env::current_exe()?;
|
||||||
|
Command::new(current_exe).arg("-d").spawn()?;
|
||||||
|
println!("Daemon restarted.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print usage help text.
|
||||||
|
fn print_help() {
|
||||||
|
println!("Usage:");
|
||||||
|
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!(" -h, --help Show this help message");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// === Main Entry Point: Mode Selection Based on Command‑Line Arguments ===
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let xdg_runtime_dir = get_env_var("XDG_RUNTIME_DIR");
|
// Load configuration from $XDG_CONFIG_HOME/hyprman/config.toml
|
||||||
let hypr_instance_signature = get_env_var("HYPRLAND_INSTANCE_SIGNATURE");
|
let config_dir = env::var("XDG_CONFIG_HOME")
|
||||||
|
.unwrap_or_else(|_| panic!("Environment variable XDG_CONFIG_HOME is not set"));
|
||||||
|
let config_path = format!("{}/hyprman/config.toml", config_dir);
|
||||||
|
let mut config = load_config(&config_path);
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
let hypr_rundir_path = format!("{}/hypr/{}", xdg_runtime_dir, hypr_instance_signature);
|
// Ensure $XDG_RUNTIME_DIR/hyprman/ exists.
|
||||||
println!("Using hypr runtime directory: {}", hypr_rundir_path);
|
let xdg_runtime_dir = env::var("XDG_RUNTIME_DIR")
|
||||||
|
.expect("XDG_RUNTIME_DIR not set");
|
||||||
let socket1_path = format!("{}/.socket.sock", hypr_rundir_path);
|
let hyprman_dir = format!("{}/hyprman", xdg_runtime_dir);
|
||||||
let socket2_path = format!("{}/.socket2.sock", hypr_rundir_path);
|
if fs::metadata(&hyprman_dir).is_err() {
|
||||||
println!("Using socket1 path: {}", socket1_path);
|
fs::create_dir_all(&hyprman_dir)
|
||||||
println!("Using socket2 path: {}", socket2_path);
|
.expect("Failed to create hyprman runtime directory");
|
||||||
|
|
||||||
// let socket1 = create_socket(socket1_path);
|
|
||||||
let socket2 = create_socket(&socket2_path);
|
|
||||||
let stream = BufReader::new(socket2);
|
|
||||||
for line in stream.lines() {
|
|
||||||
match line {
|
|
||||||
Ok(line_content) => {
|
|
||||||
// Spawn a thread to handle the event. The closure takes ownership of `line_content`.
|
|
||||||
thread::spawn(move || handle_event(line_content));
|
|
||||||
}
|
}
|
||||||
Err(e) => eprintln!("Error reading line: {}", e),
|
// If the socket path from the config is relative, interpret it relative to hyprman_dir.
|
||||||
|
if !config.client_socket_path.starts_with("/") {
|
||||||
|
config.client_socket_path = format!("{}/{}", hyprman_dir, config.client_socket_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also, compute the PID file path to be used.
|
||||||
|
let pid_file_path = format!("{}/hyprman.pid", hyprman_dir);
|
||||||
|
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() > 1 {
|
||||||
|
match args[1].as_str() {
|
||||||
|
"-d" | "--daemon" => {
|
||||||
|
// Check if daemon is already running.
|
||||||
|
if let Ok(pid_str) = fs::read_to_string(&pid_file_path) {
|
||||||
|
if let Ok(pid) = pid_str.trim().parse::<i32>() {
|
||||||
|
if unsafe { libc::kill(pid, 0) } == 0 {
|
||||||
|
eprintln!("Daemon already running with PID {}.", pid);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let daemonize = Daemonize::new()
|
||||||
|
.pid_file(&pid_file_path)
|
||||||
|
.working_directory("/")
|
||||||
|
.umask(0o022)
|
||||||
|
.privileged_action(|| {
|
||||||
|
info!("Daemon started successfully");
|
||||||
|
});
|
||||||
|
if let Err(e) = daemonize.start() {
|
||||||
|
eprintln!("Error daemonizing: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
run_daemon(config);
|
||||||
|
}
|
||||||
|
"-r" | "--restart" => {
|
||||||
|
if let Err(e) = restart_daemon() {
|
||||||
|
eprintln!("Error restarting daemon: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"-k" | "--kill" => {
|
||||||
|
if let Err(e) = stop_daemon() {
|
||||||
|
eprintln!("Error stopping daemon: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"-f" | "--filter" => {
|
||||||
|
// Client mode with a subscription filter.
|
||||||
|
let query = if args.len() > 2 {
|
||||||
|
args[2].clone()
|
||||||
|
} else {
|
||||||
|
"all".to_string()
|
||||||
|
};
|
||||||
|
run_client(&config, &query);
|
||||||
|
}
|
||||||
|
"-h" | "--help" => {
|
||||||
|
print_help();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("Unknown option.");
|
||||||
|
print_help();
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No arguments provided: run as client with "all" subscription.
|
||||||
|
run_client(&config, "all");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user