use crate::config::application::OnlyOrNot;
use crate::util::print_table;
use anyhow::Context;
use serde::{Deserialize, Serialize};
#[cfg(feature = "cosmic")]
mod cosmic_client;
#[cfg(feature = "cosmic")]
mod cosmic_protocols;
#[cfg(feature = "gnome")]
mod gnome_client;
#[cfg(feature = "hypr")]
mod hypr_client;
#[cfg(feature = "kde")]
mod kde;
#[cfg(feature = "niri")]
mod niri_client;
#[cfg(feature = "pantheon")]
mod pantheon_client;
#[cfg(feature = "socket")]
mod socket_client;
#[cfg(feature = "socket")]
mod socket_monitor;
#[cfg(feature = "wlroots")]
mod wlroots_client;
#[cfg(feature = "x11")]
mod x11_client;
pub mod null_client;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct WindowInfo {
pub app_class: Option<String>,
pub title: Option<String>,
pub winid: Option<String>,
}
pub trait Client {
fn supported(&mut self) -> bool;
fn current_application(&mut self) -> Option<String>;
fn current_window(&mut self) -> Option<String>;
fn run(&mut self, _command: &Vec<String>) -> anyhow::Result<bool> {
Ok(false)
}
fn window_list(&mut self) -> anyhow::Result<Vec<WindowInfo>>;
fn close_windows_by_app_class(&mut self, app_class: &str) -> anyhow::Result<()>;
}
pub struct WMClient {
pub name: String,
pub client: Box<dyn Client>,
supported: Option<bool>,
last_application: String,
last_window: String,
log_window_changes: bool,
application_cache: Option<String>,
title_cache: Option<String>,
}
impl WMClient {
pub fn new(name: &str, client: Box<dyn Client>, log_window_changes: bool) -> WMClient {
WMClient {
name: name.to_string(),
client,
supported: None,
last_application: String::new(),
last_window: String::new(),
log_window_changes,
application_cache: None,
title_cache: None,
}
}
fn check_supported(&mut self) -> Option<()> {
if self.supported.is_none() {
let supported = self.client.supported();
self.supported = Some(supported);
println!("application-client: {} (supported: {})", self.name, supported);
}
self.supported.unwrap().then_some(())
}
pub fn current_window(&mut self) -> Option<String> {
self.check_supported()?;
let result = self.client.current_window();
if self.log_window_changes {
if let Some(window) = &result {
if &self.last_window != window {
self.last_window = window.clone();
println!("window: {window}");
}
}
}
result
}
pub fn current_application(&mut self) -> Option<String> {
self.check_supported()?;
let result = self.client.current_application();
if self.log_window_changes {
if let Some(application) = &result {
if &self.last_application != application {
self.last_application = application.clone();
println!("application: {application}");
}
}
}
result
}
pub fn run(&mut self, command: &Vec<String>) -> anyhow::Result<bool> {
if self.check_supported().is_some() {
return self.client.run(command);
}
Ok(false)
}
pub fn window_list(&mut self) -> anyhow::Result<Vec<WindowInfo>> {
self.client.window_list()
}
pub fn close_windows_by_app_class(&mut self, app_class: &str) -> anyhow::Result<()> {
self.client
.close_windows_by_app_class(&app_class)
.context("Failed to close by app_class.")
}
pub fn clear_app_class_and_title(&mut self) {
self.application_cache = None; self.title_cache = None; }
pub fn match_window(&mut self, window_matcher: &OnlyOrNot) -> bool {
if self.title_cache.is_none() {
match self.current_window() {
Some(title) => self.title_cache = Some(title),
None => self.title_cache = Some(String::new()),
}
}
if let Some(title) = &self.title_cache {
if let Some(title_only) = &window_matcher.only {
return title_only.iter().any(|m| m.matches(title));
}
if let Some(title_not) = &window_matcher.not {
return title_not.iter().all(|m| !m.matches(title));
}
}
false
}
pub fn match_application(&mut self, application_matcher: &OnlyOrNot) -> bool {
if self.application_cache.is_none() {
match self.current_application() {
Some(application) => self.application_cache = Some(application),
None => self.application_cache = Some(String::new()),
}
}
if let Some(application) = &self.application_cache {
if let Some(application_only) = &application_matcher.only {
return application_only.iter().any(|m| m.matches(application));
}
if let Some(application_not) = &application_matcher.not {
return application_not.iter().all(|m| !m.matches(application));
}
}
false
}
}
pub fn build_client(log_window_changes: bool) -> WMClient {
let clients: Vec<WMClient> = vec![
#[cfg(feature = "gnome")]
WMClient::new("GNOME", Box::new(gnome_client::GnomeClient::new()), log_window_changes),
#[cfg(feature = "kde")]
WMClient::new("KDE", Box::new(kde::KdeClient::new(log_window_changes)), log_window_changes),
#[cfg(feature = "hypr")]
WMClient::new("Hypr", Box::new(hypr_client::HyprlandClient::new()), log_window_changes),
#[cfg(feature = "x11")]
WMClient::new("X11", Box::new(x11_client::X11Client::new()), log_window_changes),
#[cfg(feature = "wlroots")]
WMClient::new("wlroots", Box::new(wlroots_client::WlRootsClient::new()), log_window_changes),
#[cfg(feature = "niri")]
WMClient::new("Niri", Box::new(niri_client::NiriClient::new()), log_window_changes),
#[cfg(feature = "cosmic")]
WMClient::new("COSMIC", Box::new(cosmic_client::CosmicClient::new()), log_window_changes),
#[cfg(feature = "pantheon")]
WMClient::new("Pantheon", Box::new(pantheon_client::PantheonClient::new()), log_window_changes),
#[cfg(feature = "socket")]
WMClient::new("Socket", Box::new(socket_client::SocketClient::new()), log_window_changes),
#[cfg(feature = "device-test")]
WMClient::new("DeviceTest", Box::new(null_client::DeviceTestClient), log_window_changes),
];
if clients.len() == 0 {
WMClient::new("none", Box::new(null_client::NullClient), log_window_changes)
} else if clients.len() == 1 {
clients.into_iter().next().unwrap()
} else {
panic!("There is no way to run with multiple clients enabled.")
}
}
pub fn print_open_windows() -> anyhow::Result<()> {
let mut wmclient = build_client(false);
if !wmclient.client.supported() {
eprintln!("{} is not supported.", wmclient.name);
return Ok(());
}
let mut windows = wmclient.window_list()?;
windows.sort();
let mut table: Vec<Vec<String>> = vec![];
table.push(vec!["APP_CLASS".into(), "TITLE".into(), "WIN_ID".into()]);
for WindowInfo {
app_class,
winid,
title,
} in windows
{
table.push(vec![
app_class.unwrap_or_default(),
title.unwrap_or_default(),
winid.unwrap_or_default(),
]);
}
print_table(table);
Ok(())
}