1use crate::command::CommandBuilder;
2use crate::commands::process::ProcessInfo;
3use crate::error::CommandError;
4use crate::protocol::KittyMessage;
5use serde::Deserialize;
6use serde_json::Value;
7use std::collections::HashMap;
8
9#[derive(Debug, Deserialize)]
10pub struct WindowInfo {
11 pub id: Option<u64>,
12 pub title: Option<String>,
13 pub pid: Option<u64>,
14 pub cwd: Option<String>,
15 #[serde(default)]
16 pub cmdline: Vec<String>,
17 #[serde(default)]
18 pub foreground_processes: Vec<ProcessInfo>,
19 pub at_prompt: Option<bool>,
20 pub columns: Option<u64>,
21 pub created_at: Option<u64>,
22 #[serde(default)]
23 pub env: HashMap<String, String>,
24 pub in_alternate_screen: Option<bool>,
25 pub is_active: Option<bool>,
26 pub is_focused: Option<bool>,
27 pub is_self: Option<bool>,
28 pub last_cmd_exit_status: Option<i32>,
29 pub last_reported_cmdline: Option<String>,
30 pub lines: Option<u64>,
31 #[serde(default)]
32 pub user_vars: HashMap<String, String>,
33}
34
35#[derive(Debug, Deserialize)]
36pub struct LayoutOpts {
37 #[serde(default)]
38 pub bias: i32,
39 #[serde(default)]
40 pub full_size: i32,
41 #[serde(default)]
42 pub mirrored: String,
43}
44
45#[derive(Debug, Deserialize)]
46pub struct WindowGroup {
47 pub id: u64,
48 #[serde(default)]
49 pub window_ids: Vec<u64>,
50}
51
52#[derive(Debug, Deserialize)]
53pub struct AllWindows {
54 #[serde(default)]
55 pub active_group_history: Vec<u64>,
56 pub active_group_idx: Option<u64>,
57 #[serde(default)]
58 pub window_groups: Vec<WindowGroup>,
59}
60
61#[derive(Debug, Deserialize)]
62pub struct LayoutState {
63 pub all_windows: Option<AllWindows>,
64 #[serde(default)]
65 pub biased_map: HashMap<String, serde_json::Value>,
66 pub class: Option<String>,
67 #[serde(default)]
68 pub main_bias: Vec<f32>,
69 pub opts: Option<LayoutOpts>,
70}
71
72#[derive(Debug, Deserialize)]
73pub struct TabGroup {
74 pub id: u64,
75 #[serde(default)]
76 pub windows: Vec<u64>,
77}
78
79#[derive(Debug, Deserialize)]
80pub struct TabInfo {
81 #[serde(default)]
82 pub windows: Vec<WindowInfo>,
83 #[serde(default)]
84 pub active_window_history: Vec<u64>,
85 #[serde(default)]
86 pub enabled_layouts: Vec<String>,
87 #[serde(default)]
88 pub groups: Vec<TabGroup>,
89 pub id: Option<u64>,
90 pub is_active: Option<bool>,
91 pub is_focused: Option<bool>,
92 pub layout: Option<String>,
93 pub layout_opts: Option<LayoutOpts>,
94 pub layout_state: Option<LayoutState>,
95 pub title: Option<String>,
96}
97
98#[derive(Debug, Deserialize)]
99pub struct OsInstance {
100 #[serde(default)]
101 pub tabs: Vec<TabInfo>,
102 pub background_opacity: Option<f32>,
103 pub id: Option<u64>,
104 pub is_active: Option<bool>,
105 pub is_focused: Option<bool>,
106 pub last_focused: Option<bool>,
107 pub platform_window_id: Option<u64>,
108 pub wm_class: Option<String>,
109 pub wm_name: Option<String>,
110}
111
112pub fn parse_response_data(data: &Value) -> Result<Vec<OsInstance>, serde_json::Error> {
113 let parsed_data = if let Some(s) = data.as_str() {
114 serde_json::from_str(s)?
115 } else {
116 data.clone()
117 };
118 serde_json::from_value(parsed_data)
119}
120
121use crate::protocol::KittyResponse;
122
123pub struct LsCommand {
124 all_env_vars: bool,
125 match_spec: Option<String>,
126 match_tab: Option<String>,
127 self_window: bool,
128}
129
130impl LsCommand {
131 pub fn new() -> Self {
132 Self {
133 all_env_vars: false,
134 match_spec: None,
135 match_tab: None,
136 self_window: false,
137 }
138 }
139
140 pub fn all_env_vars(mut self, value: bool) -> Self {
141 self.all_env_vars = value;
142 self
143 }
144
145 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
146 self.match_spec = Some(spec.into());
147 self
148 }
149
150 pub fn match_tab(mut self, spec: impl Into<String>) -> Self {
151 self.match_tab = Some(spec.into());
152 self
153 }
154
155 pub fn self_window(mut self, value: bool) -> Self {
156 self.self_window = value;
157 self
158 }
159
160 pub fn build(self) -> Result<KittyMessage, CommandError> {
161 let mut payload = serde_json::Map::new();
162
163 if self.all_env_vars {
164 payload.insert("all_env_vars".to_string(), serde_json::Value::Bool(true));
165 }
166
167 if let Some(match_spec) = self.match_spec {
168 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
169 }
170
171 if let Some(match_tab) = self.match_tab {
172 payload.insert(
173 "match_tab".to_string(),
174 serde_json::Value::String(match_tab),
175 );
176 }
177
178 if self.self_window {
179 payload.insert("self".to_string(), serde_json::Value::Bool(true));
180 }
181
182 Ok(CommandBuilder::new("ls")
183 .payload(serde_json::Value::Object(payload))
184 .build())
185 }
186
187 pub fn parse_response(response: &KittyResponse) -> Result<Vec<OsInstance>, serde_json::Error> {
188 if let Some(data) = &response.data {
189 parse_response_data(data)
190 } else {
191 Ok(vec![])
192 }
193 }
194}
195
196pub struct SendTextCommand {
197 data: String,
198 match_spec: Option<String>,
199 match_tab: Option<String>,
200 all: bool,
201 exclude_active: bool,
202 bracketed_paste: String,
203}
204
205impl SendTextCommand {
206 pub fn new(data: impl Into<String>) -> Self {
207 Self {
208 data: data.into(),
209 match_spec: None,
210 match_tab: None,
211 all: false,
212 exclude_active: false,
213 bracketed_paste: "disable".to_string(),
214 }
215 }
216
217 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
218 self.match_spec = Some(spec.into());
219 self
220 }
221
222 pub fn match_tab(mut self, spec: impl Into<String>) -> Self {
223 self.match_tab = Some(spec.into());
224 self
225 }
226
227 pub fn all(mut self, value: bool) -> Self {
228 self.all = value;
229 self
230 }
231
232 pub fn exclude_active(mut self, value: bool) -> Self {
233 self.exclude_active = value;
234 self
235 }
236
237 pub fn bracketed_paste(mut self, value: impl Into<String>) -> Self {
238 self.bracketed_paste = value.into();
239 self
240 }
241
242 pub fn build(self) -> Result<KittyMessage, CommandError> {
243 let mut payload = serde_json::Map::new();
244
245 if self.data.is_empty() {
246 return Err(CommandError::MissingParameter(
247 "data".to_string(),
248 "send-text".to_string(),
249 ));
250 }
251
252 payload.insert("data".to_string(), serde_json::Value::String(self.data));
253
254 if let Some(match_spec) = self.match_spec {
255 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
256 }
257
258 if let Some(match_tab) = self.match_tab {
259 payload.insert(
260 "match_tab".to_string(),
261 serde_json::Value::String(match_tab),
262 );
263 }
264
265 if self.all {
266 payload.insert("all".to_string(), serde_json::Value::Bool(true));
267 }
268
269 if self.exclude_active {
270 payload.insert("exclude_active".to_string(), serde_json::Value::Bool(true));
271 }
272
273 if self.bracketed_paste != "disable" {
274 payload.insert(
275 "bracketed_paste".to_string(),
276 serde_json::Value::String(self.bracketed_paste),
277 );
278 }
279
280 Ok(CommandBuilder::new("send-text")
281 .payload(serde_json::Value::Object(payload))
282 .build())
283 }
284}
285
286pub struct SendKeyCommand {
287 keys: String,
288 match_spec: Option<String>,
289 match_tab: Option<String>,
290 all: bool,
291 exclude_active: bool,
292}
293
294impl SendKeyCommand {
295 pub fn new(keys: impl Into<String>) -> Self {
296 Self {
297 keys: keys.into(),
298 match_spec: None,
299 match_tab: None,
300 all: false,
301 exclude_active: false,
302 }
303 }
304
305 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
306 self.match_spec = Some(spec.into());
307 self
308 }
309
310 pub fn match_tab(mut self, spec: impl Into<String>) -> Self {
311 self.match_tab = Some(spec.into());
312 self
313 }
314
315 pub fn all(mut self, value: bool) -> Self {
316 self.all = value;
317 self
318 }
319
320 pub fn exclude_active(mut self, value: bool) -> Self {
321 self.exclude_active = value;
322 self
323 }
324
325 pub fn build(self) -> Result<KittyMessage, CommandError> {
326 let mut payload = serde_json::Map::new();
327
328 if self.keys.is_empty() {
329 return Err(CommandError::MissingParameter(
330 "keys".to_string(),
331 "send-key".to_string(),
332 ));
333 }
334
335 payload.insert("keys".to_string(), serde_json::Value::String(self.keys));
336
337 if let Some(match_spec) = self.match_spec {
338 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
339 }
340
341 if let Some(match_tab) = self.match_tab {
342 payload.insert(
343 "match_tab".to_string(),
344 serde_json::Value::String(match_tab),
345 );
346 }
347
348 if self.all {
349 payload.insert("all".to_string(), serde_json::Value::Bool(true));
350 }
351
352 if self.exclude_active {
353 payload.insert("exclude_active".to_string(), serde_json::Value::Bool(true));
354 }
355
356 Ok(CommandBuilder::new("send-key")
357 .payload(serde_json::Value::Object(payload))
358 .build())
359 }
360}
361
362pub struct CloseWindowCommand {
363 match_spec: Option<String>,
364 self_window: bool,
365 ignore_no_match: bool,
366}
367
368impl CloseWindowCommand {
369 pub fn new() -> Self {
370 Self {
371 match_spec: None,
372 self_window: false,
373 ignore_no_match: false,
374 }
375 }
376
377 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
378 self.match_spec = Some(spec.into());
379 self
380 }
381
382 pub fn self_window(mut self, value: bool) -> Self {
383 self.self_window = value;
384 self
385 }
386
387 pub fn ignore_no_match(mut self, value: bool) -> Self {
388 self.ignore_no_match = value;
389 self
390 }
391
392 pub fn build(self) -> Result<KittyMessage, CommandError> {
393 let mut payload = serde_json::Map::new();
394
395 if let Some(match_spec) = self.match_spec {
396 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
397 }
398
399 if self.self_window {
400 payload.insert("self".to_string(), serde_json::Value::Bool(true));
401 }
402
403 if self.ignore_no_match {
404 payload.insert("ignore_no_match".to_string(), serde_json::Value::Bool(true));
405 }
406
407 Ok(CommandBuilder::new("close-window")
408 .payload(serde_json::Value::Object(payload))
409 .build())
410 }
411}
412
413pub struct ResizeWindowCommand {
414 match_spec: Option<String>,
415 self_window: bool,
416 increment: i32,
417 axis: String,
418}
419
420impl ResizeWindowCommand {
421 pub fn new() -> Self {
422 Self {
423 match_spec: None,
424 self_window: false,
425 increment: 2,
426 axis: "horizontal".to_string(),
427 }
428 }
429
430 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
431 self.match_spec = Some(spec.into());
432 self
433 }
434
435 pub fn self_window(mut self, value: bool) -> Self {
436 self.self_window = value;
437 self
438 }
439
440 pub fn increment(mut self, value: i32) -> Self {
441 self.increment = value;
442 self
443 }
444
445 pub fn axis(mut self, value: impl Into<String>) -> Self {
446 self.axis = value.into();
447 self
448 }
449
450 pub fn build(self) -> Result<KittyMessage, CommandError> {
451 let mut payload = serde_json::Map::new();
452
453 if let Some(match_spec) = self.match_spec {
454 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
455 }
456
457 if self.self_window {
458 payload.insert("self".to_string(), serde_json::Value::Bool(true));
459 }
460
461 payload.insert(
462 "increment".to_string(),
463 serde_json::Value::Number(self.increment.into()),
464 );
465
466 if self.axis != "horizontal" {
467 payload.insert("axis".to_string(), serde_json::Value::String(self.axis));
468 }
469
470 Ok(CommandBuilder::new("resize-window")
471 .payload(serde_json::Value::Object(payload))
472 .build())
473 }
474}
475
476pub struct FocusWindowCommand {
477 match_spec: Option<String>,
478}
479
480impl FocusWindowCommand {
481 pub fn new() -> Self {
482 Self { match_spec: None }
483 }
484
485 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
486 self.match_spec = Some(spec.into());
487 self
488 }
489
490 pub fn build(self) -> Result<KittyMessage, CommandError> {
491 let mut payload = serde_json::Map::new();
492
493 if let Some(match_spec) = self.match_spec {
494 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
495 }
496
497 Ok(CommandBuilder::new("focus-window")
498 .payload(serde_json::Value::Object(payload))
499 .build())
500 }
501}
502
503pub struct SelectWindowCommand {
504 match_spec: Option<String>,
505 title: Option<String>,
506 exclude_active: bool,
507 reactivate_prev_tab: bool,
508}
509
510impl SelectWindowCommand {
511 pub fn new() -> Self {
512 Self {
513 match_spec: None,
514 title: None,
515 exclude_active: false,
516 reactivate_prev_tab: false,
517 }
518 }
519
520 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
521 self.match_spec = Some(spec.into());
522 self
523 }
524
525 pub fn title(mut self, value: impl Into<String>) -> Self {
526 self.title = Some(value.into());
527 self
528 }
529
530 pub fn exclude_active(mut self, value: bool) -> Self {
531 self.exclude_active = value;
532 self
533 }
534
535 pub fn reactivate_prev_tab(mut self, value: bool) -> Self {
536 self.reactivate_prev_tab = value;
537 self
538 }
539
540 pub fn build(self) -> Result<KittyMessage, CommandError> {
541 let mut payload = serde_json::Map::new();
542
543 if let Some(match_spec) = self.match_spec {
544 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
545 }
546
547 if let Some(title) = self.title {
548 payload.insert("title".to_string(), serde_json::Value::String(title));
549 }
550
551 if self.exclude_active {
552 payload.insert("exclude_active".to_string(), serde_json::Value::Bool(true));
553 }
554
555 if self.reactivate_prev_tab {
556 payload.insert(
557 "reactivate_prev_tab".to_string(),
558 serde_json::Value::Bool(true),
559 );
560 }
561
562 Ok(CommandBuilder::new("select-window")
563 .payload(serde_json::Value::Object(payload))
564 .build())
565 }
566}
567
568pub struct NewWindowCommand {
569 args: Option<String>,
570 title: Option<String>,
571 cwd: Option<String>,
572 keep_focus: bool,
573 window_type: Option<String>,
574 new_tab: bool,
575 tab_title: Option<String>,
576}
577
578impl NewWindowCommand {
579 pub fn new() -> Self {
580 Self {
581 args: None,
582 title: None,
583 cwd: None,
584 keep_focus: false,
585 window_type: None,
586 new_tab: false,
587 tab_title: None,
588 }
589 }
590
591 pub fn args(mut self, value: impl Into<String>) -> Self {
592 self.args = Some(value.into());
593 self
594 }
595
596 pub fn title(mut self, value: impl Into<String>) -> Self {
597 self.title = Some(value.into());
598 self
599 }
600
601 pub fn cwd(mut self, value: impl Into<String>) -> Self {
602 self.cwd = Some(value.into());
603 self
604 }
605
606 pub fn keep_focus(mut self, value: bool) -> Self {
607 self.keep_focus = value;
608 self
609 }
610
611 pub fn window_type(mut self, value: impl Into<String>) -> Self {
612 self.window_type = Some(value.into());
613 self
614 }
615
616 pub fn new_tab(mut self, value: bool) -> Self {
617 self.new_tab = value;
618 self
619 }
620
621 pub fn tab_title(mut self, value: impl Into<String>) -> Self {
622 self.tab_title = Some(value.into());
623 self
624 }
625
626 pub fn build(self) -> Result<KittyMessage, CommandError> {
627 let mut payload = serde_json::Map::new();
628
629 if let Some(args) = self.args {
630 payload.insert("args".to_string(), serde_json::Value::String(args));
631 }
632
633 if let Some(title) = self.title {
634 payload.insert("title".to_string(), serde_json::Value::String(title));
635 }
636
637 if let Some(cwd) = self.cwd {
638 payload.insert("cwd".to_string(), serde_json::Value::String(cwd));
639 }
640
641 if self.keep_focus {
642 payload.insert("keep_focus".to_string(), serde_json::Value::Bool(true));
643 }
644
645 if let Some(window_type) = self.window_type {
646 payload.insert(
647 "window_type".to_string(),
648 serde_json::Value::String(window_type),
649 );
650 }
651
652 if self.new_tab {
653 payload.insert("new_tab".to_string(), serde_json::Value::Bool(true));
654 }
655
656 if let Some(tab_title) = self.tab_title {
657 payload.insert(
658 "tab_title".to_string(),
659 serde_json::Value::String(tab_title),
660 );
661 }
662
663 Ok(CommandBuilder::new("new-window")
664 .payload(serde_json::Value::Object(payload))
665 .build())
666 }
667}
668
669pub struct DetachWindowCommand {
670 match_spec: Option<String>,
671 target_tab: Option<String>,
672 self_window: bool,
673 stay_in_tab: bool,
674}
675
676impl DetachWindowCommand {
677 pub fn new() -> Self {
678 Self {
679 match_spec: None,
680 target_tab: None,
681 self_window: false,
682 stay_in_tab: false,
683 }
684 }
685
686 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
687 self.match_spec = Some(spec.into());
688 self
689 }
690
691 pub fn target_tab(mut self, spec: impl Into<String>) -> Self {
692 self.target_tab = Some(spec.into());
693 self
694 }
695
696 pub fn self_window(mut self, value: bool) -> Self {
697 self.self_window = value;
698 self
699 }
700
701 pub fn stay_in_tab(mut self, value: bool) -> Self {
702 self.stay_in_tab = value;
703 self
704 }
705
706 pub fn build(self) -> Result<KittyMessage, CommandError> {
707 let mut payload = serde_json::Map::new();
708
709 if let Some(match_spec) = self.match_spec {
710 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
711 }
712
713 if let Some(target_tab) = self.target_tab {
714 payload.insert(
715 "target_tab".to_string(),
716 serde_json::Value::String(target_tab),
717 );
718 }
719
720 if self.self_window {
721 payload.insert("self".to_string(), serde_json::Value::Bool(true));
722 }
723
724 if self.stay_in_tab {
725 payload.insert("stay_in_tab".to_string(), serde_json::Value::Bool(true));
726 }
727
728 Ok(CommandBuilder::new("detach-window")
729 .payload(serde_json::Value::Object(payload))
730 .build())
731 }
732}
733
734pub struct SetWindowTitleCommand {
735 match_spec: Option<String>,
736 title: String,
737 temporary: bool,
738}
739
740impl SetWindowTitleCommand {
741 pub fn new(title: impl Into<String>) -> Self {
742 Self {
743 match_spec: None,
744 title: title.into(),
745 temporary: false,
746 }
747 }
748
749 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
750 self.match_spec = Some(spec.into());
751 self
752 }
753
754 pub fn temporary(mut self, value: bool) -> Self {
755 self.temporary = value;
756 self
757 }
758
759 pub fn build(self) -> Result<KittyMessage, CommandError> {
760 let mut payload = serde_json::Map::new();
761
762 if self.title.is_empty() {
763 return Err(CommandError::MissingParameter(
764 "title".to_string(),
765 "set-window-title".to_string(),
766 ));
767 }
768
769 payload.insert("title".to_string(), serde_json::Value::String(self.title));
770
771 if let Some(match_spec) = self.match_spec {
772 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
773 }
774
775 if self.temporary {
776 payload.insert("temporary".to_string(), serde_json::Value::Bool(true));
777 }
778
779 Ok(CommandBuilder::new("set-window-title")
780 .payload(serde_json::Value::Object(payload))
781 .build())
782 }
783}
784
785pub struct SetWindowLogoCommand {
786 match_spec: Option<String>,
787 data: Option<String>,
788 position: Option<String>,
789 alpha: Option<f32>,
790 self_window: bool,
791}
792
793impl SetWindowLogoCommand {
794 pub fn new() -> Self {
795 Self {
796 match_spec: None,
797 data: None,
798 position: None,
799 alpha: None,
800 self_window: false,
801 }
802 }
803
804 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
805 self.match_spec = Some(spec.into());
806 self
807 }
808
809 pub fn data(mut self, value: impl Into<String>) -> Self {
810 self.data = Some(value.into());
811 self
812 }
813
814 pub fn position(mut self, value: impl Into<String>) -> Self {
815 self.position = Some(value.into());
816 self
817 }
818
819 pub fn alpha(mut self, value: f32) -> Self {
820 self.alpha = Some(value);
821 self
822 }
823
824 pub fn self_window(mut self, value: bool) -> Self {
825 self.self_window = value;
826 self
827 }
828
829 pub fn build(self) -> Result<KittyMessage, CommandError> {
830 let mut payload = serde_json::Map::new();
831
832 if let Some(match_spec) = self.match_spec {
833 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
834 }
835
836 if let Some(data) = self.data {
837 payload.insert("data".to_string(), serde_json::Value::String(data));
838 }
839
840 if let Some(position) = self.position {
841 payload.insert("position".to_string(), serde_json::Value::String(position));
842 }
843
844 if let Some(alpha) = self.alpha {
845 payload.insert("alpha".to_string(), serde_json::json!(alpha));
846 }
847
848 if self.self_window {
849 payload.insert("self".to_string(), serde_json::Value::Bool(true));
850 }
851
852 Ok(CommandBuilder::new("set-window-logo")
853 .payload(serde_json::Value::Object(payload))
854 .build())
855 }
856}
857
858pub struct GetTextCommand {
859 match_spec: Option<String>,
860 extent: Option<String>,
861 ansi: bool,
862 cursor: bool,
863 wrap_markers: bool,
864 clear_selection: bool,
865 self_window: bool,
866}
867
868impl GetTextCommand {
869 pub fn new() -> Self {
870 Self {
871 match_spec: None,
872 extent: None,
873 ansi: false,
874 cursor: false,
875 wrap_markers: false,
876 clear_selection: false,
877 self_window: false,
878 }
879 }
880
881 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
882 self.match_spec = Some(spec.into());
883 self
884 }
885
886 pub fn extent(mut self, value: impl Into<String>) -> Self {
887 self.extent = Some(value.into());
888 self
889 }
890
891 pub fn ansi(mut self, value: bool) -> Self {
892 self.ansi = value;
893 self
894 }
895
896 pub fn cursor(mut self, value: bool) -> Self {
897 self.cursor = value;
898 self
899 }
900
901 pub fn wrap_markers(mut self, value: bool) -> Self {
902 self.wrap_markers = value;
903 self
904 }
905
906 pub fn clear_selection(mut self, value: bool) -> Self {
907 self.clear_selection = value;
908 self
909 }
910
911 pub fn self_window(mut self, value: bool) -> Self {
912 self.self_window = value;
913 self
914 }
915
916 pub fn build(self) -> Result<KittyMessage, CommandError> {
917 let mut payload = serde_json::Map::new();
918
919 if let Some(match_spec) = self.match_spec {
920 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
921 }
922
923 if let Some(extent) = self.extent {
924 payload.insert("extent".to_string(), serde_json::Value::String(extent));
925 }
926
927 if self.ansi {
928 payload.insert("ansi".to_string(), serde_json::Value::Bool(true));
929 }
930
931 if self.cursor {
932 payload.insert("cursor".to_string(), serde_json::Value::Bool(true));
933 }
934
935 if self.wrap_markers {
936 payload.insert("wrap_markers".to_string(), serde_json::Value::Bool(true));
937 }
938
939 if self.clear_selection {
940 payload.insert("clear_selection".to_string(), serde_json::Value::Bool(true));
941 }
942
943 if self.self_window {
944 payload.insert("self".to_string(), serde_json::Value::Bool(true));
945 }
946
947 Ok(CommandBuilder::new("get-text")
948 .payload(serde_json::Value::Object(payload))
949 .build())
950 }
951}
952
953pub struct ScrollWindowCommand {
954 amount: i32,
955 match_spec: Option<String>,
956}
957
958impl ScrollWindowCommand {
959 pub fn new(amount: i32) -> Self {
960 Self {
961 amount,
962 match_spec: None,
963 }
964 }
965
966 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
967 self.match_spec = Some(spec.into());
968 self
969 }
970
971 pub fn build(self) -> Result<KittyMessage, CommandError> {
972 let mut payload = serde_json::Map::new();
973
974 payload.insert("amount".to_string(), serde_json::json!(self.amount));
975
976 if let Some(match_spec) = self.match_spec {
977 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
978 }
979
980 Ok(CommandBuilder::new("scroll-window")
981 .payload(serde_json::Value::Object(payload))
982 .build())
983 }
984}
985
986pub struct CreateMarkerCommand {
987 match_spec: Option<String>,
988 self_window: bool,
989 marker_spec: Option<String>,
990}
991
992impl CreateMarkerCommand {
993 pub fn new() -> Self {
994 Self {
995 match_spec: None,
996 self_window: false,
997 marker_spec: None,
998 }
999 }
1000
1001 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
1002 self.match_spec = Some(spec.into());
1003 self
1004 }
1005
1006 pub fn self_window(mut self, value: bool) -> Self {
1007 self.self_window = value;
1008 self
1009 }
1010
1011 pub fn marker_spec(mut self, value: impl Into<String>) -> Self {
1012 self.marker_spec = Some(value.into());
1013 self
1014 }
1015
1016 pub fn build(self) -> Result<KittyMessage, CommandError> {
1017 let mut payload = serde_json::Map::new();
1018
1019 if let Some(match_spec) = self.match_spec {
1020 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
1021 }
1022
1023 if self.self_window {
1024 payload.insert("self".to_string(), serde_json::Value::Bool(true));
1025 }
1026
1027 if let Some(marker_spec) = self.marker_spec {
1028 payload.insert(
1029 "marker_spec".to_string(),
1030 serde_json::Value::String(marker_spec),
1031 );
1032 }
1033
1034 Ok(CommandBuilder::new("create-marker")
1035 .payload(serde_json::Value::Object(payload))
1036 .build())
1037 }
1038}
1039
1040pub struct RemoveMarkerCommand {
1041 match_spec: Option<String>,
1042 self_window: bool,
1043}
1044
1045impl RemoveMarkerCommand {
1046 pub fn new() -> Self {
1047 Self {
1048 match_spec: None,
1049 self_window: false,
1050 }
1051 }
1052
1053 pub fn match_spec(mut self, spec: impl Into<String>) -> Self {
1054 self.match_spec = Some(spec.into());
1055 self
1056 }
1057
1058 pub fn self_window(mut self, value: bool) -> Self {
1059 self.self_window = value;
1060 self
1061 }
1062
1063 pub fn build(self) -> Result<KittyMessage, CommandError> {
1064 let mut payload = serde_json::Map::new();
1065
1066 if let Some(match_spec) = self.match_spec {
1067 payload.insert("match".to_string(), serde_json::Value::String(match_spec));
1068 }
1069
1070 if self.self_window {
1071 payload.insert("self".to_string(), serde_json::Value::Bool(true));
1072 }
1073
1074 Ok(CommandBuilder::new("remove-marker")
1075 .payload(serde_json::Value::Object(payload))
1076 .build())
1077 }
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082 use super::*;
1083
1084 #[test]
1085 fn test_ls_basic() {
1086 let cmd = LsCommand::new().build();
1087 assert!(cmd.is_ok());
1088 let msg = cmd.unwrap();
1089 assert_eq!(msg.cmd, "ls");
1090 }
1091
1092 #[test]
1093 fn test_ls_with_options() {
1094 let cmd = LsCommand::new()
1095 .all_env_vars(true)
1096 .self_window(true)
1097 .build();
1098 assert!(cmd.is_ok());
1099 let msg = cmd.unwrap();
1100 assert_eq!(msg.cmd, "ls");
1101 }
1102
1103 #[test]
1104 fn test_ls_with_match() {
1105 let cmd = LsCommand::new().match_spec("id:1").build();
1106 assert!(cmd.is_ok());
1107 let msg = cmd.unwrap();
1108 assert_eq!(msg.cmd, "ls");
1109 }
1110
1111 #[test]
1112 fn test_send_text_basic() {
1113 let cmd = SendTextCommand::new("text:hello").build();
1114 assert!(cmd.is_ok());
1115 let msg = cmd.unwrap();
1116 assert_eq!(msg.cmd, "send-text");
1117 }
1118
1119 #[test]
1120 fn test_send_text_empty() {
1121 let cmd = SendTextCommand::new("").build();
1122 assert!(cmd.is_err());
1123 if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
1124 assert_eq!(field, "data");
1125 assert_eq!(cmd_name, "send-text");
1126 } else {
1127 panic!("Expected MissingParameter error");
1128 }
1129 }
1130
1131 #[test]
1132 fn test_send_text_with_options() {
1133 let cmd = SendTextCommand::new("text:test")
1134 .match_spec("id:1")
1135 .all(true)
1136 .build();
1137 assert!(cmd.is_ok());
1138 let msg = cmd.unwrap();
1139 assert_eq!(msg.cmd, "send-text");
1140 }
1141
1142 #[test]
1143 fn test_send_key_basic() {
1144 let cmd = SendKeyCommand::new("ctrl+c").build();
1145 assert!(cmd.is_ok());
1146 let msg = cmd.unwrap();
1147 assert_eq!(msg.cmd, "send-key");
1148 }
1149
1150 #[test]
1151 fn test_send_key_empty() {
1152 let cmd = SendKeyCommand::new("").build();
1153 assert!(cmd.is_err());
1154 if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
1155 assert_eq!(field, "keys");
1156 assert_eq!(cmd_name, "send-key");
1157 } else {
1158 panic!("Expected MissingParameter error");
1159 }
1160 }
1161
1162 #[test]
1163 fn test_send_key_with_options() {
1164 let cmd = SendKeyCommand::new("alt+f4")
1165 .match_spec("id:1")
1166 .all(true)
1167 .build();
1168 assert!(cmd.is_ok());
1169 let msg = cmd.unwrap();
1170 assert_eq!(msg.cmd, "send-key");
1171 }
1172
1173 #[test]
1174 fn test_close_window_basic() {
1175 let cmd = CloseWindowCommand::new().build();
1176 assert!(cmd.is_ok());
1177 let msg = cmd.unwrap();
1178 assert_eq!(msg.cmd, "close-window");
1179 }
1180
1181 #[test]
1182 fn test_close_window_with_options() {
1183 let cmd = CloseWindowCommand::new()
1184 .match_spec("id:1")
1185 .self_window(true)
1186 .ignore_no_match(true)
1187 .build();
1188 assert!(cmd.is_ok());
1189 let msg = cmd.unwrap();
1190 assert_eq!(msg.cmd, "close-window");
1191 }
1192
1193 #[test]
1194 fn test_resize_window_basic() {
1195 let cmd = ResizeWindowCommand::new().build();
1196 assert!(cmd.is_ok());
1197 let msg = cmd.unwrap();
1198 assert_eq!(msg.cmd, "resize-window");
1199 }
1200
1201 #[test]
1202 fn test_resize_window_with_options() {
1203 let cmd = ResizeWindowCommand::new()
1204 .match_spec("id:1")
1205 .increment(5)
1206 .axis("vertical")
1207 .build();
1208 assert!(cmd.is_ok());
1209 let msg = cmd.unwrap();
1210 assert_eq!(msg.cmd, "resize-window");
1211 }
1212
1213 #[test]
1214 fn test_focus_window_basic() {
1215 let cmd = FocusWindowCommand::new().build();
1216 assert!(cmd.is_ok());
1217 let msg = cmd.unwrap();
1218 assert_eq!(msg.cmd, "focus-window");
1219 }
1220
1221 #[test]
1222 fn test_focus_window_with_match() {
1223 let cmd = FocusWindowCommand::new().match_spec("id:1").build();
1224 assert!(cmd.is_ok());
1225 let msg = cmd.unwrap();
1226 assert_eq!(msg.cmd, "focus-window");
1227 }
1228
1229 #[test]
1230 fn test_select_window_basic() {
1231 let cmd = SelectWindowCommand::new().build();
1232 assert!(cmd.is_ok());
1233 let msg = cmd.unwrap();
1234 assert_eq!(msg.cmd, "select-window");
1235 }
1236
1237 #[test]
1238 fn test_select_window_with_options() {
1239 let cmd = SelectWindowCommand::new()
1240 .match_spec("id:1")
1241 .title("Select Me")
1242 .exclude_active(true)
1243 .reactivate_prev_tab(true)
1244 .build();
1245 assert!(cmd.is_ok());
1246 let msg = cmd.unwrap();
1247 assert_eq!(msg.cmd, "select-window");
1248 }
1249
1250 #[test]
1251 fn test_new_window_basic() {
1252 let cmd = NewWindowCommand::new().build();
1253 assert!(cmd.is_ok());
1254 let msg = cmd.unwrap();
1255 assert_eq!(msg.cmd, "new-window");
1256 }
1257
1258 #[test]
1259 fn test_new_window_with_options() {
1260 let cmd = NewWindowCommand::new()
1261 .args("bash")
1262 .title("My Window")
1263 .cwd("/home/user")
1264 .keep_focus(true)
1265 .window_type("overlay")
1266 .new_tab(true)
1267 .tab_title("New Tab")
1268 .build();
1269 assert!(cmd.is_ok());
1270 let msg = cmd.unwrap();
1271 assert_eq!(msg.cmd, "new-window");
1272 }
1273
1274 #[test]
1275 fn test_detach_window_basic() {
1276 let cmd = DetachWindowCommand::new().build();
1277 assert!(cmd.is_ok());
1278 let msg = cmd.unwrap();
1279 assert_eq!(msg.cmd, "detach-window");
1280 }
1281
1282 #[test]
1283 fn test_detach_window_with_options() {
1284 let cmd = DetachWindowCommand::new()
1285 .match_spec("id:1")
1286 .target_tab("id:2")
1287 .self_window(true)
1288 .stay_in_tab(true)
1289 .build();
1290 assert!(cmd.is_ok());
1291 let msg = cmd.unwrap();
1292 assert_eq!(msg.cmd, "detach-window");
1293 }
1294
1295 #[test]
1296 fn test_set_window_title_basic() {
1297 let cmd = SetWindowTitleCommand::new("My Title").build();
1298 assert!(cmd.is_ok());
1299 let msg = cmd.unwrap();
1300 assert_eq!(msg.cmd, "set-window-title");
1301 }
1302
1303 #[test]
1304 fn test_set_window_title_empty() {
1305 let cmd = SetWindowTitleCommand::new("").build();
1306 assert!(cmd.is_err());
1307 if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
1308 assert_eq!(field, "title");
1309 assert_eq!(cmd_name, "set-window-title");
1310 } else {
1311 panic!("Expected MissingParameter error");
1312 }
1313 }
1314
1315 #[test]
1316 fn test_set_window_title_with_options() {
1317 let cmd = SetWindowTitleCommand::new("New Title")
1318 .match_spec("id:1")
1319 .temporary(true)
1320 .build();
1321 assert!(cmd.is_ok());
1322 let msg = cmd.unwrap();
1323 assert_eq!(msg.cmd, "set-window-title");
1324 }
1325
1326 #[test]
1327 fn test_set_window_logo_basic() {
1328 let cmd = SetWindowLogoCommand::new().build();
1329 assert!(cmd.is_ok());
1330 let msg = cmd.unwrap();
1331 assert_eq!(msg.cmd, "set-window-logo");
1332 }
1333
1334 #[test]
1335 fn test_set_window_logo_with_options() {
1336 let cmd = SetWindowLogoCommand::new()
1337 .match_spec("id:1")
1338 .data("base64data")
1339 .position("top-left")
1340 .alpha(0.5)
1341 .self_window(true)
1342 .build();
1343 assert!(cmd.is_ok());
1344 let msg = cmd.unwrap();
1345 assert_eq!(msg.cmd, "set-window-logo");
1346 }
1347
1348 #[test]
1349 fn test_get_text_basic() {
1350 let cmd = GetTextCommand::new().build();
1351 assert!(cmd.is_ok());
1352 let msg = cmd.unwrap();
1353 assert_eq!(msg.cmd, "get-text");
1354 }
1355
1356 #[test]
1357 fn test_get_text_with_options() {
1358 let cmd = GetTextCommand::new()
1359 .match_spec("id:1")
1360 .extent("all")
1361 .ansi(true)
1362 .cursor(true)
1363 .wrap_markers(true)
1364 .clear_selection(true)
1365 .self_window(true)
1366 .build();
1367 assert!(cmd.is_ok());
1368 let msg = cmd.unwrap();
1369 assert_eq!(msg.cmd, "get-text");
1370 }
1371
1372 #[test]
1373 fn test_scroll_window_basic() {
1374 let cmd = ScrollWindowCommand::new(5).build();
1375 assert!(cmd.is_ok());
1376 let msg = cmd.unwrap();
1377 assert_eq!(msg.cmd, "scroll-window");
1378 }
1379
1380 #[test]
1381 fn test_scroll_window_with_match() {
1382 let cmd = ScrollWindowCommand::new(-5).match_spec("id:1").build();
1383 assert!(cmd.is_ok());
1384 let msg = cmd.unwrap();
1385 assert_eq!(msg.cmd, "scroll-window");
1386 }
1387
1388 #[test]
1389 fn test_create_marker_basic() {
1390 let cmd = CreateMarkerCommand::new().build();
1391 assert!(cmd.is_ok());
1392 let msg = cmd.unwrap();
1393 assert_eq!(msg.cmd, "create-marker");
1394 }
1395
1396 #[test]
1397 fn test_create_marker_with_options() {
1398 let cmd = CreateMarkerCommand::new()
1399 .match_spec("id:1")
1400 .self_window(true)
1401 .marker_spec("marker1")
1402 .build();
1403 assert!(cmd.is_ok());
1404 let msg = cmd.unwrap();
1405 assert_eq!(msg.cmd, "create-marker");
1406 }
1407
1408 #[test]
1409 fn test_remove_marker_basic() {
1410 let cmd = RemoveMarkerCommand::new().build();
1411 assert!(cmd.is_ok());
1412 let msg = cmd.unwrap();
1413 assert_eq!(msg.cmd, "remove-marker");
1414 }
1415
1416 #[test]
1417 fn test_remove_marker_with_options() {
1418 let cmd = RemoveMarkerCommand::new()
1419 .match_spec("id:1")
1420 .self_window(true)
1421 .build();
1422 assert!(cmd.is_ok());
1423 let msg = cmd.unwrap();
1424 assert_eq!(msg.cmd, "remove-marker");
1425 }
1426
1427 #[test]
1428 fn test_parse_ls_response() {
1429 let json_data = serde_json::json!([
1430 {
1431 "tabs": [
1432 {
1433 "windows": [
1434 {
1435 "id": 1,
1436 "title": "Test Window",
1437 "pid": 12345,
1438 "cwd": "/home/user",
1439 "cmdline": ["/bin/bash"],
1440 "foreground_processes": []
1441 }
1442 ]
1443 }
1444 ]
1445 }
1446 ]);
1447
1448 let response = KittyResponse {
1449 ok: true,
1450 data: Some(json_data),
1451 error: None,
1452 };
1453
1454 let instances = LsCommand::parse_response(&response).unwrap();
1455 assert_eq!(instances.len(), 1);
1456 assert_eq!(instances[0].tabs.len(), 1);
1457 assert_eq!(instances[0].tabs[0].windows.len(), 1);
1458 assert_eq!(instances[0].tabs[0].windows[0].id, Some(1));
1459 assert_eq!(
1460 instances[0].tabs[0].windows[0].title,
1461 Some("Test Window".to_string())
1462 );
1463 }
1464
1465 #[test]
1466 fn test_parse_ls_response_empty() {
1467 let response = KittyResponse {
1468 ok: true,
1469 data: None,
1470 error: None,
1471 };
1472
1473 let instances = LsCommand::parse_response(&response).unwrap();
1474 assert!(instances.is_empty());
1475 }
1476}