1use crate::command::CommandBuilder;
2use crate::commands::process::ProcessInfo;
3use crate::error::CommandError;
4use crate::protocol::KittyMessage;
5use bon::Builder;
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use std::collections::HashMap;
9
10fn is_false(v: &bool) -> bool {
11 !v
12}
13
14#[derive(Debug, Deserialize)]
15pub struct WindowInfo {
16 pub id: Option<u64>,
17 pub title: Option<String>,
18 pub pid: Option<u64>,
19 pub cwd: Option<String>,
20 #[serde(default)]
21 pub cmdline: Vec<String>,
22 #[serde(default)]
23 pub foreground_processes: Vec<ProcessInfo>,
24 pub at_prompt: Option<bool>,
25 pub columns: Option<u64>,
26 pub created_at: Option<u64>,
27 #[serde(default)]
28 pub env: HashMap<String, String>,
29 pub in_alternate_screen: Option<bool>,
30 pub is_active: Option<bool>,
31 pub is_focused: Option<bool>,
32 pub is_self: Option<bool>,
33 pub last_cmd_exit_status: Option<i32>,
34 pub last_reported_cmdline: Option<String>,
35 pub lines: Option<u64>,
36 #[serde(default)]
37 pub user_vars: HashMap<String, String>,
38}
39
40#[derive(Debug, Deserialize)]
41pub struct LayoutOpts {
42 #[serde(default)]
43 pub bias: i32,
44 #[serde(default)]
45 pub full_size: i32,
46 #[serde(default)]
47 pub mirrored: String,
48}
49
50#[derive(Debug, Deserialize)]
51pub struct WindowGroup {
52 pub id: u64,
53 #[serde(default)]
54 pub window_ids: Vec<u64>,
55}
56
57#[derive(Debug, Deserialize)]
58pub struct AllWindows {
59 #[serde(default)]
60 pub active_group_history: Vec<u64>,
61 pub active_group_idx: Option<u64>,
62 #[serde(default)]
63 pub window_groups: Vec<WindowGroup>,
64}
65
66#[derive(Debug, Deserialize)]
67pub struct LayoutState {
68 pub all_windows: Option<AllWindows>,
69 #[serde(default)]
70 pub biased_map: HashMap<String, serde_json::Value>,
71 pub class: Option<String>,
72 #[serde(default)]
73 pub main_bias: Vec<f32>,
74 pub opts: Option<LayoutOpts>,
75}
76
77#[derive(Debug, Deserialize)]
78pub struct TabGroup {
79 pub id: u64,
80 #[serde(default)]
81 pub windows: Vec<u64>,
82}
83
84#[derive(Debug, Deserialize)]
85pub struct TabInfo {
86 #[serde(default)]
87 pub windows: Vec<WindowInfo>,
88 #[serde(default)]
89 pub active_window_history: Vec<u64>,
90 #[serde(default)]
91 pub enabled_layouts: Vec<String>,
92 #[serde(default)]
93 pub groups: Vec<TabGroup>,
94 pub id: Option<u64>,
95 pub is_active: Option<bool>,
96 pub is_focused: Option<bool>,
97 pub layout: Option<String>,
98 pub layout_opts: Option<LayoutOpts>,
99 pub layout_state: Option<LayoutState>,
100 pub title: Option<String>,
101}
102
103#[derive(Debug, Deserialize)]
104pub struct OsInstance {
105 #[serde(default)]
106 pub tabs: Vec<TabInfo>,
107 pub background_opacity: Option<f32>,
108 pub id: Option<u64>,
109 pub is_active: Option<bool>,
110 pub is_focused: Option<bool>,
111 pub last_focused: Option<bool>,
112 pub platform_window_id: Option<u64>,
113 pub wm_class: Option<String>,
114 pub wm_name: Option<String>,
115}
116
117pub fn parse_response_data(data: &Value) -> Result<Vec<OsInstance>, serde_json::Error> {
118 let parsed_data = if let Some(s) = data.as_str() {
119 serde_json::from_str(s)?
120 } else {
121 data.clone()
122 };
123 serde_json::from_value(parsed_data)
124}
125
126use crate::protocol::KittyResponse;
127
128#[derive(Builder, Serialize)]
129pub struct LsCommand {
130 #[builder(default = false)]
131 #[serde(skip_serializing_if = "is_false")]
132 all_env_vars: bool,
133
134 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
135 match_spec: Option<String>,
136
137 #[serde(skip_serializing_if = "Option::is_none", default)]
138 match_tab: Option<String>,
139
140 #[builder(default = false)]
141 #[serde(skip_serializing_if = "is_false", rename = "self")]
142 self_window: bool,
143}
144
145impl LsCommand {
146 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
147 let payload =
148 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
149
150 Ok(CommandBuilder::new("ls").payload(payload).build())
151 }
152
153 pub fn parse_response(response: &KittyResponse) -> Result<Vec<OsInstance>, serde_json::Error> {
154 if let Some(data) = &response.data {
155 parse_response_data(data)
156 } else {
157 Ok(vec![])
158 }
159 }
160}
161
162#[derive(Builder, Serialize)]
163pub struct SendTextCommand {
164 data: String,
165
166 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
167 match_spec: Option<String>,
168
169 #[serde(skip_serializing_if = "Option::is_none", default)]
170 match_tab: Option<String>,
171
172 #[builder(default = false)]
173 #[serde(skip_serializing_if = "is_false")]
174 all: bool,
175
176 #[builder(default = false)]
177 #[serde(skip_serializing_if = "is_false")]
178 exclude_active: bool,
179
180 #[builder(default = "disable".to_string())]
181 #[serde(skip_serializing_if = "is_skip_bracketed_paste")]
182 bracketed_paste: String,
183}
184
185fn is_skip_bracketed_paste(v: &String) -> bool {
186 v == "disable"
187}
188
189impl SendTextCommand {
190 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
191 if self.data.is_empty() {
192 return Err(CommandError::MissingParameter(
193 "data".to_string(),
194 "send-text".to_string(),
195 ));
196 }
197
198 let payload =
199 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
200
201 Ok(CommandBuilder::new("send-text").payload(payload).build())
202 }
203}
204
205#[derive(Builder, Serialize)]
206pub struct SendKeyCommand {
207 keys: String,
208
209 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
210 match_spec: Option<String>,
211
212 #[serde(skip_serializing_if = "Option::is_none", default)]
213 match_tab: Option<String>,
214
215 #[builder(default = false)]
216 #[serde(skip_serializing_if = "is_false")]
217 all: bool,
218
219 #[builder(default = false)]
220 #[serde(skip_serializing_if = "is_false")]
221 exclude_active: bool,
222}
223
224impl SendKeyCommand {
225 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
226 if self.keys.is_empty() {
227 return Err(CommandError::MissingParameter(
228 "keys".to_string(),
229 "send-key".to_string(),
230 ));
231 }
232
233 let payload =
234 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
235
236 Ok(CommandBuilder::new("send-key").payload(payload).build())
237 }
238}
239
240#[derive(Builder, Serialize)]
241pub struct CloseWindowCommand {
242 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
243 match_spec: Option<String>,
244
245 #[builder(default = false)]
246 #[serde(skip_serializing_if = "is_false", rename = "self")]
247 self_window: bool,
248
249 #[builder(default = false)]
250 #[serde(skip_serializing_if = "is_false")]
251 ignore_no_match: bool,
252}
253
254impl CloseWindowCommand {
255 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
256 let payload =
257 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
258
259 Ok(CommandBuilder::new("close-window").payload(payload).build())
260 }
261}
262
263#[derive(Builder, Serialize)]
264pub struct ResizeWindowCommand {
265 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
266 match_spec: Option<String>,
267
268 #[builder(default = false)]
269 #[serde(skip_serializing_if = "is_false", rename = "self")]
270 self_window: bool,
271
272 #[builder(default = 2)]
273 increment: i32,
274
275 #[builder(default = "horizontal".to_string())]
276 #[serde(skip_serializing_if = "is_skip_horizontal")]
277 axis: String,
278}
279
280fn is_skip_horizontal(v: &String) -> bool {
281 v == "horizontal"
282}
283
284impl ResizeWindowCommand {
285 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
286 let payload =
287 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
288
289 Ok(CommandBuilder::new("resize-window")
290 .payload(payload)
291 .build())
292 }
293}
294
295#[derive(Builder, Serialize)]
296pub struct FocusWindowCommand {
297 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
298 match_spec: Option<String>,
299}
300
301impl FocusWindowCommand {
302 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
303 let payload =
304 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
305
306 Ok(CommandBuilder::new("focus-window").payload(payload).build())
307 }
308}
309
310#[derive(Builder, Serialize)]
311pub struct SelectWindowCommand {
312 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
313 match_spec: Option<String>,
314
315 #[serde(skip_serializing_if = "Option::is_none", default)]
316 title: Option<String>,
317
318 #[builder(default = false)]
319 #[serde(skip_serializing_if = "is_false")]
320 exclude_active: bool,
321
322 #[builder(default = false)]
323 #[serde(skip_serializing_if = "is_false")]
324 reactivate_prev_tab: bool,
325}
326
327impl SelectWindowCommand {
328 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
329 let payload =
330 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
331
332 Ok(CommandBuilder::new("select-window")
333 .payload(payload)
334 .build())
335 }
336}
337
338#[derive(Builder, Serialize)]
339pub struct NewWindowCommand {
340 #[serde(skip_serializing_if = "Option::is_none", default)]
341 args: Option<String>,
342
343 #[serde(skip_serializing_if = "Option::is_none", default)]
344 title: Option<String>,
345
346 #[serde(skip_serializing_if = "Option::is_none", default)]
347 cwd: Option<String>,
348
349 #[builder(default = false)]
350 #[serde(skip_serializing_if = "is_false")]
351 keep_focus: bool,
352
353 #[serde(skip_serializing_if = "Option::is_none", default)]
354 window_type: Option<String>,
355
356 #[builder(default = false)]
357 #[serde(skip_serializing_if = "is_false")]
358 new_tab: bool,
359
360 #[serde(skip_serializing_if = "Option::is_none", default)]
361 tab_title: Option<String>,
362}
363
364impl NewWindowCommand {
365 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
366 let payload =
367 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
368
369 Ok(CommandBuilder::new("new-window").payload(payload).build())
370 }
371}
372
373#[derive(Builder, Serialize)]
374pub struct DetachWindowCommand {
375 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
376 match_spec: Option<String>,
377
378 #[serde(skip_serializing_if = "Option::is_none", default)]
379 target_tab: Option<String>,
380
381 #[builder(default = false)]
382 #[serde(skip_serializing_if = "is_false", rename = "self")]
383 self_window: bool,
384
385 #[builder(default = false)]
386 #[serde(skip_serializing_if = "is_false")]
387 stay_in_tab: bool,
388}
389
390impl DetachWindowCommand {
391 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
392 let payload =
393 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
394
395 Ok(CommandBuilder::new("detach-window")
396 .payload(payload)
397 .build())
398 }
399}
400
401#[derive(Builder, Serialize)]
402pub struct SetWindowTitleCommand {
403 title: String,
404
405 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
406 match_spec: Option<String>,
407
408 #[builder(default = false)]
409 #[serde(skip_serializing_if = "is_false")]
410 temporary: bool,
411}
412
413impl SetWindowTitleCommand {
414 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
415 if self.title.is_empty() {
416 return Err(CommandError::MissingParameter(
417 "title".to_string(),
418 "set-window-title".to_string(),
419 ));
420 }
421
422 let payload =
423 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
424
425 Ok(CommandBuilder::new("set-window-title")
426 .payload(payload)
427 .build())
428 }
429}
430
431#[derive(Builder, Serialize)]
432pub struct SetWindowLogoCommand {
433 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
434 match_spec: Option<String>,
435
436 #[serde(skip_serializing_if = "Option::is_none", default)]
437 data: Option<String>,
438
439 #[serde(skip_serializing_if = "Option::is_none", default)]
440 position: Option<String>,
441
442 #[serde(skip_serializing_if = "Option::is_none", default)]
443 alpha: Option<f32>,
444
445 #[builder(default = false)]
446 #[serde(skip_serializing_if = "is_false", rename = "self")]
447 self_window: bool,
448}
449
450impl SetWindowLogoCommand {
451 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
452 let payload =
453 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
454
455 Ok(CommandBuilder::new("set-window-logo")
456 .payload(payload)
457 .build())
458 }
459}
460
461#[derive(Builder, Serialize)]
462pub struct GetTextCommand {
463 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
464 match_spec: Option<String>,
465
466 #[serde(skip_serializing_if = "Option::is_none", default)]
467 extent: Option<String>,
468
469 #[builder(default = false)]
470 #[serde(skip_serializing_if = "is_false")]
471 ansi: bool,
472
473 #[builder(default = false)]
474 #[serde(skip_serializing_if = "is_false")]
475 cursor: bool,
476
477 #[builder(default = false)]
478 #[serde(skip_serializing_if = "is_false")]
479 wrap_markers: bool,
480
481 #[builder(default = false)]
482 #[serde(skip_serializing_if = "is_false")]
483 clear_selection: bool,
484
485 #[builder(default = false)]
486 #[serde(skip_serializing_if = "is_false", rename = "self")]
487 self_window: bool,
488}
489
490impl GetTextCommand {
491 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
492 let payload =
493 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
494
495 Ok(CommandBuilder::new("get-text").payload(payload).build())
496 }
497}
498
499#[derive(Builder, Serialize)]
500pub struct ScrollWindowCommand {
501 amount: i32,
502
503 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
504 match_spec: Option<String>,
505}
506
507impl ScrollWindowCommand {
508 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
509 let payload =
510 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
511
512 Ok(CommandBuilder::new("scroll-window")
513 .payload(payload)
514 .build())
515 }
516}
517
518#[derive(Builder, Serialize)]
519pub struct CreateMarkerCommand {
520 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
521 match_spec: Option<String>,
522
523 #[builder(default = false)]
524 #[serde(skip_serializing_if = "is_false", rename = "self")]
525 self_window: bool,
526
527 #[serde(skip_serializing_if = "Option::is_none", default)]
528 marker_spec: Option<String>,
529}
530
531impl CreateMarkerCommand {
532 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
533 let payload =
534 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
535
536 Ok(CommandBuilder::new("create-marker")
537 .payload(payload)
538 .build())
539 }
540}
541
542#[derive(Builder, Serialize)]
543pub struct RemoveMarkerCommand {
544 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
545 match_spec: Option<String>,
546
547 #[builder(default = false)]
548 #[serde(skip_serializing_if = "is_false", rename = "self")]
549 self_window: bool,
550}
551
552impl RemoveMarkerCommand {
553 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
554 let payload =
555 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
556
557 Ok(CommandBuilder::new("remove-marker")
558 .payload(payload)
559 .build())
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn test_ls_basic() {
569 let cmd = LsCommand::builder().build().to_message();
570 assert!(cmd.is_ok());
571 let msg = cmd.unwrap();
572 assert_eq!(msg.cmd, "ls");
573 }
574
575 #[test]
576 fn test_ls_with_options() {
577 let cmd = LsCommand::builder()
578 .all_env_vars(true)
579 .self_window(true)
580 .build()
581 .to_message();
582 assert!(cmd.is_ok());
583 let msg = cmd.unwrap();
584 assert_eq!(msg.cmd, "ls");
585 }
586
587 #[test]
588 fn test_ls_with_match() {
589 let cmd = LsCommand::builder()
590 .match_spec("id:1".to_string())
591 .build()
592 .to_message();
593 assert!(cmd.is_ok());
594 let msg = cmd.unwrap();
595 assert_eq!(msg.cmd, "ls");
596 }
597
598 #[test]
599 fn test_send_text_basic() {
600 let cmd = SendTextCommand::builder()
601 .data("text:hello".to_string())
602 .build()
603 .to_message();
604 assert!(cmd.is_ok());
605 let msg = cmd.unwrap();
606 assert_eq!(msg.cmd, "send-text");
607 }
608
609 #[test]
610 fn test_send_text_empty() {
611 let cmd = SendTextCommand::builder()
612 .data("".to_string())
613 .build()
614 .to_message();
615 assert!(cmd.is_err());
616 if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
617 assert_eq!(field, "data");
618 assert_eq!(cmd_name, "send-text");
619 } else {
620 panic!("Expected MissingParameter error");
621 }
622 }
623
624 #[test]
625 fn test_send_text_with_options() {
626 let cmd = SendTextCommand::builder()
627 .data("text:test".to_string())
628 .match_spec("id:1".to_string())
629 .all(true)
630 .build()
631 .to_message();
632 assert!(cmd.is_ok());
633 let msg = cmd.unwrap();
634 assert_eq!(msg.cmd, "send-text");
635 }
636
637 #[test]
638 fn test_send_key_basic() {
639 let cmd = SendKeyCommand::builder()
640 .keys("ctrl+c".to_string())
641 .build()
642 .to_message();
643 assert!(cmd.is_ok());
644 let msg = cmd.unwrap();
645 assert_eq!(msg.cmd, "send-key");
646 }
647
648 #[test]
649 fn test_send_key_empty() {
650 let cmd = SendKeyCommand::builder()
651 .keys("".to_string())
652 .build()
653 .to_message();
654 assert!(cmd.is_err());
655 if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
656 assert_eq!(field, "keys");
657 assert_eq!(cmd_name, "send-key");
658 } else {
659 panic!("Expected MissingParameter error");
660 }
661 }
662
663 #[test]
664 fn test_send_key_with_options() {
665 let cmd = SendKeyCommand::builder()
666 .keys("alt+f4".to_string())
667 .match_spec("id:1".to_string())
668 .all(true)
669 .build()
670 .to_message();
671 assert!(cmd.is_ok());
672 let msg = cmd.unwrap();
673 assert_eq!(msg.cmd, "send-key");
674 }
675
676 #[test]
677 fn test_close_window_basic() {
678 let cmd = CloseWindowCommand::builder().build().to_message();
679 assert!(cmd.is_ok());
680 let msg = cmd.unwrap();
681 assert_eq!(msg.cmd, "close-window");
682 }
683
684 #[test]
685 fn test_close_window_with_options() {
686 let cmd = CloseWindowCommand::builder()
687 .match_spec("id:1".to_string())
688 .self_window(true)
689 .ignore_no_match(true)
690 .build()
691 .to_message();
692 assert!(cmd.is_ok());
693 let msg = cmd.unwrap();
694 assert_eq!(msg.cmd, "close-window");
695 }
696
697 #[test]
698 fn test_resize_window_basic() {
699 let cmd = ResizeWindowCommand::builder().build().to_message();
700 assert!(cmd.is_ok());
701 let msg = cmd.unwrap();
702 assert_eq!(msg.cmd, "resize-window");
703 }
704
705 #[test]
706 fn test_resize_window_with_options() {
707 let cmd = ResizeWindowCommand::builder()
708 .match_spec("id:1".to_string())
709 .increment(5)
710 .axis("vertical".to_string())
711 .build()
712 .to_message();
713 assert!(cmd.is_ok());
714 let msg = cmd.unwrap();
715 assert_eq!(msg.cmd, "resize-window");
716 }
717
718 #[test]
719 fn test_focus_window_basic() {
720 let cmd = FocusWindowCommand::builder().build().to_message();
721 assert!(cmd.is_ok());
722 let msg = cmd.unwrap();
723 assert_eq!(msg.cmd, "focus-window");
724 }
725
726 #[test]
727 fn test_focus_window_with_match() {
728 let cmd = FocusWindowCommand::builder()
729 .match_spec("id:1".to_string())
730 .build()
731 .to_message();
732 assert!(cmd.is_ok());
733 let msg = cmd.unwrap();
734 assert_eq!(msg.cmd, "focus-window");
735 }
736
737 #[test]
738 fn test_select_window_basic() {
739 let cmd = SelectWindowCommand::builder().build().to_message();
740 assert!(cmd.is_ok());
741 let msg = cmd.unwrap();
742 assert_eq!(msg.cmd, "select-window");
743 }
744
745 #[test]
746 fn test_select_window_with_options() {
747 let cmd = SelectWindowCommand::builder()
748 .match_spec("id:1".to_string())
749 .title("Select Me".to_string())
750 .exclude_active(true)
751 .reactivate_prev_tab(true)
752 .build()
753 .to_message();
754 assert!(cmd.is_ok());
755 let msg = cmd.unwrap();
756 assert_eq!(msg.cmd, "select-window");
757 }
758
759 #[test]
760 fn test_new_window_basic() {
761 let cmd = NewWindowCommand::builder().build().to_message();
762 assert!(cmd.is_ok());
763 let msg = cmd.unwrap();
764 assert_eq!(msg.cmd, "new-window");
765 }
766
767 #[test]
768 fn test_new_window_with_options() {
769 let cmd = NewWindowCommand::builder()
770 .args("bash".to_string())
771 .title("My Window".to_string())
772 .cwd("/home/user".to_string())
773 .keep_focus(true)
774 .window_type("overlay".to_string())
775 .new_tab(true)
776 .tab_title("New Tab".to_string())
777 .build()
778 .to_message();
779 assert!(cmd.is_ok());
780 let msg = cmd.unwrap();
781 assert_eq!(msg.cmd, "new-window");
782 }
783
784 #[test]
785 fn test_detach_window_basic() {
786 let cmd = DetachWindowCommand::builder().build().to_message();
787 assert!(cmd.is_ok());
788 let msg = cmd.unwrap();
789 assert_eq!(msg.cmd, "detach-window");
790 }
791
792 #[test]
793 fn test_detach_window_with_options() {
794 let cmd = DetachWindowCommand::builder()
795 .match_spec("id:1".to_string())
796 .target_tab("id:2".to_string())
797 .self_window(true)
798 .stay_in_tab(true)
799 .build()
800 .to_message();
801 assert!(cmd.is_ok());
802 let msg = cmd.unwrap();
803 assert_eq!(msg.cmd, "detach-window");
804 }
805
806 #[test]
807 fn test_set_window_title_basic() {
808 let cmd = SetWindowTitleCommand::builder()
809 .title("My Title".to_string())
810 .build()
811 .to_message();
812 assert!(cmd.is_ok());
813 let msg = cmd.unwrap();
814 assert_eq!(msg.cmd, "set-window-title");
815 }
816
817 #[test]
818 fn test_set_window_title_empty() {
819 let cmd = SetWindowTitleCommand::builder()
820 .title("".to_string())
821 .build()
822 .to_message();
823 assert!(cmd.is_err());
824 if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
825 assert_eq!(field, "title");
826 assert_eq!(cmd_name, "set-window-title");
827 } else {
828 panic!("Expected MissingParameter error");
829 }
830 }
831
832 #[test]
833 fn test_set_window_title_with_options() {
834 let cmd = SetWindowTitleCommand::builder()
835 .title("New Title".to_string())
836 .match_spec("id:1".to_string())
837 .temporary(true)
838 .build()
839 .to_message();
840 assert!(cmd.is_ok());
841 let msg = cmd.unwrap();
842 assert_eq!(msg.cmd, "set-window-title");
843 }
844
845 #[test]
846 fn test_set_window_logo_basic() {
847 let cmd = SetWindowLogoCommand::builder().build().to_message();
848 assert!(cmd.is_ok());
849 let msg = cmd.unwrap();
850 assert_eq!(msg.cmd, "set-window-logo");
851 }
852
853 #[test]
854 fn test_set_window_logo_with_options() {
855 let cmd = SetWindowLogoCommand::builder()
856 .match_spec("id:1".to_string())
857 .data("base64data".to_string())
858 .position("top-left".to_string())
859 .alpha(0.5)
860 .self_window(true)
861 .build()
862 .to_message();
863 assert!(cmd.is_ok());
864 let msg = cmd.unwrap();
865 assert_eq!(msg.cmd, "set-window-logo");
866 }
867
868 #[test]
869 fn test_get_text_basic() {
870 let cmd = GetTextCommand::builder().build().to_message();
871 assert!(cmd.is_ok());
872 let msg = cmd.unwrap();
873 assert_eq!(msg.cmd, "get-text");
874 }
875
876 #[test]
877 fn test_get_text_with_options() {
878 let cmd = GetTextCommand::builder()
879 .match_spec("id:1".to_string())
880 .extent("all".to_string())
881 .ansi(true)
882 .cursor(true)
883 .wrap_markers(true)
884 .clear_selection(true)
885 .self_window(true)
886 .build()
887 .to_message();
888 assert!(cmd.is_ok());
889 let msg = cmd.unwrap();
890 assert_eq!(msg.cmd, "get-text");
891 }
892
893 #[test]
894 fn test_scroll_window_basic() {
895 let cmd = ScrollWindowCommand::builder()
896 .amount(5)
897 .build()
898 .to_message();
899 assert!(cmd.is_ok());
900 let msg = cmd.unwrap();
901 assert_eq!(msg.cmd, "scroll-window");
902 }
903
904 #[test]
905 fn test_scroll_window_with_match() {
906 let cmd = ScrollWindowCommand::builder()
907 .amount(-5)
908 .match_spec("id:1".to_string())
909 .build()
910 .to_message();
911 assert!(cmd.is_ok());
912 let msg = cmd.unwrap();
913 assert_eq!(msg.cmd, "scroll-window");
914 }
915
916 #[test]
917 fn test_create_marker_basic() {
918 let cmd = CreateMarkerCommand::builder().build().to_message();
919 assert!(cmd.is_ok());
920 let msg = cmd.unwrap();
921 assert_eq!(msg.cmd, "create-marker");
922 }
923
924 #[test]
925 fn test_create_marker_with_options() {
926 let cmd = CreateMarkerCommand::builder()
927 .match_spec("id:1".to_string())
928 .self_window(true)
929 .marker_spec("marker1".to_string())
930 .build()
931 .to_message();
932 assert!(cmd.is_ok());
933 let msg = cmd.unwrap();
934 assert_eq!(msg.cmd, "create-marker");
935 }
936
937 #[test]
938 fn test_remove_marker_basic() {
939 let cmd = RemoveMarkerCommand::builder().build().to_message();
940 assert!(cmd.is_ok());
941 let msg = cmd.unwrap();
942 assert_eq!(msg.cmd, "remove-marker");
943 }
944
945 #[test]
946 fn test_remove_marker_with_options() {
947 let cmd = RemoveMarkerCommand::builder()
948 .match_spec("id:1".to_string())
949 .self_window(true)
950 .build()
951 .to_message();
952 assert!(cmd.is_ok());
953 let msg = cmd.unwrap();
954 assert_eq!(msg.cmd, "remove-marker");
955 }
956
957 #[test]
958 fn test_parse_ls_response() {
959 let json_data = serde_json::json!([
960 {
961 "tabs": [
962 {
963 "windows": [
964 {
965 "id": 1,
966 "title": "Test Window",
967 "pid": 12345,
968 "cwd": "/home/user",
969 "cmdline": ["/bin/bash"],
970 "foreground_processes": []
971 }
972 ]
973 }
974 ]
975 }
976 ]);
977
978 let response = KittyResponse {
979 ok: true,
980 data: Some(json_data),
981 error: None,
982 };
983
984 let instances = LsCommand::parse_response(&response).unwrap();
985 assert_eq!(instances.len(), 1);
986 assert_eq!(instances[0].tabs.len(), 1);
987 assert_eq!(instances[0].tabs[0].windows.len(), 1);
988 assert_eq!(instances[0].tabs[0].windows[0].id, Some(1));
989 assert_eq!(
990 instances[0].tabs[0].windows[0].title,
991 Some("Test Window".to_string())
992 );
993 }
994
995 #[test]
996 fn test_parse_ls_response_empty() {
997 let response = KittyResponse {
998 ok: true,
999 data: None,
1000 error: None,
1001 };
1002
1003 let instances = LsCommand::parse_response(&response).unwrap();
1004 assert!(instances.is_empty());
1005 }
1006}