Merge a20f09ef58d390e3dbe6f24b641cacb36caafcd2 into b4cf791a5efc3d561ecb96ac8cfaa831f9b76877

This commit is contained in:
powellnorma 2024-10-09 11:10:02 -04:00 committed by GitHub
commit 04ce01a1c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 101 additions and 47 deletions

View File

@ -41,18 +41,18 @@ impl ReportClient {
Fut: Future<Output = Result<T, E>>, Fut: Future<Output = Result<T, E>>,
E: std::error::Error + Send + Sync + 'static, E: std::error::Error + Send + Sync + 'static,
{ {
for (attempt, &secs) in [1, 2].iter().enumerate() { for (attempt, secs) in [0.01, 0.1, 1., 2.].iter().enumerate() {
match f().await { match f().await {
Ok(val) => return Ok(val), Ok(val) => {
Err(e) if attempt > 0 {
if e.to_string() debug!("OK at attempt #{}", attempt + 1);
.contains("tcp connect error: Connection refused") => }
{ return Ok(val);
warn!("Failed to connect on attempt #{attempt}, retrying: {}", e); }
Err(e) => {
tokio::time::sleep(tokio::time::Duration::from_secs(secs)).await; warn!("Failed on attempt #{}, retrying in {:.1}s: {}", attempt + 1, secs, e);
tokio::time::sleep(tokio::time::Duration::from_secs_f64(*secs)).await;
} }
Err(e) => return Err(e),
} }
} }
@ -94,6 +94,15 @@ impl ReportClient {
} }
pub async fn send_active_window(&self, app_id: &str, title: &str) -> anyhow::Result<()> { pub async fn send_active_window(&self, app_id: &str, title: &str) -> anyhow::Result<()> {
self.send_active_window_with_instance(app_id, title, None).await
}
pub async fn send_active_window_with_instance(
&self,
app_id: &str,
title: &str,
wm_instance: Option<&str>,
) -> anyhow::Result<()> {
let mut data = Map::new(); let mut data = Map::new();
let replacement = self.config.window_data_replacement(app_id, title); let replacement = self.config.window_data_replacement(app_id, title);
@ -116,6 +125,11 @@ impl ReportClient {
); );
data.insert("app".to_string(), Value::String(inserted_app_id)); data.insert("app".to_string(), Value::String(inserted_app_id));
data.insert("title".to_string(), Value::String(inserted_title)); data.insert("title".to_string(), Value::String(inserted_title));
if let Some(instance) = wm_instance {
data.insert("wm_instance".to_string(), Value::String(instance.to_string()));
}
let event = AwEvent { let event = AwEvent {
id: None, id: None,
timestamp: Utc::now(), timestamp: Utc::now(),

View File

@ -9,6 +9,7 @@ use x11rb::rust_connection::RustConnection;
pub struct WindowData { pub struct WindowData {
pub title: String, pub title: String,
pub app_id: String, pub app_id: String,
pub wm_instance: String,
} }
pub struct X11Client { pub struct X11Client {
@ -66,31 +67,38 @@ impl X11Client {
}) })
} }
pub fn active_window_data(&mut self) -> anyhow::Result<WindowData> { pub fn active_window_data(&mut self) -> anyhow::Result<Option<WindowData>> {
self.execute_with_reconnect(|client| { self.execute_with_reconnect(|client| {
let focus: Window = client.find_active_window()?; let focus = client.find_active_window()?;
let name = client.get_property( match focus {
focus, Some(window) => {
client.intern_atom("_NET_WM_NAME")?, let name = client.get_property(
"_NET_WM_NAME", window,
client.intern_atom("UTF8_STRING")?, client.intern_atom("_NET_WM_NAME")?,
u32::MAX, "_NET_WM_NAME",
)?; client.intern_atom("UTF8_STRING")?,
let class = client.get_property( u32::MAX,
focus, )?;
AtomEnum::WM_CLASS.into(), let class = client.get_property(
"WM_CLASS", window,
AtomEnum::STRING.into(), AtomEnum::WM_CLASS.into(),
u32::MAX, "WM_CLASS",
)?; AtomEnum::STRING.into(),
u32::MAX,
)?;
let title = str::from_utf8(&name.value).with_context(|| "Invalid title UTF")?; let title = str::from_utf8(&name.value).with_context(|| "Invalid title UTF")?;
let (instance, class) = parse_wm_class(&class)?;
Ok(WindowData { Ok(Some(WindowData {
title: title.to_string(), title: title.to_string(),
app_id: parse_wm_class(&class)?.to_string(), app_id: class,
}) wm_instance: instance,
}))
}
None => Ok(None),
}
}) })
} }
@ -119,7 +127,7 @@ impl X11Client {
.atom) .atom)
} }
fn find_active_window(&self) -> anyhow::Result<Window> { fn find_active_window(&self) -> anyhow::Result<Option<Window>> {
let window: Atom = AtomEnum::WINDOW.into(); let window: Atom = AtomEnum::WINDOW.into();
let net_active_window = self.intern_atom("_NET_ACTIVE_WINDOW")?; let net_active_window = self.intern_atom("_NET_ACTIVE_WINDOW")?;
let active_window = self.get_property( let active_window = self.get_property(
@ -131,39 +139,51 @@ impl X11Client {
)?; )?;
if active_window.format == 32 && active_window.length == 1 { if active_window.format == 32 && active_window.length == 1 {
active_window let window_id = active_window
.value32() .value32()
.ok_or(anyhow!("Invalid message. Expected value with format = 32"))? .ok_or(anyhow!("Invalid message. Expected value with format = 32"))?
.next() .next()
.ok_or(anyhow!("Active window is not found")) .ok_or(anyhow!("Active window is not found"))?;
// Check if the window_id is 0 (no active window)
if window_id == 0 {
return Ok(None);
}
Ok(Some(window_id))
} else { } else {
// Query the input focus // Query the input focus
Ok(self Ok(Some(self
.connection .connection
.get_input_focus() .get_input_focus()
.with_context(|| "Failed to get input focus")? .with_context(|| "Failed to get input focus")?
.reply() .reply()
.with_context(|| "Failed to read input focus from reply")? .with_context(|| "Failed to read input focus from reply")?
.focus) .focus))
} }
} }
} }
fn parse_wm_class(property: &GetPropertyReply) -> anyhow::Result<&str> { fn parse_wm_class(property: &GetPropertyReply) -> anyhow::Result<(String, String)> {
if property.format != 8 { if property.format != 8 {
bail!("Malformed property: wrong format"); bail!("Malformed property: wrong format");
} }
let value = &property.value; let value = &property.value;
// The property should contain two null-terminated strings. Find them. // The property should contain two null-terminated strings. Find them.
if let Some(middle) = value.iter().position(|&b| b == 0) { if let Some(middle) = value.iter().position(|&b| b == 0) {
let (_, class) = value.split_at(middle); let (instance, class) = value.split_at(middle);
// Skip the null byte at the beginning // Remove the null byte at the end of the instance
let instance = &instance[..instance.len()];
// Skip the null byte at the beginning of the class
let mut class = &class[1..]; let mut class = &class[1..];
// Remove the last null byte from the class, if it is there. // Remove the last null byte from the class, if it is there.
if class.last() == Some(&0) { if class.last() == Some(&0) {
class = &class[..class.len() - 1]; class = &class[..class.len() - 1];
} }
Ok(std::str::from_utf8(class)?) Ok((
std::str::from_utf8(instance)?.to_string(),
std::str::from_utf8(class)?.to_string(),
))
} else { } else {
bail!("Missing null byte") bail!("Missing null byte")
} }

View File

@ -6,25 +6,44 @@ use std::sync::Arc;
pub struct WindowWatcher { pub struct WindowWatcher {
client: X11Client, client: X11Client,
last_title: String,
last_app_id: String, last_app_id: String,
last_title: String,
last_wm_instance: String,
} }
impl WindowWatcher { impl WindowWatcher {
async fn send_active_window(&mut self, client: &ReportClient) -> anyhow::Result<()> { async fn send_active_window(&mut self, client: &ReportClient) -> anyhow::Result<()> {
let data = self.client.active_window_data()?; let data = self.client.active_window_data()?;
if data.app_id != self.last_app_id || data.title != self.last_title { let (app_id, title, wm_instance) = match data {
Some(window_data) => (
window_data.app_id,
window_data.title,
window_data.wm_instance,
),
None => {
// No active window, set all values to "aw-none"
("aw-none".to_string(), "aw-none".to_string(), "aw-none".to_string())
}
};
if app_id != self.last_app_id || title != self.last_title || wm_instance != self.last_wm_instance {
debug!( debug!(
r#"Changed window app_id="{}", title="{}""#, r#"Changed window app_id="{}", title="{}", wm_instance="{}""#,
data.app_id, data.title app_id, title, wm_instance
); );
self.last_app_id = data.app_id; client
self.last_title = data.title; .send_active_window_with_instance(&self.last_app_id, &self.last_title, Some(&self.last_wm_instance))
.await
.with_context(|| "Failed to send heartbeat for previous window")?;
self.last_app_id = app_id.clone();
self.last_title = title.clone();
self.last_wm_instance = wm_instance.clone();
} }
client client
.send_active_window(&self.last_app_id, &self.last_title) .send_active_window_with_instance(&app_id, &title, Some(&wm_instance))
.await .await
.with_context(|| "Failed to send heartbeat for active window") .with_context(|| "Failed to send heartbeat for active window")
} }
@ -40,6 +59,7 @@ impl Watcher for WindowWatcher {
client, client,
last_title: String::new(), last_title: String::new(),
last_app_id: String::new(), last_app_id: String::new(),
last_wm_instance: String::new(),
}) })
} }