From 3c51ad8685cb80956010d2dffd6fbb71e6bad18d Mon Sep 17 00:00:00 2001 From: Demmie <2e3s19@gmail.com> Date: Tue, 25 Apr 2023 01:25:14 -0400 Subject: [PATCH] Introduce anyhow errors --- Cargo.lock | 7 +++++ Cargo.toml | 1 + src/config.rs | 3 +- src/config/file_config.rs | 13 +++++---- src/main.rs | 8 ++---- src/report_client.rs | 18 ++++++------ src/watchers.rs | 6 ++-- src/watchers/gnome_idle.rs | 9 +++--- src/watchers/idle.rs | 6 ++-- src/watchers/kwin_window.rs | 27 +++++++++--------- src/watchers/wl_connection.rs | 14 +++++----- src/watchers/wl_foreign_toplevel.rs | 12 ++++---- src/watchers/wl_kwin_idle.rs | 5 ++-- src/watchers/x11_connection.rs | 42 +++++++++++++--------------- src/watchers/x11_screensaver_idle.rs | 9 +++--- src/watchers/x11_window.rs | 9 +++--- 16 files changed, 97 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0be5177..a46f656 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + [[package]] name = "async-broadcast" version = "0.5.1" @@ -216,6 +222,7 @@ dependencies = [ name = "awatcher" version = "0.1.0" dependencies = [ + "anyhow", "aw-client-rust", "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 5dea735..21c8e47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ dirs = "5.0.0" serde = { version = "1.0.160", features = ["derive"] } serde_default = "0.1.0" regex = "1.8.1" +anyhow = "1.0.70" diff --git a/src/config.rs b/src/config.rs index 2433ea2..0bf6c64 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,7 +2,6 @@ mod defaults; mod file_config; mod filters; -use crate::BoxedError; use clap::{arg, value_parser, ArgAction, Command}; use file_config::FileConfig; use std::{path::PathBuf, time::Duration}; @@ -22,7 +21,7 @@ pub struct Config { } impl Config { - pub fn from_cli() -> Result { + pub fn from_cli() -> anyhow::Result { let matches = Command::new("Activity Watcher") .version("0.1.0") .about("A set of ActivityWatch desktop watchers") diff --git a/src/config/file_config.rs b/src/config/file_config.rs index d5113e7..4b8949a 100644 --- a/src/config/file_config.rs +++ b/src/config/file_config.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Context}; use clap::{parser::ValueSource, ArgMatches}; use serde::Deserialize; use serde_default::DefaultFromSerde; @@ -7,7 +8,7 @@ use std::{ time::Duration, }; -use crate::{config::defaults, BoxedError}; +use crate::config::defaults; use super::filters::Filter; @@ -56,8 +57,9 @@ pub struct FileConfig { } impl FileConfig { - pub fn new(matches: &ArgMatches) -> Result { - let mut config_path: PathBuf = dirs::config_dir().ok_or("Config directory is unknown")?; + pub fn new(matches: &ArgMatches) -> anyhow::Result { + let mut config_path: PathBuf = + dirs::config_dir().ok_or(anyhow!("Config directory is unknown"))?; config_path.push("awatcher"); config_path.push("config.toml"); if matches.contains_id("config") { @@ -73,8 +75,9 @@ impl FileConfig { let mut config = if config_path.exists() { debug!("Reading config at {}", config_path.display()); - let config_content = std::fs::read_to_string(config_path) - .map_err(|e| format!("Impossible to read config file: {e}"))?; + let config_content = std::fs::read_to_string(&config_path).with_context(|| { + format!("Impossible to read config file {}", config_path.display()) + })?; toml::from_str(&config_content)? } else { diff --git a/src/main.rs b/src/main.rs index ec6cc6a..268d5ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,12 +11,10 @@ use config::Config; use fern::colors::{Color, ColoredLevelConfig}; use report_client::ReportClient; use std::env; -use std::{error::Error, str::FromStr, sync::Arc, thread}; +use std::{sync::Arc, thread}; use crate::watchers::ConstructorFilter; -type BoxedError = Box; - fn setup_logger() -> Result<(), fern::InitError> { let log_setting = env::var("AWATCHER_LOG").unwrap_or("info".to_string()); @@ -37,14 +35,14 @@ fn setup_logger() -> Result<(), fern::InitError> { .level(log::LevelFilter::Warn) .level_for( "awatcher", - FromStr::from_str(&log_setting).unwrap_or(log::LevelFilter::Info), + log_setting.parse().unwrap_or(log::LevelFilter::Info), ) .chain(std::io::stdout()) .apply()?; Ok(()) } -fn main() -> Result<(), BoxedError> { +fn main() -> anyhow::Result<()> { setup_logger()?; let client = ReportClient::new(Config::from_cli()?)?; diff --git a/src/report_client.rs b/src/report_client.rs index 0aaf369..809b234 100644 --- a/src/report_client.rs +++ b/src/report_client.rs @@ -1,7 +1,5 @@ -use std::error::Error; - -use super::BoxedError; use super::Config; +use anyhow::Context; use aw_client_rust::{AwClient, Event as AwEvent}; use chrono::{DateTime, Duration, Utc}; use serde_json::{Map, Value}; @@ -12,7 +10,7 @@ pub struct ReportClient { } impl ReportClient { - pub fn new(config: Config) -> Result { + pub fn new(config: Config) -> anyhow::Result { let client = AwClient::new(&config.host, &config.port.to_string(), "awatcher"); if !config.mock_server { @@ -28,7 +26,7 @@ impl ReportClient { is_idle: bool, timestamp: DateTime, duration: Duration, - ) -> Result<(), Box> { + ) -> anyhow::Result<()> { let mut data = Map::new(); data.insert( "status".to_string(), @@ -49,10 +47,10 @@ impl ReportClient { let pulsetime = (self.config.idle_timeout + self.config.poll_time_idle).as_secs_f64(); self.client .heartbeat(&self.config.idle_bucket_name, &event, pulsetime) - .map_err(|_| "Failed to send heartbeat".into()) + .with_context(|| "Failed to send heartbeat") } - pub fn send_active_window(&self, app_id: &str, title: &str) -> Result<(), BoxedError> { + pub fn send_active_window(&self, app_id: &str, title: &str) -> anyhow::Result<()> { let mut data = Map::new(); let mut data_insert = |k: &str, v: String| data.insert(k.to_string(), Value::String(v)); @@ -89,16 +87,16 @@ impl ReportClient { &event, interval_margin, ) - .map_err(|_| "Failed to send heartbeat for active window".into()) + .with_context(|| "Failed to send heartbeat for active window") } fn create_bucket( client: &AwClient, bucket_name: &str, bucket_type: &str, - ) -> Result<(), Box> { + ) -> anyhow::Result<()> { client .create_bucket_simple(bucket_name, bucket_type) - .map_err(|e| format!("Failed to create bucket {bucket_name}: {e}").into()) + .with_context(|| format!("Failed to create bucket {bucket_name}")) } } diff --git a/src/watchers.rs b/src/watchers.rs index 8dc0736..5dcae18 100644 --- a/src/watchers.rs +++ b/src/watchers.rs @@ -9,11 +9,11 @@ mod x11_connection; mod x11_screensaver_idle; mod x11_window; -use crate::{report_client::ReportClient, BoxedError}; +use crate::report_client::ReportClient; use std::sync::Arc; pub trait Watcher: Send { - fn new() -> Result + fn new() -> anyhow::Result where Self: Sized; fn watch(&mut self, client: &Arc); @@ -21,7 +21,7 @@ pub trait Watcher: Send { type BoxedWatcher = Box; -type WatcherConstructor = (&'static str, fn() -> Result); +type WatcherConstructor = (&'static str, fn() -> anyhow::Result); type WatcherConstructors = [WatcherConstructor]; pub trait ConstructorFilter { diff --git a/src/watchers/gnome_idle.rs b/src/watchers/gnome_idle.rs index f8442ca..1ae804e 100644 --- a/src/watchers/gnome_idle.rs +++ b/src/watchers/gnome_idle.rs @@ -1,5 +1,6 @@ use super::{idle, Watcher}; -use crate::{report_client::ReportClient, BoxedError}; +use crate::report_client::ReportClient; +use anyhow::Context; use std::{sync::Arc, thread}; use zbus::blocking::Connection; @@ -8,7 +9,7 @@ pub struct IdleWatcher { } impl idle::SinceLastInput for IdleWatcher { - fn seconds_since_input(&self) -> Result { + fn seconds_since_input(&self) -> anyhow::Result { let ms = self .dbus_connection .call_method( @@ -19,12 +20,12 @@ impl idle::SinceLastInput for IdleWatcher { &(), )? .body::()?; - u32::try_from(ms / 1000).map_err(|_| format!("Number {ms} is invalid").into()) + u32::try_from(ms / 1000).with_context(|| format!("Number {ms} is invalid")) } } impl Watcher for IdleWatcher { - fn new() -> Result { + fn new() -> anyhow::Result { let watcher = Self { dbus_connection: Connection::session()?, }; diff --git a/src/watchers/idle.rs b/src/watchers/idle.rs index fb1b861..4a34729 100644 --- a/src/watchers/idle.rs +++ b/src/watchers/idle.rs @@ -1,16 +1,16 @@ -use crate::{report_client::ReportClient, BoxedError}; +use crate::report_client::ReportClient; use chrono::{Duration, Utc}; use std::sync::Arc; pub trait SinceLastInput { - fn seconds_since_input(&self) -> Result; + fn seconds_since_input(&self) -> anyhow::Result; } pub fn ping_since_last_input( watcher: &impl SinceLastInput, is_idle: bool, client: &Arc, -) -> Result { +) -> anyhow::Result { // The logic is rewritten from the original Python code: // https://github.com/ActivityWatch/aw-watcher-afk/blob/ef531605cd8238e00138bbb980e5457054e05248/aw_watcher_afk/afk.py#L73 let duration_1ms: Duration = Duration::milliseconds(1); diff --git a/src/watchers/kwin_window.rs b/src/watchers/kwin_window.rs index 973c733..d4b2bb6 100644 --- a/src/watchers/kwin_window.rs +++ b/src/watchers/kwin_window.rs @@ -3,8 +3,9 @@ * For the moment of writing, KWin doesn't implement the appropriate protocols to get a top level window. * Inspired by https://github.com/k0kubun/xremap/ */ -use super::{BoxedError, Watcher}; +use super::Watcher; use crate::report_client::ReportClient; +use anyhow::{anyhow, Context}; use std::env::temp_dir; use std::path::Path; use std::sync::{mpsc::channel, Arc, Mutex}; @@ -28,7 +29,7 @@ impl KWinScript { } } - fn load(&mut self) -> Result<(), BoxedError> { + fn load(&mut self) -> anyhow::Result<()> { let path = temp_dir().join("kwin_window.js"); std::fs::write(&path, KWIN_SCRIPT).unwrap(); @@ -41,7 +42,7 @@ impl KWinScript { result } - fn is_loaded(&self) -> Result { + fn is_loaded(&self) -> anyhow::Result { self.dbus_connection .call_method( Some("org.kde.KWin"), @@ -54,10 +55,10 @@ impl KWinScript { .map_err(std::convert::Into::into) } - fn get_registered_number(&self, path: &Path) -> Result { + fn get_registered_number(&self, path: &Path) -> anyhow::Result { let temp_path = path .to_str() - .ok_or::("Temporary file path is not valid".into())?; + .ok_or(anyhow!("Temporary file path is not valid"))?; self.dbus_connection .call_method( @@ -72,7 +73,7 @@ impl KWinScript { .map_err(std::convert::Into::into) } - fn unload(&self) -> Result { + fn unload(&self) -> anyhow::Result { self.dbus_connection .call_method( Some("org.kde.KWin"), @@ -85,7 +86,7 @@ impl KWinScript { .map_err(std::convert::Into::into) } - fn start(&self, script_number: i32) -> Result<(), BoxedError> { + fn start(&self, script_number: i32) -> anyhow::Result<()> { debug!("Starting KWin script {script_number}"); self.dbus_connection .call_method( @@ -95,8 +96,8 @@ impl KWinScript { "run", &(), ) - .map_err(|e| format!("Error on starting the script {e}").into()) - .map(|_| ()) + .with_context(|| "Error on starting the script")?; + Ok(()) } } @@ -114,12 +115,12 @@ impl Drop for KWinScript { fn send_active_window( client: &ReportClient, active_window: &Arc>, -) -> Result<(), BoxedError> { - let active_window = active_window.lock().map_err(|e| format!("{e}"))?; +) -> anyhow::Result<()> { + let active_window = active_window.lock().expect("Lock cannot be acquired"); client .send_active_window(&active_window.resource_class, &active_window.caption) - .map_err(|_| "Failed to send heartbeat for active window".into()) + .with_context(|| "Failed to send heartbeat for active window") } struct ActiveWindow { @@ -153,7 +154,7 @@ pub struct WindowWatcher { } impl Watcher for WindowWatcher { - fn new() -> Result { + fn new() -> anyhow::Result { let kwin_script = KWinScript::new(Connection::session()?); if kwin_script.is_loaded()? { warn!("KWin script is already loaded, unloading"); diff --git a/src/watchers/wl_connection.rs b/src/watchers/wl_connection.rs index 16353b4..2fdb66a 100644 --- a/src/watchers/wl_connection.rs +++ b/src/watchers/wl_connection.rs @@ -1,5 +1,5 @@ use super::wl_bindings; -use crate::BoxedError; +use anyhow::Context; use wayland_client::{ globals::{registry_queue_init, GlobalList, GlobalListContents}, protocol::{wl_registry, wl_seat::WlSeat}, @@ -38,9 +38,9 @@ where + Dispatch + 'static, { - pub fn connect() -> Result { - let connection = - Connection::connect_to_env().map_err(|_| "Unable to connect to Wayland compositor")?; + pub fn connect() -> anyhow::Result { + let connection = Connection::connect_to_env() + .with_context(|| "Unable to connect to Wayland compositor")?; let display = connection.display(); let (globals, event_queue) = registry_queue_init::(&connection)?; @@ -55,7 +55,7 @@ where }) } - pub fn get_foreign_toplevel_manager(&self) -> Result + pub fn get_foreign_toplevel_manager(&self) -> anyhow::Result where T: Dispatch, { @@ -68,7 +68,7 @@ where .map_err(std::convert::Into::into) } - pub fn get_kwin_idle(&self) -> Result + pub fn get_kwin_idle(&self) -> anyhow::Result where T: Dispatch, { @@ -81,7 +81,7 @@ where .map_err(std::convert::Into::into) } - pub fn get_kwin_idle_timeout(&self, timeout: u32) -> Result + pub fn get_kwin_idle_timeout(&self, timeout: u32) -> anyhow::Result where T: Dispatch + Dispatch diff --git a/src/watchers/wl_foreign_toplevel.rs b/src/watchers/wl_foreign_toplevel.rs index 7864846..ed4c759 100644 --- a/src/watchers/wl_foreign_toplevel.rs +++ b/src/watchers/wl_foreign_toplevel.rs @@ -5,9 +5,9 @@ use super::wl_bindings::wlr_foreign_toplevel::zwlr_foreign_toplevel_manager_v1:: Event as ManagerEvent, ZwlrForeignToplevelManagerV1, EVT_TOPLEVEL_OPCODE, }; use super::wl_connection::WlEventConnection; -use super::BoxedError; use super::{wl_connection::subscribe_state, Watcher}; use crate::report_client::ReportClient; +use anyhow::{anyhow, Context}; use std::collections::HashMap; use std::{sync::Arc, thread}; use wayland_client::{ @@ -114,18 +114,18 @@ impl Dispatch for ToplevelState { } impl ToplevelState { - fn send_active_window(&self) -> Result<(), BoxedError> { + fn send_active_window(&self) -> anyhow::Result<()> { let active_window_id = self .current_window_id .as_ref() - .ok_or("Current window is unknown")?; - let active_window = self.windows.get(active_window_id).ok_or(format!( + .ok_or(anyhow!("Current window is unknown"))?; + let active_window = self.windows.get(active_window_id).ok_or(anyhow!( "Current window is not found by ID {active_window_id}" ))?; self.client .send_active_window(&active_window.app_id, &active_window.title) - .map_err(|_| "Failed to send heartbeat for active window".into()) + .with_context(|| "Failed to send heartbeat for active window") } } @@ -134,7 +134,7 @@ pub struct WindowWatcher { } impl Watcher for WindowWatcher { - fn new() -> Result { + fn new() -> anyhow::Result { let connection: WlEventConnection = WlEventConnection::connect()?; connection.get_foreign_toplevel_manager()?; diff --git a/src/watchers/wl_kwin_idle.rs b/src/watchers/wl_kwin_idle.rs index 58096ac..b823bf7 100644 --- a/src/watchers/wl_kwin_idle.rs +++ b/src/watchers/wl_kwin_idle.rs @@ -1,6 +1,5 @@ use super::wl_bindings; use super::wl_connection::{subscribe_state, WlEventConnection}; -use super::BoxedError; use super::Watcher; use crate::report_client::ReportClient; use chrono::{DateTime, Duration, Utc}; @@ -54,7 +53,7 @@ impl IdleState { debug!("Resumed"); } - fn send_ping(&mut self) -> Result<(), BoxedError> { + fn send_ping(&mut self) -> anyhow::Result<()> { let now = Utc::now(); if !self.is_idle { self.last_input_time = now; @@ -122,7 +121,7 @@ pub struct IdleWatcher { } impl Watcher for IdleWatcher { - fn new() -> Result { + fn new() -> anyhow::Result { let connection: WlEventConnection = WlEventConnection::connect()?; connection.get_kwin_idle()?; diff --git a/src/watchers/x11_connection.rs b/src/watchers/x11_connection.rs index 470bc43..3b8c248 100644 --- a/src/watchers/x11_connection.rs +++ b/src/watchers/x11_connection.rs @@ -1,13 +1,11 @@ -use std::{env, str}; - +use anyhow::{anyhow, bail, Context}; use log::warn; +use std::{env, str}; use x11rb::connection::Connection; use x11rb::protocol::screensaver::ConnectionExt as ScreensaverConnectionExt; use x11rb::protocol::xproto::{Atom, AtomEnum, ConnectionExt, GetPropertyReply, Window}; use x11rb::rust_connection::RustConnection; -use crate::BoxedError; - pub struct WindowData { pub title: String, pub app_id: String, @@ -19,7 +17,7 @@ pub struct X11Connection { } impl X11Connection { - pub fn new() -> Result { + pub fn new() -> anyhow::Result { if env::var("DISPLAY").is_err() { warn!("DISPLAY is not set, setting to the default value \":0\""); env::set_var("DISPLAY", ":0"); @@ -34,7 +32,7 @@ impl X11Connection { }) } - pub fn seconds_since_last_input(&self) -> Result { + pub fn seconds_since_last_input(&self) -> anyhow::Result { let reply = self .connection .screensaver_query_info(self.screen_root)? @@ -43,7 +41,7 @@ impl X11Connection { Ok(reply.ms_since_user_input / 1000) } - pub fn active_window_data(&self) -> Result { + pub fn active_window_data(&self) -> anyhow::Result { let focus: Window = self.find_active_window()?; let name = self.get_property( @@ -61,7 +59,7 @@ impl X11Connection { u32::MAX, )?; - let title = str::from_utf8(&name.value).map_err(|e| format!("Invalid title UTF: {e}"))?; + let title = str::from_utf8(&name.value).with_context(|| "Invalid title UTF")?; Ok(WindowData { title: title.to_string(), @@ -76,25 +74,25 @@ impl X11Connection { property_name: &str, property_type: Atom, long_length: u32, - ) -> Result { + ) -> anyhow::Result { self.connection .get_property(false, window, property, property_type, 0, long_length) - .map_err(|e| format!("GetPropertyRequest[{property_name}] failed: {e}"))? + .with_context(|| format!("GetPropertyRequest[{property_name}] failed"))? .reply() - .map_err(|e| format!("GetPropertyReply[{property_name}] failed: {e}").into()) + .with_context(|| format!("GetPropertyReply[{property_name}] failed")) } - fn intern_atom(&self, name: &str) -> Result { + fn intern_atom(&self, name: &str) -> anyhow::Result { Ok(self .connection .intern_atom(false, name.as_bytes()) - .map_err(|_| format!("InternAtomRequest[{name}] failed"))? + .with_context(|| format!("InternAtomRequest[{name}] failed"))? .reply() - .map_err(|_| format!("InternAtomReply[{name}] failed"))? + .with_context(|| format!("InternAtomReply[{name}] failed"))? .atom) } - fn find_active_window(&self) -> Result { + fn find_active_window(&self) -> anyhow::Result { let window: Atom = AtomEnum::WINDOW.into(); let net_active_window = self.intern_atom("_NET_ACTIVE_WINDOW")?; let active_window = self.get_property( @@ -108,25 +106,25 @@ impl X11Connection { if active_window.format == 32 && active_window.length == 1 { active_window .value32() - .ok_or("Invalid message. Expected value with format = 32")? + .ok_or(anyhow!("Invalid message. Expected value with format = 32"))? .next() - .ok_or("Active window is not found".into()) + .ok_or(anyhow!("Active window is not found")) } else { // Query the input focus Ok(self .connection .get_input_focus() - .map_err(|e| format!("Failed to get input focus: {e}"))? + .with_context(|| "Failed to get input focus")? .reply() - .map_err(|e| format!("Failed to read input focus from reply: {e}"))? + .with_context(|| "Failed to read input focus from reply")? .focus) } } } -fn parse_wm_class(property: &GetPropertyReply) -> Result<&str, BoxedError> { +fn parse_wm_class(property: &GetPropertyReply) -> anyhow::Result<&str> { if property.format != 8 { - return Err("Malformed property: wrong format".into()); + bail!("Malformed property: wrong format"); } let value = &property.value; // The property should contain two null-terminated strings. Find them. @@ -140,6 +138,6 @@ fn parse_wm_class(property: &GetPropertyReply) -> Result<&str, BoxedError> { } Ok(std::str::from_utf8(class)?) } else { - Err("Missing null byte".into()) + bail!("Missing null byte") } } diff --git a/src/watchers/x11_screensaver_idle.rs b/src/watchers/x11_screensaver_idle.rs index 6779bf7..8def1bf 100644 --- a/src/watchers/x11_screensaver_idle.rs +++ b/src/watchers/x11_screensaver_idle.rs @@ -1,20 +1,19 @@ -use std::{sync::Arc, thread}; - -use super::{idle, x11_connection::X11Connection, BoxedError, Watcher}; +use super::{idle, x11_connection::X11Connection, Watcher}; use crate::report_client::ReportClient; +use std::{sync::Arc, thread}; pub struct IdleWatcher { connection: X11Connection, } impl idle::SinceLastInput for IdleWatcher { - fn seconds_since_input(&self) -> Result { + fn seconds_since_input(&self) -> anyhow::Result { self.connection.seconds_since_last_input() } } impl Watcher for IdleWatcher { - fn new() -> Result { + fn new() -> anyhow::Result { let connection = X11Connection::new()?; // Check if screensaver extension is supported diff --git a/src/watchers/x11_window.rs b/src/watchers/x11_window.rs index f425af3..6aab7f9 100644 --- a/src/watchers/x11_window.rs +++ b/src/watchers/x11_window.rs @@ -1,5 +1,6 @@ -use super::{x11_connection::X11Connection, BoxedError, Watcher}; +use super::{x11_connection::X11Connection, Watcher}; use crate::report_client::ReportClient; +use anyhow::Context; use std::thread; pub struct WindowWatcher { @@ -9,7 +10,7 @@ pub struct WindowWatcher { } impl WindowWatcher { - fn send_active_window(&mut self, client: &ReportClient) -> Result<(), BoxedError> { + fn send_active_window(&mut self, client: &ReportClient) -> anyhow::Result<()> { let data = self.connection.active_window_data()?; if data.app_id != self.last_app_id || data.title != self.last_title { @@ -23,12 +24,12 @@ impl WindowWatcher { client .send_active_window(&self.last_app_id, &self.last_title) - .map_err(|_| "Failed to send heartbeat for active window".into()) + .with_context(|| "Failed to send heartbeat for active window") } } impl Watcher for WindowWatcher { - fn new() -> Result { + fn new() -> anyhow::Result { let connection = X11Connection::new()?; connection.active_window_data()?;