diff --git a/crates/komorebi-core/tests/common/fixtures.rs b/crates/komorebi-core/tests/common/fixtures.rs new file mode 100644 index 00000000..f2e3c695 --- /dev/null +++ b/crates/komorebi-core/tests/common/fixtures.rs @@ -0,0 +1,1037 @@ +//! Real `komorebic state` output of the three daemons, copied from `e2e/lib/tests/fixtures` +//! (Windows `kfw-*`, KDE `kfde-*`) as Rust text, because the nix source filters keep `.rs` files and +//! not `.json`. `state_roundtrip.rs` checks the copies against the originals when those exist. + +#![allow(dead_code)] + +pub const KFW_TILED: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 65537, + "name": "DISPLAY1", + "device": "Default_Monitor", + "device_id": "Default_Monitor-4&183790ca&0&UID0", + "serial_number_id": null, + "size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 800 + }, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 752 + }, + "work_area_offset": null, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "lHDkMLvTxsJGQfBTiKnkt", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 262540, + "title": "win1", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 0 + } + }, + { + "id": "Zqsuvee3D_fegP33OgBnq", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 131234, + "title": "win2", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 657, + "top": 27, + "right": 596, + "bottom": 332 + } + } + ], + "focused": 0 + } + }, + { + "id": "fop08rEXwYLbkqzpy3nE9", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 327778, + "title": "win3", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 657, + "top": 393, + "right": 596, + "bottom": 332 + } + } + ], + "focused": 0 + } + } + ], + "focused": 2 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "work_area_offset_rules": [], + "layout_flip": null, + "workspace_padding": 10, + "container_padding": 10, + "latest_layout": [ + { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + }, + { + "left": 657, + "top": 27, + "right": 596, + "bottom": 332 + }, + { + "left": 657, + "top": 393, + "right": 596, + "bottom": 332 + } + ], + "resize_dimensions": [ + null, + null, + null + ], + "tile": true, + "work_area_offset": null, + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Tiling", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "workspace_names": {}, + "container_padding": null, + "workspace_padding": null, + "wallpaper": null, + "floating_layer_behaviour": null + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true, + "has_pending_raise_op": false, + "virtual_desktop_id": null +}"####; + +pub const KFW_STACK: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 65537, + "name": "DISPLAY1", + "device": "Default_Monitor", + "device_id": "Default_Monitor-4&183790ca&0&UID0", + "serial_number_id": null, + "size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 800 + }, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 752 + }, + "work_area_offset": null, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "lHDkMLvTxsJGQfBTiKnkt", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 262540, + "title": "win1", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + } + }, + { + "hwnd": 327778, + "title": "win3", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 1 + } + }, + { + "id": "Zqsuvee3D_fegP33OgBnq", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 131234, + "title": "win2", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 657, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 0 + } + } + ], + "focused": 0 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "work_area_offset_rules": [], + "layout_flip": null, + "workspace_padding": 10, + "container_padding": 10, + "latest_layout": [ + { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + }, + { + "left": 657, + "top": 27, + "right": 596, + "bottom": 698 + } + ], + "resize_dimensions": [ + null, + null + ], + "tile": true, + "work_area_offset": null, + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Tiling", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "workspace_names": {}, + "container_padding": null, + "workspace_padding": null, + "wallpaper": null, + "floating_layer_behaviour": null + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true, + "has_pending_raise_op": false, + "virtual_desktop_id": null +}"####; + +pub const KFW_FLOAT: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 65537, + "name": "DISPLAY1", + "device": "Default_Monitor", + "device_id": "Default_Monitor-4&183790ca&0&UID0", + "serial_number_id": null, + "size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 800 + }, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1280, + "bottom": 752 + }, + "work_area_offset": null, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "lHDkMLvTxsJGQfBTiKnkt", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 262540, + "title": "win1", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 0 + } + }, + { + "id": "Zqsuvee3D_fegP33OgBnq", + "locked": false, + "windows": { + "elements": [ + { + "hwnd": 131234, + "title": "win2", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 657, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 0 + } + } + ], + "focused": 0 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [ + { + "hwnd": 327778, + "title": "win3", + "exe": "komorebi-testwin.exe", + "class": "KomorebiTestWin", + "rect": { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + } + } + ], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "work_area_offset_rules": [], + "layout_flip": null, + "workspace_padding": 10, + "container_padding": 10, + "latest_layout": [ + { + "left": 27, + "top": 27, + "right": 596, + "bottom": 698 + }, + { + "left": 657, + "top": 27, + "right": 596, + "bottom": 698 + } + ], + "resize_dimensions": [ + null, + null + ], + "tile": true, + "work_area_offset": null, + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Floating", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "workspace_names": {}, + "container_padding": null, + "workspace_padding": null, + "wallpaper": null, + "floating_layer_behaviour": null + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true, + "has_pending_raise_op": false, + "virtual_desktop_id": null +}"####; + +pub const KFDE_TILED: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 1230296105, + "device": "Virtual-0", + "serial_number_id": "Virtual-0", + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "maAEzGA_0nRWS6PpcDLlI", + "locked": false, + "windows": { + "elements": [ + { + "id": 4, + "rect": { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + "details": { + "title": "neutral1", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + } + ], + "focused": 0 + } + }, + { + "id": "f63LOWKm57v9SDinvNb_K", + "locked": false, + "windows": { + "elements": [ + { + "id": 6, + "rect": { + "left": 975, + "top": 20, + "right": 924, + "bottom": 504 + }, + "details": { + "title": "neutral3", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + } + ], + "focused": 0 + } + }, + { + "id": "z2cN_l7IEJBa53ymxMgvA", + "locked": false, + "windows": { + "elements": [ + { + "id": 5, + "rect": { + "left": 975, + "top": 555, + "right": 924, + "bottom": 504 + }, + "details": { + "title": "neutral2", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + } + ], + "focused": 0 + } + } + ], + "focused": 1 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "layout_flip": null, + "workspace_padding": null, + "container_padding": null, + "latest_layout": [ + { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + { + "left": 975, + "top": 20, + "right": 925, + "bottom": 505 + }, + { + "left": 975, + "top": 555, + "right": 925, + "bottom": 505 + } + ], + "resize_dimensions": [ + null, + null, + null + ], + "tile": true, + "work_area_offset": null, + "work_area_offset_rules": [], + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Tiling", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "work_area_offset": null, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "container_padding": null, + "workspace_padding": null, + "last_focused_workspace": 0, + "wallpaper": null, + "floating_layer_behaviour": null + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true +}"####; + +pub const KFDE_STACK: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 1230296105, + "device": "Virtual-0", + "serial_number_id": "Virtual-0", + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "maAEzGA_0nRWS6PpcDLlI", + "locked": false, + "windows": { + "elements": [ + { + "id": 4, + "rect": { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + "details": { + "title": "neutral1", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + }, + { + "id": 6, + "rect": { + "left": 20, + "top": 20, + "right": 924, + "bottom": 1037 + }, + "details": { + "title": "neutral3", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + } + ], + "focused": 1 + } + }, + { + "id": "z2cN_l7IEJBa53ymxMgvA", + "locked": false, + "windows": { + "elements": [ + { + "id": 5, + "rect": { + "left": 975, + "top": 20, + "right": 924, + "bottom": 1037 + }, + "details": { + "title": "neutral2", + "exe": "foot", + "class": "foot", + "path": "/nix/store/0gkg75w7bmwd7klmf23qy8m697abypvc-foot-1.28.0/bin/foot" + } + } + ], + "focused": 0 + } + } + ], + "focused": 0 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "layout_flip": null, + "workspace_padding": null, + "container_padding": null, + "latest_layout": [ + { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + { + "left": 975, + "top": 20, + "right": 925, + "bottom": 1040 + } + ], + "resize_dimensions": [ + null, + null + ], + "tile": true, + "work_area_offset": null, + "work_area_offset_rules": [], + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Tiling", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "work_area_offset": null, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "container_padding": null, + "workspace_padding": null, + "last_focused_workspace": 0, + "wallpaper": null, + "floating_layer_behaviour": null + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true +}"####; + +/// Shaped like the macOS state (windows as `id`, `rect`, `details` with role and subrole; the monitor +/// with `window_hiding_position` last), derived from `kfde-tiled` with jq: NOT real macOS output, which +/// cannot be produced on this host. Replace it with the output of `komorebic state` on a Mac when +/// one is at hand. +pub const MACOS_TILED: &str = r####"{ + "monitors": { + "elements": [ + { + "id": 1, + "device": "Virtual-0", + "workspaces": { + "elements": [ + { + "name": null, + "containers": { + "elements": [ + { + "id": "maAEzGA_0nRWS6PpcDLlI", + "locked": false, + "windows": { + "elements": [ + { + "id": 4, + "rect": { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + "details": { + "title": "neutral1", + "exe": "foot", + "role": "AXWindow", + "subrole": "AXStandardWindow", + "icon_path": "/Applications/Terminal.app/Contents/Resources/Terminal.icns" + } + } + ], + "focused": 0 + } + }, + { + "id": "f63LOWKm57v9SDinvNb_K", + "locked": false, + "windows": { + "elements": [ + { + "id": 6, + "rect": { + "left": 975, + "top": 20, + "right": 924, + "bottom": 504 + }, + "details": { + "title": "neutral3", + "exe": "foot", + "role": "AXWindow", + "subrole": "AXStandardWindow", + "icon_path": "/Applications/Terminal.app/Contents/Resources/Terminal.icns" + } + } + ], + "focused": 0 + } + }, + { + "id": "z2cN_l7IEJBa53ymxMgvA", + "locked": false, + "windows": { + "elements": [ + { + "id": 5, + "rect": { + "left": 975, + "top": 555, + "right": 924, + "bottom": 504 + }, + "details": { + "title": "neutral2", + "exe": "foot", + "role": "AXWindow", + "subrole": "AXStandardWindow", + "icon_path": "/Applications/Terminal.app/Contents/Resources/Terminal.icns" + } + } + ], + "focused": 0 + } + } + ], + "focused": 1 + }, + "monocle_container": null, + "maximized_window": null, + "floating_windows": { + "elements": [], + "focused": 0 + }, + "layout": { + "Default": "BSP" + }, + "layout_options": null, + "layout_rules": [], + "layout_options_rules": [], + "layout_flip": null, + "workspace_padding": null, + "container_padding": null, + "latest_layout": [ + { + "left": 20, + "top": 20, + "right": 925, + "bottom": 1040 + }, + { + "left": 975, + "top": 20, + "right": 925, + "bottom": 505 + }, + { + "left": 975, + "top": 555, + "right": 925, + "bottom": 505 + } + ], + "resize_dimensions": [ + null, + null, + null + ], + "tile": true, + "work_area_offset": null, + "work_area_offset_rules": [], + "apply_window_based_work_area_offset": true, + "window_container_behaviour": null, + "window_container_behaviour_rules": null, + "float_override": null, + "layer": "Tiling", + "floating_layer_behaviour": null, + "wallpaper": null + } + ], + "focused": 0 + }, + "size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "work_area_offset": null, + "work_area_size": { + "left": 0, + "top": 0, + "right": 1920, + "bottom": 1080 + }, + "window_based_work_area_offset": null, + "window_based_work_area_offset_limit": 1, + "container_padding": null, + "workspace_padding": null, + "last_focused_workspace": 0, + "wallpaper": null, + "floating_layer_behaviour": null, + "window_hiding_position": "BottomLeft", + "serial_number_id": "C02XXXXXXXX" + } + ], + "focused": 0 + }, + "monitor_usr_idx_map": { + "0": 0 + }, + "is_paused": false, + "resize_delta": 50, + "new_window_behaviour": "Create", + "float_override": false, + "cross_monitor_move_behaviour": "Swap", + "monocle_focus_behaviour": "NoOp", + "unmanaged_window_operation_behaviour": "Op", + "work_area_offset": null, + "focus_follows_mouse": null, + "mouse_follows_focus": true +}"####; diff --git a/crates/komorebi-core/tests/common/mod.rs b/crates/komorebi-core/tests/common/mod.rs index b8482c62..7903a1dd 100644 --- a/crates/komorebi-core/tests/common/mod.rs +++ b/crates/komorebi-core/tests/common/mod.rs @@ -483,3 +483,4 @@ pub fn work_area(right: i32, bottom: i32) -> Rect { bottom, } } +pub mod fixtures; diff --git a/crates/komorebi-core/tests/state_roundtrip.rs b/crates/komorebi-core/tests/state_roundtrip.rs new file mode 100644 index 00000000..c55c92b1 --- /dev/null +++ b/crates/komorebi-core/tests/state_roundtrip.rs @@ -0,0 +1,561 @@ +//! State JSON round trips from real fixtures: `komorebic state` output of the Windows and KDE +//! daemons (copied from `e2e/lib/tests/fixtures`) and a macOS-shaped one, read into the shared +//! `Monitor

