Skip to main content

kitty_rc/commands/
window.rs

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}