Unify idle logic

This commit is contained in:
Demmie 2024-05-16 15:36:04 -04:00
parent f960dd6697
commit ea215b432d
No known key found for this signature in database
GPG Key ID: B06DAA3D432C6E9A
7 changed files with 210 additions and 249 deletions

View File

@ -1,16 +1,17 @@
use super::{gnome_wayland::load_watcher, gnome_wayland::GnomeWatcher, idle, Watcher};
use super::{gnome_wayland::load_watcher, idle, Watcher};
use crate::report_client::ReportClient;
use anyhow::Context;
use async_trait::async_trait;
use chrono::Duration;
use std::sync::Arc;
use zbus::Connection;
pub struct IdleWatcher {
dbus_connection: Connection,
is_idle: bool,
idle_state: idle::State,
}
impl idle::SinceLastInput for IdleWatcher {
impl IdleWatcher {
async fn seconds_since_input(&mut self) -> anyhow::Result<u32> {
let ms = self
.dbus_connection
@ -27,26 +28,26 @@ impl idle::SinceLastInput for IdleWatcher {
}
}
impl GnomeWatcher for IdleWatcher {
async fn load() -> anyhow::Result<Self> {
let mut watcher = Self {
dbus_connection: Connection::session().await?,
is_idle: false,
};
idle::SinceLastInput::seconds_since_input(&mut watcher).await?;
Ok(watcher)
}
}
#[async_trait]
impl Watcher for IdleWatcher {
async fn new(_: &Arc<ReportClient>) -> anyhow::Result<Self> {
load_watcher().await
async fn new(client: &Arc<ReportClient>) -> anyhow::Result<Self> {
let duration = Duration::from_std(client.config.idle_timeout).unwrap();
load_watcher(|| async move {
let mut watcher = Self {
dbus_connection: Connection::session().await?,
idle_state: idle::State::new(duration),
};
watcher.seconds_since_input().await?;
Ok(watcher)
})
.await
}
async fn run_iteration(&mut self, client: &Arc<ReportClient>) -> anyhow::Result<()> {
self.is_idle = idle::ping_since_last_input(self, self.is_idle, client).await?;
let seconds = self.seconds_since_input().await?;
self.idle_state
.send_with_last_input(seconds, client)
.await?;
Ok(())
}

View File

@ -1,9 +1,4 @@
// The extension may not be loaded and available right away in Gnome, this mod will retry a few times.
pub trait GnomeWatcher {
async fn load() -> anyhow::Result<Self>
where
Self: Sized;
}
use std::future::Future;
fn is_gnome() -> bool {
if let Ok(de) = std::env::var("XDG_CURRENT_DESKTOP") {
@ -21,12 +16,16 @@ fn is_wayland() -> bool {
.contains("wayland")
}
pub async fn load_watcher<T: GnomeWatcher>() -> anyhow::Result<T> {
pub async fn load_watcher<T, F, Fut>(loader: F) -> anyhow::Result<T>
where
F: Fn() -> Fut,
Fut: Future<Output = anyhow::Result<T>>,
{
if is_gnome() && is_wayland() {
debug!("Gnome Wayland detected");
let mut watcher = Err(anyhow::anyhow!(""));
for _ in 0..3 {
watcher = T::load().await;
watcher = loader().await;
if let Err(e) = &watcher {
debug!("Failed to load Gnome Wayland watcher: {e}");
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
@ -35,6 +34,6 @@ pub async fn load_watcher<T: GnomeWatcher>() -> anyhow::Result<T> {
watcher
} else {
T::load().await
loader().await
}
}

View File

@ -5,7 +5,7 @@ use serde::Deserialize;
use std::sync::Arc;
use zbus::Connection;
use super::{gnome_wayland::load_watcher, gnome_wayland::GnomeWatcher, Watcher};
use super::{gnome_wayland::load_watcher, Watcher};
pub struct WindowWatcher {
dbus_connection: Connection,
@ -79,8 +79,10 @@ impl WindowWatcher {
}
}
impl GnomeWatcher for WindowWatcher {
async fn load() -> anyhow::Result<Self> {
#[async_trait]
impl Watcher for WindowWatcher {
async fn new(_: &Arc<ReportClient>) -> anyhow::Result<Self> {
load_watcher(|| async move {
let watcher = Self {
dbus_connection: Connection::session().await?,
last_app_id: String::new(),
@ -89,13 +91,8 @@ impl GnomeWatcher for WindowWatcher {
watcher.get_window_data().await?;
Ok(watcher)
}
}
#[async_trait]
impl Watcher for WindowWatcher {
async fn new(_: &Arc<ReportClient>) -> anyhow::Result<Self> {
load_watcher().await
})
.await
}
async fn run_iteration(&mut self, client: &Arc<ReportClient>) -> anyhow::Result<()> {

View File

@ -1,53 +1,119 @@
use crate::report_client::ReportClient;
use chrono::{Duration, Utc};
use chrono::{DateTime, Duration, Utc};
use std::sync::Arc;
pub trait SinceLastInput {
async fn seconds_since_input(&mut self) -> anyhow::Result<u32>;
pub struct State {
last_input_time: DateTime<Utc>,
is_idle: bool,
is_changed: bool,
idle_timeout: Duration,
}
pub async fn ping_since_last_input(
watcher: &mut impl SinceLastInput,
is_idle: bool,
client: &Arc<ReportClient>,
) -> anyhow::Result<bool> {
impl State {
pub fn new(idle_timeout: Duration) -> Self {
Self {
last_input_time: Utc::now(),
is_idle: false,
is_changed: false,
idle_timeout,
}
}
pub fn mark_not_idle(&mut self) {
self.is_idle = false;
self.is_changed = true;
self.last_input_time = Utc::now();
}
pub fn mark_idle(&mut self) {
self.is_idle = true;
self.is_changed = true;
self.last_input_time -= self.idle_timeout;
}
// 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);
let duration_zero: Duration = Duration::zero();
let seconds_since_input = watcher.seconds_since_input().await?;
let now = Utc::now();
pub async fn send_with_last_input(
&mut self,
seconds_since_input: u32,
client: &Arc<ReportClient>,
) -> anyhow::Result<()> {
let time_since_input = Duration::seconds(i64::from(seconds_since_input));
let last_input = now - time_since_input;
let mut is_idle_again = is_idle;
if is_idle && u64::from(seconds_since_input) < client.config.idle_timeout.as_secs() {
self.last_input_time = Utc::now() - time_since_input;
if self.is_idle
&& u64::from(seconds_since_input) < self.idle_timeout.num_seconds().try_into().unwrap()
{
debug!("No longer idle");
client.ping(is_idle, last_input, duration_zero).await?;
is_idle_again = false;
// ping with timestamp+1ms with the next event (to ensure the latest event gets retrieved by get_event)
client
.ping(is_idle, last_input + duration_1ms, duration_zero)
.await?;
} else if !is_idle && u64::from(seconds_since_input) >= client.config.idle_timeout.as_secs() {
self.is_idle = false;
self.is_changed = true;
} else if !self.is_idle
&& u64::from(seconds_since_input) >= self.idle_timeout.num_seconds().try_into().unwrap()
{
debug!("Idle again");
client.ping(is_idle, last_input, duration_zero).await?;
is_idle_again = true;
self.is_idle = true;
self.is_changed = true;
}
self.send_ping(client).await?;
Ok(())
}
pub async fn send_reactive(&mut self, client: &Arc<ReportClient>) -> anyhow::Result<()> {
let now = Utc::now();
if !self.is_idle {
self.last_input_time = now;
}
self.send_ping(client).await
}
async fn send_ping(&mut self, client: &Arc<ReportClient>) -> anyhow::Result<()> {
let now = Utc::now();
if self.is_changed {
let result = if self.is_idle {
debug!("Reporting as changed to idle");
client
.ping(false, self.last_input_time, Duration::zero())
.await?;
// ping with timestamp+1ms with the next event (to ensure the latest event gets retrieved by get_event)
client
.ping(is_idle, last_input + duration_1ms, time_since_input)
.await?;
.ping(
true,
self.last_input_time + Duration::milliseconds(1),
Duration::zero(),
)
.await
} else {
// Send a heartbeat if no state change was made
if is_idle {
debug!("Reporting as no longer idle");
client
.ping(true, self.last_input_time, Duration::zero())
.await?;
client
.ping(
false,
self.last_input_time + Duration::milliseconds(1),
Duration::zero(),
)
.await
};
self.is_changed = false;
result
} else if self.is_idle {
trace!("Reporting as idle");
client.ping(is_idle, last_input, time_since_input).await?;
client
.ping(true, self.last_input_time, now - self.last_input_time)
.await
} else {
trace!("Reporting as not idle");
client.ping(is_idle, last_input, duration_zero).await?;
client
.ping(false, self.last_input_time, Duration::zero())
.await
}
}
Ok(is_idle_again)
}

View File

@ -1,9 +1,10 @@
use super::idle;
use super::wl_connection::{subscribe_state, WlEventConnection};
use super::Watcher;
use crate::report_client::ReportClient;
use anyhow::anyhow;
use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use chrono::Duration;
use std::sync::Arc;
use wayland_client::{
globals::GlobalListContents,
@ -14,101 +15,43 @@ use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notification_v1::E
use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notification_v1::ExtIdleNotificationV1;
use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notifier_v1::ExtIdleNotifierV1;
struct IdleState {
struct WatcherState {
idle_notification: ExtIdleNotificationV1,
last_input_time: DateTime<Utc>,
is_idle: bool,
is_changed: bool,
idle_timeout: Duration,
idle_state: idle::State,
}
impl Drop for IdleState {
impl Drop for WatcherState {
fn drop(&mut self) {
info!("Releasing idle notification");
self.idle_notification.destroy();
}
}
impl IdleState {
impl WatcherState {
fn new(idle_notification: ExtIdleNotificationV1, idle_timeout: Duration) -> Self {
Self {
idle_notification,
last_input_time: Utc::now(),
is_idle: false,
is_changed: false,
idle_timeout,
idle_state: idle::State::new(idle_timeout),
}
}
fn idle(&mut self) {
self.is_idle = true;
self.is_changed = true;
self.last_input_time -= self.idle_timeout;
self.idle_state.mark_idle();
debug!("Idle");
}
fn resume(&mut self) {
self.is_idle = false;
self.last_input_time = Utc::now();
self.is_changed = true;
self.idle_state.mark_not_idle();
debug!("Resumed");
}
async fn send_ping(&mut self, client: &Arc<ReportClient>) -> anyhow::Result<()> {
let now = Utc::now();
if !self.is_idle {
self.last_input_time = now;
}
if self.is_changed {
let result = if self.is_idle {
debug!("Reporting as changed to idle");
client
.ping(false, self.last_input_time, Duration::zero())
.await?;
client
.ping(
true,
self.last_input_time + Duration::milliseconds(1),
Duration::zero(),
)
.await
} else {
debug!("Reporting as no longer idle");
client
.ping(true, self.last_input_time, Duration::zero())
.await?;
client
.ping(
false,
self.last_input_time + Duration::milliseconds(1),
Duration::zero(),
)
.await
};
self.is_changed = false;
result
} else if self.is_idle {
trace!("Reporting as idle");
client
.ping(true, self.last_input_time, now - self.last_input_time)
.await
} else {
trace!("Reporting as not idle");
client
.ping(false, self.last_input_time, Duration::zero())
.await
}
}
}
subscribe_state!(wl_registry::WlRegistry, GlobalListContents, IdleState);
subscribe_state!(wl_registry::WlRegistry, (), IdleState);
subscribe_state!(WlSeat, (), IdleState);
subscribe_state!(ExtIdleNotifierV1, (), IdleState);
subscribe_state!(wl_registry::WlRegistry, GlobalListContents, WatcherState);
subscribe_state!(wl_registry::WlRegistry, (), WatcherState);
subscribe_state!(WlSeat, (), WatcherState);
subscribe_state!(ExtIdleNotifierV1, (), WatcherState);
impl Dispatch<ExtIdleNotificationV1, ()> for IdleState {
impl Dispatch<ExtIdleNotificationV1, ()> for WatcherState {
fn event(
state: &mut Self,
_: &ExtIdleNotificationV1,
@ -126,37 +69,40 @@ impl Dispatch<ExtIdleNotificationV1, ()> for IdleState {
}
pub struct IdleWatcher {
connection: WlEventConnection<IdleState>,
idle_state: IdleState,
connection: WlEventConnection<WatcherState>,
watcher_state: WatcherState,
}
#[async_trait]
impl Watcher for IdleWatcher {
async fn new(client: &Arc<ReportClient>) -> anyhow::Result<Self> {
let mut connection: WlEventConnection<IdleState> = WlEventConnection::connect()?;
let mut connection: WlEventConnection<WatcherState> = WlEventConnection::connect()?;
connection.get_ext_idle()?;
let timeout = u32::try_from(client.config.idle_timeout.as_secs() * 1000);
let mut idle_state = IdleState::new(
let mut watcher_state = WatcherState::new(
connection
.get_ext_idle_notification(timeout.unwrap())
.unwrap(),
Duration::from_std(client.config.idle_timeout).unwrap(),
);
connection.event_queue.roundtrip(&mut idle_state).unwrap();
connection
.event_queue
.roundtrip(&mut watcher_state)
.unwrap();
Ok(Self {
connection,
idle_state,
watcher_state,
})
}
async fn run_iteration(&mut self, client: &Arc<ReportClient>) -> anyhow::Result<()> {
self.connection
.event_queue
.roundtrip(&mut self.idle_state)
.roundtrip(&mut self.watcher_state)
.map_err(|e| anyhow!("Event queue is not processed: {e}"))?;
self.idle_state.send_ping(client).await
self.watcher_state.idle_state.send_reactive(client).await
}
}

View File

@ -1,9 +1,10 @@
use super::idle;
use super::wl_connection::{subscribe_state, WlEventConnection};
use super::Watcher;
use crate::report_client::ReportClient;
use anyhow::anyhow;
use async_trait::async_trait;
use chrono::{DateTime, Duration, Utc};
use chrono::Duration;
use std::sync::Arc;
use wayland_client::{
globals::GlobalListContents,
@ -15,101 +16,43 @@ use wayland_protocols_plasma::idle::client::org_kde_kwin_idle_timeout::{
Event as OrgKdeKwinIdleTimeoutEvent, OrgKdeKwinIdleTimeout,
};
struct IdleState {
struct WatcherState {
kwin_idle_timeout: OrgKdeKwinIdleTimeout,
last_input_time: DateTime<Utc>,
is_idle: bool,
is_changed: bool,
idle_timeout: Duration,
idle_state: idle::State,
}
impl Drop for IdleState {
impl Drop for WatcherState {
fn drop(&mut self) {
info!("Releasing idle timeout");
self.kwin_idle_timeout.release();
}
}
impl IdleState {
impl WatcherState {
fn new(kwin_idle_timeout: OrgKdeKwinIdleTimeout, idle_timeout: Duration) -> Self {
Self {
kwin_idle_timeout,
last_input_time: Utc::now(),
is_idle: false,
is_changed: false,
idle_timeout,
idle_state: idle::State::new(idle_timeout),
}
}
fn idle(&mut self) {
self.is_idle = true;
self.is_changed = true;
self.last_input_time -= self.idle_timeout;
self.idle_state.mark_idle();
debug!("Idle");
}
fn resume(&mut self) {
self.is_idle = false;
self.last_input_time = Utc::now();
self.is_changed = true;
self.idle_state.mark_not_idle();
debug!("Resumed");
}
async fn send_ping(&mut self, client: &Arc<ReportClient>) -> anyhow::Result<()> {
let now = Utc::now();
if !self.is_idle {
self.last_input_time = now;
}
if self.is_changed {
let result = if self.is_idle {
debug!("Reporting as changed to idle");
client
.ping(false, self.last_input_time, Duration::zero())
.await?;
client
.ping(
true,
self.last_input_time + Duration::milliseconds(1),
Duration::zero(),
)
.await
} else {
debug!("Reporting as no longer idle");
client
.ping(true, self.last_input_time, Duration::zero())
.await?;
client
.ping(
false,
self.last_input_time + Duration::milliseconds(1),
Duration::zero(),
)
.await
};
self.is_changed = false;
result
} else if self.is_idle {
trace!("Reporting as idle");
client
.ping(true, self.last_input_time, now - self.last_input_time)
.await
} else {
trace!("Reporting as not idle");
client
.ping(false, self.last_input_time, Duration::zero())
.await
}
}
}
subscribe_state!(wl_registry::WlRegistry, GlobalListContents, IdleState);
subscribe_state!(wl_registry::WlRegistry, (), IdleState);
subscribe_state!(WlSeat, (), IdleState);
subscribe_state!(OrgKdeKwinIdle, (), IdleState);
subscribe_state!(wl_registry::WlRegistry, GlobalListContents, WatcherState);
subscribe_state!(wl_registry::WlRegistry, (), WatcherState);
subscribe_state!(WlSeat, (), WatcherState);
subscribe_state!(OrgKdeKwinIdle, (), WatcherState);
impl Dispatch<OrgKdeKwinIdleTimeout, ()> for IdleState {
impl Dispatch<OrgKdeKwinIdleTimeout, ()> for WatcherState {
fn event(
state: &mut Self,
_: &OrgKdeKwinIdleTimeout,
@ -127,35 +70,38 @@ impl Dispatch<OrgKdeKwinIdleTimeout, ()> for IdleState {
}
pub struct IdleWatcher {
connection: WlEventConnection<IdleState>,
idle_state: IdleState,
connection: WlEventConnection<WatcherState>,
watcher_state: WatcherState,
}
#[async_trait]
impl Watcher for IdleWatcher {
async fn new(client: &Arc<ReportClient>) -> anyhow::Result<Self> {
let mut connection: WlEventConnection<IdleState> = WlEventConnection::connect()?;
let mut connection: WlEventConnection<WatcherState> = WlEventConnection::connect()?;
connection.get_kwin_idle()?;
let timeout = u32::try_from(client.config.idle_timeout.as_secs() * 1000);
let mut idle_state = IdleState::new(
let mut watcher_state = WatcherState::new(
connection.get_kwin_idle_timeout(timeout.unwrap()).unwrap(),
Duration::from_std(client.config.idle_timeout).unwrap(),
);
connection.event_queue.roundtrip(&mut idle_state).unwrap();
connection
.event_queue
.roundtrip(&mut watcher_state)
.unwrap();
Ok(Self {
connection,
idle_state,
watcher_state,
})
}
async fn run_iteration(&mut self, client: &Arc<ReportClient>) -> anyhow::Result<()> {
self.connection
.event_queue
.roundtrip(&mut self.idle_state)
.roundtrip(&mut self.watcher_state)
.map_err(|e| anyhow!("Event queue is not processed: {e}"))?;
self.idle_state.send_ping(client).await
self.watcher_state.idle_state.send_reactive(client).await
}
}

View File

@ -1,4 +1,5 @@
use async_trait::async_trait;
use chrono::Duration;
use super::{idle, x11_connection::X11Client, Watcher};
use crate::report_client::ReportClient;
@ -6,10 +7,10 @@ use std::sync::Arc;
pub struct IdleWatcher {
client: X11Client,
is_idle: bool,
idle_state: idle::State,
}
impl idle::SinceLastInput for IdleWatcher {
impl IdleWatcher {
async fn seconds_since_input(&mut self) -> anyhow::Result<u32> {
self.client.seconds_since_last_input()
}
@ -17,7 +18,7 @@ impl idle::SinceLastInput for IdleWatcher {
#[async_trait]
impl Watcher for IdleWatcher {
async fn new(_: &Arc<ReportClient>) -> anyhow::Result<Self> {
async fn new(report_client: &Arc<ReportClient>) -> anyhow::Result<Self> {
let mut client = X11Client::new()?;
// Check if screensaver extension is supported
@ -25,12 +26,17 @@ impl Watcher for IdleWatcher {
Ok(IdleWatcher {
client,
is_idle: false,
idle_state: idle::State::new(
Duration::from_std(report_client.config.idle_timeout).unwrap(),
),
})
}
async fn run_iteration(&mut self, client: &Arc<ReportClient>) -> anyhow::Result<()> {
self.is_idle = idle::ping_since_last_input(self, self.is_idle, client).await?;
let seconds = self.seconds_since_input().await?;
self.idle_state
.send_with_last_input(seconds, client)
.await?;
Ok(())
}