` with each platform's window, id, serial number and extra fields, written again, +//! and compared as parsed JSON. +//! +//! The platforms here are only shaped like the real ones (what the shared types are generic over); +//! the windows keep the JSON they were read from, because the real windows write what the +//! operating system knows (title, rectangle) at the moment they are serialised. + +mod common; + +use common::fixtures::*; +use komorebi_config::behaviour::WindowHidingPosition; +use komorebi_core::ContainerWindow; +use komorebi_core::Monitor; +use komorebi_core::MonitorPlatform; +use komorebi_core::NoExtra; +use komorebi_core::WorkspacePlatform; +use komorebi_core::WorkspaceWindow; +use komorebi_core::ring::Ring; +use serde::Deserialize; +use serde::Deserializer; +use serde::Serialize; +use serde::Serializer; +use serde_json::Value; +use std::collections::HashMap; + +/// A platform shaped like Windows, KDE or macOS: its window keeps the JSON it was read from, and +/// is named by the number under `$id_key`. +macro_rules! shaped_platform { + ($platform:ident, $window:ident, $id_key:literal, $monitor_id:ty, $serial:ty, $extra:ty) => { + #[derive(Clone, Debug, PartialEq)] + struct $window { + id: i64, + json: Value, + } + + impl Serialize for $window { + fn serialize(&self, serializer: S) -> Result { + self.json.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for $window { + fn deserialize>(deserializer: D) -> Result { + let json = Value::deserialize(deserializer)?; + let id = json[$id_key] + .as_i64() + .ok_or_else(|| serde::de::Error::custom(concat!("no ", $id_key)))?; + Ok(Self { id, json }) + } + } + + impl ContainerWindow for $window { + type Id = i64; + type HideSpot = (); + + fn id(&self) -> i64 { + self.id + } + + fn exe_name(&self) -> Option { + None + } + + fn hide_window(&self, _: ()) -> color_eyre::eyre::Result<()> { + Ok(()) + } + + fn restore_window(&self) -> color_eyre::eyre::Result<()> { + Ok(()) + } + + fn hide_stacked(&self, _: ()) -> color_eyre::eyre::Result<()> { + Ok(()) + } + + fn restore_stacked(&self) -> color_eyre::eyre::Result<()> { + Ok(()) + } + } + + impl WorkspaceWindow for $window { + fn title(&self) -> Option { + None + } + + fn exe(&self) -> Option { + None + } + + fn class(&self) -> Option { + None + } + + fn path(&self) -> Option { + None + } + } + + #[derive(Clone, Debug, PartialEq)] + struct $platform; + + impl WorkspacePlatform for $platform { + type Window = $window; + type State = (); + + fn foreground_window_id() -> Option { + None + } + + fn cursor_position() -> Option<(i32, i32)> { + None + } + + fn should_act_window( + _: &str, + _: &str, + _: &str, + _: &str, + _: &[komorebi_config::matching::MatchingRule], + ) -> Option { + None + } + + fn should_act_window_individual( + _: &str, + _: &str, + _: &str, + _: &str, + _: &komorebi_config::matching::IdWithIdentifier, + ) -> bool { + false + } + + fn scrolling_idx_for_direction( + _: &komorebi_core::Workspace, + _: komorebi_layouts::OperationDirection, + ) -> Option { + None + } + + fn scrolling_insertion_idx(_: &komorebi_core::Workspace) -> Option { + None + } + + fn note_scrolling_departure(_: &komorebi_core::Workspace, _: usize) {} + } + + impl MonitorPlatform for $platform { + type Id = $monitor_id; + type SerialNumber = $serial; + type Extra = $extra; + #[cfg(feature = "schemars")] + type IdSchema = $monitor_id; + + fn serialize_id( + id: &$monitor_id, + serializer: S, + ) -> Result { + serializer.serialize_i64(*id as i64) + } + + fn deserialize_id<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result<$monitor_id, D::Error> { + <$monitor_id>::deserialize(deserializer) + } + } + }; +} + +/// What only a Windows monitor has, as the Windows daemon declares it. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct WindowsExtra { + name: String, + device_id: String, + #[serde(deserialize_with = "workspace_names")] + workspace_names: HashMap, +} + +/// A flattened map is read through a buffer that keeps its keys as strings. +fn workspace_names<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + HashMap::::deserialize(deserializer)? + .into_iter() + .map(|(key, name)| { + key.parse() + .map(|key| (key, name)) + .map_err(serde::de::Error::custom) + }) + .collect() +} + +/// What only a macOS monitor has. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct MacosExtra { + window_hiding_position: WindowHidingPosition, +} + +shaped_platform!( + Windows, + WindowsWindow, + "hwnd", + isize, + Option, + WindowsExtra +); +shaped_platform!(Kde, KdeWindow, "id", u32, String, NoExtra); +shaped_platform!(Macos, MacosWindow, "id", u32, String, MacosExtra); + +fn json(text: &str) -> Value { + serde_json::from_str(text).unwrap() +} + +/// Reads the monitors of a state file as platform `P`, writes them again. +fn round_trip(state: &Value) -> (Ring>, Value) +where + P::Window: Serialize + for<'de> Deserialize<'de>, +{ + let monitors: Ring> = serde_json::from_value(state["monitors"].clone()).unwrap(); + let written = serde_json::to_value(&monitors).unwrap(); + (monitors, written) +} + +// ---- Windows ---------------------------------------------------------------------------------- + +#[test] +fn a_windows_tiled_state_is_written_as_it_was_read() { + let state = json(KFW_TILED); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let monitor = &monitors.elements()[0]; + assert_eq!(monitor.id, 65537); + assert_eq!(monitor.serial_number_id, None); + assert_eq!(monitor.extra.name, "DISPLAY1"); + assert_eq!(monitor.extra.device_id, "Default_Monitor-4&183790ca&0&UID0"); + assert_eq!(monitor.workspaces().len(), 1); + assert_eq!(monitor.workspaces()[0].containers().len(), 3); +} + +#[test] +fn a_windows_stacked_state_is_written_as_it_was_read() { + let state = json(KFW_STACK); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let stacked = monitors.elements()[0].workspaces()[0] + .containers() + .iter() + .find(|c| c.windows().len() > 1) + .expect("the fixture has a stack"); + assert_eq!(stacked.windows().len(), 2); +} + +#[test] +fn a_windows_state_with_a_floating_window_and_the_floating_layer_is_written_as_it_was_read() { + let state = json(KFW_FLOAT); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let workspace = &monitors.elements()[0].workspaces()[0]; + assert_eq!(workspace.floating_windows().len(), 1); + assert_eq!(workspace.layer, komorebi_core::WorkspaceLayer::Floating); + assert!(workspace.contains_window(workspace.floating_windows()[0].id)); +} + +// ---- KDE -------------------------------------------------------------------------------------- + +#[test] +fn a_kde_tiled_state_is_written_as_it_was_read() { + let state = json(KFDE_TILED); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let monitor = &monitors.elements()[0]; + assert_eq!(monitor.id, 1230296105); + assert_eq!(monitor.device, "Virtual-0"); + assert_eq!(monitor.serial_number_id, "Virtual-0"); + assert_eq!(monitor.workspaces()[0].containers().len(), 3); +} + +#[test] +fn a_kde_stacked_state_is_written_as_it_was_read() { + let state = json(KFDE_STACK); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let first = &monitors.elements()[0].workspaces()[0].containers()[0]; + assert_eq!(first.windows().len(), 2); + assert_eq!(first.focused_window_idx(), 1); +} + +#[test] +fn a_kde_monitor_has_no_platform_fields_beyond_the_shared_ones() { + let state = json(KFDE_TILED); + let (_, written) = round_trip::(&state); + + let mut keys: Vec<&str> = written["elements"][0] + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect(); + keys.sort_unstable(); + + assert_eq!( + keys, + [ + "container_padding", + "device", + "floating_layer_behaviour", + "id", + "last_focused_workspace", + "serial_number_id", + "size", + "wallpaper", + "window_based_work_area_offset", + "window_based_work_area_offset_limit", + "work_area_offset", + "work_area_size", + "workspace_padding", + "workspaces", + ] + ); +} + +// ---- macOS ------------------------------------------------------------------------------------ + +#[test] +fn a_macos_shaped_state_is_written_as_it_was_read_with_the_corner_last() { + let state = json(MACOS_TILED); + + let (monitors, written) = round_trip::(&state); + + assert_eq!(written, state["monitors"]); + let monitor = &monitors.elements()[0]; + assert_eq!(monitor.id, 1); + assert_eq!(monitor.serial_number_id, "C02XXXXXXXX"); + assert_eq!( + monitor.extra.window_hiding_position, + WindowHidingPosition::BottomLeft + ); + let keys: Vec<&String> = written["elements"][0].as_object().unwrap().keys().collect(); + assert_eq!(keys.last().unwrap().as_str(), "window_hiding_position"); +} + +#[test] +fn a_macos_state_cannot_be_read_without_its_corner() { + let mut state = json(MACOS_TILED); + state["monitors"]["elements"][0] + .as_object_mut() + .unwrap() + .remove("window_hiding_position"); + + assert!(serde_json::from_value::>>(state["monitors"].clone()).is_err()); +} + +// ---- shared ----------------------------------------------------------------------------------- + +#[test] +fn the_same_workspace_keys_are_written_whatever_the_platform() { + fn workspace_keys(written: &Value) -> Vec { + let mut keys: Vec = written["elements"][0]["workspaces"]["elements"][0] + .as_object() + .unwrap() + .keys() + .cloned() + .collect(); + keys.sort(); + keys + } + + let windows = workspace_keys(&round_trip::(&json(KFW_TILED)).1); + let kde = workspace_keys(&round_trip::(&json(KFDE_TILED)).1); + let macos = workspace_keys(&round_trip::(&json(MACOS_TILED)).1); + + assert_eq!(windows, kde); + assert_eq!(kde, macos); +} + +#[test] +fn a_container_without_locked_is_read_from_a_state_file() { + let mut state = json(KFDE_TILED); + for container in + state["monitors"]["elements"][0]["workspaces"]["elements"][0]["containers"]["elements"] + .as_array_mut() + .unwrap() + { + container.as_object_mut().unwrap().remove("locked"); + } + + let (monitors, _) = round_trip::(&state); + + assert!( + monitors.elements()[0].workspaces()[0] + .containers() + .iter() + .all(|c| !c.locked) + ); +} + +/// Reads a KDE-shaped state with `key` taken out of the object at `level`, to say whether it loads. +fn loads_without(level: &str, key: &str) -> bool { + let mut state = json(KFDE_TILED); + let monitor = &mut state["monitors"]["elements"][0]; + let target = match level { + "monitor" => monitor, + "workspace" => &mut monitor["workspaces"]["elements"][0], + _ => &mut monitor["workspaces"]["elements"][0]["containers"]["elements"][0], + }; + assert!( + target.as_object_mut().unwrap().remove(key).is_some(), + "the fixture has no {level} key {key}" + ); + + serde_json::from_value::>>(state["monitors"].clone()).is_ok() +} + +#[test] +fn an_old_state_file_without_the_optional_keys_still_loads() { + let optional = [ + ("monitor", "serial_number_id"), + ("monitor", "work_area_offset"), + ("monitor", "window_based_work_area_offset"), + ("monitor", "container_padding"), + ("monitor", "workspace_padding"), + ("monitor", "last_focused_workspace"), + ("monitor", "wallpaper"), + ("monitor", "floating_layer_behaviour"), + ("workspace", "name"), + ("workspace", "monocle_container"), + ("workspace", "maximized_window"), + ("workspace", "layout_options"), + ("workspace", "layout_options_rules"), + ("workspace", "layout_flip"), + ("workspace", "workspace_padding"), + ("workspace", "container_padding"), + ("workspace", "work_area_offset"), + ("workspace", "window_container_behaviour"), + ("workspace", "window_container_behaviour_rules"), + ("workspace", "float_override"), + ("workspace", "floating_layer_behaviour"), + ("workspace", "wallpaper"), + ("container", "locked"), + ]; + + for (level, key) in optional { + assert!( + loads_without(level, key), + "a {level} without {key} should load" + ); + } +} + +#[test] +fn a_state_file_cannot_be_read_without_the_keys_the_shared_types_require() { + // characterization: these have no default, so a file from before one of them existed (the + // layer, the work area offset rules, the window-based offset flag) is not read + let required = [ + ("monitor", "id"), + ("monitor", "device"), + ("monitor", "workspaces"), + ("monitor", "size"), + ("monitor", "work_area_size"), + ("monitor", "window_based_work_area_offset_limit"), + ("workspace", "containers"), + ("workspace", "floating_windows"), + ("workspace", "layout"), + ("workspace", "layout_rules"), + ("workspace", "latest_layout"), + ("workspace", "resize_dimensions"), + ("workspace", "tile"), + ("workspace", "work_area_offset_rules"), + ("workspace", "apply_window_based_work_area_offset"), + ("workspace", "layer"), + ("container", "id"), + ("container", "windows"), + ]; + + for (level, key) in required { + assert!( + !loads_without(level, key), + "a {level} without {key} should not load" + ); + } +} + +#[test] +fn a_workspace_written_after_a_missing_optional_key_has_the_key_again() { + let mut state = json(KFDE_TILED); + state["monitors"]["elements"][0]["workspaces"]["elements"][0] + .as_object_mut() + .unwrap() + .remove("layout_options_rules"); + + let (_, written) = round_trip::(&state); + + assert_eq!( + written["elements"][0]["workspaces"]["elements"][0]["layout_options_rules"], + serde_json::json!([]) + ); +} + +#[test] +fn the_runtime_only_parts_of_a_workspace_are_never_written() { + let (monitors, written) = round_trip::(&json(KFDE_TILED)); + let mut monitors = monitors; + let workspace = &mut monitors.elements_mut()[0].workspaces_mut()[0]; + workspace.globals.border_width = 9; + workspace.monocle_container_restore_idx = Some(1); + + let rewritten = serde_json::to_value(&monitors).unwrap(); + let keys = rewritten["elements"][0]["workspaces"]["elements"][0] + .as_object() + .unwrap(); + + assert!(!keys.contains_key("globals")); + assert!(!keys.contains_key("state")); + assert!(!keys.contains_key("scrolling")); + // a restore index is written once there is one, and not before + assert!(keys.contains_key("monocle_container_restore_idx")); + assert!( + !written["elements"][0]["workspaces"]["elements"][0] + .as_object() + .unwrap() + .contains_key("monocle_container_restore_idx") + ); +} + +/// The copies in `common/fixtures.rs` are the files of `e2e/lib/tests/fixtures` when the checkout +/// has them (the nix source of the crate does not). +#[test] +fn the_fixtures_are_the_ones_the_e2e_library_has() { + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../e2e/lib/tests/fixtures"); + if !dir.is_dir() { + return; + } + + for (name, copy) in [ + ("kfw-tiled", KFW_TILED), + ("kfw-stack", KFW_STACK), + ("kfw-float", KFW_FLOAT), + ("kfde-tiled", KFDE_TILED), + ("kfde-stack", KFDE_STACK), + ] { + let original = std::fs::read_to_string(dir.join(format!("{name}.json"))).unwrap(); + assert_eq!( + json(&original), + json(copy), + "{name}.json changed: update common/fixtures.rs" + ); + } +}