use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StateQueryKind {
State,
VirtualWorkspaces,
Active,
OnScreen,
}
impl StateQueryKind {
pub const ALL: [StateQueryKind; 4] = [
StateQueryKind::State,
StateQueryKind::VirtualWorkspaces,
StateQueryKind::Active,
StateQueryKind::OnScreen,
];
pub const SHORTHANDS: [(&'static str, StateQueryKind); 4] = [
("query_state", StateQueryKind::State),
("query_active", StateQueryKind::Active),
("query_workspaces", StateQueryKind::VirtualWorkspaces),
("query_on_screen", StateQueryKind::OnScreen),
];
#[must_use]
pub fn token(self) -> &'static str {
match self {
StateQueryKind::State => "state",
StateQueryKind::VirtualWorkspaces => "virtual-workspaces",
StateQueryKind::Active => "active",
StateQueryKind::OnScreen => "on-screen",
}
}
#[must_use]
pub fn parse(token: &str) -> Option<Self> {
Self::ALL.into_iter().find(|kind| kind.token() == token)
}
#[must_use]
pub fn tokens() -> String {
Self::ALL
.iter()
.map(|kind| kind.token())
.collect::<Vec<_>>()
.join(", ")
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct QueryState {
pub version: u32,
pub timestamp: u64,
pub active: ActiveState,
pub virtual_workspaces: Vec<VirtualWorkspaceState>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct ActiveState {
pub display_id: Option<u32>,
pub native_workspace_id: Option<u64>,
pub virtual_workspace_number: Option<u32>,
pub focused_window_id: Option<i32>,
pub focused_bundle_id: Option<String>,
pub focused_app_name: Option<String>,
pub focused_window_title: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct VirtualWorkspaceState {
pub number: u32,
pub native_workspace_id: u64,
pub active: bool,
pub windows: Vec<WindowState>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Frame {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct WindowState {
pub window_id: i32,
pub bundle_id: String,
pub app_name: String,
pub title: String,
pub focused: bool,
pub floating: bool,
pub display_id: Option<u32>,
pub frame: Option<Frame>,
pub visible: bool,
}
impl QueryState {
#[must_use]
pub fn on_screen(&self) -> Vec<&WindowState> {
let mut on_screen = self
.virtual_workspaces
.iter()
.flat_map(|workspace| workspace.windows.iter())
.filter(|window| window.visible)
.collect::<Vec<_>>();
on_screen.sort_by_key(|window| {
(
window.display_id,
window.frame.map(|frame| frame.x),
window.window_id,
)
});
on_screen
}
pub fn to_query_json(&self, kind: StateQueryKind) -> serde_json::Result<String> {
match kind {
StateQueryKind::State => serde_json::to_string(self),
StateQueryKind::VirtualWorkspaces => serde_json::to_string(&self.virtual_workspaces),
StateQueryKind::Active => serde_json::to_string(&self.active),
StateQueryKind::OnScreen => serde_json::to_string(&self.on_screen()),
}
}
#[must_use]
pub fn to_query_payload(&self, kind: StateQueryKind) -> crate::types::wire::QueryPayload {
use crate::types::wire::QueryPayload;
match kind {
StateQueryKind::State => QueryPayload::State(Box::new(self.clone())),
StateQueryKind::VirtualWorkspaces => {
QueryPayload::VirtualWorkspaces(self.virtual_workspaces.clone())
}
StateQueryKind::Active => QueryPayload::Active(Box::new(self.active.clone())),
StateQueryKind::OnScreen => {
QueryPayload::OnScreen(self.on_screen().into_iter().cloned().collect())
}
}
}
pub fn to_query_value(&self, kind: StateQueryKind) -> serde_json::Result<serde_json::Value> {
match kind {
StateQueryKind::State => serde_json::to_value(self),
StateQueryKind::VirtualWorkspaces => serde_json::to_value(&self.virtual_workspaces),
StateQueryKind::Active => serde_json::to_value(&self.active),
StateQueryKind::OnScreen => serde_json::to_value(self.on_screen()),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum StateEvent {
VirtualWorkspaceChanged { active: ActiveState },
WindowsChanged {
virtual_workspace_number: Option<u32>,
active: ActiveState,
},
WindowFocused {
window_id: Option<i32>,
bundle_id: Option<String>,
title: Option<String>,
virtual_workspace_number: Option<u32>,
},
OnScreenChanged {
windows: Vec<WindowState>,
active: ActiveState,
},
WindowTitleChanged { window_id: i32, title: String },
DisplayChanged { display_id: Option<u32> },
}
impl StateEvent {
pub fn to_json(&self) -> serde_json::Result<serde_json::Value> {
Ok(crate::types::json::flatten_tag(
serde_json::to_value(self)?,
"event",
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::wire::{Codec, MessagePack};
#[test]
fn query_kinds_round_trip_through_their_tokens() {
for kind in [
StateQueryKind::State,
StateQueryKind::VirtualWorkspaces,
StateQueryKind::Active,
StateQueryKind::OnScreen,
] {
assert_eq!(StateQueryKind::parse(kind.token()), Some(kind));
}
assert_eq!(StateQueryKind::parse("nonsense"), None);
}
#[test]
fn events_round_trip_through_json() {
let event = StateEvent::OnScreenChanged {
windows: vec![WindowState {
window_id: 1,
bundle_id: "com.example.app".into(),
app_name: "Example".into(),
title: "window".into(),
focused: true,
floating: false,
display_id: Some(1),
frame: Some(Frame {
x: 0,
y: 0,
width: 800,
height: 600,
}),
visible: true,
}],
active: ActiveState::default(),
};
let line = serde_json::to_string(&event.to_json().unwrap()).unwrap();
assert!(line.contains(r#""event":"on_screen_changed""#));
assert!(line.contains(r#""windows":"#));
let bytes = MessagePack.encode(&event).unwrap();
assert_eq!(
MessagePack.decode::<StateEvent>(&bytes).unwrap(),
event,
"clients must decode exactly what the daemon emits"
);
}
#[test]
fn on_screen_is_the_visible_subset_ordered_left_to_right() {
let window = |window_id, x, visible| WindowState {
window_id,
bundle_id: String::new(),
app_name: String::new(),
title: String::new(),
focused: false,
floating: false,
display_id: Some(1),
frame: Some(Frame {
x,
y: 0,
width: 100,
height: 100,
}),
visible,
};
let state = QueryState {
version: 1,
timestamp: 0,
active: ActiveState::default(),
virtual_workspaces: vec![VirtualWorkspaceState {
number: 1,
native_workspace_id: 1,
active: true,
windows: vec![
window(1, 500, true),
window(2, 0, false),
window(3, 100, true),
],
}],
};
let visible: Vec<i32> = state
.on_screen()
.iter()
.map(|window| window.window_id)
.collect();
assert_eq!(visible, vec![3, 1], "off-screen windows are left out");
}
}