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