1use anyhow::Context;
2use comfy_table::{Table, presets::UTF8_FULL};
3use core_types::{EventKind, SecurityLevel};
4use owo_colors::OwoColorize;
5use std::time::Duration;
6
7use crate::ipc::{connect, rpc};
8
9pub(crate) async fn cmd_wm_list() -> anyhow::Result<()> {
10 let client = connect().await?;
11
12 match rpc(&client, EventKind::WmListWindows, SecurityLevel::Internal).await? {
13 EventKind::WmListWindowsResponse { windows } => {
14 if windows.is_empty() {
15 println!("{}", "No windows tracked.".dimmed());
16 return Ok(());
17 }
18
19 let mut table = Table::new();
20 table.load_preset(UTF8_FULL);
21 table.set_header(vec!["ID", "App", "Title", "Focused"]);
22
23 for w in &windows {
24 let focused = if w.is_focused {
25 "yes".green().to_string()
26 } else {
27 "".to_string()
28 };
29 table.add_row(vec![
30 &w.id.to_string(),
31 &w.app_id.to_string(),
32 &w.title,
33 &focused,
34 ]);
35 }
36
37 println!("{table}");
38 }
39 other => anyhow::bail!("unexpected response: {other:?}"),
40 }
41
42 Ok(())
43}
44
45pub(crate) async fn cmd_wm_switch(backward: bool) -> anyhow::Result<()> {
46 let client = connect().await?;
47
48 let windows = match rpc(&client, EventKind::WmListWindows, SecurityLevel::Internal).await? {
50 EventKind::WmListWindowsResponse { windows } => windows,
51 other => anyhow::bail!("unexpected response: {other:?}"),
52 };
53
54 if windows.is_empty() {
55 println!("{}", "No windows to switch to.".dimmed());
56 return Ok(());
57 }
58
59 if windows.len() <= 1 {
63 tracing::debug!("only one window open, nothing to switch to");
64 return Ok(());
65 }
66 let target_idx = if backward { windows.len() - 1 } else { 1 };
67
68 let target_id = windows[target_idx].id.to_string();
69
70 match rpc(
71 &client,
72 EventKind::WmActivateWindow {
73 window_id: target_id.clone(),
74 },
75 SecurityLevel::Internal,
76 )
77 .await?
78 {
79 EventKind::WmActivateWindowResponse { success: true } => {
80 println!(
81 "Switched to: {} ({})",
82 windows[target_idx].title.green(),
83 windows[target_idx].app_id,
84 );
85 }
86 EventKind::WmActivateWindowResponse { success: false } => {
87 anyhow::bail!("failed to activate window '{target_id}'");
88 }
89 other => anyhow::bail!("unexpected response: {other:?}"),
90 }
91
92 Ok(())
93}
94
95pub(crate) async fn cmd_wm_focus(window_id: &str) -> anyhow::Result<()> {
96 let client = connect().await?;
97
98 match rpc(
99 &client,
100 EventKind::WmActivateWindow {
101 window_id: window_id.to_owned(),
102 },
103 SecurityLevel::Internal,
104 )
105 .await?
106 {
107 EventKind::WmActivateWindowResponse { success: true } => {
108 println!("Focused window: {}", window_id.green());
109 }
110 EventKind::WmActivateWindowResponse { success: false } => {
111 anyhow::bail!("window '{window_id}' not found");
112 }
113 other => anyhow::bail!("unexpected response: {other:?}"),
114 }
115
116 Ok(())
117}
118
119pub(crate) async fn cmd_wm_overlay(launcher: bool, backward: bool) -> anyhow::Result<()> {
120 let variant = match (launcher, backward) {
121 (true, true) => "overlay-launcher-backward",
122 (true, false) => "overlay-launcher",
123 (false, true) => "overlay-backward",
124 (false, false) => "overlay",
125 };
126
127 if try_send_fast_path(variant) {
129 return Ok(());
130 }
131
132 let client = connect().await?;
134 let event = match variant {
135 "overlay-launcher" => EventKind::WmActivateOverlayLauncher,
136 "overlay-launcher-backward" => EventKind::WmActivateOverlayLauncherBackward,
137 "overlay-backward" => EventKind::WmActivateOverlayBackward,
138 _ => EventKind::WmActivateOverlay,
139 };
140 client
141 .publish(event, SecurityLevel::Internal)
142 .await
143 .map_err(|e| anyhow::anyhow!("{e}"))?;
144
145 spawn_resident();
147
148 client.shutdown().await;
149 Ok(())
150}
151
152fn try_send_fast_path(variant: &str) -> bool {
156 let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") else {
157 return false;
158 };
159 let pid_path = format!("{runtime_dir}/pds/wm-fast.pid");
160 let sock_path = format!("{runtime_dir}/pds/wm-fast.sock");
161
162 let Ok(pid_content) = std::fs::read_to_string(&pid_path) else {
164 return false;
165 };
166 let Ok(pid) = pid_content.trim().parse::<i32>() else {
167 return false;
168 };
169
170 if unsafe { libc::kill(pid, 0) } != 0 {
172 let _ = std::fs::remove_file(&pid_path);
173 let _ = std::fs::remove_file(&sock_path);
174 return false;
175 }
176
177 let Ok(sock) = std::os::unix::net::UnixDatagram::unbound() else {
179 return false;
180 };
181 sock.send_to(variant.as_bytes(), &sock_path).is_ok()
182}
183
184fn spawn_resident() {
186 let Ok(exe) = std::env::current_exe() else {
187 return;
188 };
189 let _ = std::process::Command::new(exe)
190 .args(["wm", "overlay-resident"])
191 .stdin(std::process::Stdio::null())
192 .stdout(std::process::Stdio::null())
193 .stderr(std::process::Stdio::null())
194 .spawn();
195}
196
197pub(crate) async fn cmd_wm_overlay_resident() -> anyhow::Result<()> {
201 let runtime_dir = std::env::var("XDG_RUNTIME_DIR").context("XDG_RUNTIME_DIR not set")?;
202 let pds_dir = format!("{runtime_dir}/pds");
203 let pid_path = format!("{pds_dir}/wm-fast.pid");
204 let sock_path = format!("{pds_dir}/wm-fast.sock");
205
206 if let Ok(existing_pid) = std::fs::read_to_string(&pid_path)
208 && let Ok(pid) = existing_pid.trim().parse::<i32>()
209 && unsafe { libc::kill(pid, 0) } == 0
210 {
211 return Ok(());
212 }
213
214 std::fs::write(&pid_path, std::process::id().to_string())?;
216
217 let _ = std::fs::remove_file(&sock_path);
219 let dgram = tokio::net::UnixDatagram::bind(&sock_path)?;
220
221 #[cfg(unix)]
222 {
223 use std::os::unix::fs::PermissionsExt;
224 std::fs::set_permissions(&sock_path, std::fs::Permissions::from_mode(0o600))?;
225 }
226
227 let client = connect().await?;
229
230 let idle_timeout = Duration::from_secs(300);
232 let mut buf = [0u8; 64];
233
234 loop {
235 match tokio::time::timeout(idle_timeout, dgram.recv(&mut buf)).await {
236 Ok(Ok(n)) => {
237 let variant = std::str::from_utf8(&buf[..n]).unwrap_or("");
238 let event = match variant {
239 "overlay" => EventKind::WmActivateOverlay,
240 "overlay-backward" => EventKind::WmActivateOverlayBackward,
241 "overlay-launcher" => EventKind::WmActivateOverlayLauncher,
242 "overlay-launcher-backward" => EventKind::WmActivateOverlayLauncherBackward,
243 _ => continue,
244 };
245 if client
246 .publish(event, SecurityLevel::Internal)
247 .await
248 .is_err()
249 {
250 break;
251 }
252 }
253 Ok(Err(_)) => break,
254 Err(_) => break, }
256 }
257
258 let _ = std::fs::remove_file(&sock_path);
260 let _ = std::fs::remove_file(&pid_path);
261 client.shutdown().await;
262 Ok(())
263}