1use crate::command::CommandBuilder;
2use crate::error::CommandError;
3use crate::protocol::KittyMessage;
4use bon::Builder;
5use serde::Serialize;
6
7fn is_false(v: &bool) -> bool {
8 !v
9}
10
11#[derive(Builder, Serialize)]
12pub struct FocusTabCommand {
13 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
14 match_spec: Option<String>,
15}
16
17impl FocusTabCommand {
18 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
19 let payload =
20 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
21
22 Ok(CommandBuilder::new("focus-tab").payload(payload).build())
23 }
24}
25
26#[derive(Builder, Serialize)]
27pub struct SetTabTitleCommand {
28 title: String,
29
30 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
31 match_spec: Option<String>,
32}
33
34impl SetTabTitleCommand {
35 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
36 if self.title.is_empty() {
37 return Err(CommandError::MissingParameter(
38 "title".to_string(),
39 "set-tab-title".to_string(),
40 ));
41 }
42
43 let payload =
44 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
45
46 Ok(CommandBuilder::new("set-tab-title")
47 .payload(payload)
48 .build())
49 }
50}
51
52#[derive(Builder, Serialize)]
53pub struct CloseTabCommand {
54 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
55 match_spec: Option<String>,
56
57 #[builder(default = false)]
58 #[serde(skip_serializing_if = "is_false", rename = "self")]
59 self_tab: bool,
60
61 #[builder(default = false)]
62 #[serde(skip_serializing_if = "is_false")]
63 ignore_no_match: bool,
64}
65
66impl CloseTabCommand {
67 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
68 let payload =
69 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
70
71 Ok(CommandBuilder::new("close-tab").payload(payload).build())
72 }
73}
74
75#[derive(Builder, Serialize)]
76pub struct DetachTabCommand {
77 #[serde(skip_serializing_if = "Option::is_none", rename = "match", default)]
78 match_spec: Option<String>,
79
80 #[serde(skip_serializing_if = "Option::is_none", default)]
81 target_tab: Option<String>,
82
83 #[builder(default = false)]
84 #[serde(skip_serializing_if = "is_false", rename = "self")]
85 self_tab: bool,
86}
87
88impl DetachTabCommand {
89 pub fn to_message(self) -> Result<KittyMessage, CommandError> {
90 let payload =
91 serde_json::to_value(self).map_err(|e| CommandError::ValidationError(e.to_string()))?;
92
93 Ok(CommandBuilder::new("detach-tab").payload(payload).build())
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn test_focus_tab_basic() {
103 let cmd = FocusTabCommand::builder().build().to_message();
104 assert!(cmd.is_ok());
105 let msg = cmd.unwrap();
106 assert_eq!(msg.cmd, "focus-tab");
107 }
108
109 #[test]
110 fn test_focus_tab_with_match() {
111 let cmd = FocusTabCommand::builder()
112 .match_spec("id:0".to_string())
113 .build()
114 .to_message();
115 assert!(cmd.is_ok());
116 let msg = cmd.unwrap();
117 assert_eq!(msg.cmd, "focus-tab");
118 assert!(msg.payload.is_some());
119 }
120
121 #[test]
122 fn test_set_tab_title() {
123 let cmd = SetTabTitleCommand::builder()
124 .title("My Tab".to_string())
125 .build()
126 .to_message();
127 assert!(cmd.is_ok());
128 let msg = cmd.unwrap();
129 assert_eq!(msg.cmd, "set-tab-title");
130 assert!(msg.payload.is_some());
131 }
132
133 #[test]
134 fn test_set_tab_title_empty() {
135 let cmd = SetTabTitleCommand::builder()
136 .title("".to_string())
137 .build()
138 .to_message();
139 assert!(cmd.is_err());
140 if let Err(CommandError::MissingParameter(field, cmd_name)) = cmd {
141 assert_eq!(field, "title");
142 assert_eq!(cmd_name, "set-tab-title");
143 } else {
144 panic!("Expected MissingParameter error");
145 }
146 }
147
148 #[test]
149 fn test_set_tab_title_with_match() {
150 let cmd = SetTabTitleCommand::builder()
151 .title("New Title".to_string())
152 .match_spec("id:1".to_string())
153 .build()
154 .to_message();
155 assert!(cmd.is_ok());
156 let msg = cmd.unwrap();
157 assert_eq!(msg.cmd, "set-tab-title");
158 }
159
160 #[test]
161 fn test_close_tab_basic() {
162 let cmd = CloseTabCommand::builder().build().to_message();
163 assert!(cmd.is_ok());
164 let msg = cmd.unwrap();
165 assert_eq!(msg.cmd, "close-tab");
166 }
167
168 #[test]
169 fn test_close_tab_with_match() {
170 let cmd = CloseTabCommand::builder()
171 .match_spec("id:2".to_string())
172 .build()
173 .to_message();
174 assert!(cmd.is_ok());
175 let msg = cmd.unwrap();
176 assert_eq!(msg.cmd, "close-tab");
177 }
178
179 #[test]
180 fn test_close_tab_self() {
181 let cmd = CloseTabCommand::builder()
182 .self_tab(true)
183 .build()
184 .to_message();
185 assert!(cmd.is_ok());
186 let msg = cmd.unwrap();
187 assert_eq!(msg.cmd, "close-tab");
188 }
189
190 #[test]
191 fn test_close_tab_ignore_no_match() {
192 let cmd = CloseTabCommand::builder()
193 .ignore_no_match(true)
194 .build()
195 .to_message();
196 assert!(cmd.is_ok());
197 let msg = cmd.unwrap();
198 assert_eq!(msg.cmd, "close-tab");
199 }
200
201 #[test]
202 fn test_detach_tab_basic() {
203 let cmd = DetachTabCommand::builder().build().to_message();
204 assert!(cmd.is_ok());
205 let msg = cmd.unwrap();
206 assert_eq!(msg.cmd, "detach-tab");
207 }
208
209 #[test]
210 fn test_detach_tab_with_match() {
211 let cmd = DetachTabCommand::builder()
212 .match_spec("id:0".to_string())
213 .build()
214 .to_message();
215 assert!(cmd.is_ok());
216 let msg = cmd.unwrap();
217 assert_eq!(msg.cmd, "detach-tab");
218 }
219
220 #[test]
221 fn test_detach_tab_with_target_tab() {
222 let cmd = DetachTabCommand::builder()
223 .target_tab("id:1".to_string())
224 .build()
225 .to_message();
226 assert!(cmd.is_ok());
227 let msg = cmd.unwrap();
228 assert_eq!(msg.cmd, "detach-tab");
229 }
230
231 #[test]
232 fn test_detach_tab_self() {
233 let cmd = DetachTabCommand::builder()
234 .self_tab(true)
235 .build()
236 .to_message();
237 assert!(cmd.is_ok());
238 let msg = cmd.unwrap();
239 assert_eq!(msg.cmd, "detach-tab");
240 }
241
242 #[test]
243 fn test_detach_tab_all_options() {
244 let cmd = DetachTabCommand::builder()
245 .match_spec("id:0".to_string())
246 .target_tab("id:1".to_string())
247 .self_tab(true)
248 .build()
249 .to_message();
250 assert!(cmd.is_ok());
251 let msg = cmd.unwrap();
252 assert_eq!(msg.cmd, "detach-tab");
253 }
254}