mirror of
https://github.com/2e3s/awatcher.git
synced 2025-06-05 19:15:33 +00:00
Implement wlr foreign toplevel v1 protocol
This watches active windows.
This commit is contained in:
parent
32f0b5bccc
commit
6645695149
@ -8,6 +8,8 @@ pub struct Config {
|
||||
pub idle_timeout: u32,
|
||||
pub poll_time_idle: u32,
|
||||
pub poll_time_window: u32,
|
||||
pub idle_bucket_name: String,
|
||||
pub active_window_bucket_name: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@ -35,12 +37,18 @@ impl Config {
|
||||
])
|
||||
.get_matches();
|
||||
|
||||
let hostname = gethostname::gethostname().into_string().unwrap();
|
||||
let idle_bucket_name = format!("aw-watcher-afk_{hostname}");
|
||||
let active_window_bucket_name = format!("aw-watcher-window_{hostname}");
|
||||
|
||||
Self {
|
||||
port: *matches.get_one("port").unwrap(),
|
||||
host: String::clone(matches.get_one("host").unwrap()),
|
||||
idle_timeout: *matches.get_one("idle-timeout").unwrap(),
|
||||
poll_time_idle: *matches.get_one("poll-time-idle").unwrap(),
|
||||
poll_time_window: *matches.get_one("poll-time-window").unwrap(),
|
||||
idle_bucket_name,
|
||||
active_window_bucket_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
66
src/main.rs
66
src/main.rs
@ -7,6 +7,7 @@ mod config;
|
||||
mod report_client;
|
||||
mod wl_bindings;
|
||||
mod wl_connection;
|
||||
mod wl_foreign_toplevel;
|
||||
mod wl_kwin_idle;
|
||||
mod wl_kwin_window;
|
||||
|
||||
@ -18,8 +19,45 @@ use std::{error::Error, str::FromStr, sync::Arc, thread};
|
||||
use wl_kwin_idle::KwinIdleWatcher;
|
||||
use wl_kwin_window::KwinWindowWatcher;
|
||||
|
||||
use crate::wl_foreign_toplevel::WlrForeignToplevelWatcher;
|
||||
|
||||
type BoxedError = Box<dyn Error>;
|
||||
|
||||
trait Watcher: Send {
|
||||
fn new() -> Result<Self, BoxedError>
|
||||
where
|
||||
Self: Sized;
|
||||
fn watch(&mut self, client: &Arc<ReportClient>);
|
||||
}
|
||||
|
||||
type BoxedWatcher = Box<dyn Watcher>;
|
||||
|
||||
type WatcherConstructor = fn() -> Result<BoxedWatcher, BoxedError>;
|
||||
type WatcherConstructors = [WatcherConstructor];
|
||||
|
||||
trait WatchersFilter {
|
||||
fn filter_first_supported(&self) -> Option<BoxedWatcher>;
|
||||
}
|
||||
|
||||
impl WatchersFilter for WatcherConstructors {
|
||||
fn filter_first_supported(&self) -> Option<BoxedWatcher> {
|
||||
self.iter().find_map(|watcher| match watcher() {
|
||||
Ok(watcher) => Some(watcher),
|
||||
Err(e) => {
|
||||
info!("Watcher cannot run: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const IDLE_WATCHERS: [WatcherConstructor; 1] = [|| Ok(Box::new(KwinIdleWatcher::new()?))];
|
||||
|
||||
const ACTIVE_WINDOW_WATCHERS: [WatcherConstructor; 2] = [
|
||||
|| Ok(Box::new(WlrForeignToplevelWatcher::new()?)),
|
||||
|| Ok(Box::new(KwinWindowWatcher::new()?)),
|
||||
];
|
||||
|
||||
fn setup_logger() -> Result<(), fern::InitError> {
|
||||
let log_setting = env::var("AWATCHER_LOG").unwrap_or("info".to_string());
|
||||
|
||||
@ -47,14 +85,6 @@ fn setup_logger() -> Result<(), fern::InitError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type WatcherConstructor = fn() -> Result<Box<dyn Watcher>, BoxedError>;
|
||||
trait Watcher: Send {
|
||||
fn new() -> Result<Self, BoxedError>
|
||||
where
|
||||
Self: Sized;
|
||||
fn watch(&mut self, client: &Arc<ReportClient>);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
setup_logger().unwrap();
|
||||
|
||||
@ -69,20 +99,8 @@ fn main() {
|
||||
info!("Polling period: {} seconds", client.config.poll_time_idle);
|
||||
|
||||
let mut thread_handlers = Vec::new();
|
||||
let idle_watchers: Vec<WatcherConstructor> = vec![|| Ok(Box::new(KwinIdleWatcher::new()?))];
|
||||
let window_watchers: Vec<WatcherConstructor> = vec![|| Ok(Box::new(KwinWindowWatcher::new()?))];
|
||||
|
||||
let filter_watcher = |watchers: Vec<WatcherConstructor>| {
|
||||
watchers.iter().find_map(|watcher| match watcher() {
|
||||
Ok(watcher) => Some(watcher),
|
||||
Err(e) => {
|
||||
info!("Watcher cannot run: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let idle_watcher = filter_watcher(idle_watchers);
|
||||
let idle_watcher = IDLE_WATCHERS.filter_first_supported();
|
||||
if let Some(mut watcher) = idle_watcher {
|
||||
let thread_client = Arc::clone(&client);
|
||||
let idle_handler = thread::spawn(move || watcher.watch(&thread_client));
|
||||
@ -91,7 +109,7 @@ fn main() {
|
||||
warn!("No supported idle handler is found");
|
||||
}
|
||||
|
||||
let window_watcher = filter_watcher(window_watchers);
|
||||
let window_watcher = ACTIVE_WINDOW_WATCHERS.filter_first_supported();
|
||||
if let Some(mut watcher) = window_watcher {
|
||||
let thread_client = Arc::clone(&client);
|
||||
let active_window_handler = thread::spawn(move || watcher.watch(&thread_client));
|
||||
@ -101,6 +119,8 @@ fn main() {
|
||||
}
|
||||
|
||||
for handler in thread_handlers {
|
||||
handler.join().unwrap();
|
||||
if handler.join().is_err() {
|
||||
error!("Thread failed with error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
259
src/protocols/wlr-foreign-toplevel-management-unstable-v1.xml
Normal file
259
src/protocols/wlr-foreign-toplevel-management-unstable-v1.xml
Normal file
@ -0,0 +1,259 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="wlr_foreign_toplevel_management_unstable_v1">
|
||||
<copyright>
|
||||
Copyright © 2018 Ilia Bozhinov
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this
|
||||
software and its documentation for any purpose is hereby granted
|
||||
without fee, provided that the above copyright notice appear in
|
||||
all copies and that both that copyright notice and this permission
|
||||
notice appear in supporting documentation, and that the name of
|
||||
the copyright holders not be used in advertising or publicity
|
||||
pertaining to distribution of the software without specific,
|
||||
written prior permission. The copyright holders make no
|
||||
representations about the suitability of this software for any
|
||||
purpose. It is provided "as is" without express or implied
|
||||
warranty.
|
||||
|
||||
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
|
||||
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
||||
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<interface name="zwlr_foreign_toplevel_manager_v1" version="2">
|
||||
<description summary="list and control opened apps">
|
||||
The purpose of this protocol is to enable the creation of taskbars
|
||||
and docks by providing them with a list of opened applications and
|
||||
letting them request certain actions on them, like maximizing, etc.
|
||||
|
||||
After a client binds the zwlr_foreign_toplevel_manager_v1, each opened
|
||||
toplevel window will be sent via the toplevel event
|
||||
</description>
|
||||
|
||||
<event name="toplevel">
|
||||
<description summary="a toplevel has been created">
|
||||
This event is emitted whenever a new toplevel window is created. It
|
||||
is emitted for all toplevels, regardless of the app that has created
|
||||
them.
|
||||
|
||||
All initial details of the toplevel(title, app_id, states, etc.) will
|
||||
be sent immediately after this event via the corresponding events in
|
||||
zwlr_foreign_toplevel_handle_v1.
|
||||
</description>
|
||||
<arg name="toplevel" type="new_id" interface="zwlr_foreign_toplevel_handle_v1"/>
|
||||
</event>
|
||||
|
||||
<request name="stop">
|
||||
<description summary="stop sending events">
|
||||
Indicates the client no longer wishes to receive events for new toplevels.
|
||||
However the compositor may emit further toplevel_created events, until
|
||||
the finished event is emitted.
|
||||
|
||||
The client must not send any more requests after this one.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="finished">
|
||||
<description summary="the compositor has finished with the toplevel manager">
|
||||
This event indicates that the compositor is done sending events to the
|
||||
zwlr_foreign_toplevel_manager_v1. The server will destroy the object
|
||||
immediately after sending this request, so it will become invalid and
|
||||
the client should free any resources associated with it.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
|
||||
<interface name="zwlr_foreign_toplevel_handle_v1" version="2">
|
||||
<description summary="an opened toplevel">
|
||||
A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel
|
||||
window. Each app may have multiple opened toplevels.
|
||||
|
||||
Each toplevel has a list of outputs it is visible on, conveyed to the
|
||||
client with the output_enter and output_leave events.
|
||||
</description>
|
||||
|
||||
<event name="title">
|
||||
<description summary="title change">
|
||||
This event is emitted whenever the title of the toplevel changes.
|
||||
</description>
|
||||
<arg name="title" type="string"/>
|
||||
</event>
|
||||
|
||||
<event name="app_id">
|
||||
<description summary="app-id change">
|
||||
This event is emitted whenever the app-id of the toplevel changes.
|
||||
</description>
|
||||
<arg name="app_id" type="string"/>
|
||||
</event>
|
||||
|
||||
<event name="output_enter">
|
||||
<description summary="toplevel entered an output">
|
||||
This event is emitted whenever the toplevel becomes visible on
|
||||
the given output. A toplevel may be visible on multiple outputs.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</event>
|
||||
|
||||
<event name="output_leave">
|
||||
<description summary="toplevel left an output">
|
||||
This event is emitted whenever the toplevel stops being visible on
|
||||
the given output. It is guaranteed that an entered-output event
|
||||
with the same output has been emitted before this event.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</event>
|
||||
|
||||
<request name="set_maximized">
|
||||
<description summary="requests that the toplevel be maximized">
|
||||
Requests that the toplevel be maximized. If the maximized state actually
|
||||
changes, this will be indicated by the state event.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="unset_maximized">
|
||||
<description summary="requests that the toplevel be unmaximized">
|
||||
Requests that the toplevel be unmaximized. If the maximized state actually
|
||||
changes, this will be indicated by the state event.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="set_minimized">
|
||||
<description summary="requests that the toplevel be minimized">
|
||||
Requests that the toplevel be minimized. If the minimized state actually
|
||||
changes, this will be indicated by the state event.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="unset_minimized">
|
||||
<description summary="requests that the toplevel be unminimized">
|
||||
Requests that the toplevel be unminimized. If the minimized state actually
|
||||
changes, this will be indicated by the state event.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="activate">
|
||||
<description summary="activate the toplevel">
|
||||
Request that this toplevel be activated on the given seat.
|
||||
There is no guarantee the toplevel will be actually activated.
|
||||
</description>
|
||||
<arg name="seat" type="object" interface="wl_seat"/>
|
||||
</request>
|
||||
|
||||
<enum name="state">
|
||||
<description summary="types of states on the toplevel">
|
||||
The different states that a toplevel can have. These have the same meaning
|
||||
as the states with the same names defined in xdg-toplevel
|
||||
</description>
|
||||
|
||||
<entry name="maximized" value="0" summary="the toplevel is maximized"/>
|
||||
<entry name="minimized" value="1" summary="the toplevel is minimized"/>
|
||||
<entry name="activated" value="2" summary="the toplevel is active"/>
|
||||
<entry name="fullscreen" value="3" summary="the toplevel is fullscreen" since="2"/>
|
||||
</enum>
|
||||
|
||||
<event name="state">
|
||||
<description summary="the toplevel state changed">
|
||||
This event is emitted immediately after the zlw_foreign_toplevel_handle_v1
|
||||
is created and each time the toplevel state changes, either because of a
|
||||
compositor action or because of a request in this protocol.
|
||||
</description>
|
||||
|
||||
<arg name="state" type="array"/>
|
||||
</event>
|
||||
|
||||
<event name="done">
|
||||
<description summary="all information about the toplevel has been sent">
|
||||
This event is sent after all changes in the toplevel state have been
|
||||
sent.
|
||||
|
||||
This allows changes to the zwlr_foreign_toplevel_handle_v1 properties
|
||||
to be seen as atomic, even if they happen via multiple events.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="close">
|
||||
<description summary="request that the toplevel be closed">
|
||||
Send a request to the toplevel to close itself. The compositor would
|
||||
typically use a shell-specific method to carry out this request, for
|
||||
example by sending the xdg_toplevel.close event. However, this gives
|
||||
no guarantees the toplevel will actually be destroyed. If and when
|
||||
this happens, the zwlr_foreign_toplevel_handle_v1.closed event will
|
||||
be emitted.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="set_rectangle">
|
||||
<description summary="the rectangle which represents the toplevel">
|
||||
The rectangle of the surface specified in this request corresponds to
|
||||
the place where the app using this protocol represents the given toplevel.
|
||||
It can be used by the compositor as a hint for some operations, e.g
|
||||
minimizing. The client is however not required to set this, in which
|
||||
case the compositor is free to decide some default value.
|
||||
|
||||
If the client specifies more than one rectangle, only the last one is
|
||||
considered.
|
||||
|
||||
The dimensions are given in surface-local coordinates.
|
||||
Setting width=height=0 removes the already-set rectangle.
|
||||
</description>
|
||||
|
||||
<arg name="surface" type="object" interface="wl_surface"/>
|
||||
<arg name="x" type="int"/>
|
||||
<arg name="y" type="int"/>
|
||||
<arg name="width" type="int"/>
|
||||
<arg name="height" type="int"/>
|
||||
</request>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="invalid_rectangle" value="0"
|
||||
summary="the provided rectangle is invalid"/>
|
||||
</enum>
|
||||
|
||||
<event name="closed">
|
||||
<description summary="this toplevel has been destroyed">
|
||||
This event means the toplevel has been destroyed. It is guaranteed there
|
||||
won't be any more events for this zwlr_foreign_toplevel_handle_v1. The
|
||||
toplevel itself becomes inert so any requests will be ignored except the
|
||||
destroy request.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the zwlr_foreign_toplevel_handle_v1 object">
|
||||
Destroys the zwlr_foreign_toplevel_handle_v1 object.
|
||||
|
||||
This request should be called either when the client does not want to
|
||||
use the toplevel anymore or after the closed event to finalize the
|
||||
destruction of the object.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<!-- Version 2 additions -->
|
||||
|
||||
<request name="set_fullscreen" since="2">
|
||||
<description summary="request that the toplevel be fullscreened">
|
||||
Requests that the toplevel be fullscreened on the given output. If the
|
||||
fullscreen state and/or the outputs the toplevel is visible on actually
|
||||
change, this will be indicated by the state and output_enter/leave
|
||||
events.
|
||||
|
||||
The output parameter is only a hint to the compositor. Also, if output
|
||||
is NULL, the compositor should decide which output the toplevel will be
|
||||
fullscreened on, if at all.
|
||||
</description>
|
||||
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
|
||||
</request>
|
||||
|
||||
<request name="unset_fullscreen" since="2">
|
||||
<description summary="request that the toplevel be unfullscreened">
|
||||
Requests that the toplevel be unfullscreened. If the fullscreen state
|
||||
actually changes, this will be indicated by the state event.
|
||||
</description>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
@ -16,6 +16,10 @@ impl ReportClient {
|
||||
let host = config.host.clone();
|
||||
let port = config.port.to_string();
|
||||
|
||||
let client = AwClient::new(&host, &port, "awatcher");
|
||||
Self::create_bucket(&client, &config.idle_bucket_name, "afkstatus").unwrap();
|
||||
Self::create_bucket(&client, &config.active_window_bucket_name, "currentwindow").unwrap();
|
||||
|
||||
Self {
|
||||
config,
|
||||
client: AwClient::new(&host, &port, "awatcher"),
|
||||
@ -24,7 +28,6 @@ impl ReportClient {
|
||||
|
||||
pub fn ping(
|
||||
&self,
|
||||
bucket_name: &str,
|
||||
is_idle: bool,
|
||||
timestamp: DateTime<Utc>,
|
||||
duration: Duration,
|
||||
@ -44,25 +47,39 @@ impl ReportClient {
|
||||
|
||||
let pulsetime = f64::from(self.config.idle_timeout + self.config.poll_time_idle);
|
||||
self.client
|
||||
.heartbeat(bucket_name, &event, pulsetime)
|
||||
.heartbeat(&self.config.idle_bucket_name, &event, pulsetime)
|
||||
.map_err(|_| "Failed to send heartbeat")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn heartbeat(&self, bucket_name: &str, event: &AwEvent) -> Result<(), BoxedError> {
|
||||
pub fn send_active_window(&self, app_id: &str, title: &str) -> Result<(), BoxedError> {
|
||||
let mut data = Map::new();
|
||||
data.insert("app".to_string(), Value::String(app_id.to_string()));
|
||||
data.insert("title".to_string(), Value::String(title.to_string()));
|
||||
let event = AwEvent {
|
||||
id: None,
|
||||
timestamp: Utc::now(),
|
||||
duration: Duration::zero(),
|
||||
data,
|
||||
};
|
||||
|
||||
let interval_margin: f64 = f64::from(self.config.poll_time_idle + 1);
|
||||
self.client
|
||||
.heartbeat(bucket_name, event, interval_margin)
|
||||
.heartbeat(
|
||||
&self.config.active_window_bucket_name,
|
||||
&event,
|
||||
interval_margin,
|
||||
)
|
||||
.map_err(|_| "Failed to send heartbeat for active window".into())
|
||||
}
|
||||
|
||||
pub fn create_bucket(
|
||||
&self,
|
||||
fn create_bucket(
|
||||
client: &AwClient,
|
||||
bucket_name: &str,
|
||||
bucket_type: &str,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
self.client
|
||||
client
|
||||
.create_bucket_simple(bucket_name, bucket_type)
|
||||
.map_err(|e| format!("Failed to create bucket {bucket_name}: {e}").into())
|
||||
}
|
||||
|
@ -20,3 +20,22 @@ pub mod idle {
|
||||
|
||||
wayland_scanner::generate_client_code!("src/protocols/idle.xml");
|
||||
}
|
||||
|
||||
pub mod wlr_foreign_toplevel {
|
||||
#![allow(dead_code,non_camel_case_types,unused_unsafe,unused_variables)]
|
||||
#![allow(non_upper_case_globals,non_snake_case,unused_imports)]
|
||||
#![allow(missing_docs, clippy::all)]
|
||||
#![allow(clippy::wildcard_imports)]
|
||||
|
||||
//! Client-side API of this protocol
|
||||
use wayland_client;
|
||||
use wayland_client::protocol::*;
|
||||
|
||||
pub mod __interfaces {
|
||||
use wayland_client::protocol::__interfaces::*;
|
||||
wayland_scanner::generate_interfaces!("src/protocols/wlr-foreign-toplevel-management-unstable-v1.xml");
|
||||
}
|
||||
use self::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_client_code!("src/protocols/wlr-foreign-toplevel-management-unstable-v1.xml");
|
||||
}
|
||||
|
@ -7,6 +7,24 @@ use wayland_client::{
|
||||
};
|
||||
use wl_bindings::idle::org_kde_kwin_idle::OrgKdeKwinIdle;
|
||||
use wl_bindings::idle::org_kde_kwin_idle_timeout::OrgKdeKwinIdleTimeout;
|
||||
use wl_bindings::wlr_foreign_toplevel::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
|
||||
|
||||
macro_rules! subscribe_state {
|
||||
($struct_name:ty, $data_name:ty, $state:ty) => {
|
||||
impl Dispatch<$struct_name, $data_name> for $state {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &$struct_name,
|
||||
_: <$struct_name as Proxy>::Event,
|
||||
_: &$data_name,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use subscribe_state;
|
||||
|
||||
pub struct WlEventConnection<T> {
|
||||
pub globals: GlobalList,
|
||||
@ -37,6 +55,19 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_foreign_toplevel_manager(&self) -> Result<ZwlrForeignToplevelManagerV1, BoxedError>
|
||||
where
|
||||
T: Dispatch<ZwlrForeignToplevelManagerV1, ()>,
|
||||
{
|
||||
self.globals
|
||||
.bind::<ZwlrForeignToplevelManagerV1, T, ()>(
|
||||
&self.queue_handle,
|
||||
1..=OrgKdeKwinIdle::interface().version,
|
||||
(),
|
||||
)
|
||||
.map_err(std::convert::Into::into)
|
||||
}
|
||||
|
||||
pub fn get_kwin_idle(&self) -> Result<OrgKdeKwinIdle, BoxedError>
|
||||
where
|
||||
T: Dispatch<OrgKdeKwinIdle, ()>,
|
||||
|
175
src/wl_foreign_toplevel.rs
Normal file
175
src/wl_foreign_toplevel.rs
Normal file
@ -0,0 +1,175 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{sync::Arc, thread, time};
|
||||
|
||||
use crate::{wl_connection::subscribe_state, Watcher};
|
||||
|
||||
use super::report_client::ReportClient;
|
||||
use super::wl_bindings;
|
||||
use super::wl_connection::WlEventConnection;
|
||||
use super::BoxedError;
|
||||
use chrono::{DateTime, Utc};
|
||||
use wayland_client::{
|
||||
event_created_child, globals::GlobalListContents, protocol::wl_registry, Connection, Dispatch,
|
||||
Proxy, QueueHandle,
|
||||
};
|
||||
use wl_bindings::wlr_foreign_toplevel::zwlr_foreign_toplevel_handle_v1::{
|
||||
Event as HandleEvent, State as HandleState, ZwlrForeignToplevelHandleV1,
|
||||
};
|
||||
use wl_bindings::wlr_foreign_toplevel::zwlr_foreign_toplevel_manager_v1::{
|
||||
Event as ManagerEvent, ZwlrForeignToplevelManagerV1, EVT_TOPLEVEL_OPCODE,
|
||||
};
|
||||
|
||||
struct WindowData {
|
||||
app_id: String,
|
||||
title: String,
|
||||
}
|
||||
|
||||
struct ToplevelState {
|
||||
windows: HashMap<String, WindowData>,
|
||||
current_window_id: Option<String>,
|
||||
_last_input_time: DateTime<Utc>,
|
||||
_is_idle: bool,
|
||||
_is_changed: bool,
|
||||
client: Arc<ReportClient>,
|
||||
}
|
||||
|
||||
impl ToplevelState {
|
||||
fn new(client: Arc<ReportClient>) -> Self {
|
||||
Self {
|
||||
windows: HashMap::new(),
|
||||
current_window_id: None,
|
||||
_last_input_time: Utc::now(),
|
||||
_is_idle: false,
|
||||
_is_changed: false,
|
||||
client,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for ToplevelState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &ZwlrForeignToplevelManagerV1,
|
||||
event: <ZwlrForeignToplevelManagerV1 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
ManagerEvent::Toplevel { toplevel } => {
|
||||
debug!("Toplevel handle is received {}", toplevel.id());
|
||||
state.windows.insert(
|
||||
toplevel.id().to_string(),
|
||||
WindowData {
|
||||
app_id: "unknown".into(),
|
||||
title: "unknown".into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
ManagerEvent::Finished => {
|
||||
error!("Toplevel manager is finished, the application may crash");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
event_created_child!(ToplevelState, ZwlrForeignToplevelManagerV1, [
|
||||
EVT_TOPLEVEL_OPCODE => (ZwlrForeignToplevelHandleV1, ()),
|
||||
]);
|
||||
}
|
||||
|
||||
subscribe_state!(wl_registry::WlRegistry, GlobalListContents, ToplevelState);
|
||||
subscribe_state!(wl_registry::WlRegistry, (), ToplevelState);
|
||||
|
||||
impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for ToplevelState {
|
||||
fn event(
|
||||
toplevel_state: &mut Self,
|
||||
handle: &ZwlrForeignToplevelHandleV1,
|
||||
event: <ZwlrForeignToplevelHandleV1 as Proxy>::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let id = handle.id().to_string();
|
||||
let window = toplevel_state.windows.get_mut(&id);
|
||||
if let Some(window) = window {
|
||||
match event {
|
||||
HandleEvent::Title { title } => {
|
||||
trace!("Title is changed for {id}: {title}");
|
||||
window.title = title;
|
||||
}
|
||||
HandleEvent::AppId { app_id } => {
|
||||
trace!("App ID is changed for {id}: {app_id}");
|
||||
window.app_id = app_id;
|
||||
}
|
||||
HandleEvent::State { state } => {
|
||||
trace!("State is changed for {id}: {:?}", state);
|
||||
if state.contains(&(HandleState::Activated as u8)) {
|
||||
trace!("Window is activated: {id}");
|
||||
toplevel_state.current_window_id = Some(id);
|
||||
}
|
||||
}
|
||||
HandleEvent::Done => trace!("Done: {id}"),
|
||||
HandleEvent::Closed => {
|
||||
trace!("Window is closed: {id}");
|
||||
if toplevel_state.windows.remove(&id).is_none() {
|
||||
warn!("Window is already removed: {id}");
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
} else {
|
||||
error!("Window is not found: {id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToplevelState {
|
||||
fn send_active_window(&self) -> Result<(), BoxedError> {
|
||||
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!(
|
||||
"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())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WlrForeignToplevelWatcher {
|
||||
connection: WlEventConnection<ToplevelState>,
|
||||
}
|
||||
|
||||
impl Watcher for WlrForeignToplevelWatcher {
|
||||
fn new() -> Result<Self, BoxedError> {
|
||||
let connection: WlEventConnection<ToplevelState> = WlEventConnection::connect()?;
|
||||
connection.get_foreign_toplevel_manager()?;
|
||||
|
||||
Ok(Self { connection })
|
||||
}
|
||||
|
||||
fn watch(&mut self, client: &Arc<ReportClient>) {
|
||||
let mut toplevel_state = ToplevelState::new(Arc::clone(client));
|
||||
|
||||
self.connection
|
||||
.event_queue
|
||||
.roundtrip(&mut toplevel_state)
|
||||
.unwrap();
|
||||
|
||||
info!("Starting wlr foreign toplevel watcher");
|
||||
loop {
|
||||
if let Err(e) = self.connection.event_queue.roundtrip(&mut toplevel_state) {
|
||||
error!("Event queue is not processed: {e}");
|
||||
} else if let Err(e) = toplevel_state.send_active_window() {
|
||||
error!("Error on idle iteration {e}");
|
||||
}
|
||||
|
||||
thread::sleep(time::Duration::from_secs(u64::from(
|
||||
client.config.poll_time_window,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ use crate::Watcher;
|
||||
|
||||
use super::report_client::ReportClient;
|
||||
use super::wl_bindings;
|
||||
use super::wl_connection::WlEventConnection;
|
||||
use super::wl_connection::{subscribe_state, WlEventConnection};
|
||||
use super::BoxedError;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use std::{sync::Arc, thread, time};
|
||||
@ -22,7 +22,6 @@ struct IdleState {
|
||||
is_idle: bool,
|
||||
is_changed: bool,
|
||||
client: Arc<ReportClient>,
|
||||
bucket_name: String,
|
||||
}
|
||||
|
||||
impl Drop for IdleState {
|
||||
@ -33,18 +32,13 @@ impl Drop for IdleState {
|
||||
}
|
||||
|
||||
impl IdleState {
|
||||
fn new(
|
||||
idle_timeout: OrgKdeKwinIdleTimeout,
|
||||
client: Arc<ReportClient>,
|
||||
bucket_name: String,
|
||||
) -> Self {
|
||||
fn new(idle_timeout: OrgKdeKwinIdleTimeout, client: Arc<ReportClient>) -> Self {
|
||||
Self {
|
||||
idle_timeout,
|
||||
last_input_time: Utc::now(),
|
||||
is_idle: false,
|
||||
is_changed: false,
|
||||
client,
|
||||
bucket_name,
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,8 +55,7 @@ impl IdleState {
|
||||
debug!("Resumed");
|
||||
}
|
||||
|
||||
fn run_loop(&mut self, connection: &mut WlEventConnection<Self>) -> Result<(), BoxedError> {
|
||||
connection.event_queue.roundtrip(self).unwrap();
|
||||
fn send_ping(&mut self) -> Result<(), BoxedError> {
|
||||
let now = Utc::now();
|
||||
if !self.is_idle {
|
||||
self.last_input_time = now;
|
||||
@ -71,14 +64,9 @@ impl IdleState {
|
||||
if self.is_changed {
|
||||
let result = if self.is_idle {
|
||||
debug!("Reporting as changed to idle");
|
||||
self.client
|
||||
.ping(false, self.last_input_time, Duration::zero())?;
|
||||
self.client.ping(
|
||||
&self.bucket_name,
|
||||
false,
|
||||
self.last_input_time,
|
||||
Duration::zero(),
|
||||
)?;
|
||||
self.client.ping(
|
||||
&self.bucket_name,
|
||||
true,
|
||||
self.last_input_time + Duration::milliseconds(1),
|
||||
Duration::zero(),
|
||||
@ -86,14 +74,9 @@ impl IdleState {
|
||||
} else {
|
||||
debug!("Reporting as no longer idle");
|
||||
|
||||
self.client
|
||||
.ping(true, self.last_input_time, Duration::zero())?;
|
||||
self.client.ping(
|
||||
&self.bucket_name,
|
||||
true,
|
||||
self.last_input_time,
|
||||
Duration::zero(),
|
||||
)?;
|
||||
self.client.ping(
|
||||
&self.bucket_name,
|
||||
false,
|
||||
self.last_input_time + Duration::milliseconds(1),
|
||||
Duration::zero(),
|
||||
@ -103,44 +86,20 @@ impl IdleState {
|
||||
result
|
||||
} else if self.is_idle {
|
||||
trace!("Reporting as idle");
|
||||
self.client.ping(
|
||||
&self.bucket_name,
|
||||
true,
|
||||
self.last_input_time,
|
||||
now - self.last_input_time,
|
||||
)
|
||||
self.client
|
||||
.ping(true, self.last_input_time, now - self.last_input_time)
|
||||
} else {
|
||||
trace!("Reporting as not idle");
|
||||
self.client.ping(
|
||||
&self.bucket_name,
|
||||
false,
|
||||
self.last_input_time,
|
||||
Duration::zero(),
|
||||
)
|
||||
self.client
|
||||
.ping(false, self.last_input_time, Duration::zero())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! subscribe_state {
|
||||
($struct_name:ty, $data_name:ty) => {
|
||||
impl Dispatch<$struct_name, $data_name> for IdleState {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
_: &$struct_name,
|
||||
_: <$struct_name as Proxy>::Event,
|
||||
_: &$data_name,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
subscribe_state!(wl_registry::WlRegistry, ());
|
||||
subscribe_state!(WlSeat, ());
|
||||
subscribe_state!(OrgKdeKwinIdle, ());
|
||||
subscribe_state!(wl_registry::WlRegistry, GlobalListContents);
|
||||
subscribe_state!(wl_registry::WlRegistry, GlobalListContents, IdleState);
|
||||
subscribe_state!(wl_registry::WlRegistry, (), IdleState);
|
||||
subscribe_state!(WlSeat, (), IdleState);
|
||||
subscribe_state!(OrgKdeKwinIdle, (), IdleState);
|
||||
|
||||
impl Dispatch<OrgKdeKwinIdleTimeout, ()> for IdleState {
|
||||
fn event(
|
||||
@ -172,19 +131,11 @@ impl Watcher for KwinIdleWatcher {
|
||||
}
|
||||
|
||||
fn watch(&mut self, client: &Arc<ReportClient>) {
|
||||
let bucket_name = format!(
|
||||
"aw-watcher-afk_{}",
|
||||
gethostname::gethostname().into_string().unwrap()
|
||||
);
|
||||
|
||||
client.create_bucket(&bucket_name, "afkstatus").unwrap();
|
||||
|
||||
let mut idle_state = IdleState::new(
|
||||
self.connection
|
||||
.get_kwin_idle_timeout(client.config.idle_timeout * 1000)
|
||||
.unwrap(),
|
||||
Arc::clone(client),
|
||||
bucket_name,
|
||||
);
|
||||
self.connection
|
||||
.event_queue
|
||||
@ -193,7 +144,9 @@ impl Watcher for KwinIdleWatcher {
|
||||
|
||||
info!("Starting idle watcher");
|
||||
loop {
|
||||
if let Err(e) = idle_state.run_loop(&mut self.connection) {
|
||||
if let Err(e) = self.connection.event_queue.roundtrip(&mut idle_state) {
|
||||
error!("Event queue is not processed: {e}");
|
||||
} else if let Err(e) = idle_state.send_ping() {
|
||||
error!("Error on idle iteration {e}");
|
||||
}
|
||||
thread::sleep(time::Duration::from_secs(u64::from(
|
||||
|
@ -7,9 +7,6 @@ use crate::Watcher;
|
||||
*/
|
||||
use super::report_client::ReportClient;
|
||||
use super::BoxedError;
|
||||
use aw_client_rust::Event as AwEvent;
|
||||
use chrono::{Duration, Utc};
|
||||
use serde_json::{Map, Value};
|
||||
use std::env::temp_dir;
|
||||
use std::path::Path;
|
||||
use std::sync::{mpsc::channel, Arc, Mutex};
|
||||
@ -110,32 +107,14 @@ impl Drop for KWinScript {
|
||||
}
|
||||
}
|
||||
|
||||
fn send_heartbeat(
|
||||
fn send_active_window(
|
||||
client: &ReportClient,
|
||||
bucket_name: &str,
|
||||
active_window: &Arc<Mutex<ActiveWindow>>,
|
||||
) -> Result<(), BoxedError> {
|
||||
let event = {
|
||||
let active_window = active_window.lock().map_err(|e| format!("{e}"))?;
|
||||
let mut data = Map::new();
|
||||
data.insert(
|
||||
"app".to_string(),
|
||||
Value::String(active_window.resource_class.clone()),
|
||||
);
|
||||
data.insert(
|
||||
"title".to_string(),
|
||||
Value::String(active_window.caption.clone()),
|
||||
);
|
||||
AwEvent {
|
||||
id: None,
|
||||
timestamp: Utc::now(),
|
||||
duration: Duration::zero(),
|
||||
data,
|
||||
}
|
||||
};
|
||||
let active_window = active_window.lock().map_err(|e| format!("{e}"))?;
|
||||
|
||||
client
|
||||
.heartbeat(bucket_name, &event)
|
||||
.send_active_window(&active_window.resource_class, &active_window.caption)
|
||||
.map_err(|_| "Failed to send heartbeat for active window".into())
|
||||
}
|
||||
|
||||
@ -181,9 +160,6 @@ impl Watcher for KwinWindowWatcher {
|
||||
}
|
||||
|
||||
fn watch(&mut self, client: &Arc<ReportClient>) {
|
||||
let hostname = gethostname::gethostname().into_string().unwrap();
|
||||
let bucket_name = format!("aw-watcher-window_{hostname}");
|
||||
|
||||
self.kwin_script.load().unwrap();
|
||||
|
||||
let active_window = Arc::new(Mutex::new(ActiveWindow {
|
||||
@ -217,7 +193,7 @@ impl Watcher for KwinWindowWatcher {
|
||||
|
||||
info!("Starting active window watcher");
|
||||
loop {
|
||||
if let Err(error) = send_heartbeat(client, &bucket_name, &active_window) {
|
||||
if let Err(error) = send_active_window(client, &active_window) {
|
||||
error!("Error on sending active window heartbeat: {error}");
|
||||
}
|
||||
thread::sleep(time::Duration::from_secs(u64::from(
|
||||
|
Loading…
x
Reference in New Issue
Block a user