1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
//! Per-window state types tracked by the registry.
//!
//! The vocabulary types used throughout the window registry, representing the
//! complete lifecycle of a managed window: initial classification → state
//! transitions → removal. Every tracked window exists in exactly one
//! [`WindowState`] at any time, which determines how flow interacts with it.
//!
//! # Send safety
//!
//! All types in this module are `Send` (and most are `Send + Sync`). The tricky
//! case is `HWND`: Win32's `HWND` wraps a raw pointer (`*mut c_void`) and is
//! `!Send` by default. We work around this by storing `HWND` inside [`Window`]
//! with a manual `unsafe impl Send`, and converting `HWND` to `isize` when
//! passing window ids across thread boundaries (in
//! [`HookEvent`](super::HookEvent)).
//!
//! The manual `Send` impl is sound because all Win32 API calls using the handle
//! happen on the IPC thread (which owns the registry); `HWND` is an opaque
//! handle value, not a real pointer — Rust code never reads through it.
//!
//! # Relationship to layout types
//!
//! The registry uses [`WindowState`] to track *what* a window is doing (tiling,
//! floating, ignored). The layout engine uses
//! [`WindowId`](crate::common::WindowId) and column/row positions to track
//! *where* a window is placed. The two systems connect through
//! [`VirtualSlot`], which stores the layout engine's column/row assignment
//! inside the registry's [`Window`] struct.
//!
//! # Serde
//!
//! All types implement `Serialize` and `Deserialize` for the query API. `HWND`
//! fields use a custom `#[serde(with)]` module that serializes to `isize`
//! (HWND pointers aren't meaningful in JSON).
//!
//! See the developer guide's *Window Registry* chapter
//! (`docs/src/dev-guide/window-registry.md`) for the lifecycle state-machine
//! diagram.
use PathBuf;
use crateBorder;
use crate;
use ;
use HWND;
// ── HWND serde helper ───────────────────────────────────────────────
/// Serde helper for serializing/deserializing `HWND` as `isize`.
///
/// Win32 window handles are stored as opaque pointers, but their underlying
/// value is an `isize`. This module provides `#[serde(with)]` support for
/// fields of type `HWND`.
// ── Window ──────────────────────────────────────────────────────────
/// Per-window state tracked by the registry.
///
/// This is the authoritative record for every window the daemon manages.
/// Each [`Window`] is identified by its Win32 `HWND` and carries all metadata
/// needed for classification, layout assignment, and recovery.
///
/// # Field Design Rationale
///
/// - **`hwnd`** — The Win32 window handle. Serialized as `isize` for JSON.
/// Used as the HashMap key in [`WindowRegistry`](super::core::WindowRegistry).
///
/// - **`exe`** / **`title`** / **`class`** / **`process_path`** — Window
/// metadata used by the [`classification`](super::classification) module
/// to match against config rules. These are snapshotted at registration
/// time and not updated (window titles can change, but re-classification
/// is not currently supported).
///
/// - **`state`** — The current lifecycle state. Updated by
/// [`WindowRegistry`](super::core::WindowRegistry) methods on each hook event.
/// See [module-level docs](super) for the state machine diagram.
///
/// - **`pre_manage_rect`** — The window's position and size when flow first
/// saw it. Used by `flow restore` to return windows to their pre-managed
/// positions if the daemon exits or is stopped.
///
/// - **`last_natural_size`** — The window's preferred size, updated on
/// explicit user resize. Used when the layout engine needs to determine
/// a window's natural proportions.
///
/// - **`last_virtual_slot`** — The window's last known column/row position
/// in the layout grid. Saved when a tiled window is minimized and
/// restored when it's un-minimized. This prevents the window from losing
/// its place in the layout.
///
/// - **`tiled_rect`** — The window's current tiled position as computed by
/// the layout engine's projection layer. Updated after every mutation.
/// `None` if the window has never been positioned by the layout engine.
/// This is distinct from `pre_manage_rect` (the position *before* flow
/// managed the window) — `tiled_rect` is always the *current* position.
///
/// - **`border`** — The optional overlay border drawn around this window.
/// Owned directly by the `Window` so its lifecycle is coupled to the
/// window's own (dropped when the window leaves the registry). Skipped
/// during serde because it is a live Win32 resource, not query state.
/// See `docs/src/dev-guide/borders.md`.
///
/// # Send Safety
///
/// `Window` contains `HWND` (a raw pointer), but we treat it as an opaque
/// handle value. See the [module-level Send Safety section](super#send-safety)
/// for the full safety argument.
// SAFETY: `Window` contains `HWND` (a raw pointer), but we treat it as an
// opaque handle value. All Win32 API calls using this handle happen on the
// thread that owns the `MutexGuard<WindowRegistry>` — we never dereference
// HWND on a different thread. Safe to send across thread boundaries.
unsafe
// ── WindowState ────────────────────────────────────────────────────
/// Lifecycle state of a managed window.
///
/// Every window tracked by the registry is in exactly one of these states.
/// The state determines how the layout engine and compositor interact with
/// the window.
///
/// # State Transitions
///
/// Transitions are triggered by WinEvent hooks and applied by
/// [`WindowRegistry`](super::core::WindowRegistry):
///
/// | From | Event | To |
/// |------|-------|----|
/// | (new) | classification | `Tiling(Active)` / `Floating(Active)` / `Ignored(...)` |
/// | `Tiling(Active)` | `MinimizeStart` | `Tiling(Minimized)` |
/// | `Tiling(Minimized)` | `MinimizeEnd` | `Tiling(Active)` with restored slot |
/// | `Tiling(Active)` | `EVENT_OBJECT_HIDE` | `Tiling(Hidden)` (slot saved to `last_virtual_slot`) |
/// | `Tiling(Hidden)` | `EVENT_OBJECT_SHOW` | `Tiling(Active)` with restored slot |
/// | `Tiling(Hidden)` | Destroyed | removed from registry |
/// | `Floating(Active)` | `MinimizeStart` | `Floating(Minimized)` |
/// | `Floating(Minimized)` | `MinimizeEnd` | `Floating(Active)` with original rect |
/// | `Floating(Active)` | `EVENT_OBJECT_HIDE` | `Floating(Hidden)` |
/// | `Floating(Hidden)` | `EVENT_OBJECT_SHOW` | `Floating(Active)` with `pre_manage_rect` |
///
/// Note: tile ↔ float transitions are implemented via `flow dispatch set-window`
/// (see `docs/src/dev-guide/floating-space.md`). The remaining unimplemented
/// directions are user-driven tiling ↔ `Ignored(Maximized|Fullscreen)` (e.g.,
/// maximizing a tiled window); only the OS-driven recovery direction works —
/// a restored `Ignored(Maximized)` window is re-classified into the layout via
/// `EVENT_OBJECT_STATECHANGE`.
// ── TilingState ────────────────────────────────────────────────────
/// Sub-state for windows participating in the tiling layout.
///
/// Tiled windows can be either actively positioned at a grid slot or minimized.
/// When minimized, the window's grid position is preserved in
/// [`Window::last_virtual_slot`](Window::last_virtual_slot) so it can be
/// restored to its original position.
///
/// # Relationship to Layout Engine
///
/// The `col` and `row` values in [`Active`](TilingState::Active) correspond to
/// the layout engine's column/row indices. These are updated when the layout
/// engine assigns a new position (via mutations like swap, focus, add).
// ── FloatingState ──────────────────────────────────────────────────
/// Sub-state for floating (non-tiled) windows.
///
/// Floating windows are positioned freely by the user. flow does not manage
/// their position — it only tracks whether they are active (visible) or
/// minimized. When restored from minimize, the window returns to its
/// `pre_manage_rect` position.
// ── IgnoredReason ──────────────────────────────────────────────────
/// Reason why a window is ignored by flow.
///
/// Ignored windows are excluded from all layout operations. The reason is
/// tracked for diagnostic purposes (e.g., logs can show *why*
/// a window is not being tiled).
///
/// # Override Priority
///
/// Maximized and fullscreen checks happen **before** config rule evaluation
/// in [`classify_with_state_pipeline`](super::classification::classify_with_state_pipeline).
/// This means a window that is maximized will always be `Ignored(Maximized)`,
/// even if a config rule says to tile it. This is intentional — maximized and
/// fullscreen windows have their own window management behavior that conflicts
/// with tiling.
// ── VirtualSlot ─────────────────────────────────────────────────────
/// Virtual layout position remembered for minimize/restore cycles.
///
/// When a tiled window is minimized, its column/row position is saved here
/// so it can be restored to the same slot when un-minimized. Without this,
/// a restored window would lose its place in the layout grid.
///
/// # Bridge Between Registry and Layout Engine
///
/// `VirtualSlot` is the data bridge between the registry (which tracks window
/// state) and the layout engine (which manages grid positions). The registry
/// saves the slot on minimize and reads it on restore. The layout engine
/// assigns new slots when windows are added or moved.
///
/// # Default on Restore
///
/// If `last_virtual_slot` is `None` when a tiled window is restored (which
/// shouldn't happen in normal operation), the registry defaults to `(0, 0)`.
/// This ensures the window always gets a valid position.
// ── VisibilityChange ────────────────────────────────────────────────
/// Outcome of a visibility reconciliation pass on a tracked window.
///
/// Returned by [`WindowRegistry::reconcile_visibility`](super::core::WindowRegistry::reconcile_visibility).
/// The daemon layer uses this to decide whether to add/remove the window
/// from the live layout. The [`Unchanged`](VisibilityChange::Unchanged) case
/// is what makes reconciliation **idempotent** against duplicate
/// `EVENT_OBJECT_HIDE` events (e.g. an ordinary minimize also fires HIDE,
/// but the window is already `Minimized` → `Unchanged`).
// ── ReclassifyResult ────────────────────────────────────────────────
/// Outcome of an OS-state re-classification pass on a tracked window.
///
/// Returned by [`WindowRegistry::reclassify_os_state`](super::core::WindowRegistry::reclassify_os_state)
/// in response to [`EVENT_OBJECT_STATECHANGE`](super::HookEvent::StateChange).
/// The daemon layer uses this to decide whether to add the window to the live
/// layout: only [`ReclassifyResult::Recovered`] with `now_tiling == true`
/// triggers a layout insertion.
///
/// # Why a dedicated result type?
///
/// The daemon must distinguish several "no-op" cases from a genuine recovery:
///
/// - `Untracked` — the `STATECHANGE` was for a window flow does not manage
/// (let `NAMECHANGE`/`CREATE` handle it). Cheap HashMap miss; do nothing.
/// - `NotApplicable` — the window is tracked but not in an OS-ignored state
/// (e.g. it is already tiling, or ignored by an explicit rule). Do nothing.
/// - `Unchanged` — the window is still `Ignored(Maximized|Fullscreen)`; the
/// user has not restored it yet. Do nothing.
/// - `Recovered` — the OS state genuinely changed; the classifier was
/// re-run and the window's stored state was updated. The `now_tiling` flag
/// tells the daemon whether the new state warrants a layout insertion.
///
/// This makes the `STATECHANGE` handler both cheap (early-return on the common
/// no-op cases) and explicit (no spurious layout churn).