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