1use crate::backend::obj::*;
9use crate::backend::service::{self, WebCallType, WebReturnType};
10
11use super::super::AppState;
12use super::reducer_helpers::{dispatch_to_reducer, publish_events};
13
14pub(crate) async fn handle_switch_workspace(state: &AppState, call: &WebCallType) -> WebReturnType {
19 let store = &state.mstore;
20 let args = &call.args;
21 let window_id: String = match service::get_arg(args, 0) {
22 Ok(v) => v,
23 Err(e) => return WebReturnType::error(e),
24 };
25 let ws_id: String = match service::get_arg(args, 1) {
26 Ok(v) => v,
27 Err(e) => return WebReturnType::error(e),
28 };
29 let events = dispatch_to_reducer(
30 state,
31 agentmux_common::ipc::Command::SwitchWorkspace {
32 window_id: window_id.clone(),
33 workspace_id: ws_id.clone(),
34 },
35 )
36 .await;
37 if let Some(err_msg) = events.iter().find_map(|e| match e {
38 agentmux_common::ipc::Event::Error { message, .. } => Some(message.clone()),
39 _ => None,
40 }) {
41 return WebReturnType::error(err_msg);
42 }
43 for ev in &events {
44 if let Err(e) = crate::persist_subscriber::apply_event_to_mstore(ev, store) {
45 return WebReturnType::error(format!(
46 "SwitchWorkspace: SQLite write failed: {}",
47 e
48 ));
49 }
50 }
51 publish_events(state, &events);
52 WebReturnType::success_empty()
53}
54
55pub(crate) async fn handle_set_window_pos_and_size(state: &AppState, call: &WebCallType) -> WebReturnType {
56 let store = &state.mstore;
57 let args = &call.args;
58 let window_id: String = match service::get_arg(args, 0) {
59 Ok(v) => v,
60 Err(e) => return WebReturnType::error(e),
61 };
62 let pos: Option<Point> = service::get_optional_arg(args, 1).unwrap_or(None);
63 let size: Option<WinSize> = service::get_optional_arg(args, 2).unwrap_or(None);
64 match store.must_get::<Window>(&window_id) {
65 Ok(mut win) => {
66 if let Some(p) = pos {
67 win.pos = p;
68 }
69 if let Some(s) = size {
70 win.winsize = s;
71 }
72 match store.update(&mut win) {
73 Ok(_) => WebReturnType::success_empty(),
74 Err(e) => WebReturnType::error(e.to_string()),
75 }
76 }
77 Err(e) => WebReturnType::error(e.to_string()),
78 }
79}
80
81pub(crate) async fn handle_set_window_opacity(state: &AppState, call: &WebCallType) -> WebReturnType {
95 let store = &state.mstore;
96 let args = &call.args;
97 let window_id: String = match service::get_arg(args, 0) {
98 Ok(v) => v,
99 Err(e) => return WebReturnType::error(e),
100 };
101 let opacity: Option<f32> = match service::get_optional_arg(args, 1) {
102 Ok(v) => v,
103 Err(e) => return WebReturnType::error(e),
104 };
105 if let Some(o) = opacity {
106 if !(0.0..=1.0).contains(&o) {
107 return WebReturnType::error(format!(
108 "SetWindowOpacity: opacity must be in 0.0..=1.0, got {o}"
109 ));
110 }
111 }
112 match store.must_get::<Window>(&window_id) {
113 Ok(mut win) => {
114 win.opacity = opacity;
115 match store.update(&mut win) {
116 Ok(_) => WebReturnType::success_empty(),
117 Err(e) => WebReturnType::error(e.to_string()),
118 }
119 }
120 Err(e) => WebReturnType::error(e.to_string()),
121 }
122}
123
124pub(crate) async fn handle_set_window_topology(state: &AppState, call: &WebCallType) -> WebReturnType {
132 let store = &state.mstore;
133 let args = &call.args;
134 let window_id: String = match service::get_arg(args, 0) {
135 Ok(v) => v,
136 Err(e) => return WebReturnType::error(e),
137 };
138 let kind: Option<String> = match service::get_optional_arg(args, 1) {
139 Ok(v) => v,
140 Err(e) => return WebReturnType::error(e),
141 };
142 let parent_window_id: Option<String> = match service::get_optional_arg(args, 2) {
143 Ok(v) => v,
144 Err(e) => return WebReturnType::error(e),
145 };
146 if let Some(k) = &kind {
147 if k != "full_instance" && k != "subwindow" {
148 return WebReturnType::error(format!(
149 "SetWindowTopology: kind must be 'full_instance' or 'subwindow', got '{k}'"
150 ));
151 }
152 if k == "subwindow" && parent_window_id.is_none() {
153 return WebReturnType::error(
154 "SetWindowTopology: kind='subwindow' requires a non-null parent_window_id"
155 .to_string(),
156 );
157 }
158 }
159 if parent_window_id.is_some() && kind.as_deref() != Some("subwindow") {
165 return WebReturnType::error(
166 "SetWindowTopology: parent_window_id must only be set when kind='subwindow'"
167 .to_string(),
168 );
169 }
170 if let Some(parent_id) = &parent_window_id {
174 if parent_id == &window_id {
175 return WebReturnType::error(
176 "SetWindowTopology: parent_window_id must not equal window_id".to_string(),
177 );
178 }
179 if store.must_get::<Window>(parent_id).is_err() {
180 return WebReturnType::error(format!(
181 "SetWindowTopology: parent_window_id '{parent_id}' does not reference an existing window"
182 ));
183 }
184 }
185 match store.must_get::<Window>(&window_id) {
186 Ok(mut win) => {
187 win.kind = kind;
188 win.parent_window_id = parent_window_id;
189 match store.update(&mut win) {
190 Ok(_) => WebReturnType::success_empty(),
191 Err(e) => WebReturnType::error(e.to_string()),
192 }
193 }
194 Err(e) => WebReturnType::error(e.to_string()),
195 }
196}
197
198#[cfg(test)]
200mod set_window_opacity_tests {
201 use super::super::window::handle_window_service;
202 use crate::backend::obj::Window;
203 use crate::backend::service::WebCallType;
204 use crate::server::tests::test_state;
205
206 fn call(window_id: &str, opacity: Option<f32>) -> WebCallType {
207 WebCallType {
208 service: "window".to_string(),
209 method: "SetWindowOpacity".to_string(),
210 uicontext: None,
211 args: vec![
212 serde_json::Value::String(window_id.to_string()),
213 opacity
214 .map(|o| serde_json::json!(o))
215 .unwrap_or(serde_json::Value::Null),
216 ],
217 }
218 }
219
220 async fn seeded_window_id(state: &crate::server::AppState) -> String {
221 let ret = handle_window_service(
222 state,
223 &WebCallType {
224 service: "window".to_string(),
225 method: "CreateWindow".to_string(),
226 uicontext: None,
227 args: vec![
228 serde_json::Value::Null,
229 serde_json::Value::String(String::new()),
230 ],
231 },
232 )
233 .await;
234 assert!(ret.success, "CreateWindow failed: {:?}", ret.error);
235 ret.data
236 .expect("CreateWindow returns the Window")
237 .get("oid")
238 .and_then(|v| v.as_str())
239 .expect("window has an oid")
240 .to_string()
241 }
242
243 #[tokio::test]
244 async fn sets_and_persists_opacity() {
245 let state = test_state();
246 let window_id = seeded_window_id(&state).await;
247
248 let ret = handle_window_service(&state, &call(&window_id, Some(0.85))).await;
249 assert!(ret.success, "SetWindowOpacity failed: {:?}", ret.error);
250
251 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
252 assert_eq!(win.opacity, Some(0.85));
253 }
254
255 #[tokio::test]
256 async fn null_opacity_clears_it() {
257 let state = test_state();
258 let window_id = seeded_window_id(&state).await;
259
260 handle_window_service(&state, &call(&window_id, Some(0.5)))
261 .await;
262 let ret = handle_window_service(&state, &call(&window_id, None)).await;
263 assert!(ret.success, "SetWindowOpacity (clear) failed: {:?}", ret.error);
264
265 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
266 assert_eq!(win.opacity, None, "None must clear back to unset/opaque");
267 }
268
269 #[tokio::test]
270 async fn rejects_out_of_range_opacity() {
271 let state = test_state();
272 let window_id = seeded_window_id(&state).await;
273
274 for bad in [-0.1_f32, 1.1_f32] {
275 let ret = handle_window_service(&state, &call(&window_id, Some(bad))).await;
276 assert!(!ret.success, "opacity {bad} must be rejected");
277 }
278 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
280 assert_eq!(win.opacity, None);
281 }
282
283 #[tokio::test]
284 async fn errors_on_unknown_window() {
285 let state = test_state();
286 let ret = handle_window_service(&state, &call("ghost-window", Some(0.5))).await;
287 assert!(!ret.success, "unknown window must error");
288 }
289
290 #[tokio::test]
295 async fn malformed_opacity_errors_and_does_not_clear() {
296 let state = test_state();
297 let window_id = seeded_window_id(&state).await;
298
299 let set_ret = handle_window_service(&state, &call(&window_id, Some(0.6))).await;
300 assert!(set_ret.success);
301
302 let bad_call = WebCallType {
303 service: "window".to_string(),
304 method: "SetWindowOpacity".to_string(),
305 uicontext: None,
306 args: vec![
307 serde_json::Value::String(window_id.clone()),
308 serde_json::Value::String("not-a-number".to_string()),
309 ],
310 };
311 let ret = handle_window_service(&state, &bad_call).await;
312 assert!(!ret.success, "malformed opacity must error, not be treated as clear");
313
314 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
315 assert_eq!(
316 win.opacity,
317 Some(0.6),
318 "a rejected malformed argument must not wipe the previously-set opacity"
319 );
320 }
321
322 #[tokio::test]
323 async fn boundary_values_are_accepted() {
324 let state = test_state();
325 let window_id = seeded_window_id(&state).await;
326
327 for boundary in [0.0_f32, 1.0_f32] {
328 let ret = handle_window_service(&state, &call(&window_id, Some(boundary))).await;
329 assert!(ret.success, "boundary opacity {boundary} must be accepted");
330 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
331 assert_eq!(win.opacity, Some(boundary));
332 }
333 }
334}
335
336#[cfg(test)]
338mod set_window_topology_tests {
339 use super::super::window::handle_window_service;
340 use crate::backend::obj::Window;
341 use crate::backend::service::WebCallType;
342 use crate::server::tests::test_state;
343
344 fn call(window_id: &str, kind: Option<&str>, parent_window_id: Option<&str>) -> WebCallType {
345 WebCallType {
346 service: "window".to_string(),
347 method: "SetWindowTopology".to_string(),
348 uicontext: None,
349 args: vec![
350 serde_json::Value::String(window_id.to_string()),
351 kind.map(|k| serde_json::json!(k)).unwrap_or(serde_json::Value::Null),
352 parent_window_id
353 .map(|p| serde_json::json!(p))
354 .unwrap_or(serde_json::Value::Null),
355 ],
356 }
357 }
358
359 async fn seeded_window_id(state: &crate::server::AppState) -> String {
360 let ret = handle_window_service(
361 state,
362 &WebCallType {
363 service: "window".to_string(),
364 method: "CreateWindow".to_string(),
365 uicontext: None,
366 args: vec![
367 serde_json::Value::Null,
368 serde_json::Value::String(String::new()),
369 ],
370 },
371 )
372 .await;
373 assert!(ret.success, "CreateWindow failed: {:?}", ret.error);
374 ret.data
375 .expect("CreateWindow returns the Window")
376 .get("oid")
377 .and_then(|v| v.as_str())
378 .expect("window has an oid")
379 .to_string()
380 }
381
382 #[tokio::test]
383 async fn sets_and_persists_full_instance() {
384 let state = test_state();
385 let window_id = seeded_window_id(&state).await;
386
387 let ret = handle_window_service(&state, &call(&window_id, Some("full_instance"), None)).await;
388 assert!(ret.success, "SetWindowTopology failed: {:?}", ret.error);
389
390 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
391 assert_eq!(win.kind, Some("full_instance".to_string()));
392 assert_eq!(win.parent_window_id, None);
393 }
394
395 #[tokio::test]
396 async fn sets_and_persists_subwindow_with_parent() {
397 let state = test_state();
398 let parent_id = seeded_window_id(&state).await;
399 let window_id = seeded_window_id(&state).await;
400
401 let ret =
402 handle_window_service(&state, &call(&window_id, Some("subwindow"), Some(&parent_id))).await;
403 assert!(ret.success, "SetWindowTopology failed: {:?}", ret.error);
404
405 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
406 assert_eq!(win.kind, Some("subwindow".to_string()));
407 assert_eq!(win.parent_window_id, Some(parent_id));
408 }
409
410 #[tokio::test]
411 async fn rejects_subwindow_without_parent() {
412 let state = test_state();
413 let window_id = seeded_window_id(&state).await;
414
415 let ret = handle_window_service(&state, &call(&window_id, Some("subwindow"), None)).await;
416 assert!(!ret.success, "subwindow with no parent_window_id must be rejected");
417
418 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
419 assert_eq!(win.kind, None, "rejected write must not persist");
420 }
421
422 #[tokio::test]
423 async fn rejects_unknown_kind() {
424 let state = test_state();
425 let window_id = seeded_window_id(&state).await;
426
427 let ret = handle_window_service(&state, &call(&window_id, Some("floating"), None)).await;
428 assert!(!ret.success, "unknown kind must be rejected");
429 }
430
431 #[tokio::test]
435 async fn rejects_full_instance_with_parent() {
436 let state = test_state();
437 let parent_id = seeded_window_id(&state).await;
438 let window_id = seeded_window_id(&state).await;
439
440 let ret =
441 handle_window_service(&state, &call(&window_id, Some("full_instance"), Some(&parent_id)))
442 .await;
443 assert!(!ret.success, "full_instance with a parent_window_id must be rejected");
444
445 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
446 assert_eq!(win.kind, None, "rejected write must not persist");
447 }
448
449 #[tokio::test]
452 async fn rejects_none_kind_with_parent() {
453 let state = test_state();
454 let parent_id = seeded_window_id(&state).await;
455 let window_id = seeded_window_id(&state).await;
456
457 let ret = handle_window_service(&state, &call(&window_id, None, Some(&parent_id))).await;
458 assert!(!ret.success, "kind=None with a parent_window_id must be rejected");
459 }
460
461 #[tokio::test]
464 async fn rejects_dangling_parent_window_id() {
465 let state = test_state();
466 let window_id = seeded_window_id(&state).await;
467
468 let ret =
469 handle_window_service(&state, &call(&window_id, Some("subwindow"), Some("ghost-parent")))
470 .await;
471 assert!(!ret.success, "a parent_window_id that doesn't exist must be rejected");
472
473 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
474 assert_eq!(win.kind, None, "rejected write must not persist");
475 }
476
477 #[tokio::test]
479 async fn rejects_self_referential_parent() {
480 let state = test_state();
481 let window_id = seeded_window_id(&state).await;
482
483 let ret =
484 handle_window_service(&state, &call(&window_id, Some("subwindow"), Some(&window_id))).await;
485 assert!(!ret.success, "a window referencing itself as parent must be rejected");
486 }
487
488 #[tokio::test]
489 async fn null_kind_clears_it() {
490 let state = test_state();
491 let window_id = seeded_window_id(&state).await;
492
493 handle_window_service(&state, &call(&window_id, Some("full_instance"), None)).await;
494 let ret = handle_window_service(&state, &call(&window_id, None, None)).await;
495 assert!(ret.success, "SetWindowTopology (clear) failed: {:?}", ret.error);
496
497 let win = state.mstore.must_get::<Window>(&window_id).unwrap();
498 assert_eq!(win.kind, None);
499 assert_eq!(win.parent_window_id, None);
500 }
501
502 #[tokio::test]
503 async fn errors_on_unknown_window() {
504 let state = test_state();
505 let ret = handle_window_service(&state, &call("ghost-window", Some("full_instance"), None)).await;
506 assert!(!ret.success, "unknown window must error");
507 }
508}