1use agentmux_common::ipc::{Command, DriftKind, Event};
45
46use crate::state::State;
47
48#[derive(Debug, Clone)]
51pub struct Ctx {
52 pub now_rfc3339: String,
56 pub conn_id: u64,
60 pub registered_pid: Option<u32>,
67 pub now_ms: u64,
74}
75
76mod pool;
77mod window;
78mod saga;
79mod connection;
80
81pub fn update(state: &mut State, cmd: Command, ctx: &Ctx) -> Vec<Event> {
85 let _ = ctx.conn_id; let mut cmd_events = match cmd {
88 Command::Register {
89 kind,
90 pid,
91 version,
92 } => connection::handle_register(state, ctx, kind, pid, version),
93 Command::Ping { nonce } => {
94 let v = state.bump_version();
95 vec![Event::Pong { nonce, version: v }]
96 }
97 Command::Goodbye => connection::handle_goodbye(state, ctx.registered_pid.unwrap_or(0)),
98 Command::ReportWindowOpened {
99 label,
100 kind,
101 parent_label,
102 } => {
103 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportWindowOpened") {
104 return vec![err];
105 }
106 window::handle_report_window_opened(state, ctx, label, kind, parent_label)
107 }
108 Command::ReportWindowClosed { label } => {
109 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportWindowClosed") {
110 return vec![err];
111 }
112 window::handle_report_window_closed(state, label)
113 }
114 Command::ReportPoolWindowAdded { label, saga_id } => {
115 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportPoolWindowAdded") {
116 return vec![err];
117 }
118 pool::handle_report_pool_window_added(state, label, saga_id)
119 }
120 Command::ReportPoolWindowRemoved { label } => {
121 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportPoolWindowRemoved") {
122 return vec![err];
123 }
124 pool::handle_report_pool_window_removed(state, label)
125 }
126 Command::ReportPoolWindowPromoted { label } => {
133 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportPoolWindowPromoted") {
134 return vec![err];
135 }
136 pool::handle_report_pool_window_promoted(state, label)
137 }
138 Command::SpawnPoolWindow { .. } => {
145 let v = state.bump_version();
146 vec![Event::Error {
147 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
148 message: "SpawnPoolWindow is a launcher→host command; sent to launcher pipe by mistake".into(),
149 fatal: false,
150 version: v,
151 }]
152 }
153 Command::ReportPanesReaped { label, saga_id } => {
163 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportPanesReaped") {
164 return vec![err];
165 }
166 saga::handle_report_panes_reaped(state, label, saga_id)
167 }
168 Command::ReportPoolDrainDecision {
174 label,
175 was_last,
176 saga_id,
177 } => {
178 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportPoolDrainDecision") {
179 return vec![err];
180 }
181 pool::handle_report_pool_drain_decision(state, label, was_last, saga_id)
182 }
183 Command::ReapPanes { .. } => {
188 let v = state.bump_version();
189 vec![Event::Error {
190 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
191 message: "ReapPanes is a launcher→host command; sent to launcher pipe by mistake".into(),
192 fatal: false,
193 version: v,
194 }]
195 }
196 Command::DrainPoolIfLast { .. } => {
197 let v = state.bump_version();
198 vec![Event::Error {
199 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
200 message: "DrainPoolIfLast is a launcher→host command; sent to launcher pipe by mistake".into(),
201 fatal: false,
202 version: v,
203 }]
204 }
205 Command::ReportSagaActionFailed { saga_id, reason } => {
212 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportSagaActionFailed") {
213 return vec![err];
214 }
215 saga::handle_report_saga_action_failed(state, saga_id, reason)
216 }
217 Command::ReportHostCounts { windows, pool } => {
218 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportHostCounts") {
219 return vec![err];
220 }
221 handle_report_host_counts(state, windows, pool)
222 }
223 Command::ReportHostPoolCount { count } => {
224 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportHostPoolCount") {
225 return vec![err];
226 }
227 pool::handle_report_host_pool_count(state, count)
228 }
229 Command::ReportBackendWindowIdRegistered { label, window_id } => {
230 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportBackendWindowIdRegistered") {
231 return vec![err];
232 }
233 window::handle_report_backend_window_id_registered(state, label, window_id)
234 }
235 Command::ReportBackendWindowIdUnregistered { label } => {
236 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportBackendWindowIdUnregistered") {
237 return vec![err];
238 }
239 window::handle_report_backend_window_id_unregistered(state, label)
240 }
241 Command::ReportHwndOpened { hwnd, class_name, title, label_hint } => {
246 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportHwndOpened") {
247 return vec![err];
248 }
249 crate::wrr::apply_hwnd_opened(state, hwnd, class_name, title, label_hint, ctx.now_ms)
250 }
251 Command::ReportHwndDestroyed { hwnd } => {
252 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportHwndDestroyed") {
253 return vec![err];
254 }
255 let host_running = connection::host_is_running(state);
256 crate::wrr::apply_hwnd_destroyed(state, hwnd, host_running)
257 }
258 Command::ReportHwndVisibilityChanged { hwnd, visible } => {
259 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportHwndVisibilityChanged") {
260 return vec![err];
261 }
262 crate::wrr::apply_hwnd_visibility_changed(state, hwnd, visible, ctx.now_ms)
263 }
264 Command::ReportHwndForegroundChanged { hwnd } => {
265 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportHwndForegroundChanged") {
266 return vec![err];
267 }
268 crate::wrr::apply_hwnd_foreground_changed(state, hwnd, ctx.now_ms)
269 }
270 Command::ReportHwndIconicChanged { hwnd, iconic } => {
271 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportHwndIconicChanged") {
272 return vec![err];
273 }
274 crate::wrr::apply_hwnd_iconic_changed(state, hwnd, iconic)
275 }
276 Command::ReportHwndPositionChanged { hwnd, rect } => {
277 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportHwndPositionChanged") {
278 return vec![err];
279 }
280 crate::wrr::apply_hwnd_position_changed(state, hwnd, rect)
281 }
282 Command::ReportMonitorTopologyChanged { rects } => {
283 if let Some(err) = connection::enforce_host_only(state, ctx, "ReportMonitorTopologyChanged") {
284 return vec![err];
285 }
286 crate::wrr::apply_monitor_topology_changed(state, rects)
287 }
288 Command::GetSnapshot => connection::handle_get_snapshot(state),
296 Command::GetEvents { .. } => Vec::new(),
304 Command::CreateWorkspace { .. } => {
307 let v = state.bump_version();
308 vec![Event::Error {
309 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
310 message: "CreateWorkspace is a srv-pipe command; sent to launcher pipe by mistake".into(),
311 fatal: false,
312 version: v,
313 }]
314 }
315 Command::DeleteWorkspace { .. } => {
316 let v = state.bump_version();
317 vec![Event::Error {
318 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
319 message: "DeleteWorkspace is a srv-pipe command; sent to launcher pipe by mistake".into(),
320 fatal: false,
321 version: v,
322 }]
323 }
324 Command::CreateTab { .. } => {
326 let v = state.bump_version();
327 vec![Event::Error {
328 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
329 message: "CreateTab is a srv-pipe command; sent to launcher pipe by mistake".into(),
330 fatal: false,
331 version: v,
332 }]
333 }
334 Command::DeleteTab { .. } => {
335 let v = state.bump_version();
336 vec![Event::Error {
337 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
338 message: "DeleteTab is a srv-pipe command; sent to launcher pipe by mistake".into(),
339 fatal: false,
340 version: v,
341 }]
342 }
343 Command::SetActiveTab { .. } => {
344 let v = state.bump_version();
345 vec![Event::Error {
346 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
347 message: "SetActiveTab is a srv-pipe command; sent to launcher pipe by mistake".into(),
348 fatal: false,
349 version: v,
350 }]
351 }
352 Command::ReorderTab { .. } => {
353 let v = state.bump_version();
354 vec![Event::Error {
355 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
356 message: "ReorderTab is a srv-pipe command; sent to launcher pipe by mistake".into(),
357 fatal: false,
358 version: v,
359 }]
360 }
361 Command::CreateWindow { .. } => {
363 let v = state.bump_version();
364 vec![Event::Error {
365 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
366 message: "CreateWindow is a srv-pipe command; sent to launcher pipe by mistake".into(),
367 fatal: false,
368 version: v,
369 }]
370 }
371 Command::CloseWindowInternal { .. } => {
372 let v = state.bump_version();
373 vec![Event::Error {
374 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
375 message: "CloseWindowInternal is a srv-pipe command; sent to launcher pipe by mistake".into(),
376 fatal: false,
377 version: v,
378 }]
379 }
380 Command::SwitchWorkspace { .. } => {
381 let v = state.bump_version();
382 vec![Event::Error {
383 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
384 message: "SwitchWorkspace is a srv-pipe command; sent to launcher pipe by mistake".into(),
385 fatal: false,
386 version: v,
387 }]
388 }
389 Command::ReorderTabsBulk { .. } => {
391 let v = state.bump_version();
392 vec![Event::Error {
393 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
394 message: "ReorderTabsBulk is a srv-pipe command; sent to launcher pipe by mistake".into(),
395 fatal: false,
396 version: v,
397 }]
398 }
399 Command::RenameWorkspace { .. } => {
400 let v = state.bump_version();
401 vec![Event::Error {
402 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
403 message: "RenameWorkspace is a srv-pipe command; sent to launcher pipe by mistake".into(),
404 fatal: false,
405 version: v,
406 }]
407 }
408 Command::RenameTab { .. } => {
409 let v = state.bump_version();
410 vec![Event::Error {
411 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
412 message: "RenameTab is a srv-pipe command; sent to launcher pipe by mistake".into(),
413 fatal: false,
414 version: v,
415 }]
416 }
417 Command::UpdateWorkspaceMeta { .. } => {
418 let v = state.bump_version();
419 vec![Event::Error {
420 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
421 message: "UpdateWorkspaceMeta is a srv-pipe command; sent to launcher pipe by mistake".into(),
422 fatal: false,
423 version: v,
424 }]
425 }
426 Command::UpdateTabMeta { .. } => {
427 let v = state.bump_version();
428 vec![Event::Error {
429 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
430 message: "UpdateTabMeta is a srv-pipe command; sent to launcher pipe by mistake".into(),
431 fatal: false,
432 version: v,
433 }]
434 }
435 Command::UpdateBlockMeta { .. } => {
436 let v = state.bump_version();
437 vec![Event::Error {
438 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
439 message: "UpdateBlockMeta is a srv-pipe command; sent to launcher pipe by mistake".into(),
440 fatal: false,
441 version: v,
442 }]
443 }
444 Command::CreateBlock { .. } => {
446 let v = state.bump_version();
447 vec![Event::Error {
448 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
449 message: "CreateBlock is a srv-pipe command; sent to launcher pipe by mistake".into(),
450 fatal: false,
451 version: v,
452 }]
453 }
454 Command::DeleteBlock { .. } => {
455 let v = state.bump_version();
456 vec![Event::Error {
457 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
458 message: "DeleteBlock is a srv-pipe command; sent to launcher pipe by mistake".into(),
459 fatal: false,
460 version: v,
461 }]
462 }
463 Command::MoveTab { .. } => {
465 let v = state.bump_version();
466 vec![Event::Error {
467 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
468 message: "MoveTab is a srv-pipe command; sent to launcher pipe by mistake".into(),
469 fatal: false,
470 version: v,
471 }]
472 }
473 Command::MoveBlock { .. } => {
474 let v = state.bump_version();
475 vec![Event::Error {
476 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
477 message: "MoveBlock is a srv-pipe command; sent to launcher pipe by mistake".into(),
478 fatal: false,
479 version: v,
480 }]
481 }
482 Command::SetFocusedNode { .. } => {
486 let v = state.bump_version();
487 vec![Event::Error {
488 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
489 message: "SetFocusedNode is a srv-pipe command; sent to launcher pipe by mistake".into(),
490 fatal: false,
491 version: v,
492 }]
493 }
494 Command::SetMagnifiedNode { .. } => {
495 let v = state.bump_version();
496 vec![Event::Error {
497 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
498 message: "SetMagnifiedNode is a srv-pipe command; sent to launcher pipe by mistake".into(),
499 fatal: false,
500 version: v,
501 }]
502 }
503 Command::GetSrvSnapshot => {
510 let v = state.bump_version();
511 vec![Event::Error {
512 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
513 message: "GetSrvSnapshot is a srv-pipe command; sent to launcher pipe by mistake".to_string(),
514 fatal: false,
515 version: v,
516 }]
517 }
518 Command::LayoutInsertNode { .. }
521 | Command::LayoutInsertNodeAtIndex { .. }
522 | Command::LayoutDeleteNode { .. }
523 | Command::LayoutMoveNode { .. }
524 | Command::LayoutSwapNodes { .. }
525 | Command::LayoutResizeNodes { .. }
526 | Command::LayoutReplaceNode { .. }
527 | Command::LayoutSplitHorizontal { .. }
528 | Command::LayoutSplitVertical { .. }
529 | Command::LayoutClear { .. }
530 | Command::LayoutSetTree { .. }
531 | Command::UpdateWindowMeta { .. } => {
532 let v = state.bump_version();
533 vec![Event::Error {
534 code: agentmux_common::ipc::ErrorCode::InvalidCommand,
535 message: "Srv-pipe command (Layout/UpdateWindowMeta) sent to launcher pipe by mistake".to_string(),
536 fatal: false,
537 version: v,
538 }]
539 }
540 };
541
542 let mut deferred = crate::wrr::drain_deferred_hidden_since_open(state, ctx.now_ms);
553 cmd_events.append(&mut deferred);
554 cmd_events
555}
556
557
558
559fn handle_report_host_counts(state: &mut State, host_windows: u32, host_pool: u32) -> Vec<Event> {
568 let mut out = Vec::new();
569 let mirror_windows = state.windows.len() as u32;
570 let mirror_pool = state.pool.len() as u32;
571 if mirror_windows != host_windows {
572 let v = state.bump_version();
573 out.push(Event::DriftDetected {
574 kind: DriftKind::Windows,
575 host_count: host_windows,
576 mirror_count: mirror_windows,
577 version: v,
578 });
579 }
580 if mirror_pool != host_pool {
581 let v = state.bump_version();
582 out.push(Event::DriftDetected {
583 kind: DriftKind::Pool,
584 host_count: host_pool,
585 mirror_count: mirror_pool,
586 version: v,
587 });
588 }
589 out
590}
591
592
593
594
595
596
597
598
599
600
601#[cfg(test)]
602mod tests;