Skip to main content

kitty_rc/commands/
window.rs

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}