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
//! FlowWM constructor — performs all startup work.
//!
//! This module contains the [`FlowWM::new`] method which:
//!
//! 1. Creates [`WindowRegistry`] from user and default rules.
//! 2. Scans existing windows (populates registry before hooks start).
//! 3. Queries monitor work area via Win32.
//! 4. Derives layout parameters from the [`FlowConfig`].
//! 5. Creates [`ScrollingSpace`] with those parameters.
//! 6. Batch-initializes layout from existing tiling windows (sorted by x
//! coordinate for deterministic column assignment; viewport centered
//! on the focused column).
//! 7. Creates [`WindowAnimator`] with Win32 backend and the user's
//! configured animation duration (instant if animation is disabled).
//! 8. Animates the initial layout (windows animate to tiling positions).
//! 9. Starts the WinEvent hook thread.
//! 10. Creates the IPC named pipe server.
//!
//! # Config Model
//!
//! The `app_config` parameter is already a fully-resolved [`FlowConfig`]:
//! serde defaults (see [`config::defaults`]) fill in any fields absent from
//! the user's `flow.toml`. No further merging is needed here.
use std::time::Duration;
use crate::animation::WindowAnimator;
use crate::animation::backend::win32::Win32Backend;
use crate::common::{Rect, WindowId};
use crate::config::dirs::history_rules_path_in;
use crate::config::history::HistoryStore;
use crate::config::types::{FlowConfig, WindowRulesConfig};
use crate::ipc::transport::PipeServer;
use crate::layout::types::MonitorInfo;
use crate::registry::types::{FloatingState, WindowState};
use crate::registry::{WindowRegistry, hooks, win32 as registry_win32};
use crate::workspace::{Monitor, ScrollingSpace, Workspace, WorkspaceId};
use windows::Win32::Foundation::HWND;
use super::animation::animate_layout_raw;
use super::config_derive;
use super::types::FlowWM;
impl FlowWM {
/// Construct and initialize the daemon.
///
/// Performs all startup work in sequence. See the [module documentation](self)
/// for the complete initialization flow.
///
/// # Arguments
///
/// * `app_config` - Application settings (serde defaults + user overrides).
/// * `user_rules` - User-defined window rules from `flow-rules.toml`.
/// * `default_rules` - Bundled default rules.
/// * `config_dir` - Path to the configuration directory.
/// * `desktop_name` - Optional test desktop name (debug builds only).
///
/// # Errors
///
/// Returns `Err(String)` if any startup step fails:
/// - Window scan failure (non-fatal — logged, returns `Ok`).
/// - Monitor work area query failure.
/// - Hook thread start failure.
/// - Named pipe creation failure (likely another daemon running).
pub fn new(
app_config: FlowConfig,
user_rules: WindowRulesConfig,
default_rules: WindowRulesConfig,
config_dir: std::path::PathBuf,
desktop_name: Option<String>,
) -> Result<Self, String> {
log::debug!(
"effective config: columns_per_screen={}, column_width={:?}, min_column_width_px={}, padding.window_gap={}, padding.up={}, padding.down={}, animation.duration_ms={}",
app_config.columns_per_screen,
app_config.column_width,
app_config.min_column_width_px,
app_config.padding.window_gap,
app_config.padding.up,
app_config.padding.down,
app_config.animation.duration_ms,
);
// 1. Create registry from rules.
let mut registry = WindowRegistry::new(&user_rules, &default_rules);
// Load learned rules from `history-flow-rules.toml` and push them into
// the registry's classification pipeline BEFORE scanning existing
// windows, so previously-recorded decisions apply to windows that
// were already open when flowd started.
let history = HistoryStore::load(&history_rules_path_in(&config_dir));
if !history.is_empty() {
log::info!("loaded {} learned rules from history", history.len());
registry.set_learned_rules(history.rules().to_vec());
}
// 2. Scan existing windows before hooks start.
registry.scan_existing_windows()?;
// 3. Get monitor geometry via Win32 — both the full physical screen
// rect (for parking workspaces off-screen) and the taskbar-excluded
// work area (for in-workspace window placement). See
// registry::win32::MonitorGeometry for why both are needed.
let geometry = registry_win32::get_primary_monitor_info()?;
let monitor = MonitorInfo {
work_area: geometry.work_area,
};
// 4. Derive layout parameters from the app config.
let layout_config = config_derive::derive_layout_config(&app_config, &monitor);
// 5. Create scrolling space (the tiling half of the workspace).
let mut scrolling = ScrollingSpace::new(
monitor,
layout_config.column_width,
layout_config.min_column_width_px,
layout_config.min_window_height_px,
layout_config.padding,
app_config.columns_per_screen,
);
// 6. Batch-initialize layout from existing tiling windows.
// Sort by x-coordinate so column assignment is deterministic and
// windows travel the shortest distance to their tiling positions.
// Collect each window's pre-flow width for width-aware init.
let (tiling_ids, widths): (Vec<WindowId>, Vec<u32>) = registry
.tiling_window_ids_with_widths_sorted_by_x()
.into_iter()
.unzip();
log::debug!(
"init: {} tiling windows (sorted by x), widths: {:?}",
tiling_ids.len(),
widths
);
let initial_diff = if !tiling_ids.is_empty() {
// Query the actual foreground window so init focuses the column
// the user was last interacting with, rather than blindly picking
// the rightmost window.
let fg_hwnd = registry_win32::get_foreground_window();
// Sync the registry's OS-focus tracker from the live foreground
// window. Without this, `registry.focused()` stays `None` until
// the first EVENT_SYSTEM_FOREGROUND arrives, so the initial
// `refresh_all_border_styles` pass resolves the Unfocused color
// for every border — including the real foreground window's.
// `set_focused` no-ops for HWNDs not in the registry, so this is
// safe even when the foreground is a non-managed window (taskbar,
// dialog). See `docs/src/dev-guide/borders.md`.
if let Some(hwnd) = fg_hwnd {
registry.set_focused(hwnd);
}
let focus_col = fg_hwnd.and_then(|hwnd| {
let wid = WindowId(hwnd);
tiling_ids.iter().position(|&id| id == wid)
});
log::debug!("init: focus_col = {focus_col:?} (foreground window lookup)");
let diff = scrolling.initialize_windows(tiling_ids, focus_col, Some(&widths));
for entry in &diff.actual_layout.entries {
log::trace!(
"init target: {:?} rect ({},{},{},{})",
entry.window_id,
entry.rect.x,
entry.rect.y,
entry.rect.width,
entry.rect.height,
);
}
Some(diff)
} else {
log::warn!("init: no tiling windows found — skipping initial layout");
None
};
// 7. Create animator with the user's configured animation duration.
// On startup, windows animate from their current positions to tiling
// positions using the user's configured speed. If animation is disabled
// in config, duration will be zero (instant snap).
let backend = Win32Backend::new();
let init_config = config_derive::derive_animator_config(&app_config, Duration::ZERO);
log::debug!(
"init: animator config duration={:?}ms, animation.enabled={}",
init_config.duration,
app_config.animation.enabled,
);
let mut animator = WindowAnimator::new(backend, init_config);
// 8. Animate initial layout with user-configured duration.
if let Some(ref diff) = initial_diff {
// Use a standalone function to avoid borrow checker issues
// — animate_layout takes &mut self, but we don't have Self yet.
log::debug!(
"init: submitting {} targets to animator",
diff.actual_layout.entries.len()
);
animate_layout_raw(
&mut animator,
diff,
®istry,
app_config.borders.thickness as i32,
app_config.borders.overlap as i32,
);
// Sync registry tiling state from the initial layout so that
// queries return correct col/row and tiled_rect immediately.
registry.update_tiling_slots_from_layout(&diff.virtual_layout);
registry.update_tiled_rects(&diff.actual_layout);
}
// 9. Start hook thread.
// Returns the mpsc receiver, thread handle (RAII cleanup via Drop),
// and a Win32 manual-reset Event (HookSignal) that the hook callback
// signals to wake the main thread's WaitForMultipleObjects loop.
let (hook_receiver, _hook_handle, hook_signal) = hooks::start_hook_thread(desktop_name)?;
// 10. Create IPC server.
let server = PipeServer::create()
.map_err(|e| format!("failed to create pipe (is another daemon running?): {e}"))?;
log::info!("flowd: daemon initialized successfully");
// ---- Build the workspace stack -------------------------------------
//
// The daemon ships with a fixed set of 10 workspaces (per the user
// spec: 1-indexed, default active = workspace 1). Workspace 1
// inherits the scrolling space that was just initialised with the
// existing tiling windows; workspaces 2..=10 start empty. All
// workspaces share the same layout parameters so columns size
// consistently when the user later switches between them or moves
// windows across.
//
// The count is hard-coded rather than read from config because the
// spec fixes it at 10 for now. If this ever becomes configurable,
// the natural home is a `[workspaces]` table in `flow.toml` with a
// default of 10 (and `default-config.toml` must then be updated in
// lockstep — see AGENTS.md).
const WORKSPACE_COUNT: u32 = 10;
let mut workspaces: Vec<Workspace> = Vec::with_capacity(WORKSPACE_COUNT as usize);
// Workspace 1 takes the already-initialised scrolling space — it owns
// every tiling window the registry knew about at startup.
workspaces.push(Workspace::new(WorkspaceId(1), scrolling));
// Workspaces 2..=10 each get a fresh empty scrolling space built from
// the same layout parameters. MonitorInfo and Padding are Copy, so
// passing `monitor` and `layout_config.padding` by value into each
// ScrollingSpace::new call is cheap and idiomatic.
for id in 2..=WORKSPACE_COUNT {
let empty_scrolling = ScrollingSpace::new(
monitor,
layout_config.column_width,
layout_config.min_column_width_px,
layout_config.min_window_height_px,
layout_config.padding,
app_config.columns_per_screen,
);
workspaces.push(Workspace::new(WorkspaceId(id), empty_scrolling));
}
let mut manager = Self {
registry,
history,
monitors: vec![Monitor::new(
geometry.screen_rect,
monitor.work_area,
workspaces,
0,
)],
active_monitor: 0,
animator,
server,
config: app_config,
config_dir,
hook_receiver,
_hook_handle,
hook_signal,
shutting_down: false,
pending_creations: Vec::new(),
float_resume_deadline: None,
last_foreground_sync: std::time::Instant::now(),
};
// Adopt pre-existing float-classified windows (found during the init
// scan) into workspace 1's FloatingSpace, at their current on-screen
// position. Without this they'd be tracked in the registry but absent
// from FloatingSpace (so workspace switching would strand them) and
// borderless (refresh_all_border_styles below would seat a border at
// their zero placeholder rect). No animation: a separate animate call
// would interrupt the in-flight tiling init animation
// (InterruptPolicy::RetargetFromCurrent). The windows are already where
// they should be, so adoption-in-place is both safe and non-disruptive.
manager.populate_existing_floats();
// Initial border overlay attach for windows found during the scan.
// Idempotent and O(N) where N = tracked windows. Runs after
// populate_existing_floats so float overlays seat at the adopted rect.
manager.refresh_all_border_styles();
Ok(manager)
}
/// Adopt every pre-existing float-classified window into the active
/// workspace's [`FloatingSpace`](crate::workspace::FloatingSpace).
///
/// Mirrors the float setup that [`on_window_created`](super::FlowWM::on_window_created)
/// performs for a freshly-classified float, but for windows that were
/// already open when the daemon launched. Each float is registered at its
/// **current on-screen position** (via [`current_visible_rect`], falling
/// back to [`centered_float_rect`](Self::centered_float_rect)) rather than
/// being yanked to centre — the daemon adopting an existing window should
/// not move it. No animation is submitted: the tiling init animation is
/// already in flight, and a second batch would replace it
/// (`InterruptPolicy::RetargetFromCurrent`).
fn populate_existing_floats(&mut self) {
// Snapshot hwnds up front: register_float takes &mut self, so we can't
// hold a borrow of the registry's window iterator across it.
let float_hwnds: Vec<isize> = self
.registry
.windows()
.filter(|w| matches!(w.state, WindowState::Floating(FloatingState::Active { .. })))
.map(|w| w.hwnd.0 as isize)
.collect();
for hwnd in float_hwnds {
let wid = WindowId(hwnd);
let rect = self
.current_visible_rect(hwnd)
.unwrap_or_else(|| self.centered_float_rect(wid));
self.register_float(wid, rect);
}
}
/// Read a window's current on-screen position as a visible rect, or `None`
/// if it can't be queried (window destroyed mid-scan, GetWindowRect
/// failure). The visible rect is what [`FloatingSpace`](crate::workspace::FloatingSpace)
/// stores — `GetWindowRect`'s window rect is translated back through the
/// window's measured [`InvisibleBounds`](crate::common::InvisibleBounds).
fn current_visible_rect(&self, hwnd: isize) -> Option<Rect> {
let hwnd_handle = HWND(hwnd as *mut _);
let window_rect = registry_win32::get_window_rect(hwnd_handle).ok()?;
let window = self.registry.get_window(hwnd_handle)?;
Some(window.invisible_bounds.window_to_visible(window_rect))
}
}
#[cfg(test)]
impl FlowWM {
/// Construct a `FlowWM` for unit tests, bypassing all Win32 startup work.
///
/// Fabricates non-essential fields (`PipeServer`, hook handles, animator)
/// with test-safe dummies — see [`PipeServer::test_dummy`],
/// [`HookThreadHandle::test_dummy`](crate::registry::hooks::HookThreadHandle::test_dummy),
/// [`HookSignal::test_dummy`](crate::registry::hooks::HookSignal::test_dummy),
/// and [`MockBackend`](crate::animation::backend::mock::MockBackend). The
/// caller supplies a fully-populated [`WindowRegistry`] and the
/// [`Vec<Monitor>`] stack; everything else defaults.
///
/// Intended for testing dispatch handlers (`dispatch_focus`,
/// `dispatch_swap_column`, etc.) without spawning a real daemon. The
/// fabricated fields are never touched on no-op code paths; positive paths
/// exercise `monitors` / `animator` / `registry` as normal.
pub(super) fn new_for_test(registry: WindowRegistry, monitors: Vec<Monitor>) -> Self {
use crate::animation::backend::mock::MockBackend;
let (_hook_sender, hook_receiver) = std::sync::mpsc::channel();
Self {
registry,
history: HistoryStore::default(),
monitors,
active_monitor: 0,
animator: WindowAnimator::new(
MockBackend::new(),
crate::animation::AnimatorConfig::default(),
),
server: PipeServer::test_dummy(),
config: FlowConfig::default(),
config_dir: std::path::PathBuf::new(),
hook_receiver,
_hook_handle: hooks::HookThreadHandle::test_dummy(),
hook_signal: hooks::HookSignal::test_dummy(),
shutting_down: false,
pending_creations: Vec::new(),
float_resume_deadline: None,
last_foreground_sync: std::time::Instant::now(),
}
}
}