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}