Skip to main content

kitty_rc/commands/
process.rs

1use crate::command::CommandBuilder;
2use crate::error::CommandError;
3use crate::protocol::KittyMessage;
4use bon::Builder;
5use serde::{Deserialize, Serialize};
6use serde_json::Map;
7
8fn is_false(v: &bool) -> bool {
9    !v
10}
11
12#[derive(Debug, Deserialize)]
13pub struct ProcessInfo {
14    pub pid: Option<u64>,
15    #[serde(default)]
16    pub cmdline: Vec<String>,
17    pub cwd: Option<String>,
18}
19
20#[derive(Builder, Serialize)]
21pub struct RunCommand {
22    #[serde(skip_serializing_if = "Option::is_none", default)]
23    data: Option<String>,
24
25    #[serde(skip_serializing_if = "Option::is_none", default)]
26    cmdline: Option<String>,
27
28    #[serde(skip_serializing_if = "Option::is_none", default)]
29    env: Option<Map<String, serde_json::Value>>,
30
31    #[builder(default = false)]
32    #[serde(skip_serializing_if = "is_false")]
33    allow_remote_control: bool,
34
35    #[serde(skip_serializing_if = "Option::is_none", default)]
36    remote_control_password: Option<String>,
37}
38
39impl RunCommand {
40    pub fn to_message(self) -> Result<KittyMessage, CommandError> {
41        let payload =
42            serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
43
44        Ok(CommandBuilder::new("run").payload(payload).build())
45    }
46}
47
48#[derive(Builder, Serialize)]
49pub struct KittenCommand {
50    #[serde(skip_serializing_if = "Option::is_none", default)]
51    args: Option<String>,
52
53    #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
54    match_spec: Option<String>,
55}
56
57impl KittenCommand {
58    pub fn to_message(self) -> Result<KittyMessage, CommandError> {
59        let payload =
60            serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
61
62        Ok(CommandBuilder::new("kitten").payload(payload).build())
63    }
64}
65
66#[derive(Builder, Serialize)]
67pub struct LaunchCommand {
68    #[serde(skip_serializing_if = "Option::is_none", default)]
69    args: Option<String>,
70
71    #[serde(skip_serializing_if = "Option::is_none", default)]
72    window_title: Option<String>,
73
74    #[serde(skip_serializing_if = "Option::is_none", default)]
75    cwd: Option<String>,
76
77    #[serde(skip_serializing_if = "Option::is_none", default)]
78    env: Option<Map<String, serde_json::Value>>,
79
80    #[serde(skip_serializing_if = "Option::is_none", default)]
81    var: Option<Map<String, serde_json::Value>>,
82
83    #[serde(skip_serializing_if = "Option::is_none", default)]
84    tab_title: Option<String>,
85
86    #[serde(skip_serializing_if = "Option::is_none", default)]
87    window_type: Option<String>,
88
89    #[builder(default = false)]
90    #[serde(skip_serializing_if = "is_false")]
91    keep_focus: bool,
92
93    #[builder(default = false)]
94    #[serde(skip_serializing_if = "is_false")]
95    copy_colors: bool,
96
97    #[builder(default = false)]
98    #[serde(skip_serializing_if = "is_false")]
99    copy_cmdline: bool,
100
101    #[builder(default = false)]
102    #[serde(skip_serializing_if = "is_false")]
103    copy_env: bool,
104
105    #[builder(default = false)]
106    #[serde(skip_serializing_if = "is_false")]
107    hold: bool,
108
109    #[serde(skip_serializing_if = "Option::is_none", default)]
110    location: Option<String>,
111
112    #[builder(default = false)]
113    #[serde(skip_serializing_if = "is_false")]
114    allow_remote_control: bool,
115
116    #[serde(skip_serializing_if = "Option::is_none", default)]
117    remote_control_password: Option<String>,
118
119    #[serde(skip_serializing_if = "Option::is_none", default)]
120    stdin_source: Option<String>,
121
122    #[builder(default = false)]
123    #[serde(skip_serializing_if = "is_false")]
124    stdin_add_formatting: bool,
125
126    #[builder(default = false)]
127    #[serde(skip_serializing_if = "is_false")]
128    stdin_add_line_wrap_markers: bool,
129
130    #[serde(skip_serializing_if = "Option::is_none", default)]
131    spacing: Option<String>,
132
133    #[serde(skip_serializing_if = "Option::is_none", default)]
134    marker: Option<String>,
135
136    #[serde(skip_serializing_if = "Option::is_none", default)]
137    logo: Option<String>,
138
139    #[serde(skip_serializing_if = "Option::is_none", default)]
140    logo_position: Option<String>,
141
142    #[serde(skip_serializing_if = "Option::is_none", default)]
143    logo_alpha: Option<f32>,
144
145    #[builder(default = false)]
146    #[serde(skip_serializing_if = "is_false", rename = "self")]
147    self_window: bool,
148
149    #[serde(skip_serializing_if = "Option::is_none", default)]
150    os_window_title: Option<String>,
151
152    #[serde(skip_serializing_if = "Option::is_none", default)]
153    os_window_name: Option<String>,
154
155    #[serde(skip_serializing_if = "Option::is_none", default)]
156    os_window_class: Option<String>,
157
158    #[serde(skip_serializing_if = "Option::is_none", default)]
159    os_window_state: Option<String>,
160
161    #[serde(skip_serializing_if = "Option::is_none", default)]
162    color: Option<String>,
163
164    #[serde(skip_serializing_if = "Option::is_none", default)]
165    watcher: Option<String>,
166
167    #[serde(skip_serializing_if = "Option::is_none", default)]
168    bias: Option<i32>,
169}
170
171impl LaunchCommand {
172    pub fn to_message(self) -> Result<KittyMessage, CommandError> {
173        let payload =
174            serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
175
176        Ok(CommandBuilder::new("launch").payload(payload).build())
177    }
178}
179
180#[derive(Builder, Serialize)]
181pub struct EnvCommand {
182    env: Map<String, serde_json::Value>,
183}
184
185impl EnvCommand {
186    pub fn to_message(self) -> Result<KittyMessage, CommandError> {
187        if self.env.is_empty() {
188            return Err(CommandError::MissingParameter(
189                "env".to_string(),
190                "env".to_string(),
191            ));
192        }
193
194        let payload =
195            serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
196
197        Ok(CommandBuilder::new("env").payload(payload).build())
198    }
199}
200
201#[derive(Builder, Serialize)]
202pub struct SetUserVarsCommand {
203    var: Vec<String>,
204
205    #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
206    match_spec: Option<String>,
207}
208
209impl SetUserVarsCommand {
210    pub fn to_message(self) -> Result<KittyMessage, CommandError> {
211        if self.var.is_empty() {
212            return Err(CommandError::MissingParameter(
213                "var".to_string(),
214                "set-user-vars".to_string(),
215            ));
216        }
217
218        let payload =
219            serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
220
221        Ok(CommandBuilder::new("set-user-vars")
222            .payload(payload)
223            .build())
224    }
225}
226
227#[derive(Builder, Serialize)]
228pub struct LoadConfigCommand {
229    paths: Vec<String>,
230
231    #[builder(default = false)]
232    #[serde(skip_serializing_if = "is_false", rename = "override")]
233    override_config: bool,
234
235    #[builder(default = false)]
236    #[serde(skip_serializing_if = "is_false")]
237    ignore_overrides: bool,
238}
239
240impl LoadConfigCommand {
241    pub fn to_message(self) -> Result<KittyMessage, CommandError> {
242        if self.paths.is_empty() {
243            return Err(CommandError::MissingParameter(
244                "paths".to_string(),
245                "load-config".to_string(),
246            ));
247        }
248
249        let payload =
250            serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
251
252        Ok(CommandBuilder::new("load-config").payload(payload).build())
253    }
254}
255
256#[derive(Builder, Serialize)]
257pub struct ResizeOSWindowCommand {
258    #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
259    match_spec: Option<String>,
260
261    #[builder(default = false)]
262    #[serde(skip_serializing_if = "is_false", rename = "self")]
263    self_window: bool,
264
265    #[builder(default = false)]
266    #[serde(skip_serializing_if = "is_false")]
267    incremental: bool,
268
269    #[serde(skip_serializing_if = "Option::is_none", default)]
270    action: Option<String>,
271
272    #[serde(skip_serializing_if = "Option::is_none", default)]
273    unit: Option<String>,
274
275    #[serde(skip_serializing_if = "Option::is_none", default)]
276    width: Option<i32>,
277
278    #[serde(skip_serializing_if = "Option::is_none", default)]
279    height: Option<i32>,
280}
281
282impl ResizeOSWindowCommand {
283    pub fn to_message(self) -> Result<KittyMessage, CommandError> {
284        let payload =
285            serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
286
287        Ok(CommandBuilder::new("resize-os-window")
288            .payload(payload)
289            .build())
290    }
291}
292
293#[derive(Builder, Serialize)]
294pub struct DisableLigaturesCommand {
295    #[serde(skip_serializing_if = "Option::is_none", default)]
296    strategy: Option<String>,
297
298    #[serde(skip_serializing_if = "Option::is_none", default)]
299    match_window: Option<String>,
300
301    #[serde(skip_serializing_if = "Option::is_none", default)]
302    match_tab: Option<String>,
303
304    #[builder(default = false)]
305    #[serde(skip_serializing_if = "is_false")]
306    all: bool,
307}
308
309impl DisableLigaturesCommand {
310    pub fn to_message(self) -> Result<KittyMessage, CommandError> {
311        let payload =
312            serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
313
314        Ok(CommandBuilder::new("disable-ligatures")
315            .payload(payload)
316            .build())
317    }
318}
319
320#[derive(Builder, Serialize)]
321pub struct SignalChildCommand {
322    signals: Vec<i32>,
323
324    #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
325    match_spec: Option<String>,
326}
327
328impl SignalChildCommand {
329    pub fn to_message(self) -> Result<KittyMessage, CommandError> {
330        if self.signals.is_empty() {
331            return Err(CommandError::MissingParameter(
332                "signals".to_string(),
333                "signal-child".to_string(),
334            ));
335        }
336
337        let payload =
338            serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
339
340        Ok(CommandBuilder::new("signal-child").payload(payload).build())
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_run_basic() {
350        let cmd = RunCommand::builder().build().to_message();
351        assert!(cmd.is_ok());
352        let msg = cmd.unwrap();
353        assert_eq!(msg.cmd, "run");
354    }
355
356    #[test]
357    fn test_run_with_options() {
358        let cmd = RunCommand::builder()
359            .data("test data".to_string())
360            .cmdline("bash".to_string())
361            .allow_remote_control(true)
362            .build()
363            .to_message();
364        assert!(cmd.is_ok());
365        let msg = cmd.unwrap();
366        assert_eq!(msg.cmd, "run");
367    }
368
369    #[test]
370    fn test_kitten_basic() {
371        let cmd = KittenCommand::builder().build().to_message();
372        assert!(cmd.is_ok());
373        let msg = cmd.unwrap();
374        assert_eq!(msg.cmd, "kitten");
375    }
376
377    #[test]
378    fn test_kitten_with_args() {
379        let cmd = KittenCommand::builder()
380            .args("diff".to_string())
381            .build()
382            .to_message();
383        assert!(cmd.is_ok());
384        let msg = cmd.unwrap();
385        assert_eq!(msg.cmd, "kitten");
386    }
387
388    #[test]
389    fn test_launch_basic() {
390        let cmd = LaunchCommand::builder().build().to_message();
391        assert!(cmd.is_ok());
392        let msg = cmd.unwrap();
393        assert_eq!(msg.cmd, "launch");
394    }
395
396    #[test]
397    fn test_launch_with_options() {
398        let cmd = LaunchCommand::builder()
399            .args("bash".to_string())
400            .window_title("Test".to_string())
401            .cwd("/home".to_string())
402            .keep_focus(true)
403            .build()
404            .to_message();
405        assert!(cmd.is_ok());
406        let msg = cmd.unwrap();
407        assert_eq!(msg.cmd, "launch");
408    }
409
410    #[test]
411    fn test_env_basic() {
412        let mut env_map = Map::new();
413        env_map.insert(
414            "PATH".to_string(),
415            serde_json::Value::String("/usr/bin".to_string()),
416        );
417        let cmd = EnvCommand::builder().env(env_map).build().to_message();
418        assert!(cmd.is_ok());
419        let msg = cmd.unwrap();
420        assert_eq!(msg.cmd, "env");
421    }
422
423    #[test]
424    fn test_env_empty() {
425        let cmd = EnvCommand::builder().env(Map::new()).build().to_message();
426        assert!(cmd.is_err());
427        if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
428            assert_eq!(field, "env");
429            assert_eq!(cmd_name, "env");
430        } else {
431            panic!("Expected MissingParameter error");
432        }
433    }
434
435    #[test]
436    fn test_set_user_vars_basic() {
437        let cmd = SetUserVarsCommand::builder()
438            .var(vec!["var1".to_string(), "var2".to_string()])
439            .build()
440            .to_message();
441        assert!(cmd.is_ok());
442        let msg = cmd.unwrap();
443        assert_eq!(msg.cmd, "set-user-vars");
444    }
445
446    #[test]
447    fn test_set_user_vars_empty() {
448        let cmd = SetUserVarsCommand::builder()
449            .var(vec![])
450            .build()
451            .to_message();
452        assert!(cmd.is_err());
453        if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
454            assert_eq!(field, "var");
455            assert_eq!(cmd_name, "set-user-vars");
456        } else {
457            panic!("Expected MissingParameter error");
458        }
459    }
460
461    #[test]
462    fn test_load_config_basic() {
463        let cmd = LoadConfigCommand::builder()
464            .paths(vec!["kitty.conf".to_string()])
465            .build()
466            .to_message();
467        assert!(cmd.is_ok());
468        let msg = cmd.unwrap();
469        assert_eq!(msg.cmd, "load-config");
470    }
471
472    #[test]
473    fn test_load_config_empty() {
474        let cmd = LoadConfigCommand::builder()
475            .paths(vec![])
476            .build()
477            .to_message();
478        assert!(cmd.is_err());
479        if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
480            assert_eq!(field, "paths");
481            assert_eq!(cmd_name, "load-config");
482        } else {
483            panic!("Expected MissingParameter error");
484        }
485    }
486
487    #[test]
488    fn test_resize_os_window_basic() {
489        let cmd = ResizeOSWindowCommand::builder().build().to_message();
490        assert!(cmd.is_ok());
491        let msg = cmd.unwrap();
492        assert_eq!(msg.cmd, "resize-os-window");
493    }
494
495    #[test]
496    fn test_resize_os_window_with_options() {
497        let cmd = ResizeOSWindowCommand::builder()
498            .width(800)
499            .height(600)
500            .unit("px".to_string())
501            .build()
502            .to_message();
503        assert!(cmd.is_ok());
504        let msg = cmd.unwrap();
505        assert_eq!(msg.cmd, "resize-os-window");
506    }
507
508    #[test]
509    fn test_disable_ligatures_basic() {
510        let cmd = DisableLigaturesCommand::builder().build().to_message();
511        assert!(cmd.is_ok());
512        let msg = cmd.unwrap();
513        assert_eq!(msg.cmd, "disable-ligatures");
514    }
515
516    #[test]
517    fn test_disable_ligatures_with_options() {
518        let cmd = DisableLigaturesCommand::builder()
519            .strategy("never".to_string())
520            .all(true)
521            .build()
522            .to_message();
523        assert!(cmd.is_ok());
524        let msg = cmd.unwrap();
525        assert_eq!(msg.cmd, "disable-ligatures");
526    }
527
528    #[test]
529    fn test_signal_child_basic() {
530        let cmd = SignalChildCommand::builder()
531            .signals(vec![9, 15])
532            .build()
533            .to_message();
534        assert!(cmd.is_ok());
535        let msg = cmd.unwrap();
536        assert_eq!(msg.cmd, "signal-child");
537    }
538
539    #[test]
540    fn test_signal_child_empty() {
541        let cmd = SignalChildCommand::builder()
542            .signals(vec![])
543            .build()
544            .to_message();
545        assert!(cmd.is_err());
546        if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
547            assert_eq!(field, "signals");
548            assert_eq!(cmd_name, "signal-child");
549        } else {
550            panic!("Expected MissingParameter error");
551        }
552    }
553}