Skip to main content

zellij_utils/ipc/
protobuf_conversion.rs

1use crate::{
2    client_server_contract::client_server_contract::{
3        client_to_server_msg, server_to_client_msg, ActionMsg, AttachClientMsg,
4        AttachWatcherClientMsg, BackgroundColorMsg, CliPipeOutputMsg, ClientExitedMsg,
5        ClientToServerMsg as ProtoClientToServerMsg, ColorRegistersMsg, ConfigFileUpdatedMsg,
6        ConnStatusMsg, ConnectedMsg, DesktopNotificationResponseMsg, DetachSessionMsg, ExitMsg,
7        ExitReason as ProtoExitReason, FailedToStartWebServerMsg, FirstClientConnectedMsg,
8        ForegroundColorMsg, ForwardQueryToHostMsg, ForwardedReplyFromHostMsg,
9        HostTerminalThemeChangedMsg,
10        HostTerminalThemeIndication as ProtoHostTerminalThemeIndication,
11        InputMode as ProtoInputMode, KeyMsg, KillSessionMsg, LayoutMetadata as ProtoLayoutMetadata,
12        LogErrorMsg, LogMsg, PaneMetadata as ProtoPaneMetadata, PaneRenderUpdateMsg,
13        QueryTerminalSizeMsg, RenamedSessionMsg, RenderMsg,
14        ServerToClientMsg as ProtoServerToClientMsg, StartWebServerMsg, SubscribeToPaneRendersMsg,
15        SubscribedPaneClosedMsg, SwitchSessionMsg, TabMetadata as ProtoTabMetadata,
16        TerminalPixelDimensionsMsg, TerminalResizeMsg, UnblockCliPipeInputMsg,
17        UnblockInputThreadMsg, WebServerStartedMsg,
18    },
19    data::{HostTerminalThemeMode, InputMode, PaneId},
20    errors::prelude::*,
21    ipc::{
22        ClientToServerMsg, ColorRegister, ExitReason, PaneReference, PixelDimensions,
23        ServerToClientMsg,
24    },
25};
26use std::collections::BTreeMap;
27use std::path::PathBuf;
28
29// Convert Rust ClientToServerMsg to protobuf
30impl From<ClientToServerMsg> for ProtoClientToServerMsg {
31    fn from(msg: ClientToServerMsg) -> Self {
32        let message = match msg {
33            ClientToServerMsg::DetachSession { client_ids } => {
34                client_to_server_msg::Message::DetachSession(DetachSessionMsg {
35                    client_ids: client_ids.into_iter().map(|id| id as u32).collect(),
36                })
37            },
38            ClientToServerMsg::TerminalPixelDimensions { pixel_dimensions } => {
39                client_to_server_msg::Message::TerminalPixelDimensions(TerminalPixelDimensionsMsg {
40                    pixel_dimensions: Some(pixel_dimensions.into()),
41                })
42            },
43            ClientToServerMsg::BackgroundColor { color } => {
44                client_to_server_msg::Message::BackgroundColor(BackgroundColorMsg { color })
45            },
46            ClientToServerMsg::ForegroundColor { color } => {
47                client_to_server_msg::Message::ForegroundColor(ForegroundColorMsg { color })
48            },
49            ClientToServerMsg::ColorRegisters { color_registers } => {
50                client_to_server_msg::Message::ColorRegisters(ColorRegistersMsg {
51                    color_registers: color_registers.into_iter().map(|cr| cr.into()).collect(),
52                })
53            },
54            ClientToServerMsg::TerminalResize { new_size } => {
55                client_to_server_msg::Message::TerminalResize(TerminalResizeMsg {
56                    new_size: Some(new_size.into()),
57                })
58            },
59            ClientToServerMsg::FirstClientConnected {
60                cli_assets,
61                is_web_client,
62            } => client_to_server_msg::Message::FirstClientConnected(FirstClientConnectedMsg {
63                cli_assets: Some(cli_assets.into()),
64                is_web_client,
65            }),
66            ClientToServerMsg::AttachClient {
67                cli_assets,
68                tab_position_to_focus,
69                pane_to_focus,
70                is_web_client,
71            } => client_to_server_msg::Message::AttachClient(AttachClientMsg {
72                cli_assets: Some(cli_assets.into()),
73                tab_position_to_focus: tab_position_to_focus.map(|pos| pos as u32),
74                pane_to_focus: pane_to_focus.map(|p| p.into()),
75                is_web_client,
76            }),
77            ClientToServerMsg::AttachWatcherClient {
78                terminal_size,
79                is_web_client,
80            } => client_to_server_msg::Message::AttachWatcherClient(AttachWatcherClientMsg {
81                terminal_size: Some(terminal_size.into()),
82                is_web_client,
83            }),
84            ClientToServerMsg::Action {
85                action,
86                terminal_id,
87                client_id,
88                is_cli_client,
89            } => client_to_server_msg::Message::Action(ActionMsg {
90                action: Some(action.into()),
91                terminal_id,
92                client_id: client_id.map(|id| id as u32),
93                is_cli_client,
94            }),
95            ClientToServerMsg::Key {
96                key,
97                raw_bytes,
98                is_kitty_keyboard_protocol,
99            } => client_to_server_msg::Message::Key(KeyMsg {
100                key: Some(key.into()),
101                raw_bytes: raw_bytes.into_iter().map(|b| b as u32).collect(),
102                is_kitty_keyboard_protocol,
103            }),
104            ClientToServerMsg::ClientExited => {
105                client_to_server_msg::Message::ClientExited(ClientExitedMsg {})
106            },
107            ClientToServerMsg::KillSession => {
108                client_to_server_msg::Message::KillSession(KillSessionMsg {})
109            },
110            ClientToServerMsg::ConnStatus => {
111                client_to_server_msg::Message::ConnStatus(ConnStatusMsg {})
112            },
113            ClientToServerMsg::WebServerStarted { base_url } => {
114                client_to_server_msg::Message::WebServerStarted(WebServerStartedMsg { base_url })
115            },
116            ClientToServerMsg::FailedToStartWebServer { error } => {
117                client_to_server_msg::Message::FailedToStartWebServer(FailedToStartWebServerMsg {
118                    error,
119                })
120            },
121            ClientToServerMsg::SubscribeToPaneRenders {
122                pane_ids,
123                scrollback,
124                ansi,
125            } => client_to_server_msg::Message::SubscribeToPaneRenders(SubscribeToPaneRendersMsg {
126                pane_ids: pane_ids.into_iter().map(|id| id.into()).collect(),
127                scrollback: scrollback.map(|s| s as u32),
128                ansi,
129            }),
130            ClientToServerMsg::DesktopNotificationResponse { raw_bytes } => {
131                client_to_server_msg::Message::DesktopNotificationResponse(
132                    DesktopNotificationResponseMsg { raw_bytes },
133                )
134            },
135            ClientToServerMsg::ForwardedReplyFromHost { token, reply_bytes } => {
136                client_to_server_msg::Message::ForwardedReplyFromHost(ForwardedReplyFromHostMsg {
137                    token,
138                    reply_bytes,
139                })
140            },
141            ClientToServerMsg::HostTerminalThemeChanged { mode } => {
142                let proto_mode: ProtoHostTerminalThemeIndication = mode.into();
143                client_to_server_msg::Message::HostTerminalThemeChanged(
144                    HostTerminalThemeChangedMsg {
145                        mode: proto_mode as i32,
146                    },
147                )
148            },
149        };
150
151        ProtoClientToServerMsg {
152            message: Some(message),
153        }
154    }
155}
156
157// Convert protobuf ClientToServerMsg to Rust
158impl TryFrom<ProtoClientToServerMsg> for ClientToServerMsg {
159    type Error = anyhow::Error;
160
161    fn try_from(msg: ProtoClientToServerMsg) -> Result<Self> {
162        match msg.message {
163            Some(client_to_server_msg::Message::DetachSession(detach)) => {
164                Ok(ClientToServerMsg::DetachSession {
165                    client_ids: detach.client_ids.into_iter().map(|id| id as u16).collect(),
166                })
167            },
168            Some(client_to_server_msg::Message::TerminalPixelDimensions(pixel_dims)) => {
169                Ok(ClientToServerMsg::TerminalPixelDimensions {
170                    pixel_dimensions: pixel_dims
171                        .pixel_dimensions
172                        .ok_or_else(|| anyhow!("Missing pixel_dimensions"))?
173                        .try_into()?,
174                })
175            },
176            Some(client_to_server_msg::Message::BackgroundColor(bg_color)) => {
177                Ok(ClientToServerMsg::BackgroundColor {
178                    color: bg_color.color,
179                })
180            },
181            Some(client_to_server_msg::Message::ForegroundColor(fg_color)) => {
182                Ok(ClientToServerMsg::ForegroundColor {
183                    color: fg_color.color,
184                })
185            },
186            Some(client_to_server_msg::Message::ColorRegisters(color_regs)) => {
187                Ok(ClientToServerMsg::ColorRegisters {
188                    color_registers: color_regs
189                        .color_registers
190                        .into_iter()
191                        .map(|cr| cr.try_into())
192                        .collect::<Result<Vec<_>>>()?,
193                })
194            },
195            Some(client_to_server_msg::Message::TerminalResize(resize)) => {
196                Ok(ClientToServerMsg::TerminalResize {
197                    new_size: resize
198                        .new_size
199                        .ok_or_else(|| anyhow!("Missing new_size"))?
200                        .try_into()?,
201                })
202            },
203            Some(client_to_server_msg::Message::FirstClientConnected(first_client)) => {
204                Ok(ClientToServerMsg::FirstClientConnected {
205                    cli_assets: first_client
206                        .cli_assets
207                        .ok_or_else(|| anyhow!("Missing cli_assets"))?
208                        .try_into()?,
209                    is_web_client: first_client.is_web_client,
210                })
211            },
212            Some(client_to_server_msg::Message::AttachClient(attach)) => {
213                Ok(ClientToServerMsg::AttachClient {
214                    cli_assets: attach
215                        .cli_assets
216                        .ok_or_else(|| anyhow!("Missing cli_assets"))?
217                        .try_into()?,
218                    tab_position_to_focus: attach.tab_position_to_focus.map(|pos| pos as usize),
219                    pane_to_focus: attach.pane_to_focus.map(|p| p.try_into()).transpose()?,
220                    is_web_client: attach.is_web_client,
221                })
222            },
223            Some(client_to_server_msg::Message::AttachWatcherClient(attach_watcher)) => {
224                Ok(ClientToServerMsg::AttachWatcherClient {
225                    terminal_size: attach_watcher
226                        .terminal_size
227                        .ok_or_else(|| anyhow::anyhow!("Missing terminal_size"))?
228                        .try_into()?,
229                    is_web_client: attach_watcher.is_web_client,
230                })
231            },
232            Some(client_to_server_msg::Message::Action(action)) => Ok(ClientToServerMsg::Action {
233                action: action
234                    .action
235                    .ok_or_else(|| anyhow!("Missing action"))?
236                    .try_into()?,
237                terminal_id: action.terminal_id,
238                client_id: action.client_id.map(|id| id as u16),
239                is_cli_client: action.is_cli_client,
240            }),
241            Some(client_to_server_msg::Message::Key(key)) => Ok(ClientToServerMsg::Key {
242                key: key.key.ok_or_else(|| anyhow!("Missing key"))?.try_into()?,
243                raw_bytes: key.raw_bytes.into_iter().map(|b| b as u8).collect(),
244                is_kitty_keyboard_protocol: key.is_kitty_keyboard_protocol,
245            }),
246            Some(client_to_server_msg::Message::ClientExited(_)) => {
247                Ok(ClientToServerMsg::ClientExited)
248            },
249            Some(client_to_server_msg::Message::KillSession(_)) => {
250                Ok(ClientToServerMsg::KillSession)
251            },
252            Some(client_to_server_msg::Message::ConnStatus(_)) => Ok(ClientToServerMsg::ConnStatus),
253            Some(client_to_server_msg::Message::WebServerStarted(web_server)) => {
254                Ok(ClientToServerMsg::WebServerStarted {
255                    base_url: web_server.base_url,
256                })
257            },
258            Some(client_to_server_msg::Message::FailedToStartWebServer(failed)) => {
259                Ok(ClientToServerMsg::FailedToStartWebServer {
260                    error: failed.error,
261                })
262            },
263            Some(client_to_server_msg::Message::SubscribeToPaneRenders(msg)) => {
264                let pane_ids: Result<Vec<PaneId>> =
265                    msg.pane_ids.into_iter().map(|p| p.try_into()).collect();
266                Ok(ClientToServerMsg::SubscribeToPaneRenders {
267                    pane_ids: pane_ids?,
268                    scrollback: msg.scrollback.map(|s| s as usize),
269                    ansi: msg.ansi,
270                })
271            },
272            Some(client_to_server_msg::Message::DesktopNotificationResponse(msg)) => {
273                Ok(ClientToServerMsg::DesktopNotificationResponse {
274                    raw_bytes: msg.raw_bytes,
275                })
276            },
277            Some(client_to_server_msg::Message::ForwardedReplyFromHost(msg)) => {
278                Ok(ClientToServerMsg::ForwardedReplyFromHost {
279                    token: msg.token,
280                    reply_bytes: msg.reply_bytes,
281                })
282            },
283            Some(client_to_server_msg::Message::HostTerminalThemeChanged(msg)) => {
284                let proto_mode = ProtoHostTerminalThemeIndication::from_i32(msg.mode)
285                    .ok_or_else(|| anyhow!("Unknown HostTerminalThemeIndication: {}", msg.mode))?;
286                Ok(ClientToServerMsg::HostTerminalThemeChanged {
287                    mode: proto_mode.into(),
288                })
289            },
290            None => Err(anyhow!("Empty ClientToServerMsg message")),
291        }
292    }
293}
294
295// Convert Rust ServerToClientMsg to protobuf
296impl From<ServerToClientMsg> for ProtoServerToClientMsg {
297    fn from(msg: ServerToClientMsg) -> Self {
298        let message = match msg {
299            ServerToClientMsg::Render { content } => {
300                server_to_client_msg::Message::Render(RenderMsg { content })
301            },
302            ServerToClientMsg::UnblockInputThread => {
303                server_to_client_msg::Message::UnblockInputThread(UnblockInputThreadMsg {})
304            },
305            ServerToClientMsg::Exit { exit_reason } => {
306                let (proto_exit_reason, payload) = match exit_reason {
307                    ExitReason::Error(ref msg) => (ProtoExitReason::Error, Some(msg.clone())),
308                    ExitReason::CustomExitStatus(status) => {
309                        (ProtoExitReason::CustomExitStatus, Some(status.to_string()))
310                    },
311                    other => (ProtoExitReason::from(other), None),
312                };
313                server_to_client_msg::Message::Exit(ExitMsg {
314                    exit_reason: proto_exit_reason as i32,
315                    payload,
316                })
317            },
318            ServerToClientMsg::Connected => {
319                server_to_client_msg::Message::Connected(ConnectedMsg {})
320            },
321            ServerToClientMsg::Log { lines } => {
322                server_to_client_msg::Message::Log(LogMsg { lines })
323            },
324            ServerToClientMsg::LogError { lines } => {
325                server_to_client_msg::Message::LogError(LogErrorMsg { lines })
326            },
327            ServerToClientMsg::SwitchSession { connect_to_session } => {
328                server_to_client_msg::Message::SwitchSession(SwitchSessionMsg {
329                    connect_to_session: Some(connect_to_session.into()),
330                })
331            },
332            ServerToClientMsg::UnblockCliPipeInput { pipe_name } => {
333                server_to_client_msg::Message::UnblockCliPipeInput(UnblockCliPipeInputMsg {
334                    pipe_name,
335                })
336            },
337            ServerToClientMsg::CliPipeOutput { pipe_name, output } => {
338                server_to_client_msg::Message::CliPipeOutput(CliPipeOutputMsg { pipe_name, output })
339            },
340            ServerToClientMsg::QueryTerminalSize => {
341                server_to_client_msg::Message::QueryTerminalSize(QueryTerminalSizeMsg {})
342            },
343            ServerToClientMsg::StartWebServer => {
344                server_to_client_msg::Message::StartWebServer(StartWebServerMsg {})
345            },
346            ServerToClientMsg::RenamedSession { name } => {
347                server_to_client_msg::Message::RenamedSession(RenamedSessionMsg { name })
348            },
349            ServerToClientMsg::ConfigFileUpdated => {
350                server_to_client_msg::Message::ConfigFileUpdated(ConfigFileUpdatedMsg {})
351            },
352            ServerToClientMsg::PaneRenderUpdate {
353                pane_id,
354                viewport,
355                scrollback,
356                is_initial,
357            } => server_to_client_msg::Message::PaneRenderUpdate(PaneRenderUpdateMsg {
358                pane_id: Some(pane_id.into()),
359                viewport,
360                scrollback: scrollback.clone().unwrap_or_default(),
361                has_scrollback: scrollback.is_some(),
362                is_initial,
363            }),
364            ServerToClientMsg::SubscribedPaneClosed { pane_id } => {
365                server_to_client_msg::Message::SubscribedPaneClosed(SubscribedPaneClosedMsg {
366                    pane_id: Some(pane_id.into()),
367                })
368            },
369            ServerToClientMsg::ForwardQueryToHost { token, query_bytes } => {
370                server_to_client_msg::Message::ForwardQueryToHost(ForwardQueryToHostMsg {
371                    token,
372                    query_bytes,
373                })
374            },
375        };
376
377        ProtoServerToClientMsg {
378            message: Some(message),
379        }
380    }
381}
382
383// Convert protobuf ServerToClientMsg to Rust
384impl TryFrom<ProtoServerToClientMsg> for ServerToClientMsg {
385    type Error = anyhow::Error;
386
387    fn try_from(msg: ProtoServerToClientMsg) -> Result<Self> {
388        match msg.message {
389            Some(server_to_client_msg::Message::Render(render)) => Ok(ServerToClientMsg::Render {
390                content: render.content,
391            }),
392            Some(server_to_client_msg::Message::UnblockInputThread(_)) => {
393                Ok(ServerToClientMsg::UnblockInputThread)
394            },
395            Some(server_to_client_msg::Message::Exit(exit)) => {
396                let proto_exit_reason = ProtoExitReason::from_i32(exit.exit_reason)
397                    .ok_or_else(|| anyhow!("Invalid exit_reason"))?;
398
399                let exit_reason = match proto_exit_reason {
400                    ProtoExitReason::Error => {
401                        let error_msg =
402                            exit.payload.unwrap_or_else(|| "Protobuf error".to_string());
403                        ExitReason::Error(error_msg)
404                    },
405                    ProtoExitReason::CustomExitStatus => {
406                        let status_str = exit.payload.unwrap_or_else(|| "0".to_string());
407                        let status = status_str
408                            .parse::<i32>()
409                            .map_err(|_| anyhow!("Invalid custom exit status: {}", status_str))?;
410                        ExitReason::CustomExitStatus(status)
411                    },
412                    other => other.try_into()?,
413                };
414
415                Ok(ServerToClientMsg::Exit { exit_reason })
416            },
417            Some(server_to_client_msg::Message::Connected(_)) => Ok(ServerToClientMsg::Connected),
418            Some(server_to_client_msg::Message::Log(log)) => {
419                Ok(ServerToClientMsg::Log { lines: log.lines })
420            },
421            Some(server_to_client_msg::Message::LogError(log_error)) => {
422                Ok(ServerToClientMsg::LogError {
423                    lines: log_error.lines,
424                })
425            },
426            Some(server_to_client_msg::Message::SwitchSession(switch)) => {
427                Ok(ServerToClientMsg::SwitchSession {
428                    connect_to_session: switch
429                        .connect_to_session
430                        .ok_or_else(|| anyhow!("Missing connect_to_session"))?
431                        .try_into()?,
432                })
433            },
434            Some(server_to_client_msg::Message::UnblockCliPipeInput(unblock)) => {
435                Ok(ServerToClientMsg::UnblockCliPipeInput {
436                    pipe_name: unblock.pipe_name,
437                })
438            },
439            Some(server_to_client_msg::Message::CliPipeOutput(pipe_output)) => {
440                Ok(ServerToClientMsg::CliPipeOutput {
441                    pipe_name: pipe_output.pipe_name,
442                    output: pipe_output.output,
443                })
444            },
445            Some(server_to_client_msg::Message::QueryTerminalSize(_)) => {
446                Ok(ServerToClientMsg::QueryTerminalSize)
447            },
448            Some(server_to_client_msg::Message::StartWebServer(_)) => {
449                Ok(ServerToClientMsg::StartWebServer)
450            },
451            Some(server_to_client_msg::Message::RenamedSession(renamed)) => {
452                Ok(ServerToClientMsg::RenamedSession { name: renamed.name })
453            },
454            Some(server_to_client_msg::Message::ConfigFileUpdated(_)) => {
455                Ok(ServerToClientMsg::ConfigFileUpdated)
456            },
457            Some(server_to_client_msg::Message::PaneRenderUpdate(msg)) => {
458                let pane_id: PaneId = msg
459                    .pane_id
460                    .ok_or_else(|| anyhow!("Missing pane_id"))?
461                    .try_into()?;
462                let scrollback = if msg.has_scrollback {
463                    Some(msg.scrollback)
464                } else {
465                    None
466                };
467                Ok(ServerToClientMsg::PaneRenderUpdate {
468                    pane_id,
469                    viewport: msg.viewport,
470                    scrollback,
471                    is_initial: msg.is_initial,
472                })
473            },
474            Some(server_to_client_msg::Message::SubscribedPaneClosed(msg)) => {
475                let pane_id: PaneId = msg
476                    .pane_id
477                    .ok_or_else(|| anyhow!("Missing pane_id"))?
478                    .try_into()?;
479                Ok(ServerToClientMsg::SubscribedPaneClosed { pane_id })
480            },
481            Some(server_to_client_msg::Message::ForwardQueryToHost(msg)) => {
482                Ok(ServerToClientMsg::ForwardQueryToHost {
483                    token: msg.token,
484                    query_bytes: msg.query_bytes,
485                })
486            },
487            None => Err(anyhow!("Empty ServerToClientMsg message")),
488        }
489    }
490}
491
492// Basic type conversions
493impl From<crate::pane_size::Size> for crate::client_server_contract::client_server_contract::Size {
494    fn from(size: crate::pane_size::Size) -> Self {
495        Self {
496            cols: size.cols as u32,
497            rows: size.rows as u32,
498        }
499    }
500}
501
502impl TryFrom<crate::client_server_contract::client_server_contract::Size>
503    for crate::pane_size::Size
504{
505    type Error = anyhow::Error;
506    fn try_from(size: crate::client_server_contract::client_server_contract::Size) -> Result<Self> {
507        Ok(Self {
508            rows: size.rows as usize,
509            cols: size.cols as usize,
510        })
511    }
512}
513
514impl From<PixelDimensions>
515    for crate::client_server_contract::client_server_contract::PixelDimensions
516{
517    fn from(pixel_dims: PixelDimensions) -> Self {
518        Self {
519            text_area_size: pixel_dims.text_area_size.map(|size| {
520                crate::client_server_contract::client_server_contract::SizeInPixels {
521                    width: size.width as u32,
522                    height: size.height as u32,
523                }
524            }),
525            character_cell_size: pixel_dims.character_cell_size.map(|size| {
526                crate::client_server_contract::client_server_contract::SizeInPixels {
527                    width: size.width as u32,
528                    height: size.height as u32,
529                }
530            }),
531        }
532    }
533}
534
535impl TryFrom<crate::client_server_contract::client_server_contract::PixelDimensions>
536    for PixelDimensions
537{
538    type Error = anyhow::Error;
539    fn try_from(
540        pixel_dims: crate::client_server_contract::client_server_contract::PixelDimensions,
541    ) -> Result<Self> {
542        Ok(Self {
543            text_area_size: pixel_dims
544                .text_area_size
545                .map(|size| crate::pane_size::SizeInPixels {
546                    width: size.width as usize,
547                    height: size.height as usize,
548                }),
549            character_cell_size: pixel_dims.character_cell_size.map(|size| {
550                crate::pane_size::SizeInPixels {
551                    width: size.width as usize,
552                    height: size.height as usize,
553                }
554            }),
555        })
556    }
557}
558
559impl From<PaneReference> for crate::client_server_contract::client_server_contract::PaneReference {
560    fn from(pane_ref: PaneReference) -> Self {
561        Self {
562            pane_id: pane_ref.pane_id,
563            is_plugin: pane_ref.is_plugin,
564        }
565    }
566}
567
568impl TryFrom<crate::client_server_contract::client_server_contract::PaneReference>
569    for PaneReference
570{
571    type Error = anyhow::Error;
572    fn try_from(
573        pane_ref: crate::client_server_contract::client_server_contract::PaneReference,
574    ) -> Result<Self> {
575        Ok(Self {
576            pane_id: pane_ref.pane_id,
577            is_plugin: pane_ref.is_plugin,
578        })
579    }
580}
581
582impl From<ColorRegister> for crate::client_server_contract::client_server_contract::ColorRegister {
583    fn from(color_reg: ColorRegister) -> Self {
584        Self {
585            index: color_reg.index as u32,
586            color: color_reg.color,
587        }
588    }
589}
590
591impl TryFrom<crate::client_server_contract::client_server_contract::ColorRegister>
592    for ColorRegister
593{
594    type Error = anyhow::Error;
595    fn try_from(
596        color_reg: crate::client_server_contract::client_server_contract::ColorRegister,
597    ) -> Result<Self> {
598        Ok(Self {
599            index: color_reg.index as usize,
600            color: color_reg.color,
601        })
602    }
603}
604
605impl From<crate::input::cli_assets::CliAssets>
606    for crate::client_server_contract::client_server_contract::CliAssets
607{
608    fn from(cli_assets: crate::input::cli_assets::CliAssets) -> Self {
609        Self {
610            config_file_path: cli_assets
611                .config_file_path
612                .map(|p| p.to_string_lossy().to_string()),
613            config_dir: cli_assets
614                .config_dir
615                .map(|p| p.to_string_lossy().to_string()),
616            should_ignore_config: cli_assets.should_ignore_config,
617            configuration_options: cli_assets.configuration_options.map(|o| o.into()),
618            layout: cli_assets.layout.map(|l| l.into()),
619            terminal_window_size: Some(cli_assets.terminal_window_size.into()),
620            data_dir: cli_assets.data_dir.map(|p| p.to_string_lossy().to_string()),
621            is_debug: cli_assets.is_debug,
622            max_panes: cli_assets.max_panes.map(|m| m as u32),
623            force_run_layout_commands: cli_assets.force_run_layout_commands,
624            cwd: cli_assets.cwd.map(|p| p.to_string_lossy().to_string()),
625        }
626    }
627}
628
629impl TryFrom<crate::client_server_contract::client_server_contract::CliAssets>
630    for crate::input::cli_assets::CliAssets
631{
632    type Error = anyhow::Error;
633    fn try_from(
634        cli_assets: crate::client_server_contract::client_server_contract::CliAssets,
635    ) -> Result<Self> {
636        Ok(Self {
637            config_file_path: cli_assets.config_file_path.map(PathBuf::from),
638            config_dir: cli_assets.config_dir.map(PathBuf::from),
639            should_ignore_config: cli_assets.should_ignore_config,
640            configuration_options: cli_assets
641                .configuration_options
642                .map(|o| o.try_into())
643                .transpose()?,
644            layout: cli_assets.layout.map(|l| l.try_into()).transpose()?,
645            terminal_window_size: cli_assets
646                .terminal_window_size
647                .ok_or_else(|| anyhow!("CliAssets missing terminal_window_size"))?
648                .try_into()?,
649            data_dir: cli_assets.data_dir.map(PathBuf::from),
650            is_debug: cli_assets.is_debug,
651            max_panes: cli_assets.max_panes.map(|m| m as usize),
652            force_run_layout_commands: cli_assets.force_run_layout_commands,
653            cwd: cli_assets.cwd.map(PathBuf::from),
654        })
655    }
656}
657
658impl From<crate::input::options::Options>
659    for crate::client_server_contract::client_server_contract::Options
660{
661    fn from(options: crate::input::options::Options) -> Self {
662        use crate::client_server_contract::client_server_contract::{
663            Clipboard as ProtoClipboard, OnForceClose as ProtoOnForceClose,
664            WebSharing as ProtoWebSharing,
665        };
666
667        Self {
668            simplified_ui: options.simplified_ui,
669            theme: options.theme,
670            theme_dark: options.theme_dark,
671            theme_light: options.theme_light,
672            default_mode: options.default_mode.map(|m| input_mode_to_proto_i32(m)),
673            default_shell: options
674                .default_shell
675                .map(|p| p.to_string_lossy().to_string()),
676            default_cwd: options.default_cwd.map(|p| p.to_string_lossy().to_string()),
677            default_layout: options
678                .default_layout
679                .map(|p| p.to_string_lossy().to_string()),
680            layout_dir: options.layout_dir.map(|p| p.to_string_lossy().to_string()),
681            theme_dir: options.theme_dir.map(|p| p.to_string_lossy().to_string()),
682            mouse_mode: options.mouse_mode,
683            pane_frames: options.pane_frames,
684            mirror_session: options.mirror_session,
685            on_force_close: options.on_force_close.map(|o| match o {
686                crate::input::options::OnForceClose::Quit => ProtoOnForceClose::Quit as i32,
687                crate::input::options::OnForceClose::Detach => ProtoOnForceClose::Detach as i32,
688            }),
689            scroll_buffer_size: options.scroll_buffer_size.map(|s| s as u32),
690            copy_command: options.copy_command,
691            copy_clipboard: options.copy_clipboard.map(|c| match c {
692                crate::input::options::Clipboard::System => ProtoClipboard::System as i32,
693                crate::input::options::Clipboard::Primary => ProtoClipboard::Primary as i32,
694            }),
695            copy_on_select: options.copy_on_select,
696            osc8_hyperlinks: options.osc8_hyperlinks,
697            scrollback_editor: options
698                .scrollback_editor
699                .map(|p| p.to_string_lossy().to_string()),
700            session_name: options.session_name,
701            attach_to_session: options.attach_to_session,
702            auto_layout: options.auto_layout,
703            session_serialization: options.session_serialization,
704            serialize_pane_viewport: options.serialize_pane_viewport,
705            scrollback_lines_to_serialize: options.scrollback_lines_to_serialize.map(|s| s as u32),
706            styled_underlines: options.styled_underlines,
707            serialization_interval: options.serialization_interval,
708            disable_session_metadata: options.disable_session_metadata,
709            support_kitty_keyboard_protocol: options.support_kitty_keyboard_protocol,
710            web_server: options.web_server,
711            web_sharing: options.web_sharing.map(|w| match w {
712                crate::data::WebSharing::On => ProtoWebSharing::On as i32,
713                crate::data::WebSharing::Off => ProtoWebSharing::Off as i32,
714                crate::data::WebSharing::Disabled => ProtoWebSharing::Disabled as i32,
715            }),
716            stacked_resize: options.stacked_resize,
717            show_startup_tips: options.show_startup_tips,
718            show_release_notes: options.show_release_notes,
719            advanced_mouse_actions: options.advanced_mouse_actions,
720            mouse_hover_effects: options.mouse_hover_effects,
721            web_server_ip: options.web_server_ip.map(|ip| ip.to_string()),
722            web_server_port: options.web_server_port.map(|p| p as u32),
723            web_server_cert: options
724                .web_server_cert
725                .map(|p| p.to_string_lossy().to_string()),
726            web_server_key: options
727                .web_server_key
728                .map(|p| p.to_string_lossy().to_string()),
729            enforce_https_for_localhost: options.enforce_https_for_localhost,
730            post_command_discovery_hook: options.post_command_discovery_hook,
731            client_async_worker_tasks: options.client_async_worker_tasks.map(|v| v as u64),
732            visual_bell: options.visual_bell,
733            focus_follows_mouse: options.focus_follows_mouse,
734            mouse_click_through: options.mouse_click_through,
735        }
736    }
737}
738
739impl TryFrom<crate::client_server_contract::client_server_contract::Options>
740    for crate::input::options::Options
741{
742    type Error = anyhow::Error;
743    fn try_from(
744        options: crate::client_server_contract::client_server_contract::Options,
745    ) -> Result<Self> {
746        use crate::client_server_contract::client_server_contract::{
747            Clipboard as ProtoClipboard, OnForceClose as ProtoOnForceClose,
748            WebSharing as ProtoWebSharing,
749        };
750
751        Ok(Self {
752            simplified_ui: options.simplified_ui,
753            theme: options.theme,
754            theme_dark: options.theme_dark,
755            theme_light: options.theme_light,
756            default_mode: options
757                .default_mode
758                .map(|m| proto_i32_to_input_mode(m))
759                .transpose()?,
760            default_shell: options.default_shell.map(std::path::PathBuf::from),
761            default_cwd: options.default_cwd.map(std::path::PathBuf::from),
762            default_layout: options.default_layout.map(std::path::PathBuf::from),
763            layout_dir: options.layout_dir.map(std::path::PathBuf::from),
764            theme_dir: options.theme_dir.map(std::path::PathBuf::from),
765            mouse_mode: options.mouse_mode,
766            pane_frames: options.pane_frames,
767            mirror_session: options.mirror_session,
768            on_force_close: options
769                .on_force_close
770                .map(|o| match ProtoOnForceClose::from_i32(o) {
771                    Some(ProtoOnForceClose::Quit) => Ok(crate::input::options::OnForceClose::Quit),
772                    Some(ProtoOnForceClose::Detach) => {
773                        Ok(crate::input::options::OnForceClose::Detach)
774                    },
775                    _ => Err(anyhow!("Invalid OnForceClose value: {}", o)),
776                })
777                .transpose()?,
778            scroll_buffer_size: options.scroll_buffer_size.map(|s| s as usize),
779            copy_command: options.copy_command,
780            copy_clipboard: options
781                .copy_clipboard
782                .map(|c| match ProtoClipboard::from_i32(c) {
783                    Some(ProtoClipboard::System) => Ok(crate::input::options::Clipboard::System),
784                    Some(ProtoClipboard::Primary) => Ok(crate::input::options::Clipboard::Primary),
785                    _ => Err(anyhow!("Invalid Clipboard value: {}", c)),
786                })
787                .transpose()?,
788            copy_on_select: options.copy_on_select,
789            osc8_hyperlinks: options.osc8_hyperlinks,
790            scrollback_editor: options.scrollback_editor.map(std::path::PathBuf::from),
791            session_name: options.session_name,
792            attach_to_session: options.attach_to_session,
793            auto_layout: options.auto_layout,
794            session_serialization: options.session_serialization,
795            serialize_pane_viewport: options.serialize_pane_viewport,
796            scrollback_lines_to_serialize: options
797                .scrollback_lines_to_serialize
798                .map(|s| s as usize),
799            styled_underlines: options.styled_underlines,
800            serialization_interval: options.serialization_interval,
801            disable_session_metadata: options.disable_session_metadata,
802            support_kitty_keyboard_protocol: options.support_kitty_keyboard_protocol,
803            web_server: options.web_server,
804            web_sharing: options
805                .web_sharing
806                .map(|w| match ProtoWebSharing::from_i32(w) {
807                    Some(ProtoWebSharing::On) => Ok(crate::data::WebSharing::On),
808                    Some(ProtoWebSharing::Off) => Ok(crate::data::WebSharing::Off),
809                    Some(ProtoWebSharing::Disabled) => Ok(crate::data::WebSharing::Disabled),
810                    _ => Err(anyhow!("Invalid WebSharing value: {}", w)),
811                })
812                .transpose()?,
813            stacked_resize: options.stacked_resize,
814            show_startup_tips: options.show_startup_tips,
815            show_release_notes: options.show_release_notes,
816            advanced_mouse_actions: options.advanced_mouse_actions,
817            mouse_hover_effects: options.mouse_hover_effects,
818            web_server_ip: options
819                .web_server_ip
820                .map(|ip| ip.parse())
821                .transpose()
822                .map_err(|e| anyhow!("Invalid IP address: {}", e))?,
823            web_server_port: options.web_server_port.map(|p| p as u16),
824            web_server_cert: options.web_server_cert.map(std::path::PathBuf::from),
825            web_server_key: options.web_server_key.map(std::path::PathBuf::from),
826            enforce_https_for_localhost: options.enforce_https_for_localhost,
827            post_command_discovery_hook: options.post_command_discovery_hook,
828            client_async_worker_tasks: options.client_async_worker_tasks.map(|v| v as usize),
829            visual_bell: options.visual_bell,
830            focus_follows_mouse: options.focus_follows_mouse,
831            mouse_click_through: options.mouse_click_through,
832        })
833    }
834}
835
836// Complete Action conversion implementation - all 91 variants
837impl From<crate::input::actions::Action>
838    for crate::client_server_contract::client_server_contract::Action
839{
840    fn from(action: crate::input::actions::Action) -> Self {
841        use crate::client_server_contract::client_server_contract::{
842            action::ActionType,
843            AreFloatingPanesVisibleAction,
844            BreakPaneAction,
845            BreakPaneLeftAction,
846            BreakPaneRightAction,
847            ChangeFloatingPaneCoordinatesAction,
848            ClearScreenAction,
849            ClearScreenByPaneIdAction,
850            CliPipeAction,
851            CloseFocusAction,
852            CloseFocusByPaneIdAction,
853            ClosePluginPaneAction,
854            CloseTabAction,
855            CloseTabByIdAction,
856            CloseTerminalPaneAction,
857            ConfirmAction,
858            CopyAction,
859            CurrentTabInfoAction,
860            DenyAction,
861            DetachAction,
862            DumpLayoutAction,
863            DumpScreenAction,
864            EditFileAction,
865            EditScrollbackAction,
866            EditScrollbackByPaneIdAction,
867            FocusNextPaneAction,
868            FocusPaneByPaneIdAction,
869            FocusPluginPaneWithIdAction,
870            FocusPreviousPaneAction,
871            FocusTerminalPaneWithIdAction,
872            GoToNextTabAction,
873            GoToPreviousTabAction,
874            GoToTabAction,
875            GoToTabByIdAction,
876            GoToTabNameAction,
877            HalfPageScrollDownAction,
878            HalfPageScrollDownByPaneIdAction,
879            HalfPageScrollUpAction,
880            HalfPageScrollUpByPaneIdAction,
881            HideFloatingPanesAction,
882            KeybindPipeAction,
883            LaunchOrFocusPluginAction,
884            LaunchPluginAction,
885            ListClientsAction,
886            ListPanesAction,
887            ListTabsAction,
888            MouseEventAction,
889            MoveFocusAction,
890            MoveFocusOrTabAction,
891            MovePaneAction,
892            MovePaneBackwardsAction,
893            MovePaneBackwardsByPaneIdAction,
894            MovePaneByPaneIdAction,
895            MoveTabAction,
896            MoveTabByTabIdAction,
897            NewBlockingPaneAction,
898            NewFloatingPaneAction,
899            NewFloatingPluginPaneAction,
900            NewInPlacePaneAction,
901            NewInPlacePluginPaneAction,
902            NewPaneAction,
903            NewStackedPaneAction,
904            NewTabAction,
905            NewTiledPaneAction,
906            NewTiledPluginPaneAction,
907            NextSwapLayoutAction,
908            NextSwapLayoutByTabIdAction,
909            NoOpAction,
910            OverrideLayoutAction,
911            PageScrollDownAction,
912            PageScrollDownByPaneIdAction,
913            PageScrollUpAction,
914            PageScrollUpByPaneIdAction,
915            PaneIdWithPlugin,
916            PaneNameInputAction,
917            PasteAction,
918            PreviousSwapLayoutAction,
919            PreviousSwapLayoutByTabIdAction,
920            QueryTabNamesAction,
921            QuitAction,
922            RenamePaneByPaneIdAction,
923            RenamePluginPaneAction,
924            RenameSessionAction,
925            RenameTabAction,
926            RenameTabByIdAction,
927            RenameTerminalPaneAction,
928            ResizeAction,
929            ResizeByPaneIdAction,
930            RunAction,
931            SaveSessionAction,
932            ScrollDownAction,
933            ScrollDownAtAction,
934            ScrollDownByPaneIdAction,
935            ScrollToBottomAction,
936            ScrollToBottomByPaneIdAction,
937            ScrollToTopAction,
938            ScrollToTopByPaneIdAction,
939            ScrollUpAction,
940            ScrollUpAtAction,
941            // Pane-targeting
942            ScrollUpByPaneIdAction,
943            SearchAction,
944            SearchInputAction,
945            SearchToggleOptionAction,
946            SetDarkThemeAction,
947            SetLightThemeAction,
948            SetPaneBorderlessAction,
949            SetPaneColorAction,
950            ShowFloatingPanesAction,
951            SkipConfirmAction,
952            StackPanesAction,
953            StartOrReloadPluginAction,
954            SwitchFocusAction,
955            SwitchModeForAllClientsAction,
956            SwitchSessionAction,
957            SwitchToModeAction,
958            TabNameInputAction,
959            ToggleActiveSyncTabAction,
960            ToggleActiveSyncTabByTabIdAction,
961            ToggleFloatingPanesAction,
962            ToggleFloatingPanesByTabIdAction,
963            ToggleFocusFullscreenAction,
964            ToggleFullscreenByPaneIdAction,
965            ToggleGroupMarkingAction,
966            ToggleMouseModeAction,
967            TogglePaneBorderlessAction,
968            TogglePaneEmbedOrFloatingAction,
969            TogglePaneEmbedOrFloatingByPaneIdAction,
970            TogglePaneFramesAction,
971            TogglePaneInGroupAction,
972            TogglePanePinnedAction,
973            TogglePanePinnedByPaneIdAction,
974            ToggleTabAction,
975            ToggleThemeAction,
976            UndoRenamePaneAction,
977            UndoRenamePaneByPaneIdAction,
978            UndoRenameTabAction,
979            // Tab-targeting
980            UndoRenameTabByTabIdAction,
981            WriteAction,
982            WriteCharsAction,
983            WriteCharsToPaneIdAction,
984            WriteToPaneIdAction,
985        };
986        use std::collections::HashMap;
987
988        let action_type = match action {
989            crate::input::actions::Action::Quit => ActionType::Quit(QuitAction {}),
990            crate::input::actions::Action::Write {
991                key_with_modifier,
992                bytes,
993                is_kitty_keyboard_protocol,
994            } => ActionType::Write(WriteAction {
995                key_with_modifier: key_with_modifier.map(|k| k.into()),
996                bytes: bytes.into_iter().map(|b| b as u32).collect(),
997                is_kitty_keyboard_protocol,
998            }),
999            crate::input::actions::Action::WriteChars { chars } => {
1000                ActionType::WriteChars(WriteCharsAction { chars })
1001            },
1002            crate::input::actions::Action::WriteToPaneId { bytes, pane_id } => {
1003                ActionType::WriteToPaneId(WriteToPaneIdAction {
1004                    pane_id: Some(pane_id.into()),
1005                    bytes: bytes.into_iter().map(|b| b as u32).collect(),
1006                })
1007            },
1008            crate::input::actions::Action::WriteCharsToPaneId { chars, pane_id } => {
1009                ActionType::WriteCharsToPaneId(WriteCharsToPaneIdAction {
1010                    pane_id: Some(pane_id.into()),
1011                    chars,
1012                })
1013            },
1014            crate::input::actions::Action::Paste { chars, pane_id } => {
1015                ActionType::Paste(PasteAction {
1016                    chars,
1017                    pane_id: pane_id.map(|p| p.into()),
1018                })
1019            },
1020            crate::input::actions::Action::SwitchToMode { input_mode } => {
1021                ActionType::SwitchToMode(SwitchToModeAction {
1022                    input_mode: input_mode_to_proto_i32(input_mode),
1023                })
1024            },
1025            crate::input::actions::Action::SwitchModeForAllClients { input_mode } => {
1026                ActionType::SwitchModeForAllClients(SwitchModeForAllClientsAction {
1027                    input_mode: input_mode_to_proto_i32(input_mode),
1028                })
1029            },
1030            crate::input::actions::Action::Resize { resize, direction } => {
1031                ActionType::Resize(ResizeAction {
1032                    resize: resize_to_proto_i32(resize),
1033                    direction: direction.map(|d| direction_to_proto_i32(d)),
1034                })
1035            },
1036            crate::input::actions::Action::FocusNextPane => {
1037                ActionType::FocusNextPane(FocusNextPaneAction {})
1038            },
1039            crate::input::actions::Action::FocusPreviousPane => {
1040                ActionType::FocusPreviousPane(FocusPreviousPaneAction {})
1041            },
1042            crate::input::actions::Action::SwitchFocus => {
1043                ActionType::SwitchFocus(SwitchFocusAction {})
1044            },
1045            crate::input::actions::Action::MoveFocus { direction } => {
1046                ActionType::MoveFocus(MoveFocusAction {
1047                    direction: direction_to_proto_i32(direction),
1048                })
1049            },
1050            crate::input::actions::Action::MoveFocusOrTab { direction } => {
1051                ActionType::MoveFocusOrTab(MoveFocusOrTabAction {
1052                    direction: direction_to_proto_i32(direction),
1053                })
1054            },
1055            crate::input::actions::Action::MovePane { direction } => {
1056                ActionType::MovePane(MovePaneAction {
1057                    direction: direction.map(|d| direction_to_proto_i32(d)),
1058                })
1059            },
1060            crate::input::actions::Action::MovePaneBackwards => {
1061                ActionType::MovePaneBackwards(MovePaneBackwardsAction {})
1062            },
1063            crate::input::actions::Action::ClearScreen => {
1064                ActionType::ClearScreen(ClearScreenAction {})
1065            },
1066            crate::input::actions::Action::DumpScreen {
1067                file_path,
1068                include_scrollback,
1069                pane_id,
1070                ansi,
1071            } => {
1072                let dump_to_stdout = file_path.is_none();
1073                ActionType::DumpScreen(DumpScreenAction {
1074                    file_path: file_path.unwrap_or_default(),
1075                    include_scrollback,
1076                    pane_id: pane_id.map(|p| p.into()),
1077                    dump_to_stdout,
1078                    ansi,
1079                })
1080            },
1081            crate::input::actions::Action::DumpLayout => {
1082                ActionType::DumpLayout(DumpLayoutAction {})
1083            },
1084            crate::input::actions::Action::EditScrollback { ansi } => {
1085                ActionType::EditScrollback(EditScrollbackAction { ansi })
1086            },
1087            crate::input::actions::Action::ScrollUp => ActionType::ScrollUp(ScrollUpAction {}),
1088            crate::input::actions::Action::ScrollUpAt { position } => {
1089                ActionType::ScrollUpAt(ScrollUpAtAction {
1090                    position: Some(position.into()),
1091                })
1092            },
1093            crate::input::actions::Action::ScrollDown => {
1094                ActionType::ScrollDown(ScrollDownAction {})
1095            },
1096            crate::input::actions::Action::ScrollDownAt { position } => {
1097                ActionType::ScrollDownAt(ScrollDownAtAction {
1098                    position: Some(position.into()),
1099                })
1100            },
1101            crate::input::actions::Action::ScrollToBottom => {
1102                ActionType::ScrollToBottom(ScrollToBottomAction {})
1103            },
1104            crate::input::actions::Action::ScrollToTop => {
1105                ActionType::ScrollToTop(ScrollToTopAction {})
1106            },
1107            crate::input::actions::Action::PageScrollUp => {
1108                ActionType::PageScrollUp(PageScrollUpAction {})
1109            },
1110            crate::input::actions::Action::PageScrollDown => {
1111                ActionType::PageScrollDown(PageScrollDownAction {})
1112            },
1113            crate::input::actions::Action::HalfPageScrollUp => {
1114                ActionType::HalfPageScrollUp(HalfPageScrollUpAction {})
1115            },
1116            crate::input::actions::Action::HalfPageScrollDown => {
1117                ActionType::HalfPageScrollDown(HalfPageScrollDownAction {})
1118            },
1119            crate::input::actions::Action::ToggleFocusFullscreen => {
1120                ActionType::ToggleFocusFullscreen(ToggleFocusFullscreenAction {})
1121            },
1122            crate::input::actions::Action::TogglePaneFrames => {
1123                ActionType::TogglePaneFrames(TogglePaneFramesAction {})
1124            },
1125            crate::input::actions::Action::ToggleActiveSyncTab => {
1126                ActionType::ToggleActiveSyncTab(ToggleActiveSyncTabAction {})
1127            },
1128            crate::input::actions::Action::NewPane {
1129                direction,
1130                pane_name,
1131                start_suppressed,
1132            } => ActionType::NewPane(NewPaneAction {
1133                direction: direction.map(|d| direction_to_proto_i32(d)),
1134                pane_name,
1135                start_suppressed,
1136                near_current_pane: false,
1137            }),
1138            crate::input::actions::Action::EditFile {
1139                payload,
1140                direction,
1141                floating,
1142                in_place,
1143                close_replaced_pane,
1144                start_suppressed,
1145                coordinates,
1146                near_current_pane,
1147                tab_id,
1148                ..
1149            } => ActionType::EditFile(EditFileAction {
1150                payload: Some(payload.into()),
1151                direction: direction.map(|d| direction_to_proto_i32(d)),
1152                floating,
1153                in_place,
1154                close_replaced_pane,
1155                start_suppressed,
1156                coordinates: coordinates.map(|c| c.into()),
1157                near_current_pane,
1158                tab_id: tab_id.map(|t| t as u32),
1159            }),
1160            crate::input::actions::Action::NewFloatingPane {
1161                command,
1162                pane_name,
1163                coordinates,
1164                near_current_pane,
1165                tab_id,
1166                ..
1167            } => ActionType::NewFloatingPane(NewFloatingPaneAction {
1168                command: command.map(|c| c.into()),
1169                pane_name,
1170                coordinates: coordinates.map(|c| c.into()),
1171                near_current_pane,
1172                tab_id: tab_id.map(|t| t as u32),
1173            }),
1174            crate::input::actions::Action::NewTiledPane {
1175                direction,
1176                command,
1177                pane_name,
1178                near_current_pane,
1179                borderless,
1180                tab_id,
1181                ..
1182            } => ActionType::NewTiledPane(NewTiledPaneAction {
1183                direction: direction.map(|d| direction_to_proto_i32(d)),
1184                command: command.map(|c| c.into()),
1185                pane_name,
1186                near_current_pane,
1187                borderless,
1188                tab_id: tab_id.map(|t| t as u32),
1189            }),
1190            crate::input::actions::Action::NewInPlacePane {
1191                command,
1192                pane_name,
1193                near_current_pane,
1194                pane_id_to_replace,
1195                close_replaced_pane,
1196                tab_id,
1197                ..
1198            } => ActionType::NewInPlacePane(NewInPlacePaneAction {
1199                command: command.map(|c| c.into()),
1200                pane_name,
1201                near_current_pane,
1202                pane_id_to_replace: pane_id_to_replace.and_then(|p| p.try_into().ok()),
1203                close_replaced_pane,
1204                tab_id: tab_id.map(|t| t as u32),
1205            }),
1206            crate::input::actions::Action::NewStackedPane {
1207                command,
1208                pane_name,
1209                near_current_pane,
1210                tab_id,
1211                ..
1212            } => ActionType::NewStackedPane(NewStackedPaneAction {
1213                command: command.map(|c| c.into()),
1214                pane_name,
1215                near_current_pane,
1216                tab_id: tab_id.map(|t| t as u32),
1217            }),
1218            crate::input::actions::Action::NewBlockingPane {
1219                placement,
1220                pane_name,
1221                command,
1222                unblock_condition,
1223                near_current_pane,
1224                tab_id,
1225                ..
1226            } => ActionType::NewBlockingPane(NewBlockingPaneAction {
1227                placement: Some(placement.into()),
1228                pane_name,
1229                command: command.map(|c| c.into()),
1230                unblock_condition: unblock_condition.map(|c| unblock_condition_to_proto_i32(c)),
1231                near_current_pane,
1232                tab_id: tab_id.map(|t| t as u32),
1233            }),
1234            crate::input::actions::Action::TogglePaneEmbedOrFloating => {
1235                ActionType::TogglePaneEmbedOrFloating(TogglePaneEmbedOrFloatingAction {})
1236            },
1237            crate::input::actions::Action::ToggleFloatingPanes => {
1238                ActionType::ToggleFloatingPanes(ToggleFloatingPanesAction {})
1239            },
1240            crate::input::actions::Action::ShowFloatingPanes { tab_id } => {
1241                ActionType::ShowFloatingPanes(ShowFloatingPanesAction {
1242                    tab_id: tab_id.map(|id| id as u32),
1243                })
1244            },
1245            crate::input::actions::Action::HideFloatingPanes { tab_id } => {
1246                ActionType::HideFloatingPanes(HideFloatingPanesAction {
1247                    tab_id: tab_id.map(|id| id as u32),
1248                })
1249            },
1250            crate::input::actions::Action::AreFloatingPanesVisible { tab_id } => {
1251                ActionType::AreFloatingPanesVisible(AreFloatingPanesVisibleAction {
1252                    tab_id: tab_id.map(|id| id as u32),
1253                })
1254            },
1255            crate::input::actions::Action::CloseFocus => {
1256                ActionType::CloseFocus(CloseFocusAction {})
1257            },
1258            crate::input::actions::Action::PaneNameInput { input } => {
1259                ActionType::PaneNameInput(PaneNameInputAction {
1260                    input: input.into_iter().map(|b| b as u32).collect(),
1261                })
1262            },
1263            crate::input::actions::Action::UndoRenamePane => {
1264                ActionType::UndoRenamePane(UndoRenamePaneAction {})
1265            },
1266            crate::input::actions::Action::NewTab {
1267                tiled_layout,
1268                floating_layouts,
1269                swap_tiled_layouts,
1270                swap_floating_layouts,
1271                tab_name,
1272                should_change_focus_to_new_tab,
1273                cwd,
1274                initial_panes,
1275                first_pane_unblock_condition,
1276            } => ActionType::NewTab(NewTabAction {
1277                tiled_layout: tiled_layout.map(|l| l.into()),
1278                floating_layouts: floating_layouts.into_iter().map(|l| l.into()).collect(),
1279                swap_tiled_layouts: swap_tiled_layouts
1280                    .map(|layouts| layouts.into_iter().map(|l| l.into()).collect())
1281                    .unwrap_or_default(),
1282                swap_floating_layouts: swap_floating_layouts
1283                    .map(|layouts| layouts.into_iter().map(|l| l.into()).collect())
1284                    .unwrap_or_default(),
1285                tab_name,
1286                should_change_focus_to_new_tab,
1287                cwd: cwd.map(|p| p.to_string_lossy().to_string()),
1288                initial_panes: initial_panes
1289                    .map(|panes| panes.into_iter().map(|p| p.into()).collect())
1290                    .unwrap_or_default(),
1291                first_pane_unblock_condition: first_pane_unblock_condition
1292                    .map(|c| unblock_condition_to_proto_i32(c)),
1293            }),
1294            crate::input::actions::Action::NoOp => ActionType::NoOp(NoOpAction {}),
1295            crate::input::actions::Action::GoToNextTab => {
1296                ActionType::GoToNextTab(GoToNextTabAction {})
1297            },
1298            crate::input::actions::Action::GoToPreviousTab => {
1299                ActionType::GoToPreviousTab(GoToPreviousTabAction {})
1300            },
1301            crate::input::actions::Action::CloseTab => ActionType::CloseTab(CloseTabAction {}),
1302            crate::input::actions::Action::GoToTab { index } => {
1303                ActionType::GoToTab(GoToTabAction { index })
1304            },
1305            crate::input::actions::Action::GoToTabName { name, create } => {
1306                ActionType::GoToTabName(GoToTabNameAction { name, create })
1307            },
1308            crate::input::actions::Action::ToggleTab => ActionType::ToggleTab(ToggleTabAction {}),
1309            crate::input::actions::Action::TabNameInput { input } => {
1310                ActionType::TabNameInput(TabNameInputAction {
1311                    input: input.into_iter().map(|b| b as u32).collect(),
1312                })
1313            },
1314            crate::input::actions::Action::UndoRenameTab => {
1315                ActionType::UndoRenameTab(UndoRenameTabAction {})
1316            },
1317            crate::input::actions::Action::MoveTab { direction } => {
1318                ActionType::MoveTab(MoveTabAction {
1319                    direction: direction_to_proto_i32(direction),
1320                })
1321            },
1322            crate::input::actions::Action::Run {
1323                command,
1324                near_current_pane,
1325            } => ActionType::Run(RunAction {
1326                command: Some(command.into()),
1327                near_current_pane,
1328            }),
1329            crate::input::actions::Action::Detach => ActionType::Detach(DetachAction {}),
1330            crate::input::actions::Action::SetDarkTheme => {
1331                ActionType::SetDarkTheme(SetDarkThemeAction {})
1332            },
1333            crate::input::actions::Action::SetLightTheme => {
1334                ActionType::SetLightTheme(SetLightThemeAction {})
1335            },
1336            crate::input::actions::Action::ToggleTheme => {
1337                ActionType::ToggleTheme(ToggleThemeAction {})
1338            },
1339            crate::input::actions::Action::SwitchSession {
1340                name,
1341                tab_position,
1342                pane_id,
1343                layout,
1344                cwd,
1345            } => ActionType::SwitchSession(SwitchSessionAction {
1346                name: name.clone(),
1347                tab_position: tab_position.map(|p| p as u32),
1348                pane_id: pane_id.map(|(id, is_plugin)| PaneIdWithPlugin {
1349                    pane_id: id,
1350                    is_plugin: is_plugin,
1351                }),
1352                layout: layout.as_ref().map(|l| l.clone().into()),
1353                cwd: cwd.as_ref().map(|p| p.to_string_lossy().to_string()),
1354            }),
1355            crate::input::actions::Action::LaunchOrFocusPlugin {
1356                plugin,
1357                should_float,
1358                move_to_focused_tab,
1359                should_open_in_place,
1360                close_replaced_pane,
1361                skip_cache,
1362                tab_id,
1363                ..
1364            } => ActionType::LaunchOrFocusPlugin(LaunchOrFocusPluginAction {
1365                plugin: Some(plugin.into()),
1366                should_float,
1367                move_to_focused_tab,
1368                should_open_in_place,
1369                close_replaced_pane,
1370                skip_cache,
1371                tab_id: tab_id.map(|t| t as u32),
1372            }),
1373            crate::input::actions::Action::LaunchPlugin {
1374                plugin,
1375                should_float,
1376                should_open_in_place,
1377                close_replaced_pane,
1378                skip_cache,
1379                cwd,
1380                tab_id,
1381                ..
1382            } => ActionType::LaunchPlugin(LaunchPluginAction {
1383                plugin: Some(plugin.into()),
1384                should_float,
1385                should_open_in_place,
1386                close_replaced_pane,
1387                skip_cache,
1388                cwd: cwd.map(|p| p.to_string_lossy().to_string()),
1389                tab_id: tab_id.map(|t| t as u32),
1390            }),
1391            crate::input::actions::Action::MouseEvent { event } => {
1392                ActionType::MouseEvent(MouseEventAction {
1393                    event: Some(event.into()),
1394                })
1395            },
1396            crate::input::actions::Action::Copy => ActionType::Copy(CopyAction {}),
1397            crate::input::actions::Action::Confirm => ActionType::Confirm(ConfirmAction {}),
1398            crate::input::actions::Action::Deny => ActionType::Deny(DenyAction {}),
1399            crate::input::actions::Action::SkipConfirm { action } => {
1400                ActionType::SkipConfirm(Box::new(SkipConfirmAction {
1401                    action: Some(Box::new((*action).into())),
1402                }))
1403            },
1404            crate::input::actions::Action::SearchInput { input } => {
1405                ActionType::SearchInput(SearchInputAction {
1406                    input: input.into_iter().map(|b| b as u32).collect(),
1407                })
1408            },
1409            crate::input::actions::Action::Search { direction } => {
1410                ActionType::Search(SearchAction {
1411                    direction: search_direction_to_proto_i32(direction),
1412                })
1413            },
1414            crate::input::actions::Action::SearchToggleOption { option } => {
1415                ActionType::SearchToggleOption(SearchToggleOptionAction {
1416                    option: search_option_to_proto_i32(option),
1417                })
1418            },
1419            crate::input::actions::Action::ToggleMouseMode => {
1420                ActionType::ToggleMouseMode(ToggleMouseModeAction {})
1421            },
1422            crate::input::actions::Action::PreviousSwapLayout => {
1423                ActionType::PreviousSwapLayout(PreviousSwapLayoutAction {})
1424            },
1425            crate::input::actions::Action::NextSwapLayout => {
1426                ActionType::NextSwapLayout(NextSwapLayoutAction {})
1427            },
1428            crate::input::actions::Action::OverrideLayout {
1429                tabs,
1430                retain_existing_terminal_panes,
1431                retain_existing_plugin_panes,
1432                apply_only_to_active_tab,
1433            } => ActionType::OverrideLayout(OverrideLayoutAction {
1434                tabs: tabs.into_iter().map(|t| t.into()).collect(),
1435                retain_existing_terminal_panes,
1436                retain_existing_plugin_panes,
1437                apply_only_to_active_tab,
1438            }),
1439            crate::input::actions::Action::QueryTabNames => {
1440                ActionType::QueryTabNames(QueryTabNamesAction {})
1441            },
1442            crate::input::actions::Action::NewTiledPluginPane {
1443                plugin,
1444                pane_name,
1445                skip_cache,
1446                cwd,
1447                tab_id,
1448                ..
1449            } => ActionType::NewTiledPluginPane(NewTiledPluginPaneAction {
1450                plugin: Some(plugin.into()),
1451                pane_name,
1452                skip_cache,
1453                cwd: cwd.map(|p| p.to_string_lossy().to_string()),
1454                tab_id: tab_id.map(|t| t as u32),
1455            }),
1456            crate::input::actions::Action::NewFloatingPluginPane {
1457                plugin,
1458                pane_name,
1459                skip_cache,
1460                cwd,
1461                coordinates,
1462                tab_id,
1463                ..
1464            } => ActionType::NewFloatingPluginPane(NewFloatingPluginPaneAction {
1465                plugin: Some(plugin.into()),
1466                pane_name,
1467                skip_cache,
1468                cwd: cwd.map(|p| p.to_string_lossy().to_string()),
1469                coordinates: coordinates.map(|c| c.into()),
1470                tab_id: tab_id.map(|t| t as u32),
1471            }),
1472            crate::input::actions::Action::NewInPlacePluginPane {
1473                plugin,
1474                pane_name,
1475                skip_cache,
1476                close_replaced_pane,
1477                tab_id,
1478                ..
1479            } => ActionType::NewInPlacePluginPane(NewInPlacePluginPaneAction {
1480                plugin: Some(plugin.into()),
1481                pane_name,
1482                skip_cache,
1483                close_replaced_pane,
1484                tab_id: tab_id.map(|t| t as u32),
1485            }),
1486            crate::input::actions::Action::StartOrReloadPlugin { plugin } => {
1487                ActionType::StartOrReloadPlugin(StartOrReloadPluginAction {
1488                    plugin: Some(plugin.into()),
1489                })
1490            },
1491            crate::input::actions::Action::CloseTerminalPane { pane_id } => {
1492                ActionType::CloseTerminalPane(CloseTerminalPaneAction { pane_id })
1493            },
1494            crate::input::actions::Action::ClosePluginPane { pane_id } => {
1495                ActionType::ClosePluginPane(ClosePluginPaneAction { pane_id })
1496            },
1497            crate::input::actions::Action::FocusTerminalPaneWithId {
1498                pane_id,
1499                should_float_if_hidden,
1500                should_be_in_place_if_hidden,
1501            } => ActionType::FocusTerminalPaneWithId(FocusTerminalPaneWithIdAction {
1502                pane_id,
1503                should_float_if_hidden,
1504                should_be_in_place_if_hidden,
1505            }),
1506            crate::input::actions::Action::FocusPluginPaneWithId {
1507                pane_id,
1508                should_float_if_hidden,
1509                should_be_in_place_if_hidden,
1510            } => ActionType::FocusPluginPaneWithId(FocusPluginPaneWithIdAction {
1511                pane_id,
1512                should_float_if_hidden,
1513                should_be_in_place_if_hidden,
1514            }),
1515            crate::input::actions::Action::RenameTerminalPane { pane_id, name } => {
1516                ActionType::RenameTerminalPane(RenameTerminalPaneAction {
1517                    pane_id,
1518                    name: name.into_iter().map(|b| b as u32).collect(),
1519                })
1520            },
1521            crate::input::actions::Action::RenamePluginPane { pane_id, name } => {
1522                ActionType::RenamePluginPane(RenamePluginPaneAction {
1523                    pane_id,
1524                    name: name.into_iter().map(|b| b as u32).collect(),
1525                })
1526            },
1527            crate::input::actions::Action::RenameTab { tab_index, name } => {
1528                ActionType::RenameTab(RenameTabAction {
1529                    tab_index,
1530                    name: name.into_iter().map(|b| b as u32).collect(),
1531                })
1532            },
1533            crate::input::actions::Action::GoToTabById { id } => {
1534                ActionType::GoToTabById(GoToTabByIdAction { id })
1535            },
1536            crate::input::actions::Action::CloseTabById { id } => {
1537                ActionType::CloseTabById(CloseTabByIdAction { id })
1538            },
1539            crate::input::actions::Action::RenameTabById { id, name } => {
1540                ActionType::RenameTabById(RenameTabByIdAction { id, name })
1541            },
1542            crate::input::actions::Action::BreakPane => ActionType::BreakPane(BreakPaneAction {}),
1543            crate::input::actions::Action::BreakPaneRight => {
1544                ActionType::BreakPaneRight(BreakPaneRightAction {})
1545            },
1546            crate::input::actions::Action::BreakPaneLeft => {
1547                ActionType::BreakPaneLeft(BreakPaneLeftAction {})
1548            },
1549            crate::input::actions::Action::RenameSession { name } => {
1550                ActionType::RenameSession(RenameSessionAction { name })
1551            },
1552            crate::input::actions::Action::CliPipe {
1553                pipe_id,
1554                name,
1555                payload,
1556                args,
1557                plugin,
1558                configuration,
1559                launch_new,
1560                skip_cache,
1561                floating,
1562                in_place,
1563                cwd,
1564                pane_title,
1565            } => ActionType::CliPipe(CliPipeAction {
1566                pipe_id,
1567                name,
1568                payload,
1569                args: args
1570                    .map(|a| a.into_iter().collect::<HashMap<_, _>>())
1571                    .unwrap_or_default(),
1572                plugin,
1573                configuration: configuration
1574                    .map(|c| c.into_iter().collect::<HashMap<_, _>>())
1575                    .unwrap_or_default(),
1576                launch_new,
1577                skip_cache,
1578                floating,
1579                in_place,
1580                cwd: cwd.map(|p| p.to_string_lossy().to_string()),
1581                pane_title,
1582            }),
1583            crate::input::actions::Action::KeybindPipe {
1584                name,
1585                payload,
1586                args,
1587                plugin,
1588                plugin_id,
1589                configuration,
1590                launch_new,
1591                skip_cache,
1592                floating,
1593                in_place,
1594                cwd,
1595                pane_title,
1596            } => ActionType::KeybindPipe(KeybindPipeAction {
1597                name,
1598                payload,
1599                args: args
1600                    .map(|a| a.into_iter().collect::<HashMap<_, _>>())
1601                    .unwrap_or_default(),
1602                plugin,
1603                plugin_id,
1604                configuration: configuration
1605                    .map(|c| c.into_iter().collect::<HashMap<_, _>>())
1606                    .unwrap_or_default(),
1607                launch_new,
1608                skip_cache,
1609                floating,
1610                in_place,
1611                cwd: cwd.map(|p| p.to_string_lossy().to_string()),
1612                pane_title,
1613            }),
1614            crate::input::actions::Action::ListClients => {
1615                ActionType::ListClients(ListClientsAction {})
1616            },
1617            crate::input::actions::Action::ListPanes {
1618                show_tab,
1619                show_command,
1620                show_state,
1621                show_geometry,
1622                show_all,
1623                output_json,
1624            } => ActionType::ListPanes(ListPanesAction {
1625                show_tab,
1626                show_command,
1627                show_state,
1628                show_geometry,
1629                show_all,
1630                output_json,
1631            }),
1632            crate::input::actions::Action::TogglePanePinned => {
1633                ActionType::TogglePanePinned(TogglePanePinnedAction {})
1634            },
1635            crate::input::actions::Action::StackPanes { pane_ids } => {
1636                ActionType::StackPanes(StackPanesAction {
1637                    pane_ids: pane_ids.into_iter().map(|id| id.into()).collect(),
1638                })
1639            },
1640            crate::input::actions::Action::ChangeFloatingPaneCoordinates {
1641                pane_id,
1642                coordinates,
1643            } => ActionType::ChangeFloatingPaneCoordinates(ChangeFloatingPaneCoordinatesAction {
1644                pane_id: Some(pane_id.into()),
1645                coordinates: Some(coordinates.into()),
1646            }),
1647            crate::input::actions::Action::TogglePaneBorderless { pane_id } => {
1648                ActionType::TogglePaneBorderless(TogglePaneBorderlessAction {
1649                    pane_id: Some(pane_id.into()),
1650                })
1651            },
1652            crate::input::actions::Action::SetPaneBorderless {
1653                pane_id,
1654                borderless,
1655            } => ActionType::SetPaneBorderless(SetPaneBorderlessAction {
1656                pane_id: Some(pane_id.into()),
1657                borderless,
1658            }),
1659            crate::input::actions::Action::TogglePaneInGroup => {
1660                ActionType::TogglePaneInGroup(TogglePaneInGroupAction {})
1661            },
1662            crate::input::actions::Action::ToggleGroupMarking => {
1663                ActionType::ToggleGroupMarking(ToggleGroupMarkingAction {})
1664            },
1665            crate::input::actions::Action::SaveSession => {
1666                ActionType::SaveSession(SaveSessionAction {})
1667            },
1668            crate::input::actions::Action::ListTabs {
1669                show_state,
1670                show_dimensions,
1671                show_panes,
1672                show_layout,
1673                show_all,
1674                output_json,
1675            } => ActionType::ListTabs(ListTabsAction {
1676                show_state,
1677                show_dimensions,
1678                show_panes,
1679                show_layout,
1680                show_all,
1681                output_json,
1682            }),
1683            crate::input::actions::Action::CurrentTabInfo { output_json } => {
1684                ActionType::CurrentTabInfo(CurrentTabInfoAction { output_json })
1685            },
1686            crate::input::actions::Action::SetPaneColor { pane_id, fg, bg } => {
1687                ActionType::SetPaneColor(SetPaneColorAction {
1688                    pane_id: Some(pane_id.into()),
1689                    fg,
1690                    bg,
1691                })
1692            },
1693            // Pane-targeting CLI-only variants
1694            crate::input::actions::Action::ScrollUpByPaneId { pane_id } => {
1695                ActionType::ScrollUpByPaneId(ScrollUpByPaneIdAction {
1696                    pane_id: Some(pane_id.into()),
1697                })
1698            },
1699            crate::input::actions::Action::ScrollDownByPaneId { pane_id } => {
1700                ActionType::ScrollDownByPaneId(ScrollDownByPaneIdAction {
1701                    pane_id: Some(pane_id.into()),
1702                })
1703            },
1704            crate::input::actions::Action::ScrollToTopByPaneId { pane_id } => {
1705                ActionType::ScrollToTopByPaneId(ScrollToTopByPaneIdAction {
1706                    pane_id: Some(pane_id.into()),
1707                })
1708            },
1709            crate::input::actions::Action::ScrollToBottomByPaneId { pane_id } => {
1710                ActionType::ScrollToBottomByPaneId(ScrollToBottomByPaneIdAction {
1711                    pane_id: Some(pane_id.into()),
1712                })
1713            },
1714            crate::input::actions::Action::PageScrollUpByPaneId { pane_id } => {
1715                ActionType::PageScrollUpByPaneId(PageScrollUpByPaneIdAction {
1716                    pane_id: Some(pane_id.into()),
1717                })
1718            },
1719            crate::input::actions::Action::PageScrollDownByPaneId { pane_id } => {
1720                ActionType::PageScrollDownByPaneId(PageScrollDownByPaneIdAction {
1721                    pane_id: Some(pane_id.into()),
1722                })
1723            },
1724            crate::input::actions::Action::HalfPageScrollUpByPaneId { pane_id } => {
1725                ActionType::HalfPageScrollUpByPaneId(HalfPageScrollUpByPaneIdAction {
1726                    pane_id: Some(pane_id.into()),
1727                })
1728            },
1729            crate::input::actions::Action::HalfPageScrollDownByPaneId { pane_id } => {
1730                ActionType::HalfPageScrollDownByPaneId(HalfPageScrollDownByPaneIdAction {
1731                    pane_id: Some(pane_id.into()),
1732                })
1733            },
1734            crate::input::actions::Action::ResizeByPaneId {
1735                pane_id,
1736                resize,
1737                direction,
1738            } => ActionType::ResizeByPaneId(ResizeByPaneIdAction {
1739                pane_id: Some(pane_id.into()),
1740                resize_action: Some(ResizeAction {
1741                    resize: resize_to_proto_i32(resize),
1742                    direction: direction.map(|d| direction_to_proto_i32(d)),
1743                }),
1744            }),
1745            crate::input::actions::Action::MovePaneByPaneId { pane_id, direction } => {
1746                ActionType::MovePaneByPaneId(MovePaneByPaneIdAction {
1747                    pane_id: Some(pane_id.into()),
1748                    direction: direction.map(|d| direction_to_proto_i32(d)),
1749                })
1750            },
1751            crate::input::actions::Action::MovePaneBackwardsByPaneId { pane_id } => {
1752                ActionType::MovePaneBackwardsByPaneId(MovePaneBackwardsByPaneIdAction {
1753                    pane_id: Some(pane_id.into()),
1754                })
1755            },
1756            crate::input::actions::Action::ClearScreenByPaneId { pane_id } => {
1757                ActionType::ClearScreenByPaneId(ClearScreenByPaneIdAction {
1758                    pane_id: Some(pane_id.into()),
1759                })
1760            },
1761            crate::input::actions::Action::EditScrollbackByPaneId { pane_id, ansi } => {
1762                ActionType::EditScrollbackByPaneId(EditScrollbackByPaneIdAction {
1763                    pane_id: Some(pane_id.into()),
1764                    ansi,
1765                })
1766            },
1767            crate::input::actions::Action::ToggleFocusFullscreenByPaneId { pane_id } => {
1768                ActionType::ToggleFullscreenByPaneId(ToggleFullscreenByPaneIdAction {
1769                    pane_id: Some(pane_id.into()),
1770                })
1771            },
1772            crate::input::actions::Action::TogglePaneEmbedOrFloatingByPaneId { pane_id } => {
1773                ActionType::TogglePaneEmbedOrFloatingByPaneId(
1774                    TogglePaneEmbedOrFloatingByPaneIdAction {
1775                        pane_id: Some(pane_id.into()),
1776                    },
1777                )
1778            },
1779            crate::input::actions::Action::CloseFocusByPaneId { pane_id } => {
1780                ActionType::CloseFocusByPaneId(CloseFocusByPaneIdAction {
1781                    pane_id: Some(pane_id.into()),
1782                })
1783            },
1784            crate::input::actions::Action::RenamePaneByPaneId { pane_id, name } => {
1785                ActionType::RenamePaneByPaneId(RenamePaneByPaneIdAction {
1786                    pane_id: pane_id.map(|id| id.into()),
1787                    name,
1788                })
1789            },
1790            crate::input::actions::Action::UndoRenamePaneByPaneId { pane_id } => {
1791                ActionType::UndoRenamePaneByPaneId(UndoRenamePaneByPaneIdAction {
1792                    pane_id: Some(pane_id.into()),
1793                })
1794            },
1795            crate::input::actions::Action::TogglePanePinnedByPaneId { pane_id } => {
1796                ActionType::TogglePanePinnedByPaneId(TogglePanePinnedByPaneIdAction {
1797                    pane_id: Some(pane_id.into()),
1798                })
1799            },
1800            crate::input::actions::Action::FocusPaneByPaneId { pane_id } => {
1801                ActionType::FocusPaneByPaneId(FocusPaneByPaneIdAction {
1802                    pane_id: Some(pane_id.into()),
1803                })
1804            },
1805            // Tab-targeting CLI-only variants
1806            crate::input::actions::Action::UndoRenameTabByTabId { id } => {
1807                ActionType::UndoRenameTabByTabId(UndoRenameTabByTabIdAction { id })
1808            },
1809            crate::input::actions::Action::ToggleActiveSyncTabByTabId { id } => {
1810                ActionType::ToggleActiveSyncTabByTabId(ToggleActiveSyncTabByTabIdAction { id })
1811            },
1812            crate::input::actions::Action::ToggleFloatingPanesByTabId { id } => {
1813                ActionType::ToggleFloatingPanesByTabId(ToggleFloatingPanesByTabIdAction { id })
1814            },
1815            crate::input::actions::Action::PreviousSwapLayoutByTabId { id } => {
1816                ActionType::PreviousSwapLayoutByTabId(PreviousSwapLayoutByTabIdAction { id })
1817            },
1818            crate::input::actions::Action::NextSwapLayoutByTabId { id } => {
1819                ActionType::NextSwapLayoutByTabId(NextSwapLayoutByTabIdAction { id })
1820            },
1821            crate::input::actions::Action::MoveTabByTabId { id, direction } => {
1822                ActionType::MoveTabByTabId(MoveTabByTabIdAction {
1823                    id,
1824                    direction: direction_to_proto_i32(direction),
1825                })
1826            },
1827        };
1828
1829        Self {
1830            action_type: Some(action_type),
1831        }
1832    }
1833}
1834
1835impl TryFrom<crate::client_server_contract::client_server_contract::Action>
1836    for crate::input::actions::Action
1837{
1838    type Error = anyhow::Error;
1839    fn try_from(
1840        action: crate::client_server_contract::client_server_contract::Action,
1841    ) -> Result<Self> {
1842        use crate::client_server_contract::client_server_contract::action::ActionType;
1843
1844        let action_type = action
1845            .action_type
1846            .ok_or_else(|| anyhow!("Action missing action_type"))?;
1847
1848        match action_type {
1849            ActionType::Quit(_) => Ok(crate::input::actions::Action::Quit),
1850            ActionType::Write(write_action) => Ok(crate::input::actions::Action::Write {
1851                key_with_modifier: write_action
1852                    .key_with_modifier
1853                    .map(|k| k.try_into())
1854                    .transpose()?,
1855                bytes: write_action.bytes.into_iter().map(|b| b as u8).collect(),
1856                is_kitty_keyboard_protocol: write_action.is_kitty_keyboard_protocol,
1857            }),
1858            ActionType::WriteChars(write_chars_action) => {
1859                Ok(crate::input::actions::Action::WriteChars {
1860                    chars: write_chars_action.chars,
1861                })
1862            },
1863            ActionType::WriteToPaneId(write_to_pane_id_action) => {
1864                Ok(crate::input::actions::Action::WriteToPaneId {
1865                    bytes: write_to_pane_id_action
1866                        .bytes
1867                        .into_iter()
1868                        .map(|b| b as u8)
1869                        .collect(),
1870                    pane_id: write_to_pane_id_action
1871                        .pane_id
1872                        .ok_or_else(|| anyhow!("WriteToPaneId missing pane_id"))?
1873                        .try_into()?,
1874                })
1875            },
1876            ActionType::WriteCharsToPaneId(write_chars_to_pane_id_action) => {
1877                Ok(crate::input::actions::Action::WriteCharsToPaneId {
1878                    chars: write_chars_to_pane_id_action.chars,
1879                    pane_id: write_chars_to_pane_id_action
1880                        .pane_id
1881                        .ok_or_else(|| anyhow!("WriteCharsToPaneId missing pane_id"))?
1882                        .try_into()?,
1883                })
1884            },
1885            ActionType::Paste(paste_action) => Ok(crate::input::actions::Action::Paste {
1886                chars: paste_action.chars,
1887                pane_id: paste_action.pane_id.map(|p| p.try_into()).transpose()?,
1888            }),
1889            ActionType::SwitchToMode(switch_mode_action) => {
1890                Ok(crate::input::actions::Action::SwitchToMode {
1891                    input_mode: proto_i32_to_input_mode(switch_mode_action.input_mode)?,
1892                })
1893            },
1894            ActionType::SwitchModeForAllClients(switch_mode_action) => {
1895                Ok(crate::input::actions::Action::SwitchModeForAllClients {
1896                    input_mode: proto_i32_to_input_mode(switch_mode_action.input_mode)?,
1897                })
1898            },
1899            ActionType::Resize(resize_action) => Ok(crate::input::actions::Action::Resize {
1900                resize: proto_i32_to_resize(resize_action.resize)?,
1901                direction: resize_action
1902                    .direction
1903                    .map(|d| proto_i32_to_direction(d))
1904                    .transpose()?,
1905            }),
1906            ActionType::FocusNextPane(_) => Ok(crate::input::actions::Action::FocusNextPane),
1907            ActionType::FocusPreviousPane(_) => {
1908                Ok(crate::input::actions::Action::FocusPreviousPane)
1909            },
1910            ActionType::SwitchFocus(_) => Ok(crate::input::actions::Action::SwitchFocus),
1911            ActionType::MoveFocus(move_focus_action) => {
1912                Ok(crate::input::actions::Action::MoveFocus {
1913                    direction: proto_i32_to_direction(move_focus_action.direction)?,
1914                })
1915            },
1916            ActionType::MoveFocusOrTab(move_focus_action) => {
1917                Ok(crate::input::actions::Action::MoveFocusOrTab {
1918                    direction: proto_i32_to_direction(move_focus_action.direction)?,
1919                })
1920            },
1921            ActionType::MovePane(move_pane_action) => Ok(crate::input::actions::Action::MovePane {
1922                direction: move_pane_action
1923                    .direction
1924                    .map(|d| proto_i32_to_direction(d))
1925                    .transpose()?,
1926            }),
1927            ActionType::MovePaneBackwards(_) => {
1928                Ok(crate::input::actions::Action::MovePaneBackwards)
1929            },
1930            ActionType::ClearScreen(_) => Ok(crate::input::actions::Action::ClearScreen),
1931            ActionType::DumpScreen(dump_screen_action) => {
1932                let file_path = if dump_screen_action.dump_to_stdout {
1933                    None
1934                } else {
1935                    Some(dump_screen_action.file_path)
1936                };
1937                Ok(crate::input::actions::Action::DumpScreen {
1938                    file_path,
1939                    include_scrollback: dump_screen_action.include_scrollback,
1940                    pane_id: dump_screen_action.pane_id.and_then(|p| p.try_into().ok()),
1941                    ansi: dump_screen_action.ansi,
1942                })
1943            },
1944            ActionType::DumpLayout(_) => Ok(crate::input::actions::Action::DumpLayout),
1945            ActionType::SaveSession(_) => Ok(crate::input::actions::Action::SaveSession),
1946            ActionType::EditScrollback(edit_scrollback_action) => {
1947                Ok(crate::input::actions::Action::EditScrollback {
1948                    ansi: edit_scrollback_action.ansi,
1949                })
1950            },
1951            ActionType::ScrollUp(_) => Ok(crate::input::actions::Action::ScrollUp),
1952            ActionType::ScrollUpAt(scroll_action) => {
1953                Ok(crate::input::actions::Action::ScrollUpAt {
1954                    position: scroll_action
1955                        .position
1956                        .ok_or_else(|| anyhow!("ScrollUpAt missing position"))?
1957                        .try_into()?,
1958                })
1959            },
1960            ActionType::ScrollDown(_) => Ok(crate::input::actions::Action::ScrollDown),
1961            ActionType::ScrollDownAt(scroll_action) => {
1962                Ok(crate::input::actions::Action::ScrollDownAt {
1963                    position: scroll_action
1964                        .position
1965                        .ok_or_else(|| anyhow!("ScrollDownAt missing position"))?
1966                        .try_into()?,
1967                })
1968            },
1969            ActionType::ScrollToBottom(_) => Ok(crate::input::actions::Action::ScrollToBottom),
1970            ActionType::ScrollToTop(_) => Ok(crate::input::actions::Action::ScrollToTop),
1971            ActionType::PageScrollUp(_) => Ok(crate::input::actions::Action::PageScrollUp),
1972            ActionType::PageScrollDown(_) => Ok(crate::input::actions::Action::PageScrollDown),
1973            ActionType::HalfPageScrollUp(_) => Ok(crate::input::actions::Action::HalfPageScrollUp),
1974            ActionType::HalfPageScrollDown(_) => {
1975                Ok(crate::input::actions::Action::HalfPageScrollDown)
1976            },
1977            ActionType::ToggleFocusFullscreen(_) => {
1978                Ok(crate::input::actions::Action::ToggleFocusFullscreen)
1979            },
1980            ActionType::TogglePaneFrames(_) => Ok(crate::input::actions::Action::TogglePaneFrames),
1981            ActionType::ToggleActiveSyncTab(_) => {
1982                Ok(crate::input::actions::Action::ToggleActiveSyncTab)
1983            },
1984            ActionType::NewPane(new_pane_action) => Ok(crate::input::actions::Action::NewPane {
1985                direction: new_pane_action
1986                    .direction
1987                    .map(|d| proto_i32_to_direction(d))
1988                    .transpose()?,
1989                pane_name: new_pane_action.pane_name,
1990                start_suppressed: new_pane_action.start_suppressed,
1991            }),
1992            ActionType::EditFile(edit_file_action) => Ok(crate::input::actions::Action::EditFile {
1993                payload: edit_file_action
1994                    .payload
1995                    .ok_or_else(|| anyhow!("EditFile missing payload"))?
1996                    .try_into()?,
1997                direction: edit_file_action
1998                    .direction
1999                    .map(|d| proto_i32_to_direction(d))
2000                    .transpose()?,
2001                floating: edit_file_action.floating,
2002                in_place: edit_file_action.in_place,
2003                close_replaced_pane: edit_file_action.close_replaced_pane,
2004                start_suppressed: edit_file_action.start_suppressed,
2005                coordinates: edit_file_action
2006                    .coordinates
2007                    .map(|c| c.try_into())
2008                    .transpose()?,
2009                near_current_pane: edit_file_action.near_current_pane,
2010                tab_id: edit_file_action.tab_id.map(|t| t as usize),
2011            }),
2012            ActionType::NewFloatingPane(new_floating_action) => {
2013                Ok(crate::input::actions::Action::NewFloatingPane {
2014                    command: new_floating_action
2015                        .command
2016                        .map(|c| c.try_into())
2017                        .transpose()?,
2018                    pane_name: new_floating_action.pane_name,
2019                    coordinates: new_floating_action
2020                        .coordinates
2021                        .map(|c| c.try_into())
2022                        .transpose()?,
2023                    near_current_pane: new_floating_action.near_current_pane,
2024                    tab_id: new_floating_action.tab_id.map(|t| t as usize),
2025                })
2026            },
2027            ActionType::NewTiledPane(new_tiled_action) => {
2028                Ok(crate::input::actions::Action::NewTiledPane {
2029                    direction: new_tiled_action
2030                        .direction
2031                        .map(|d| proto_i32_to_direction(d))
2032                        .transpose()?,
2033                    command: new_tiled_action.command.map(|c| c.try_into()).transpose()?,
2034                    pane_name: new_tiled_action.pane_name,
2035                    near_current_pane: new_tiled_action.near_current_pane,
2036                    borderless: new_tiled_action.borderless,
2037                    tab_id: new_tiled_action.tab_id.map(|t| t as usize),
2038                })
2039            },
2040            ActionType::NewInPlacePane(new_in_place_action) => {
2041                Ok(crate::input::actions::Action::NewInPlacePane {
2042                    command: new_in_place_action
2043                        .command
2044                        .map(|c| c.try_into())
2045                        .transpose()?,
2046                    pane_name: new_in_place_action.pane_name,
2047                    near_current_pane: new_in_place_action.near_current_pane,
2048                    pane_id_to_replace: new_in_place_action
2049                        .pane_id_to_replace
2050                        .and_then(|p| p.try_into().ok()),
2051                    close_replaced_pane: new_in_place_action.close_replaced_pane,
2052                    tab_id: new_in_place_action.tab_id.map(|t| t as usize),
2053                })
2054            },
2055            ActionType::NewStackedPane(new_stacked_action) => {
2056                Ok(crate::input::actions::Action::NewStackedPane {
2057                    command: new_stacked_action
2058                        .command
2059                        .map(|c| c.try_into())
2060                        .transpose()?,
2061                    pane_name: new_stacked_action.pane_name,
2062                    near_current_pane: new_stacked_action.near_current_pane,
2063                    tab_id: new_stacked_action.tab_id.map(|t| t as usize),
2064                })
2065            },
2066            ActionType::NewBlockingPane(new_blocking_action) => {
2067                Ok(crate::input::actions::Action::NewBlockingPane {
2068                    placement: new_blocking_action
2069                        .placement
2070                        .ok_or_else(|| anyhow!("NewBlockingPane missing placement"))?
2071                        .try_into()?,
2072                    pane_name: new_blocking_action.pane_name,
2073                    command: new_blocking_action
2074                        .command
2075                        .map(|c| c.try_into())
2076                        .transpose()?,
2077                    unblock_condition: new_blocking_action
2078                        .unblock_condition
2079                        .map(|c| proto_i32_to_unblock_condition(c))
2080                        .transpose()?,
2081                    near_current_pane: new_blocking_action.near_current_pane,
2082                    tab_id: new_blocking_action.tab_id.map(|t| t as usize),
2083                })
2084            },
2085            ActionType::TogglePaneEmbedOrFloating(_) => {
2086                Ok(crate::input::actions::Action::TogglePaneEmbedOrFloating)
2087            },
2088            ActionType::ToggleFloatingPanes(_) => {
2089                Ok(crate::input::actions::Action::ToggleFloatingPanes)
2090            },
2091            ActionType::ShowFloatingPanes(a) => {
2092                Ok(crate::input::actions::Action::ShowFloatingPanes {
2093                    tab_id: a.tab_id.map(|id| id as usize),
2094                })
2095            },
2096            ActionType::HideFloatingPanes(a) => {
2097                Ok(crate::input::actions::Action::HideFloatingPanes {
2098                    tab_id: a.tab_id.map(|id| id as usize),
2099                })
2100            },
2101            ActionType::AreFloatingPanesVisible(a) => {
2102                Ok(crate::input::actions::Action::AreFloatingPanesVisible {
2103                    tab_id: a.tab_id.map(|id| id as usize),
2104                })
2105            },
2106            ActionType::CloseFocus(_) => Ok(crate::input::actions::Action::CloseFocus),
2107            ActionType::PaneNameInput(pane_name_action) => {
2108                Ok(crate::input::actions::Action::PaneNameInput {
2109                    input: pane_name_action
2110                        .input
2111                        .into_iter()
2112                        .map(|b| b as u8)
2113                        .collect(),
2114                })
2115            },
2116            ActionType::UndoRenamePane(_) => Ok(crate::input::actions::Action::UndoRenamePane),
2117            ActionType::NewTab(new_tab_action) => Ok(crate::input::actions::Action::NewTab {
2118                tiled_layout: new_tab_action
2119                    .tiled_layout
2120                    .map(|l| l.try_into())
2121                    .transpose()?,
2122                floating_layouts: new_tab_action
2123                    .floating_layouts
2124                    .into_iter()
2125                    .map(|l| l.try_into())
2126                    .collect::<Result<Vec<_>>>()?,
2127                swap_tiled_layouts: if new_tab_action.swap_tiled_layouts.is_empty() {
2128                    None
2129                } else {
2130                    Some(
2131                        new_tab_action
2132                            .swap_tiled_layouts
2133                            .into_iter()
2134                            .map(|l| l.try_into())
2135                            .collect::<Result<Vec<_>>>()?,
2136                    )
2137                },
2138                swap_floating_layouts: if new_tab_action.swap_floating_layouts.is_empty() {
2139                    None
2140                } else {
2141                    Some(
2142                        new_tab_action
2143                            .swap_floating_layouts
2144                            .into_iter()
2145                            .map(|l| l.try_into())
2146                            .collect::<Result<Vec<_>>>()?,
2147                    )
2148                },
2149                tab_name: new_tab_action.tab_name,
2150                should_change_focus_to_new_tab: new_tab_action.should_change_focus_to_new_tab,
2151                cwd: new_tab_action.cwd.map(PathBuf::from),
2152                initial_panes: if new_tab_action.initial_panes.is_empty() {
2153                    None
2154                } else {
2155                    Some(
2156                        new_tab_action
2157                            .initial_panes
2158                            .into_iter()
2159                            .map(|p| p.try_into())
2160                            .collect::<Result<Vec<_>>>()?,
2161                    )
2162                },
2163
2164                first_pane_unblock_condition: new_tab_action
2165                    .first_pane_unblock_condition
2166                    .map(|c| proto_i32_to_unblock_condition(c))
2167                    .transpose()?,
2168            }),
2169            ActionType::NoOp(_) => Ok(crate::input::actions::Action::NoOp),
2170            ActionType::GoToNextTab(_) => Ok(crate::input::actions::Action::GoToNextTab),
2171            ActionType::GoToPreviousTab(_) => Ok(crate::input::actions::Action::GoToPreviousTab),
2172            ActionType::CloseTab(_) => Ok(crate::input::actions::Action::CloseTab),
2173            ActionType::GoToTab(go_to_tab_action) => Ok(crate::input::actions::Action::GoToTab {
2174                index: go_to_tab_action.index,
2175            }),
2176            ActionType::GoToTabName(go_to_tab_name_action) => {
2177                Ok(crate::input::actions::Action::GoToTabName {
2178                    name: go_to_tab_name_action.name,
2179                    create: go_to_tab_name_action.create,
2180                })
2181            },
2182            ActionType::ToggleTab(_) => Ok(crate::input::actions::Action::ToggleTab),
2183            ActionType::TabNameInput(tab_name_action) => {
2184                Ok(crate::input::actions::Action::TabNameInput {
2185                    input: tab_name_action.input.into_iter().map(|b| b as u8).collect(),
2186                })
2187            },
2188            ActionType::UndoRenameTab(_) => Ok(crate::input::actions::Action::UndoRenameTab),
2189            ActionType::MoveTab(move_tab_action) => Ok(crate::input::actions::Action::MoveTab {
2190                direction: proto_i32_to_direction(move_tab_action.direction)?,
2191            }),
2192            ActionType::Run(run_action) => Ok(crate::input::actions::Action::Run {
2193                command: run_action
2194                    .command
2195                    .ok_or_else(|| anyhow!("Run missing command"))?
2196                    .try_into()?,
2197                near_current_pane: run_action.near_current_pane,
2198            }),
2199            ActionType::Detach(_) => Ok(crate::input::actions::Action::Detach),
2200            ActionType::SetDarkTheme(_) => Ok(crate::input::actions::Action::SetDarkTheme),
2201            ActionType::SetLightTheme(_) => Ok(crate::input::actions::Action::SetLightTheme),
2202            ActionType::ToggleTheme(_) => Ok(crate::input::actions::Action::ToggleTheme),
2203            ActionType::SwitchSession(switch_session_action) => {
2204                Ok(crate::input::actions::Action::SwitchSession {
2205                    name: switch_session_action.name.clone(),
2206                    tab_position: switch_session_action.tab_position.map(|p| p as usize),
2207                    pane_id: switch_session_action
2208                        .pane_id
2209                        .as_ref()
2210                        .map(|p| (p.pane_id, p.is_plugin)),
2211                    layout: switch_session_action
2212                        .layout
2213                        .map(|l| l.try_into())
2214                        .transpose()?,
2215                    cwd: switch_session_action.cwd.map(PathBuf::from),
2216                })
2217            },
2218            ActionType::LaunchOrFocusPlugin(launch_plugin_action) => {
2219                Ok(crate::input::actions::Action::LaunchOrFocusPlugin {
2220                    plugin: launch_plugin_action
2221                        .plugin
2222                        .ok_or_else(|| anyhow!("LaunchOrFocusPlugin missing plugin"))?
2223                        .try_into()?,
2224                    should_float: launch_plugin_action.should_float,
2225                    move_to_focused_tab: launch_plugin_action.move_to_focused_tab,
2226                    should_open_in_place: launch_plugin_action.should_open_in_place,
2227                    close_replaced_pane: launch_plugin_action.close_replaced_pane,
2228                    skip_cache: launch_plugin_action.skip_cache,
2229                    tab_id: launch_plugin_action.tab_id.map(|t| t as usize),
2230                })
2231            },
2232            ActionType::LaunchPlugin(launch_plugin_action) => {
2233                Ok(crate::input::actions::Action::LaunchPlugin {
2234                    plugin: launch_plugin_action
2235                        .plugin
2236                        .ok_or_else(|| anyhow!("LaunchPlugin missing plugin"))?
2237                        .try_into()?,
2238                    should_float: launch_plugin_action.should_float,
2239                    should_open_in_place: launch_plugin_action.should_open_in_place,
2240                    close_replaced_pane: launch_plugin_action.close_replaced_pane,
2241                    skip_cache: launch_plugin_action.skip_cache,
2242                    cwd: launch_plugin_action.cwd.map(PathBuf::from),
2243                    tab_id: launch_plugin_action.tab_id.map(|t| t as usize),
2244                })
2245            },
2246            ActionType::MouseEvent(mouse_event_action) => {
2247                Ok(crate::input::actions::Action::MouseEvent {
2248                    event: mouse_event_action
2249                        .event
2250                        .ok_or_else(|| anyhow!("MouseEvent missing event"))?
2251                        .try_into()?,
2252                })
2253            },
2254            ActionType::Copy(_) => Ok(crate::input::actions::Action::Copy),
2255            ActionType::Confirm(_) => Ok(crate::input::actions::Action::Confirm),
2256            ActionType::Deny(_) => Ok(crate::input::actions::Action::Deny),
2257            ActionType::SkipConfirm(skip_confirm_action) => {
2258                Ok(crate::input::actions::Action::SkipConfirm {
2259                    action: Box::new(
2260                        skip_confirm_action
2261                            .action
2262                            .ok_or_else(|| anyhow!("SkipConfirm missing action"))?
2263                            .as_ref()
2264                            .clone()
2265                            .try_into()?,
2266                    ),
2267                })
2268            },
2269            ActionType::SearchInput(search_input_action) => {
2270                Ok(crate::input::actions::Action::SearchInput {
2271                    input: search_input_action
2272                        .input
2273                        .into_iter()
2274                        .map(|b| b as u8)
2275                        .collect(),
2276                })
2277            },
2278            ActionType::Search(search_action) => Ok(crate::input::actions::Action::Search {
2279                direction: proto_i32_to_search_direction(search_action.direction)?,
2280            }),
2281            ActionType::SearchToggleOption(search_toggle_action) => {
2282                Ok(crate::input::actions::Action::SearchToggleOption {
2283                    option: proto_i32_to_search_option(search_toggle_action.option)?,
2284                })
2285            },
2286            ActionType::ToggleMouseMode(_) => Ok(crate::input::actions::Action::ToggleMouseMode),
2287            ActionType::PreviousSwapLayout(_) => {
2288                Ok(crate::input::actions::Action::PreviousSwapLayout)
2289            },
2290            ActionType::NextSwapLayout(_) => Ok(crate::input::actions::Action::NextSwapLayout),
2291            ActionType::OverrideLayout(override_layout_action) => {
2292                Ok(crate::input::actions::Action::OverrideLayout {
2293                    tabs: override_layout_action
2294                        .tabs
2295                        .into_iter()
2296                        .map(|t| t.try_into())
2297                        .collect::<Result<Vec<_>>>()?,
2298                    retain_existing_terminal_panes: override_layout_action
2299                        .retain_existing_terminal_panes,
2300                    retain_existing_plugin_panes: override_layout_action
2301                        .retain_existing_plugin_panes,
2302                    apply_only_to_active_tab: override_layout_action.apply_only_to_active_tab,
2303                })
2304            },
2305            ActionType::QueryTabNames(_) => Ok(crate::input::actions::Action::QueryTabNames),
2306            ActionType::NewTiledPluginPane(new_tiled_plugin_action) => {
2307                Ok(crate::input::actions::Action::NewTiledPluginPane {
2308                    plugin: new_tiled_plugin_action
2309                        .plugin
2310                        .ok_or_else(|| anyhow!("NewTiledPluginPane missing plugin"))?
2311                        .try_into()?,
2312                    pane_name: new_tiled_plugin_action.pane_name,
2313                    skip_cache: new_tiled_plugin_action.skip_cache,
2314                    cwd: new_tiled_plugin_action.cwd.map(PathBuf::from),
2315                    tab_id: new_tiled_plugin_action.tab_id.map(|t| t as usize),
2316                })
2317            },
2318            ActionType::NewFloatingPluginPane(new_floating_plugin_action) => {
2319                Ok(crate::input::actions::Action::NewFloatingPluginPane {
2320                    plugin: new_floating_plugin_action
2321                        .plugin
2322                        .ok_or_else(|| anyhow!("NewFloatingPluginPane missing plugin"))?
2323                        .try_into()?,
2324                    pane_name: new_floating_plugin_action.pane_name,
2325                    skip_cache: new_floating_plugin_action.skip_cache,
2326                    cwd: new_floating_plugin_action.cwd.map(PathBuf::from),
2327                    coordinates: new_floating_plugin_action
2328                        .coordinates
2329                        .map(|c| c.try_into())
2330                        .transpose()?,
2331                    tab_id: new_floating_plugin_action.tab_id.map(|t| t as usize),
2332                })
2333            },
2334            ActionType::NewInPlacePluginPane(new_in_place_plugin_action) => {
2335                Ok(crate::input::actions::Action::NewInPlacePluginPane {
2336                    plugin: new_in_place_plugin_action
2337                        .plugin
2338                        .ok_or_else(|| anyhow!("NewInPlacePluginPane missing plugin"))?
2339                        .try_into()?,
2340                    pane_name: new_in_place_plugin_action.pane_name,
2341                    skip_cache: new_in_place_plugin_action.skip_cache,
2342                    close_replaced_pane: new_in_place_plugin_action.close_replaced_pane,
2343                    tab_id: new_in_place_plugin_action.tab_id.map(|t| t as usize),
2344                })
2345            },
2346            ActionType::StartOrReloadPlugin(start_plugin_action) => {
2347                Ok(crate::input::actions::Action::StartOrReloadPlugin {
2348                    plugin: start_plugin_action
2349                        .plugin
2350                        .ok_or_else(|| anyhow!("StartOrReloadPlugin missing plugin"))?
2351                        .try_into()?,
2352                })
2353            },
2354            ActionType::CloseTerminalPane(close_pane_action) => {
2355                Ok(crate::input::actions::Action::CloseTerminalPane {
2356                    pane_id: close_pane_action.pane_id,
2357                })
2358            },
2359            ActionType::ClosePluginPane(close_pane_action) => {
2360                Ok(crate::input::actions::Action::ClosePluginPane {
2361                    pane_id: close_pane_action.pane_id,
2362                })
2363            },
2364            ActionType::FocusTerminalPaneWithId(focus_pane_action) => {
2365                Ok(crate::input::actions::Action::FocusTerminalPaneWithId {
2366                    pane_id: focus_pane_action.pane_id,
2367                    should_float_if_hidden: focus_pane_action.should_float_if_hidden,
2368                    should_be_in_place_if_hidden: focus_pane_action.should_be_in_place_if_hidden,
2369                })
2370            },
2371            ActionType::FocusPluginPaneWithId(focus_pane_action) => {
2372                Ok(crate::input::actions::Action::FocusPluginPaneWithId {
2373                    pane_id: focus_pane_action.pane_id,
2374                    should_float_if_hidden: focus_pane_action.should_float_if_hidden,
2375                    should_be_in_place_if_hidden: focus_pane_action.should_be_in_place_if_hidden,
2376                })
2377            },
2378            ActionType::RenameTerminalPane(rename_pane_action) => {
2379                Ok(crate::input::actions::Action::RenameTerminalPane {
2380                    pane_id: rename_pane_action.pane_id,
2381                    name: rename_pane_action
2382                        .name
2383                        .into_iter()
2384                        .map(|b| b as u8)
2385                        .collect(),
2386                })
2387            },
2388            ActionType::RenamePluginPane(rename_pane_action) => {
2389                Ok(crate::input::actions::Action::RenamePluginPane {
2390                    pane_id: rename_pane_action.pane_id,
2391                    name: rename_pane_action
2392                        .name
2393                        .into_iter()
2394                        .map(|b| b as u8)
2395                        .collect(),
2396                })
2397            },
2398            ActionType::RenameTab(rename_tab_action) => {
2399                Ok(crate::input::actions::Action::RenameTab {
2400                    tab_index: rename_tab_action.tab_index,
2401                    name: rename_tab_action
2402                        .name
2403                        .into_iter()
2404                        .map(|b| b as u8)
2405                        .collect(),
2406                })
2407            },
2408            ActionType::GoToTabById(go_to_tab_by_id_action) => {
2409                Ok(crate::input::actions::Action::GoToTabById {
2410                    id: go_to_tab_by_id_action.id,
2411                })
2412            },
2413            ActionType::CloseTabById(close_tab_by_id_action) => {
2414                Ok(crate::input::actions::Action::CloseTabById {
2415                    id: close_tab_by_id_action.id,
2416                })
2417            },
2418            ActionType::RenameTabById(rename_tab_by_id_action) => {
2419                Ok(crate::input::actions::Action::RenameTabById {
2420                    id: rename_tab_by_id_action.id,
2421                    name: rename_tab_by_id_action.name,
2422                })
2423            },
2424            ActionType::BreakPane(_) => Ok(crate::input::actions::Action::BreakPane),
2425            ActionType::BreakPaneRight(_) => Ok(crate::input::actions::Action::BreakPaneRight),
2426            ActionType::BreakPaneLeft(_) => Ok(crate::input::actions::Action::BreakPaneLeft),
2427            ActionType::RenameSession(rename_session_action) => {
2428                Ok(crate::input::actions::Action::RenameSession {
2429                    name: rename_session_action.name,
2430                })
2431            },
2432            ActionType::CliPipe(cli_pipe_action) => Ok(crate::input::actions::Action::CliPipe {
2433                pipe_id: cli_pipe_action.pipe_id,
2434                name: cli_pipe_action.name,
2435                payload: cli_pipe_action.payload,
2436                args: if cli_pipe_action.args.is_empty() {
2437                    None
2438                } else {
2439                    Some(cli_pipe_action.args.into_iter().collect())
2440                },
2441                plugin: cli_pipe_action.plugin,
2442                configuration: if cli_pipe_action.configuration.is_empty() {
2443                    None
2444                } else {
2445                    Some(cli_pipe_action.configuration.into_iter().collect())
2446                },
2447                launch_new: cli_pipe_action.launch_new,
2448                skip_cache: cli_pipe_action.skip_cache,
2449                floating: cli_pipe_action.floating,
2450                in_place: cli_pipe_action.in_place,
2451                cwd: cli_pipe_action.cwd.map(PathBuf::from),
2452                pane_title: cli_pipe_action.pane_title,
2453            }),
2454            ActionType::KeybindPipe(keybind_pipe_action) => {
2455                Ok(crate::input::actions::Action::KeybindPipe {
2456                    name: keybind_pipe_action.name,
2457                    payload: keybind_pipe_action.payload,
2458                    args: if keybind_pipe_action.args.is_empty() {
2459                        None
2460                    } else {
2461                        Some(keybind_pipe_action.args.into_iter().collect())
2462                    },
2463                    plugin: keybind_pipe_action.plugin,
2464                    plugin_id: keybind_pipe_action.plugin_id,
2465                    configuration: if keybind_pipe_action.configuration.is_empty() {
2466                        None
2467                    } else {
2468                        Some(keybind_pipe_action.configuration.into_iter().collect())
2469                    },
2470                    launch_new: keybind_pipe_action.launch_new,
2471                    skip_cache: keybind_pipe_action.skip_cache,
2472                    floating: keybind_pipe_action.floating,
2473                    in_place: keybind_pipe_action.in_place,
2474                    cwd: keybind_pipe_action.cwd.map(PathBuf::from),
2475                    pane_title: keybind_pipe_action.pane_title,
2476                })
2477            },
2478            ActionType::ListClients(_) => Ok(crate::input::actions::Action::ListClients),
2479            ActionType::ListPanes(list_panes_action) => {
2480                Ok(crate::input::actions::Action::ListPanes {
2481                    show_tab: list_panes_action.show_tab,
2482                    show_command: list_panes_action.show_command,
2483                    show_state: list_panes_action.show_state,
2484                    show_geometry: list_panes_action.show_geometry,
2485                    show_all: list_panes_action.show_all,
2486                    output_json: list_panes_action.output_json,
2487                })
2488            },
2489            ActionType::ListTabs(list_tabs_action) => Ok(crate::input::actions::Action::ListTabs {
2490                show_state: list_tabs_action.show_state,
2491                show_dimensions: list_tabs_action.show_dimensions,
2492                show_panes: list_tabs_action.show_panes,
2493                show_layout: list_tabs_action.show_layout,
2494                show_all: list_tabs_action.show_all,
2495                output_json: list_tabs_action.output_json,
2496            }),
2497            ActionType::CurrentTabInfo(current_tab_info_action) => {
2498                Ok(crate::input::actions::Action::CurrentTabInfo {
2499                    output_json: current_tab_info_action.output_json,
2500                })
2501            },
2502            ActionType::TogglePanePinned(_) => Ok(crate::input::actions::Action::TogglePanePinned),
2503            ActionType::StackPanes(stack_panes_action) => {
2504                Ok(crate::input::actions::Action::StackPanes {
2505                    pane_ids: stack_panes_action
2506                        .pane_ids
2507                        .into_iter()
2508                        .map(|id| id.try_into())
2509                        .collect::<Result<Vec<_>>>()?,
2510                })
2511            },
2512            ActionType::ChangeFloatingPaneCoordinates(change_coords_action) => Ok(
2513                crate::input::actions::Action::ChangeFloatingPaneCoordinates {
2514                    pane_id: change_coords_action
2515                        .pane_id
2516                        .ok_or_else(|| anyhow!("ChangeFloatingPaneCoordinates missing pane_id"))?
2517                        .try_into()?,
2518                    coordinates: change_coords_action
2519                        .coordinates
2520                        .ok_or_else(|| {
2521                            anyhow!("ChangeFloatingPaneCoordinates missing coordinates")
2522                        })?
2523                        .try_into()?,
2524                },
2525            ),
2526            ActionType::TogglePaneBorderless(toggle_borderless_action) => {
2527                Ok(crate::input::actions::Action::TogglePaneBorderless {
2528                    pane_id: toggle_borderless_action
2529                        .pane_id
2530                        .ok_or_else(|| anyhow!("TogglePaneBorderless missing pane_id"))?
2531                        .try_into()?,
2532                })
2533            },
2534            ActionType::SetPaneBorderless(set_borderless_action) => {
2535                Ok(crate::input::actions::Action::SetPaneBorderless {
2536                    pane_id: set_borderless_action
2537                        .pane_id
2538                        .ok_or_else(|| anyhow!("SetPaneBorderless missing pane_id"))?
2539                        .try_into()?,
2540                    borderless: set_borderless_action.borderless,
2541                })
2542            },
2543            ActionType::TogglePaneInGroup(_) => {
2544                Ok(crate::input::actions::Action::TogglePaneInGroup)
2545            },
2546            ActionType::ToggleGroupMarking(_) => {
2547                Ok(crate::input::actions::Action::ToggleGroupMarking)
2548            },
2549            ActionType::SetPaneColor(set_pane_color_action) => {
2550                Ok(crate::input::actions::Action::SetPaneColor {
2551                    pane_id: set_pane_color_action
2552                        .pane_id
2553                        .ok_or_else(|| anyhow!("SetPaneColor missing pane_id"))?
2554                        .try_into()?,
2555                    fg: set_pane_color_action.fg,
2556                    bg: set_pane_color_action.bg,
2557                })
2558            },
2559            // Pane-targeting CLI-only variants
2560            ActionType::ScrollUpByPaneId(a) => {
2561                Ok(crate::input::actions::Action::ScrollUpByPaneId {
2562                    pane_id: a
2563                        .pane_id
2564                        .ok_or_else(|| anyhow!("ScrollUpByPaneId missing pane_id"))?
2565                        .try_into()?,
2566                })
2567            },
2568            ActionType::ScrollDownByPaneId(a) => {
2569                Ok(crate::input::actions::Action::ScrollDownByPaneId {
2570                    pane_id: a
2571                        .pane_id
2572                        .ok_or_else(|| anyhow!("ScrollDownByPaneId missing pane_id"))?
2573                        .try_into()?,
2574                })
2575            },
2576            ActionType::ScrollToTopByPaneId(a) => {
2577                Ok(crate::input::actions::Action::ScrollToTopByPaneId {
2578                    pane_id: a
2579                        .pane_id
2580                        .ok_or_else(|| anyhow!("ScrollToTopByPaneId missing pane_id"))?
2581                        .try_into()?,
2582                })
2583            },
2584            ActionType::ScrollToBottomByPaneId(a) => {
2585                Ok(crate::input::actions::Action::ScrollToBottomByPaneId {
2586                    pane_id: a
2587                        .pane_id
2588                        .ok_or_else(|| anyhow!("ScrollToBottomByPaneId missing pane_id"))?
2589                        .try_into()?,
2590                })
2591            },
2592            ActionType::PageScrollUpByPaneId(a) => {
2593                Ok(crate::input::actions::Action::PageScrollUpByPaneId {
2594                    pane_id: a
2595                        .pane_id
2596                        .ok_or_else(|| anyhow!("PageScrollUpByPaneId missing pane_id"))?
2597                        .try_into()?,
2598                })
2599            },
2600            ActionType::PageScrollDownByPaneId(a) => {
2601                Ok(crate::input::actions::Action::PageScrollDownByPaneId {
2602                    pane_id: a
2603                        .pane_id
2604                        .ok_or_else(|| anyhow!("PageScrollDownByPaneId missing pane_id"))?
2605                        .try_into()?,
2606                })
2607            },
2608            ActionType::HalfPageScrollUpByPaneId(a) => {
2609                Ok(crate::input::actions::Action::HalfPageScrollUpByPaneId {
2610                    pane_id: a
2611                        .pane_id
2612                        .ok_or_else(|| anyhow!("HalfPageScrollUpByPaneId missing pane_id"))?
2613                        .try_into()?,
2614                })
2615            },
2616            ActionType::HalfPageScrollDownByPaneId(a) => {
2617                Ok(crate::input::actions::Action::HalfPageScrollDownByPaneId {
2618                    pane_id: a
2619                        .pane_id
2620                        .ok_or_else(|| anyhow!("HalfPageScrollDownByPaneId missing pane_id"))?
2621                        .try_into()?,
2622                })
2623            },
2624            ActionType::ResizeByPaneId(a) => {
2625                let resize_action = a
2626                    .resize_action
2627                    .ok_or_else(|| anyhow!("ResizeByPaneId missing resize_action"))?;
2628                let resize = proto_i32_to_resize(resize_action.resize)?;
2629                let direction = resize_action
2630                    .direction
2631                    .map(|d| proto_i32_to_direction(d))
2632                    .transpose()?;
2633                Ok(crate::input::actions::Action::ResizeByPaneId {
2634                    pane_id: a
2635                        .pane_id
2636                        .ok_or_else(|| anyhow!("ResizeByPaneId missing pane_id"))?
2637                        .try_into()?,
2638                    resize,
2639                    direction,
2640                })
2641            },
2642            ActionType::MovePaneByPaneId(a) => {
2643                let direction = a.direction.map(|d| proto_i32_to_direction(d)).transpose()?;
2644                Ok(crate::input::actions::Action::MovePaneByPaneId {
2645                    pane_id: a
2646                        .pane_id
2647                        .ok_or_else(|| anyhow!("MovePaneByPaneId missing pane_id"))?
2648                        .try_into()?,
2649                    direction,
2650                })
2651            },
2652            ActionType::MovePaneBackwardsByPaneId(a) => {
2653                Ok(crate::input::actions::Action::MovePaneBackwardsByPaneId {
2654                    pane_id: a
2655                        .pane_id
2656                        .ok_or_else(|| anyhow!("MovePaneBackwardsByPaneId missing pane_id"))?
2657                        .try_into()?,
2658                })
2659            },
2660            ActionType::ClearScreenByPaneId(a) => {
2661                Ok(crate::input::actions::Action::ClearScreenByPaneId {
2662                    pane_id: a
2663                        .pane_id
2664                        .ok_or_else(|| anyhow!("ClearScreenByPaneId missing pane_id"))?
2665                        .try_into()?,
2666                })
2667            },
2668            ActionType::EditScrollbackByPaneId(a) => {
2669                Ok(crate::input::actions::Action::EditScrollbackByPaneId {
2670                    pane_id: a
2671                        .pane_id
2672                        .ok_or_else(|| anyhow!("EditScrollbackByPaneId missing pane_id"))?
2673                        .try_into()?,
2674                    ansi: a.ansi,
2675                })
2676            },
2677            ActionType::ToggleFullscreenByPaneId(a) => Ok(
2678                crate::input::actions::Action::ToggleFocusFullscreenByPaneId {
2679                    pane_id: a
2680                        .pane_id
2681                        .ok_or_else(|| anyhow!("ToggleFullscreenByPaneId missing pane_id"))?
2682                        .try_into()?,
2683                },
2684            ),
2685            ActionType::TogglePaneEmbedOrFloatingByPaneId(a) => Ok(
2686                crate::input::actions::Action::TogglePaneEmbedOrFloatingByPaneId {
2687                    pane_id: a
2688                        .pane_id
2689                        .ok_or_else(|| {
2690                            anyhow!("TogglePaneEmbedOrFloatingByPaneId missing pane_id")
2691                        })?
2692                        .try_into()?,
2693                },
2694            ),
2695            ActionType::CloseFocusByPaneId(a) => {
2696                Ok(crate::input::actions::Action::CloseFocusByPaneId {
2697                    pane_id: a
2698                        .pane_id
2699                        .ok_or_else(|| anyhow!("CloseFocusByPaneId missing pane_id"))?
2700                        .try_into()?,
2701                })
2702            },
2703            ActionType::RenamePaneByPaneId(a) => {
2704                Ok(crate::input::actions::Action::RenamePaneByPaneId {
2705                    pane_id: a.pane_id.map(|p| p.try_into()).transpose()?,
2706                    name: a.name,
2707                })
2708            },
2709            ActionType::UndoRenamePaneByPaneId(a) => {
2710                Ok(crate::input::actions::Action::UndoRenamePaneByPaneId {
2711                    pane_id: a
2712                        .pane_id
2713                        .ok_or_else(|| anyhow!("UndoRenamePaneByPaneId missing pane_id"))?
2714                        .try_into()?,
2715                })
2716            },
2717            ActionType::TogglePanePinnedByPaneId(a) => {
2718                Ok(crate::input::actions::Action::TogglePanePinnedByPaneId {
2719                    pane_id: a
2720                        .pane_id
2721                        .ok_or_else(|| anyhow!("TogglePanePinnedByPaneId missing pane_id"))?
2722                        .try_into()?,
2723                })
2724            },
2725            ActionType::FocusPaneByPaneId(a) => {
2726                Ok(crate::input::actions::Action::FocusPaneByPaneId {
2727                    pane_id: a
2728                        .pane_id
2729                        .ok_or_else(|| anyhow!("FocusPaneByPaneId missing pane_id"))?
2730                        .try_into()?,
2731                })
2732            },
2733            // Tab-targeting CLI-only variants
2734            ActionType::UndoRenameTabByTabId(a) => {
2735                Ok(crate::input::actions::Action::UndoRenameTabByTabId { id: a.id })
2736            },
2737            ActionType::ToggleActiveSyncTabByTabId(a) => {
2738                Ok(crate::input::actions::Action::ToggleActiveSyncTabByTabId { id: a.id })
2739            },
2740            ActionType::ToggleFloatingPanesByTabId(a) => {
2741                Ok(crate::input::actions::Action::ToggleFloatingPanesByTabId { id: a.id })
2742            },
2743            ActionType::PreviousSwapLayoutByTabId(a) => {
2744                Ok(crate::input::actions::Action::PreviousSwapLayoutByTabId { id: a.id })
2745            },
2746            ActionType::NextSwapLayoutByTabId(a) => {
2747                Ok(crate::input::actions::Action::NextSwapLayoutByTabId { id: a.id })
2748            },
2749            ActionType::MoveTabByTabId(a) => {
2750                let direction = proto_i32_to_direction(a.direction)?;
2751                Ok(crate::input::actions::Action::MoveTabByTabId {
2752                    id: a.id,
2753                    direction,
2754                })
2755            },
2756        }
2757    }
2758}
2759
2760impl From<crate::data::KeyWithModifier>
2761    for crate::client_server_contract::client_server_contract::KeyWithModifier
2762{
2763    fn from(key: crate::data::KeyWithModifier) -> Self {
2764        use crate::ipc::enum_conversions::{bare_key_to_proto_i32, key_modifier_to_proto_i32};
2765
2766        // Handle character keys specially - store the character for Char variant
2767        let (bare_key_enum, char_data) = match &key.bare_key {
2768            crate::data::BareKey::Char(c) => (
2769                crate::client_server_contract::client_server_contract::BareKey::Char as i32,
2770                Some(c.to_string()),
2771            ),
2772            other => (bare_key_to_proto_i32(*other), None),
2773        };
2774
2775        Self {
2776            bare_key: bare_key_enum,
2777            key_modifiers: key
2778                .key_modifiers
2779                .into_iter()
2780                .map(|modifier| key_modifier_to_proto_i32(modifier))
2781                .collect(),
2782            character: char_data,
2783        }
2784    }
2785}
2786
2787impl TryFrom<crate::client_server_contract::client_server_contract::KeyWithModifier>
2788    for crate::data::KeyWithModifier
2789{
2790    type Error = anyhow::Error;
2791    fn try_from(
2792        key: crate::client_server_contract::client_server_contract::KeyWithModifier,
2793    ) -> Result<Self> {
2794        use crate::ipc::enum_conversions::{bare_key_from_proto_i32, key_modifier_from_proto_i32};
2795        use std::collections::BTreeSet;
2796
2797        // Handle character keys specially
2798        let bare_key = if key.bare_key
2799            == crate::client_server_contract::client_server_contract::BareKey::Char as i32
2800        {
2801            let character_str = key
2802                .character
2803                .ok_or_else(|| anyhow!("Character key missing character data"))?;
2804            let character = character_str
2805                .chars()
2806                .next()
2807                .ok_or_else(|| anyhow!("Empty character string"))?;
2808            crate::data::BareKey::Char(character)
2809        } else {
2810            bare_key_from_proto_i32(key.bare_key)?
2811        };
2812
2813        let key_modifiers: Result<BTreeSet<_>> = key
2814            .key_modifiers
2815            .into_iter()
2816            .map(|modifier| key_modifier_from_proto_i32(modifier))
2817            .collect();
2818
2819        Ok(Self {
2820            bare_key,
2821            key_modifiers: key_modifiers?,
2822        })
2823    }
2824}
2825
2826impl From<crate::data::ConnectToSession>
2827    for crate::client_server_contract::client_server_contract::ConnectToSession
2828{
2829    fn from(connect: crate::data::ConnectToSession) -> Self {
2830        Self {
2831            name: connect.name,
2832            tab_position: connect.tab_position.map(|p| p as u32),
2833            pane_id: connect.pane_id.map(|(id, is_plugin)| {
2834                crate::client_server_contract::client_server_contract::PaneIdWithPlugin {
2835                    pane_id: id,
2836                    is_plugin,
2837                }
2838            }),
2839            layout: connect.layout.map(|l| l.into()),
2840            cwd: connect.cwd.map(|p| p.to_string_lossy().to_string()),
2841        }
2842    }
2843}
2844
2845impl TryFrom<crate::client_server_contract::client_server_contract::ConnectToSession>
2846    for crate::data::ConnectToSession
2847{
2848    type Error = anyhow::Error;
2849    fn try_from(
2850        connect: crate::client_server_contract::client_server_contract::ConnectToSession,
2851    ) -> Result<Self> {
2852        Ok(Self {
2853            name: connect.name,
2854            tab_position: connect.tab_position.map(|p| p as usize),
2855            pane_id: connect.pane_id.map(|p| (p.pane_id, p.is_plugin)),
2856            layout: connect.layout.map(|l| l.try_into()).transpose()?,
2857            cwd: connect.cwd.map(PathBuf::from),
2858        })
2859    }
2860}
2861
2862impl From<crate::data::LayoutInfo>
2863    for crate::client_server_contract::client_server_contract::LayoutInfo
2864{
2865    fn from(layout: crate::data::LayoutInfo) -> Self {
2866        use crate::client_server_contract::client_server_contract::layout_info::LayoutType;
2867        let (layout_type, layout_metadata) = match layout {
2868            crate::data::LayoutInfo::BuiltIn(name) => (LayoutType::BuiltinName(name), None),
2869            crate::data::LayoutInfo::File(path, metadata) => {
2870                (LayoutType::FilePath(path), Some(metadata.into()))
2871            },
2872            crate::data::LayoutInfo::Url(url) => (LayoutType::Url(url), None),
2873            crate::data::LayoutInfo::Stringified(content) => {
2874                (LayoutType::Stringified(content), None)
2875            },
2876        };
2877        Self {
2878            layout_type: Some(layout_type),
2879            layout_metadata,
2880        }
2881    }
2882}
2883
2884impl TryFrom<crate::client_server_contract::client_server_contract::LayoutInfo>
2885    for crate::data::LayoutInfo
2886{
2887    type Error = anyhow::Error;
2888    fn try_from(
2889        layout: crate::client_server_contract::client_server_contract::LayoutInfo,
2890    ) -> Result<Self> {
2891        use crate::client_server_contract::client_server_contract::layout_info::LayoutType;
2892        match layout.layout_type {
2893            Some(LayoutType::BuiltinName(name)) => Ok(crate::data::LayoutInfo::BuiltIn(name)),
2894            Some(LayoutType::FilePath(path)) => {
2895                let layout_metadata = layout
2896                    .layout_metadata
2897                    .map(|m| m.try_into())
2898                    .transpose()?
2899                    .unwrap_or_default();
2900                Ok(crate::data::LayoutInfo::File(path, layout_metadata))
2901            },
2902            Some(LayoutType::Url(url)) => Ok(crate::data::LayoutInfo::Url(url)),
2903            Some(LayoutType::Stringified(content)) => {
2904                Ok(crate::data::LayoutInfo::Stringified(content))
2905            },
2906            None => Err(anyhow!("LayoutInfo missing layout_type")),
2907        }
2908    }
2909}
2910
2911impl From<crate::data::LayoutMetadata> for ProtoLayoutMetadata {
2912    fn from(metadata: crate::data::LayoutMetadata) -> Self {
2913        ProtoLayoutMetadata {
2914            tabs: metadata.tabs.into_iter().map(|t| t.into()).collect(),
2915            creation_time: metadata.creation_time,
2916            update_time: metadata.update_time,
2917        }
2918    }
2919}
2920
2921impl TryFrom<ProtoLayoutMetadata> for crate::data::LayoutMetadata {
2922    type Error = anyhow::Error;
2923    fn try_from(proto_metadata: ProtoLayoutMetadata) -> Result<Self> {
2924        let tabs = proto_metadata
2925            .tabs
2926            .into_iter()
2927            .map(|t| t.try_into())
2928            .collect::<Result<Vec<_>>>()?;
2929        Ok(crate::data::LayoutMetadata {
2930            tabs,
2931            creation_time: proto_metadata.creation_time,
2932            update_time: proto_metadata.update_time,
2933        })
2934    }
2935}
2936
2937impl From<crate::data::TabMetadata> for ProtoTabMetadata {
2938    fn from(metadata: crate::data::TabMetadata) -> Self {
2939        ProtoTabMetadata {
2940            pane_metadata: metadata.panes.into_iter().map(|p| p.into()).collect(),
2941            name: metadata.name,
2942        }
2943    }
2944}
2945
2946impl TryFrom<ProtoTabMetadata> for crate::data::TabMetadata {
2947    type Error = anyhow::Error;
2948    fn try_from(proto_metadata: ProtoTabMetadata) -> Result<Self> {
2949        let panes = proto_metadata
2950            .pane_metadata
2951            .into_iter()
2952            .map(|p| p.try_into())
2953            .collect::<Result<Vec<_>>>()?;
2954        Ok(crate::data::TabMetadata {
2955            panes,
2956            name: proto_metadata.name,
2957        })
2958    }
2959}
2960
2961impl From<crate::data::PaneMetadata> for ProtoPaneMetadata {
2962    fn from(metadata: crate::data::PaneMetadata) -> Self {
2963        ProtoPaneMetadata {
2964            name: metadata.name,
2965            is_plugin: metadata.is_plugin,
2966            is_builtin_plugin: metadata.is_builtin_plugin,
2967        }
2968    }
2969}
2970
2971impl TryFrom<ProtoPaneMetadata> for crate::data::PaneMetadata {
2972    type Error = anyhow::Error;
2973    fn try_from(proto_metadata: ProtoPaneMetadata) -> Result<Self> {
2974        Ok(crate::data::PaneMetadata {
2975            name: proto_metadata.name,
2976            is_plugin: proto_metadata.is_plugin,
2977            is_builtin_plugin: proto_metadata.is_builtin_plugin,
2978        })
2979    }
2980}
2981
2982impl From<ExitReason> for ProtoExitReason {
2983    fn from(reason: ExitReason) -> Self {
2984        match reason {
2985            ExitReason::Normal => ProtoExitReason::Normal,
2986            ExitReason::NormalDetached => ProtoExitReason::NormalDetached,
2987            ExitReason::ForceDetached => ProtoExitReason::ForceDetached,
2988            ExitReason::CannotAttach => ProtoExitReason::CannotAttach,
2989            ExitReason::Disconnect => ProtoExitReason::Disconnect,
2990            ExitReason::WebClientsForbidden => ProtoExitReason::WebClientsForbidden,
2991            ExitReason::KickedByHost => ProtoExitReason::KickedByHost,
2992            ExitReason::Error(_msg) => ProtoExitReason::Error,
2993            ExitReason::CustomExitStatus(_status) => ProtoExitReason::CustomExitStatus,
2994        }
2995    }
2996}
2997
2998impl TryFrom<ProtoExitReason> for ExitReason {
2999    type Error = anyhow::Error;
3000    fn try_from(reason: ProtoExitReason) -> Result<Self> {
3001        match reason {
3002            ProtoExitReason::Normal => Ok(ExitReason::Normal),
3003            ProtoExitReason::NormalDetached => Ok(ExitReason::NormalDetached),
3004            ProtoExitReason::ForceDetached => Ok(ExitReason::ForceDetached),
3005            ProtoExitReason::CannotAttach => Ok(ExitReason::CannotAttach),
3006            ProtoExitReason::Disconnect => Ok(ExitReason::Disconnect),
3007            ProtoExitReason::WebClientsForbidden => Ok(ExitReason::WebClientsForbidden),
3008            ProtoExitReason::KickedByHost => Ok(ExitReason::KickedByHost),
3009            ProtoExitReason::Error => Ok(ExitReason::Error("Protobuf error".to_string())),
3010            ProtoExitReason::CustomExitStatus => Ok(ExitReason::CustomExitStatus(0)),
3011            ProtoExitReason::Unspecified => Err(anyhow!("Unspecified exit reason")),
3012        }
3013    }
3014}
3015
3016impl From<HostTerminalThemeMode> for ProtoHostTerminalThemeIndication {
3017    fn from(mode: HostTerminalThemeMode) -> Self {
3018        match mode {
3019            HostTerminalThemeMode::Dark => ProtoHostTerminalThemeIndication::Dark,
3020            HostTerminalThemeMode::Light => ProtoHostTerminalThemeIndication::Light,
3021        }
3022    }
3023}
3024
3025impl From<ProtoHostTerminalThemeIndication> for HostTerminalThemeMode {
3026    fn from(mode: ProtoHostTerminalThemeIndication) -> Self {
3027        match mode {
3028            ProtoHostTerminalThemeIndication::Dark => HostTerminalThemeMode::Dark,
3029            ProtoHostTerminalThemeIndication::Light => HostTerminalThemeMode::Light,
3030        }
3031    }
3032}
3033
3034// InputMode conversion helper functions
3035fn input_mode_to_proto_i32(mode: InputMode) -> i32 {
3036    match mode {
3037        InputMode::Normal => ProtoInputMode::Normal as i32,
3038        InputMode::Locked => ProtoInputMode::Locked as i32,
3039        InputMode::Resize => ProtoInputMode::Resize as i32,
3040        InputMode::Pane => ProtoInputMode::Pane as i32,
3041        InputMode::Tab => ProtoInputMode::Tab as i32,
3042        InputMode::Scroll => ProtoInputMode::Scroll as i32,
3043        InputMode::EnterSearch => ProtoInputMode::EnterSearch as i32,
3044        InputMode::Search => ProtoInputMode::Search as i32,
3045        InputMode::RenameTab => ProtoInputMode::RenameTab as i32,
3046        InputMode::RenamePane => ProtoInputMode::RenamePane as i32,
3047        InputMode::Session => ProtoInputMode::Session as i32,
3048        InputMode::Move => ProtoInputMode::Move as i32,
3049        InputMode::Prompt => ProtoInputMode::Prompt as i32,
3050        InputMode::Tmux => ProtoInputMode::Tmux as i32,
3051    }
3052}
3053
3054fn proto_i32_to_input_mode(i: i32) -> Result<InputMode> {
3055    match ProtoInputMode::from_i32(i) {
3056        Some(ProtoInputMode::Normal) => Ok(InputMode::Normal),
3057        Some(ProtoInputMode::Locked) => Ok(InputMode::Locked),
3058        Some(ProtoInputMode::Resize) => Ok(InputMode::Resize),
3059        Some(ProtoInputMode::Pane) => Ok(InputMode::Pane),
3060        Some(ProtoInputMode::Tab) => Ok(InputMode::Tab),
3061        Some(ProtoInputMode::Scroll) => Ok(InputMode::Scroll),
3062        Some(ProtoInputMode::EnterSearch) => Ok(InputMode::EnterSearch),
3063        Some(ProtoInputMode::Search) => Ok(InputMode::Search),
3064        Some(ProtoInputMode::RenameTab) => Ok(InputMode::RenameTab),
3065        Some(ProtoInputMode::RenamePane) => Ok(InputMode::RenamePane),
3066        Some(ProtoInputMode::Session) => Ok(InputMode::Session),
3067        Some(ProtoInputMode::Move) => Ok(InputMode::Move),
3068        Some(ProtoInputMode::Prompt) => Ok(InputMode::Prompt),
3069        Some(ProtoInputMode::Tmux) => Ok(InputMode::Tmux),
3070        _ => Err(anyhow!("Invalid InputMode value: {}", i)),
3071    }
3072}
3073
3074// Additional helper functions for Action conversion
3075fn resize_to_proto_i32(resize: crate::data::Resize) -> i32 {
3076    use crate::client_server_contract::client_server_contract::ResizeType;
3077    match resize {
3078        crate::data::Resize::Increase => ResizeType::Increase as i32,
3079        crate::data::Resize::Decrease => ResizeType::Decrease as i32,
3080    }
3081}
3082
3083fn direction_to_proto_i32(direction: crate::data::Direction) -> i32 {
3084    use crate::client_server_contract::client_server_contract::Direction as ProtoDirection;
3085    match direction {
3086        crate::data::Direction::Left => ProtoDirection::Left as i32,
3087        crate::data::Direction::Right => ProtoDirection::Right as i32,
3088        crate::data::Direction::Up => ProtoDirection::Up as i32,
3089        crate::data::Direction::Down => ProtoDirection::Down as i32,
3090    }
3091}
3092
3093fn search_direction_to_proto_i32(direction: crate::input::actions::SearchDirection) -> i32 {
3094    use crate::client_server_contract::client_server_contract::SearchDirection as ProtoSearchDirection;
3095    match direction {
3096        crate::input::actions::SearchDirection::Up => ProtoSearchDirection::Up as i32,
3097        crate::input::actions::SearchDirection::Down => ProtoSearchDirection::Down as i32,
3098    }
3099}
3100
3101fn search_option_to_proto_i32(option: crate::input::actions::SearchOption) -> i32 {
3102    use crate::client_server_contract::client_server_contract::SearchOption as ProtoSearchOption;
3103    match option {
3104        crate::input::actions::SearchOption::CaseSensitivity => {
3105            ProtoSearchOption::CaseSensitivity as i32
3106        },
3107        crate::input::actions::SearchOption::Wrap => ProtoSearchOption::Wrap as i32,
3108        crate::input::actions::SearchOption::WholeWord => ProtoSearchOption::WholeWord as i32,
3109    }
3110}
3111
3112fn unblock_condition_to_proto_i32(condition: crate::data::UnblockCondition) -> i32 {
3113    use crate::client_server_contract::client_server_contract::UnblockCondition as ProtoUnblockCondition;
3114    match condition {
3115        crate::data::UnblockCondition::OnExitSuccess => ProtoUnblockCondition::OnExitSuccess as i32,
3116        crate::data::UnblockCondition::OnExitFailure => ProtoUnblockCondition::OnExitFailure as i32,
3117        crate::data::UnblockCondition::OnAnyExit => ProtoUnblockCondition::OnAnyExit as i32,
3118    }
3119}
3120
3121// Reverse helper functions for Action conversion
3122
3123fn proto_i32_to_resize(resize: i32) -> Result<crate::data::Resize> {
3124    use crate::client_server_contract::client_server_contract::ResizeType as ProtoResize;
3125    let proto_resize = match resize {
3126        x if x == ProtoResize::Increase as i32 => ProtoResize::Increase,
3127        x if x == ProtoResize::Decrease as i32 => ProtoResize::Decrease,
3128        _ => return Err(anyhow!("Invalid ResizeType: {}", resize)),
3129    };
3130    match proto_resize {
3131        ProtoResize::Increase => Ok(crate::data::Resize::Increase),
3132        ProtoResize::Decrease => Ok(crate::data::Resize::Decrease),
3133        ProtoResize::Unspecified => Err(anyhow!("Unspecified ResizeType")),
3134    }
3135}
3136
3137fn proto_i32_to_direction(direction: i32) -> Result<crate::data::Direction> {
3138    use crate::client_server_contract::client_server_contract::Direction as ProtoDirection;
3139    let proto_direction = match direction {
3140        x if x == ProtoDirection::Left as i32 => ProtoDirection::Left,
3141        x if x == ProtoDirection::Right as i32 => ProtoDirection::Right,
3142        x if x == ProtoDirection::Up as i32 => ProtoDirection::Up,
3143        x if x == ProtoDirection::Down as i32 => ProtoDirection::Down,
3144        _ => return Err(anyhow!("Invalid Direction: {}", direction)),
3145    };
3146    match proto_direction {
3147        ProtoDirection::Left => Ok(crate::data::Direction::Left),
3148        ProtoDirection::Right => Ok(crate::data::Direction::Right),
3149        ProtoDirection::Up => Ok(crate::data::Direction::Up),
3150        ProtoDirection::Down => Ok(crate::data::Direction::Down),
3151        ProtoDirection::Unspecified => Err(anyhow!("Unspecified direction")),
3152    }
3153}
3154
3155fn proto_i32_to_search_direction(direction: i32) -> Result<crate::input::actions::SearchDirection> {
3156    use crate::client_server_contract::client_server_contract::SearchDirection as ProtoSearchDirection;
3157    let proto_direction = match direction {
3158        x if x == ProtoSearchDirection::Up as i32 => ProtoSearchDirection::Up,
3159        x if x == ProtoSearchDirection::Down as i32 => ProtoSearchDirection::Down,
3160        _ => return Err(anyhow!("Invalid SearchDirection: {}", direction)),
3161    };
3162    match proto_direction {
3163        ProtoSearchDirection::Up => Ok(crate::input::actions::SearchDirection::Up),
3164        ProtoSearchDirection::Down => Ok(crate::input::actions::SearchDirection::Down),
3165        ProtoSearchDirection::Unspecified => Err(anyhow!("Unspecified search direction")),
3166    }
3167}
3168
3169fn proto_i32_to_search_option(option: i32) -> Result<crate::input::actions::SearchOption> {
3170    use crate::client_server_contract::client_server_contract::SearchOption as ProtoSearchOption;
3171    let proto_option = match option {
3172        x if x == ProtoSearchOption::CaseSensitivity as i32 => ProtoSearchOption::CaseSensitivity,
3173        x if x == ProtoSearchOption::WholeWord as i32 => ProtoSearchOption::WholeWord,
3174        x if x == ProtoSearchOption::Wrap as i32 => ProtoSearchOption::Wrap,
3175        _ => return Err(anyhow!("Invalid SearchOption: {}", option)),
3176    };
3177    match proto_option {
3178        ProtoSearchOption::CaseSensitivity => {
3179            Ok(crate::input::actions::SearchOption::CaseSensitivity)
3180        },
3181        ProtoSearchOption::Wrap => Ok(crate::input::actions::SearchOption::Wrap),
3182        ProtoSearchOption::WholeWord => Ok(crate::input::actions::SearchOption::WholeWord),
3183        ProtoSearchOption::Unspecified => Err(anyhow!("Unspecified search option")),
3184    }
3185}
3186
3187fn proto_i32_to_unblock_condition(condition: i32) -> Result<crate::data::UnblockCondition> {
3188    use crate::client_server_contract::client_server_contract::UnblockCondition as ProtoUnblockCondition;
3189    let proto_condition = match condition {
3190        x if x == ProtoUnblockCondition::OnExitSuccess as i32 => {
3191            ProtoUnblockCondition::OnExitSuccess
3192        },
3193        x if x == ProtoUnblockCondition::OnExitFailure as i32 => {
3194            ProtoUnblockCondition::OnExitFailure
3195        },
3196        x if x == ProtoUnblockCondition::OnAnyExit as i32 => ProtoUnblockCondition::OnAnyExit,
3197        _ => return Err(anyhow!("Invalid UnblockCondition: {}", condition)),
3198    };
3199    match proto_condition {
3200        ProtoUnblockCondition::OnExitSuccess => Ok(crate::data::UnblockCondition::OnExitSuccess),
3201        ProtoUnblockCondition::OnExitFailure => Ok(crate::data::UnblockCondition::OnExitFailure),
3202        ProtoUnblockCondition::OnAnyExit => Ok(crate::data::UnblockCondition::OnAnyExit),
3203        ProtoUnblockCondition::Unspecified => Err(anyhow!("Unspecified unblock condition")),
3204    }
3205}
3206
3207// Position conversion
3208impl From<crate::position::Position>
3209    for crate::client_server_contract::client_server_contract::Position
3210{
3211    fn from(pos: crate::position::Position) -> Self {
3212        Self {
3213            line: pos.line.0 as i32,
3214            column: pos.column.0 as u64,
3215        }
3216    }
3217}
3218
3219// Reverse Position conversion
3220impl TryFrom<crate::client_server_contract::client_server_contract::Position>
3221    for crate::position::Position
3222{
3223    type Error = anyhow::Error;
3224    fn try_from(
3225        pos: crate::client_server_contract::client_server_contract::Position,
3226    ) -> Result<Self> {
3227        Ok(Self {
3228            line: crate::position::Line(pos.line as isize),
3229            column: crate::position::Column(pos.column as usize),
3230        })
3231    }
3232}
3233
3234// OpenFilePayload conversion
3235impl From<crate::input::command::OpenFilePayload>
3236    for crate::client_server_contract::client_server_contract::OpenFilePayload
3237{
3238    fn from(payload: crate::input::command::OpenFilePayload) -> Self {
3239        Self {
3240            file_to_open: payload.path.to_string_lossy().to_string(),
3241            line_number: payload.line_number.map(|n| n as u32),
3242            cwd: payload.cwd.map(|p| p.to_string_lossy().to_string()),
3243            originating_plugin: payload.originating_plugin.map(|op| op.into()),
3244        }
3245    }
3246}
3247
3248// Reverse OpenFilePayload conversion
3249impl TryFrom<crate::client_server_contract::client_server_contract::OpenFilePayload>
3250    for crate::input::command::OpenFilePayload
3251{
3252    type Error = anyhow::Error;
3253    fn try_from(
3254        payload: crate::client_server_contract::client_server_contract::OpenFilePayload,
3255    ) -> Result<Self> {
3256        Ok(Self {
3257            path: PathBuf::from(payload.file_to_open),
3258            line_number: payload.line_number.map(|n| n as usize),
3259            cwd: payload.cwd.map(PathBuf::from),
3260            originating_plugin: payload
3261                .originating_plugin
3262                .map(|op| op.try_into())
3263                .transpose()?,
3264        })
3265    }
3266}
3267
3268// PaneId conversion
3269impl From<crate::data::PaneId> for crate::client_server_contract::client_server_contract::PaneId {
3270    fn from(pane_id: crate::data::PaneId) -> Self {
3271        use crate::client_server_contract::client_server_contract::pane_id::PaneType;
3272        match pane_id {
3273            crate::data::PaneId::Terminal(id) => Self {
3274                pane_type: Some(PaneType::Terminal(id)),
3275            },
3276            crate::data::PaneId::Plugin(id) => Self {
3277                pane_type: Some(PaneType::Plugin(id)),
3278            },
3279        }
3280    }
3281}
3282
3283// Reverse PaneId conversion
3284impl TryFrom<crate::client_server_contract::client_server_contract::PaneId>
3285    for crate::data::PaneId
3286{
3287    type Error = anyhow::Error;
3288    fn try_from(
3289        pane_id: crate::client_server_contract::client_server_contract::PaneId,
3290    ) -> Result<Self> {
3291        use crate::client_server_contract::client_server_contract::pane_id::PaneType;
3292        match pane_id
3293            .pane_type
3294            .ok_or_else(|| anyhow!("PaneId missing pane_type"))?
3295        {
3296            PaneType::Terminal(id) => Ok(crate::data::PaneId::Terminal(id)),
3297            PaneType::Plugin(id) => Ok(crate::data::PaneId::Plugin(id)),
3298        }
3299    }
3300}
3301
3302// FloatingCoordinate conversion - SplitSize to FloatingCoordinate
3303impl From<crate::input::layout::SplitSize>
3304    for crate::client_server_contract::client_server_contract::FloatingCoordinate
3305{
3306    fn from(size: crate::input::layout::SplitSize) -> Self {
3307        match size {
3308            crate::input::layout::SplitSize::Percent(p) => Self {
3309                coordinate_type: Some(crate::client_server_contract::client_server_contract::floating_coordinate::CoordinateType::Percent(p as f32)),
3310            },
3311            crate::input::layout::SplitSize::Fixed(f) => Self {
3312                coordinate_type: Some(crate::client_server_contract::client_server_contract::floating_coordinate::CoordinateType::Fixed(f as u32)),
3313            },
3314        }
3315    }
3316}
3317
3318// Reverse FloatingCoordinate conversion
3319impl TryFrom<crate::client_server_contract::client_server_contract::FloatingCoordinate>
3320    for crate::input::layout::SplitSize
3321{
3322    type Error = anyhow::Error;
3323    fn try_from(
3324        coord: crate::client_server_contract::client_server_contract::FloatingCoordinate,
3325    ) -> Result<Self> {
3326        use crate::client_server_contract::client_server_contract::floating_coordinate::CoordinateType;
3327        match coord
3328            .coordinate_type
3329            .ok_or_else(|| anyhow!("FloatingCoordinate missing coordinate_type"))?
3330        {
3331            CoordinateType::Percent(p) => Ok(crate::input::layout::SplitSize::Percent(p as usize)),
3332            CoordinateType::Fixed(f) => Ok(crate::input::layout::SplitSize::Fixed(f as usize)),
3333        }
3334    }
3335}
3336
3337// FloatingCoordinate conversion - PercentOrFixed to FloatingCoordinate
3338impl From<crate::input::layout::PercentOrFixed>
3339    for crate::client_server_contract::client_server_contract::FloatingCoordinate
3340{
3341    fn from(size: crate::input::layout::PercentOrFixed) -> Self {
3342        match size {
3343            crate::input::layout::PercentOrFixed::Percent(p) => Self {
3344                coordinate_type: Some(crate::client_server_contract::client_server_contract::floating_coordinate::CoordinateType::Percent(p as f32)),
3345            },
3346            crate::input::layout::PercentOrFixed::Fixed(f) => Self {
3347                coordinate_type: Some(crate::client_server_contract::client_server_contract::floating_coordinate::CoordinateType::Fixed(f as u32)),
3348            },
3349        }
3350    }
3351}
3352
3353// Reverse FloatingCoordinate conversion for PercentOrFixed
3354impl TryFrom<crate::client_server_contract::client_server_contract::FloatingCoordinate>
3355    for crate::input::layout::PercentOrFixed
3356{
3357    type Error = anyhow::Error;
3358    fn try_from(
3359        coord: crate::client_server_contract::client_server_contract::FloatingCoordinate,
3360    ) -> Result<Self> {
3361        use crate::client_server_contract::client_server_contract::floating_coordinate::CoordinateType;
3362        match coord
3363            .coordinate_type
3364            .ok_or_else(|| anyhow!("FloatingCoordinate missing coordinate_type"))?
3365        {
3366            CoordinateType::Percent(p) => {
3367                Ok(crate::input::layout::PercentOrFixed::Percent(p as usize))
3368            },
3369            CoordinateType::Fixed(f) => Ok(crate::input::layout::PercentOrFixed::Fixed(f as usize)),
3370        }
3371    }
3372}
3373
3374// FloatingPaneCoordinates conversion
3375impl From<crate::data::FloatingPaneCoordinates>
3376    for crate::client_server_contract::client_server_contract::FloatingPaneCoordinates
3377{
3378    fn from(coords: crate::data::FloatingPaneCoordinates) -> Self {
3379        Self {
3380            x: coords.x.map(|x| x.into()),
3381            y: coords.y.map(|y| y.into()),
3382            width: coords.width.map(|w| w.into()),
3383            height: coords.height.map(|h| h.into()),
3384            pinned: coords.pinned,
3385            borderless: coords.borderless,
3386        }
3387    }
3388}
3389
3390// Reverse FloatingPaneCoordinates conversion
3391impl TryFrom<crate::client_server_contract::client_server_contract::FloatingPaneCoordinates>
3392    for crate::data::FloatingPaneCoordinates
3393{
3394    type Error = anyhow::Error;
3395    fn try_from(
3396        coords: crate::client_server_contract::client_server_contract::FloatingPaneCoordinates,
3397    ) -> Result<Self> {
3398        Ok(Self {
3399            x: coords.x.map(|x| x.try_into()).transpose()?,
3400            y: coords.y.map(|y| y.try_into()).transpose()?,
3401            width: coords.width.map(|w| w.try_into()).transpose()?,
3402            height: coords.height.map(|h| h.try_into()).transpose()?,
3403            pinned: coords.pinned,
3404            borderless: coords.borderless,
3405        })
3406    }
3407}
3408
3409// NewPanePlacement conversion
3410impl From<crate::data::NewPanePlacement>
3411    for crate::client_server_contract::client_server_contract::NewPanePlacement
3412{
3413    fn from(placement: crate::data::NewPanePlacement) -> Self {
3414        use crate::client_server_contract::client_server_contract::new_pane_placement::PlacementType;
3415        use crate::client_server_contract::client_server_contract::{
3416            NoPreferencePlacement, StackedPlacement, TiledPlacement,
3417        };
3418        let placement_type = match placement {
3419            crate::data::NewPanePlacement::NoPreference {
3420                borderless: Some(b),
3421            } => PlacementType::NoPreferenceWithOptions(NoPreferencePlacement {
3422                borderless: Some(b),
3423            }),
3424            crate::data::NewPanePlacement::NoPreference { borderless: None } => {
3425                PlacementType::NoPreference(true)
3426            },
3427            crate::data::NewPanePlacement::Tiled {
3428                direction,
3429                borderless: Some(b),
3430            } => PlacementType::TiledWithOptions(TiledPlacement {
3431                direction: direction.map(direction_to_proto_i32),
3432                borderless: Some(b),
3433            }),
3434            crate::data::NewPanePlacement::Tiled {
3435                direction,
3436                borderless: None,
3437            } => PlacementType::Tiled(direction.map(direction_to_proto_i32).unwrap_or(0)),
3438            crate::data::NewPanePlacement::Floating(coords) => {
3439                PlacementType::Floating(coords.map(|c| c.into()).unwrap_or_default())
3440            },
3441            crate::data::NewPanePlacement::InPlace {
3442                pane_id_to_replace,
3443                close_replaced_pane,
3444                borderless,
3445            } => PlacementType::InPlace(
3446                crate::client_server_contract::client_server_contract::NewPanePlacementInPlace {
3447                    pane_id_to_replace: pane_id_to_replace.map(|id| id.into()),
3448                    close_replaced_pane,
3449                    borderless,
3450                },
3451            ),
3452            crate::data::NewPanePlacement::Stacked {
3453                pane_id_to_stack_under,
3454                borderless: Some(b),
3455            } => PlacementType::StackedWithOptions(StackedPlacement {
3456                pane_id_to_stack_under: pane_id_to_stack_under.map(|id| id.into()),
3457                borderless: Some(b),
3458            }),
3459            crate::data::NewPanePlacement::Stacked {
3460                pane_id_to_stack_under,
3461                borderless: None,
3462            } => PlacementType::Stacked(
3463                pane_id_to_stack_under
3464                    .map(|id| id.into())
3465                    .unwrap_or_default(),
3466            ),
3467        };
3468        Self {
3469            placement_type: Some(placement_type),
3470        }
3471    }
3472}
3473
3474// Reverse NewPanePlacement conversion
3475impl TryFrom<crate::client_server_contract::client_server_contract::NewPanePlacement>
3476    for crate::data::NewPanePlacement
3477{
3478    type Error = anyhow::Error;
3479    fn try_from(
3480        placement: crate::client_server_contract::client_server_contract::NewPanePlacement,
3481    ) -> Result<Self> {
3482        use crate::client_server_contract::client_server_contract::new_pane_placement::PlacementType;
3483        match placement
3484            .placement_type
3485            .ok_or_else(|| anyhow!("NewPanePlacement missing placement_type"))?
3486        {
3487            // New fields (with borderless support) take priority
3488            PlacementType::NoPreferenceWithOptions(opts) => {
3489                Ok(crate::data::NewPanePlacement::NoPreference {
3490                    borderless: opts.borderless,
3491                })
3492            },
3493            PlacementType::TiledWithOptions(opts) => {
3494                let direction = opts.direction.map(proto_i32_to_direction).transpose()?;
3495                Ok(crate::data::NewPanePlacement::Tiled {
3496                    direction,
3497                    borderless: opts.borderless,
3498                })
3499            },
3500            PlacementType::StackedWithOptions(opts) => {
3501                let pane_id = opts
3502                    .pane_id_to_stack_under
3503                    .map(|id| id.try_into())
3504                    .transpose()?;
3505                Ok(crate::data::NewPanePlacement::Stacked {
3506                    pane_id_to_stack_under: pane_id,
3507                    borderless: opts.borderless,
3508                })
3509            },
3510            // Legacy fields (without borderless support)
3511            PlacementType::NoPreference(_) => {
3512                Ok(crate::data::NewPanePlacement::NoPreference { borderless: None })
3513            },
3514            PlacementType::Tiled(direction) => {
3515                let direction = if direction == 0 {
3516                    None
3517                } else {
3518                    Some(proto_i32_to_direction(direction)?)
3519                };
3520                Ok(crate::data::NewPanePlacement::Tiled {
3521                    direction,
3522                    borderless: None,
3523                })
3524            },
3525            PlacementType::Floating(coords) => {
3526                let coords = if coords == Default::default() {
3527                    None
3528                } else {
3529                    Some(coords.try_into()?)
3530                };
3531                Ok(crate::data::NewPanePlacement::Floating(coords))
3532            },
3533            PlacementType::InPlace(in_place) => Ok(crate::data::NewPanePlacement::InPlace {
3534                pane_id_to_replace: in_place
3535                    .pane_id_to_replace
3536                    .map(|id| id.try_into())
3537                    .transpose()?,
3538                close_replaced_pane: in_place.close_replaced_pane,
3539                borderless: in_place.borderless,
3540            }),
3541            PlacementType::Stacked(pane_id) => {
3542                let pane_id = if pane_id == Default::default() {
3543                    None
3544                } else {
3545                    Some(pane_id.try_into()?)
3546                };
3547                Ok(crate::data::NewPanePlacement::Stacked {
3548                    pane_id_to_stack_under: pane_id,
3549                    borderless: None,
3550                })
3551            },
3552        }
3553    }
3554}
3555
3556// MouseEvent conversion
3557impl From<crate::input::mouse::MouseEvent>
3558    for crate::client_server_contract::client_server_contract::MouseEvent
3559{
3560    fn from(event: crate::input::mouse::MouseEvent) -> Self {
3561        use crate::client_server_contract::client_server_contract::{
3562            MouseEventType as ProtoMouseEventType, Position,
3563        };
3564
3565        let position = Position {
3566            line: event.position.line.0 as i32,
3567            column: event.position.column.0 as u64,
3568        };
3569
3570        let event_type = match event.event_type {
3571            crate::input::mouse::MouseEventType::Press => ProtoMouseEventType::Press as i32,
3572            crate::input::mouse::MouseEventType::Release => ProtoMouseEventType::Release as i32,
3573            crate::input::mouse::MouseEventType::Motion => ProtoMouseEventType::Motion as i32,
3574        };
3575
3576        Self {
3577            event_type,
3578            left: event.left,
3579            right: event.right,
3580            middle: event.middle,
3581            wheel_up: event.wheel_up,
3582            wheel_down: event.wheel_down,
3583            shift: event.shift,
3584            alt: event.alt,
3585            ctrl: event.ctrl,
3586            position: Some(position),
3587        }
3588    }
3589}
3590
3591// RunCommandAction conversion
3592impl From<crate::input::command::RunCommandAction>
3593    for crate::client_server_contract::client_server_contract::RunCommandAction
3594{
3595    fn from(action: crate::input::command::RunCommandAction) -> Self {
3596        Self {
3597            command: action.command.to_string_lossy().to_string(),
3598            args: action.args,
3599            cwd: action.cwd.map(|p| p.to_string_lossy().to_string()),
3600            direction: action.direction.map(|d| direction_to_proto_i32(d)),
3601            hold_on_close: action.hold_on_close,
3602            hold_on_start: action.hold_on_start,
3603            originating_plugin: action.originating_plugin.map(|op| op.into()),
3604            use_terminal_title: action.use_terminal_title,
3605        }
3606    }
3607}
3608
3609// OriginatingPlugin conversion
3610impl From<crate::data::OriginatingPlugin>
3611    for crate::client_server_contract::client_server_contract::OriginatingPlugin
3612{
3613    fn from(orig: crate::data::OriginatingPlugin) -> Self {
3614        use std::collections::HashMap;
3615        let context: HashMap<String, String> =
3616            orig.context.into_iter().map(|(k, v)| (k, v)).collect();
3617
3618        Self {
3619            plugin_id: orig.plugin_id,
3620            client_id: orig.client_id as u32,
3621            context,
3622        }
3623    }
3624}
3625
3626// OriginatingPlugin reverse conversion
3627impl TryFrom<crate::client_server_contract::client_server_contract::OriginatingPlugin>
3628    for crate::data::OriginatingPlugin
3629{
3630    type Error = anyhow::Error;
3631
3632    fn try_from(
3633        orig: crate::client_server_contract::client_server_contract::OriginatingPlugin,
3634    ) -> Result<Self> {
3635        use std::collections::BTreeMap;
3636        let context: BTreeMap<String, String> = orig.context.into_iter().collect();
3637
3638        Ok(Self {
3639            plugin_id: orig.plugin_id,
3640            client_id: orig.client_id as u16,
3641            context,
3642        })
3643    }
3644}
3645
3646// SplitDirection conversion helper
3647fn split_direction_to_proto_i32(direction: crate::input::layout::SplitDirection) -> i32 {
3648    use crate::client_server_contract::client_server_contract::SplitDirection as ProtoSplitDirection;
3649    match direction {
3650        crate::input::layout::SplitDirection::Horizontal => ProtoSplitDirection::Horizontal as i32,
3651        crate::input::layout::SplitDirection::Vertical => ProtoSplitDirection::Vertical as i32,
3652    }
3653}
3654
3655// SplitSize conversion
3656impl From<crate::input::layout::SplitSize>
3657    for crate::client_server_contract::client_server_contract::SplitSize
3658{
3659    fn from(size: crate::input::layout::SplitSize) -> Self {
3660        use crate::client_server_contract::client_server_contract::split_size::SizeType;
3661        match size {
3662            crate::input::layout::SplitSize::Percent(p) => Self {
3663                size_type: Some(SizeType::Percent(p as u32)),
3664            },
3665            crate::input::layout::SplitSize::Fixed(f) => Self {
3666                size_type: Some(SizeType::Fixed(f as u32)),
3667            },
3668        }
3669    }
3670}
3671
3672// PercentOrFixed conversion
3673impl From<crate::input::layout::PercentOrFixed>
3674    for crate::client_server_contract::client_server_contract::PercentOrFixed
3675{
3676    fn from(size: crate::input::layout::PercentOrFixed) -> Self {
3677        use crate::client_server_contract::client_server_contract::percent_or_fixed::SizeType;
3678        match size {
3679            crate::input::layout::PercentOrFixed::Percent(p) => Self {
3680                size_type: Some(SizeType::Percent(p as u32)),
3681            },
3682            crate::input::layout::PercentOrFixed::Fixed(f) => Self {
3683                size_type: Some(SizeType::Fixed(f as u32)),
3684            },
3685        }
3686    }
3687}
3688
3689// Run conversion
3690impl From<crate::input::layout::Run>
3691    for crate::client_server_contract::client_server_contract::Run
3692{
3693    fn from(run: crate::input::layout::Run) -> Self {
3694        use crate::client_server_contract::client_server_contract::run::RunType;
3695        match run {
3696            crate::input::layout::Run::Command(cmd) => Self {
3697                run_type: Some(RunType::Command(
3698                    crate::client_server_contract::client_server_contract::RunCommandAction {
3699                        command: cmd.command.to_string_lossy().to_string(),
3700                        args: cmd.args,
3701                        cwd: cmd.cwd.map(|p| p.to_string_lossy().to_string()),
3702                        direction: None, // RunCommand doesn't have direction field
3703                        hold_on_close: cmd.hold_on_close,
3704                        hold_on_start: cmd.hold_on_start,
3705                        originating_plugin: cmd.originating_plugin.map(|op| op.into()),
3706                        use_terminal_title: cmd.use_terminal_title,
3707                    },
3708                )),
3709            },
3710            crate::input::layout::Run::Plugin(plugin) => Self {
3711                run_type: Some(RunType::Plugin(plugin.into())),
3712            },
3713            crate::input::layout::Run::EditFile(path, line_number, cwd) => Self {
3714                run_type: Some(RunType::EditFile(
3715                    crate::client_server_contract::client_server_contract::RunEditFileAction {
3716                        file_path: path.to_string_lossy().to_string(),
3717                        line_number: line_number.map(|n| n as u32),
3718                        cwd: cwd.map(|p| p.to_string_lossy().to_string()),
3719                    },
3720                )),
3721            },
3722            crate::input::layout::Run::Cwd(path) => Self {
3723                run_type: Some(RunType::Cwd(path.to_string_lossy().to_string())),
3724            },
3725        }
3726    }
3727}
3728
3729// TabLayoutInfo conversion
3730impl From<crate::input::layout::TabLayoutInfo>
3731    for crate::client_server_contract::client_server_contract::TabLayoutInfo
3732{
3733    fn from(tab_info: crate::input::layout::TabLayoutInfo) -> Self {
3734        Self {
3735            tab_index: tab_info.tab_index as u32,
3736            tab_name: tab_info.tab_name,
3737            tiled_layout: Some(tab_info.tiled_layout.into()),
3738            floating_layouts: tab_info
3739                .floating_layouts
3740                .into_iter()
3741                .map(|l| l.into())
3742                .collect(),
3743            swap_tiled_layouts: tab_info
3744                .swap_tiled_layouts
3745                .unwrap_or_default()
3746                .into_iter()
3747                .map(|l| l.into())
3748                .collect(),
3749            swap_floating_layouts: tab_info
3750                .swap_floating_layouts
3751                .unwrap_or_default()
3752                .into_iter()
3753                .map(|l| l.into())
3754                .collect(),
3755        }
3756    }
3757}
3758
3759impl TryFrom<crate::client_server_contract::client_server_contract::TabLayoutInfo>
3760    for crate::input::layout::TabLayoutInfo
3761{
3762    type Error = anyhow::Error;
3763
3764    fn try_from(
3765        protobuf_tab: crate::client_server_contract::client_server_contract::TabLayoutInfo,
3766    ) -> Result<Self> {
3767        Ok(crate::input::layout::TabLayoutInfo {
3768            tab_index: protobuf_tab.tab_index as usize,
3769            tab_name: protobuf_tab.tab_name.filter(|s| !s.is_empty()),
3770            tiled_layout: protobuf_tab
3771                .tiled_layout
3772                .ok_or_else(|| anyhow!("missing tiled_layout"))?
3773                .try_into()?,
3774            floating_layouts: protobuf_tab
3775                .floating_layouts
3776                .into_iter()
3777                .map(|l| l.try_into())
3778                .collect::<Result<Vec<_>>>()?,
3779            swap_tiled_layouts: if protobuf_tab.swap_tiled_layouts.is_empty() {
3780                None
3781            } else {
3782                Some(
3783                    protobuf_tab
3784                        .swap_tiled_layouts
3785                        .into_iter()
3786                        .map(|l| l.try_into())
3787                        .collect::<Result<Vec<_>>>()?,
3788                )
3789            },
3790            swap_floating_layouts: if protobuf_tab.swap_floating_layouts.is_empty() {
3791                None
3792            } else {
3793                Some(
3794                    protobuf_tab
3795                        .swap_floating_layouts
3796                        .into_iter()
3797                        .map(|l| l.try_into())
3798                        .collect::<Result<Vec<_>>>()?,
3799                )
3800            },
3801        })
3802    }
3803}
3804
3805// TiledPaneLayout conversion
3806impl From<crate::input::layout::TiledPaneLayout>
3807    for crate::client_server_contract::client_server_contract::TiledPaneLayout
3808{
3809    fn from(layout: crate::input::layout::TiledPaneLayout) -> Self {
3810        Self {
3811            children_split_direction: split_direction_to_proto_i32(layout.children_split_direction),
3812            name: layout.name,
3813            children: layout.children.into_iter().map(|c| c.into()).collect(),
3814            split_size: layout.split_size.map(|s| s.into()),
3815            run: layout.run.map(|r| r.into()),
3816            borderless: layout.borderless,
3817            focus: layout.focus.map(|f| f.to_string()),
3818            exclude_from_sync: layout.exclude_from_sync,
3819            children_are_stacked: layout.children_are_stacked,
3820            external_children_index: layout.external_children_index.map(|l| l as u32),
3821            is_expanded_in_stack: layout.is_expanded_in_stack,
3822            hide_floating_panes: layout.hide_floating_panes,
3823            pane_initial_contents: layout.pane_initial_contents,
3824            default_fg: layout.default_fg,
3825            default_bg: layout.default_bg,
3826        }
3827    }
3828}
3829
3830impl From<crate::input::layout::FloatingPaneLayout>
3831    for crate::client_server_contract::client_server_contract::FloatingPaneLayout
3832{
3833    fn from(layout: crate::input::layout::FloatingPaneLayout) -> Self {
3834        Self {
3835            name: layout.name,
3836            height: layout.height.map(|h| h.into()),
3837            width: layout.width.map(|w| w.into()),
3838            x: layout.x.map(|x| x.into()),
3839            y: layout.y.map(|y| y.into()),
3840            pinned: layout.pinned,
3841            run: layout.run.map(|r| r.into()),
3842            focus: layout.focus,
3843            already_running: layout.already_running,
3844            pane_initial_contents: layout.pane_initial_contents,
3845            logical_position: layout.logical_position.map(|l| l as u32),
3846            borderless: layout.borderless,
3847            default_fg: layout.default_fg,
3848            default_bg: layout.default_bg,
3849        }
3850    }
3851}
3852
3853impl From<crate::input::layout::SwapTiledLayout>
3854    for crate::client_server_contract::client_server_contract::SwapTiledLayout
3855{
3856    fn from(layout: crate::input::layout::SwapTiledLayout) -> Self {
3857        use crate::client_server_contract::client_server_contract::LayoutConstraintTiledPair;
3858
3859        let constraint_map = layout
3860            .0
3861            .into_iter()
3862            .map(|(constraint, tiled_layout)| LayoutConstraintTiledPair {
3863                constraint: Some(constraint.into()),
3864                layout: Some(tiled_layout.into()),
3865            })
3866            .collect();
3867
3868        Self {
3869            constraint_map,
3870            name: layout.1,
3871        }
3872    }
3873}
3874
3875impl From<crate::input::layout::SwapFloatingLayout>
3876    for crate::client_server_contract::client_server_contract::SwapFloatingLayout
3877{
3878    fn from(layout: crate::input::layout::SwapFloatingLayout) -> Self {
3879        use crate::client_server_contract::client_server_contract::LayoutConstraintFloatingPair;
3880
3881        let constraint_map = layout
3882            .0
3883            .into_iter()
3884            .map(
3885                |(constraint, floating_layouts)| LayoutConstraintFloatingPair {
3886                    constraint: Some(constraint.into()),
3887                    layouts: floating_layouts.into_iter().map(|l| l.into()).collect(),
3888                },
3889            )
3890            .collect();
3891
3892        Self {
3893            constraint_map,
3894            name: layout.1,
3895        }
3896    }
3897}
3898
3899// PluginUserConfiguration conversion
3900impl From<crate::input::layout::PluginUserConfiguration>
3901    for crate::client_server_contract::client_server_contract::PluginUserConfiguration
3902{
3903    fn from(config: crate::input::layout::PluginUserConfiguration) -> Self {
3904        Self {
3905            configuration: config.inner().clone().into_iter().collect(), // Convert BTreeMap to HashMap
3906        }
3907    }
3908}
3909
3910// LayoutConstraint conversion
3911impl From<crate::input::layout::LayoutConstraint>
3912    for crate::client_server_contract::client_server_contract::LayoutConstraintWithValue
3913{
3914    fn from(constraint: crate::input::layout::LayoutConstraint) -> Self {
3915        use crate::client_server_contract::client_server_contract::LayoutConstraint as ProtoLayoutConstraint;
3916        match constraint {
3917            crate::input::layout::LayoutConstraint::MaxPanes(n) => Self {
3918                constraint_type: ProtoLayoutConstraint::MaxPanes as i32,
3919                value: Some(n as u32),
3920            },
3921            crate::input::layout::LayoutConstraint::MinPanes(n) => Self {
3922                constraint_type: ProtoLayoutConstraint::MinPanes as i32,
3923                value: Some(n as u32),
3924            },
3925            crate::input::layout::LayoutConstraint::ExactPanes(n) => Self {
3926                constraint_type: ProtoLayoutConstraint::ExactPanes as i32,
3927                value: Some(n as u32),
3928            },
3929            crate::input::layout::LayoutConstraint::NoConstraint => Self {
3930                constraint_type: ProtoLayoutConstraint::NoConstraint as i32,
3931                value: None,
3932            },
3933        }
3934    }
3935}
3936
3937// RunPlugin conversion
3938impl From<crate::input::layout::RunPlugin>
3939    for crate::client_server_contract::client_server_contract::RunPlugin
3940{
3941    fn from(plugin: crate::input::layout::RunPlugin) -> Self {
3942        Self {
3943            allow_exec_host_cmd: plugin._allow_exec_host_cmd,
3944            location: Some(plugin.location.into()),
3945            configuration: Some(plugin.configuration.into()),
3946            initial_cwd: plugin.initial_cwd.map(|p| p.display().to_string()),
3947        }
3948    }
3949}
3950
3951// PluginAlias conversion
3952impl From<crate::input::layout::PluginAlias>
3953    for crate::client_server_contract::client_server_contract::PluginAlias
3954{
3955    fn from(plugin: crate::input::layout::PluginAlias) -> Self {
3956        Self {
3957            name: plugin.name,
3958            configuration: plugin.configuration.map(|c| c.into()),
3959            initial_cwd: plugin.initial_cwd.map(|i| i.display().to_string()),
3960            run_plugin: plugin.run_plugin.map(|r| r.into()),
3961        }
3962    }
3963}
3964
3965// RunPluginLocation conversion
3966impl From<crate::input::layout::RunPluginLocation>
3967    for crate::client_server_contract::client_server_contract::RunPluginLocationData
3968{
3969    fn from(location: crate::input::layout::RunPluginLocation) -> Self {
3970        use crate::client_server_contract::client_server_contract::{
3971            run_plugin_location_data::LocationData, RunPluginLocation as ProtoRunPluginLocation,
3972        };
3973        match location {
3974            crate::input::layout::RunPluginLocation::File(path) => Self {
3975                location_type: ProtoRunPluginLocation::File as i32,
3976                location_data: Some(LocationData::FilePath(path.to_string_lossy().to_string())),
3977            },
3978            crate::input::layout::RunPluginLocation::Zellij(tag) => Self {
3979                location_type: ProtoRunPluginLocation::Zellij as i32,
3980                location_data: Some(LocationData::ZellijTag(
3981                    crate::client_server_contract::client_server_contract::PluginTag {
3982                        tag: tag.to_string(),
3983                    },
3984                )),
3985            },
3986            crate::input::layout::RunPluginLocation::Remote(url) => Self {
3987                location_type: ProtoRunPluginLocation::Remote as i32,
3988                location_data: Some(LocationData::RemoteUrl(url)),
3989            },
3990        }
3991    }
3992}
3993
3994// RunPluginOrAlias conversion
3995impl From<crate::input::layout::RunPluginOrAlias>
3996    for crate::client_server_contract::client_server_contract::RunPluginOrAlias
3997{
3998    fn from(plugin: crate::input::layout::RunPluginOrAlias) -> Self {
3999        use crate::client_server_contract::client_server_contract::run_plugin_or_alias::PluginType;
4000        match plugin {
4001            crate::input::layout::RunPluginOrAlias::RunPlugin(run_plugin) => Self {
4002                plugin_type: Some(PluginType::Plugin(run_plugin.into())),
4003            },
4004            crate::input::layout::RunPluginOrAlias::Alias(alias) => Self {
4005                plugin_type: Some(PluginType::Alias(alias.into())),
4006            },
4007        }
4008    }
4009}
4010
4011// CommandOrPlugin conversion
4012impl From<crate::data::CommandOrPlugin>
4013    for crate::client_server_contract::client_server_contract::CommandOrPlugin
4014{
4015    fn from(cmd_or_plugin: crate::data::CommandOrPlugin) -> Self {
4016        use crate::client_server_contract::client_server_contract::command_or_plugin::CommandOrPluginType;
4017        use crate::client_server_contract::client_server_contract::CommandOrPluginFile;
4018        match cmd_or_plugin {
4019            crate::data::CommandOrPlugin::Command(cmd) => Self {
4020                command_or_plugin_type: Some(CommandOrPluginType::Command(cmd.into())),
4021            },
4022            crate::data::CommandOrPlugin::Plugin(plugin) => Self {
4023                command_or_plugin_type: Some(CommandOrPluginType::Plugin(plugin.into())),
4024            },
4025            crate::data::CommandOrPlugin::File(f) => Self {
4026                command_or_plugin_type: Some(CommandOrPluginType::File(CommandOrPluginFile {
4027                    path: f.path.display().to_string(),
4028                    line_number: f.line_number.map(|n| n as i32),
4029                    cwd: f.cwd.map(|c| c.display().to_string()),
4030                })),
4031            },
4032        }
4033    }
4034}
4035
4036impl TryFrom<crate::client_server_contract::client_server_contract::CommandOrPlugin>
4037    for crate::data::CommandOrPlugin
4038{
4039    type Error = anyhow::Error;
4040
4041    fn try_from(
4042        proto: crate::client_server_contract::client_server_contract::CommandOrPlugin,
4043    ) -> Result<Self> {
4044        use crate::client_server_contract::client_server_contract::command_or_plugin::CommandOrPluginType;
4045
4046        let cmd_or_plugin_type = proto
4047            .command_or_plugin_type
4048            .ok_or_else(|| anyhow!("CommandOrPlugin missing command_or_plugin_type"))?;
4049        match cmd_or_plugin_type {
4050            CommandOrPluginType::Command(cmd) => {
4051                Ok(crate::data::CommandOrPlugin::Command(cmd.try_into()?))
4052            },
4053            CommandOrPluginType::Plugin(plugin) => {
4054                Ok(crate::data::CommandOrPlugin::Plugin(plugin.try_into()?))
4055            },
4056            CommandOrPluginType::File(f) => Ok(crate::data::CommandOrPlugin::File(
4057                crate::data::FileToOpen {
4058                    path: std::path::PathBuf::from(&f.path),
4059                    line_number: f.line_number.map(|n| n as usize),
4060                    cwd: f.cwd.map(std::path::PathBuf::from),
4061                },
4062            )),
4063        }
4064    }
4065}
4066
4067// Run reverse conversion
4068impl TryFrom<crate::client_server_contract::client_server_contract::Run>
4069    for crate::input::layout::Run
4070{
4071    type Error = anyhow::Error;
4072
4073    fn try_from(run: crate::client_server_contract::client_server_contract::Run) -> Result<Self> {
4074        use crate::client_server_contract::client_server_contract::run::RunType;
4075
4076        let run_type = run
4077            .run_type
4078            .ok_or_else(|| anyhow!("Run missing run_type"))?;
4079        match run_type {
4080            RunType::Command(cmd) => Ok(crate::input::layout::Run::Command(
4081                crate::input::command::RunCommand {
4082                    command: std::path::PathBuf::from(cmd.command),
4083                    args: cmd.args,
4084                    cwd: cmd.cwd.map(std::path::PathBuf::from),
4085                    hold_on_close: cmd.hold_on_close,
4086                    hold_on_start: cmd.hold_on_start,
4087                    originating_plugin: cmd
4088                        .originating_plugin
4089                        .map(|op| op.try_into())
4090                        .transpose()?,
4091                    use_terminal_title: cmd.use_terminal_title,
4092                },
4093            )),
4094            RunType::EditFile(edit) => Ok(crate::input::layout::Run::EditFile(
4095                std::path::PathBuf::from(edit.file_path),
4096                edit.line_number.map(|n| n as usize),
4097                edit.cwd.map(std::path::PathBuf::from),
4098            )),
4099            RunType::Cwd(cwd_str) => Ok(crate::input::layout::Run::Cwd(std::path::PathBuf::from(
4100                cwd_str,
4101            ))),
4102            RunType::Plugin(plugin) => Ok(crate::input::layout::Run::Plugin(plugin.try_into()?)),
4103        }
4104    }
4105}
4106
4107// PercentOrFixed reverse conversion
4108impl TryFrom<crate::client_server_contract::client_server_contract::PercentOrFixed>
4109    for crate::input::layout::PercentOrFixed
4110{
4111    type Error = anyhow::Error;
4112
4113    fn try_from(
4114        value: crate::client_server_contract::client_server_contract::PercentOrFixed,
4115    ) -> Result<Self> {
4116        use crate::client_server_contract::client_server_contract::percent_or_fixed::SizeType;
4117
4118        let size_type = value
4119            .size_type
4120            .ok_or_else(|| anyhow!("PercentOrFixed missing size_type"))?;
4121        match size_type {
4122            SizeType::Percent(percent) => Ok(crate::input::layout::PercentOrFixed::Percent(
4123                percent as usize,
4124            )),
4125            SizeType::Fixed(fixed) => {
4126                Ok(crate::input::layout::PercentOrFixed::Fixed(fixed as usize))
4127            },
4128        }
4129    }
4130}
4131
4132// ===== REVERSE CONVERSIONS =====
4133
4134// MouseEvent reverse conversion
4135impl TryFrom<crate::client_server_contract::client_server_contract::MouseEvent>
4136    for crate::input::mouse::MouseEvent
4137{
4138    type Error = anyhow::Error;
4139
4140    fn try_from(
4141        event: crate::client_server_contract::client_server_contract::MouseEvent,
4142    ) -> Result<Self> {
4143        use crate::client_server_contract::client_server_contract::MouseEventType as ProtoMouseEventType;
4144
4145        let event_type = match event.event_type {
4146            x if x == ProtoMouseEventType::Press as i32 => {
4147                crate::input::mouse::MouseEventType::Press
4148            },
4149            x if x == ProtoMouseEventType::Release as i32 => {
4150                crate::input::mouse::MouseEventType::Release
4151            },
4152            x if x == ProtoMouseEventType::Motion as i32 => {
4153                crate::input::mouse::MouseEventType::Motion
4154            },
4155            _ => return Err(anyhow!("Invalid MouseEventType: {}", event.event_type)),
4156        };
4157
4158        let position = event
4159            .position
4160            .ok_or_else(|| anyhow!("MouseEvent missing position"))?
4161            .try_into()?;
4162
4163        Ok(crate::input::mouse::MouseEvent {
4164            event_type,
4165            left: event.left,
4166            right: event.right,
4167            middle: event.middle,
4168            wheel_up: event.wheel_up,
4169            wheel_down: event.wheel_down,
4170            shift: event.shift,
4171            alt: event.alt,
4172            ctrl: event.ctrl,
4173            position,
4174        })
4175    }
4176}
4177
4178// RunCommandAction reverse conversion
4179impl TryFrom<crate::client_server_contract::client_server_contract::RunCommandAction>
4180    for crate::input::command::RunCommandAction
4181{
4182    type Error = anyhow::Error;
4183
4184    fn try_from(
4185        action: crate::client_server_contract::client_server_contract::RunCommandAction,
4186    ) -> Result<Self> {
4187        Ok(crate::input::command::RunCommandAction {
4188            command: std::path::PathBuf::from(action.command),
4189            args: action.args,
4190            cwd: action.cwd.map(std::path::PathBuf::from),
4191            direction: action.direction.map(proto_i32_to_direction).transpose()?,
4192            hold_on_close: action.hold_on_close,
4193            hold_on_start: action.hold_on_start,
4194            originating_plugin: action
4195                .originating_plugin
4196                .map(|op| op.try_into())
4197                .transpose()?,
4198            use_terminal_title: action.use_terminal_title,
4199        })
4200    }
4201}
4202
4203// TiledPaneLayout reverse conversion
4204impl TryFrom<crate::client_server_contract::client_server_contract::TiledPaneLayout>
4205    for crate::input::layout::TiledPaneLayout
4206{
4207    type Error = anyhow::Error;
4208
4209    fn try_from(
4210        layout: crate::client_server_contract::client_server_contract::TiledPaneLayout,
4211    ) -> Result<Self> {
4212        use crate::input::layout::{SplitDirection, SplitSize, TiledPaneLayout};
4213
4214        let children_split_direction = match layout.children_split_direction {
4215            x if x
4216                == crate::client_server_contract::client_server_contract::SplitDirection::Horizontal
4217                    as i32 =>
4218            {
4219                SplitDirection::Horizontal
4220            },
4221            x if x
4222                == crate::client_server_contract::client_server_contract::SplitDirection::Vertical
4223                    as i32 =>
4224            {
4225                SplitDirection::Vertical
4226            },
4227            _ => SplitDirection::Horizontal, // default
4228        };
4229
4230        let children: Result<Vec<_>> = layout.children.into_iter().map(|c| c.try_into()).collect();
4231        let run = layout.run.map(|r| r.try_into()).transpose()?;
4232
4233        let split_size = layout.split_size.and_then(|size| {
4234            use crate::client_server_contract::client_server_contract::split_size::SizeType;
4235            match size.size_type {
4236                Some(SizeType::Percent(percent)) => Some(SplitSize::Percent(percent as usize)),
4237                Some(SizeType::Fixed(fixed)) => Some(SplitSize::Fixed(fixed as usize)),
4238                None => None,
4239            }
4240        });
4241
4242        Ok(TiledPaneLayout {
4243            children_split_direction,
4244            name: layout.name,
4245            children: children?,
4246            split_size,
4247            run,
4248            borderless: layout.borderless,
4249            focus: layout.focus.map(|f| f == "true"), // Convert string to bool
4250            external_children_index: layout.external_children_index.map(|l| l as usize),
4251            children_are_stacked: layout.children_are_stacked,
4252            is_expanded_in_stack: layout.is_expanded_in_stack,
4253            exclude_from_sync: layout.exclude_from_sync,
4254            run_instructions_to_ignore: vec![], // not represented in protobuf
4255            hide_floating_panes: layout.hide_floating_panes,
4256            pane_initial_contents: layout.pane_initial_contents,
4257            default_fg: layout.default_fg,
4258            default_bg: layout.default_bg,
4259        })
4260    }
4261}
4262
4263// FloatingPaneLayout reverse conversion
4264impl TryFrom<crate::client_server_contract::client_server_contract::FloatingPaneLayout>
4265    for crate::input::layout::FloatingPaneLayout
4266{
4267    type Error = anyhow::Error;
4268
4269    fn try_from(
4270        layout: crate::client_server_contract::client_server_contract::FloatingPaneLayout,
4271    ) -> Result<Self> {
4272        let run = layout.run.map(|r| r.try_into()).transpose()?;
4273        let height = layout.height.map(|h| h.try_into()).transpose()?;
4274        let width = layout.width.map(|w| w.try_into()).transpose()?;
4275        let x = layout.x.map(|x| x.try_into()).transpose()?;
4276        let y = layout.y.map(|y| y.try_into()).transpose()?;
4277
4278        Ok(crate::input::layout::FloatingPaneLayout {
4279            name: layout.name,
4280            height,
4281            width,
4282            x,
4283            y,
4284            pinned: layout.pinned,
4285            run,
4286            focus: layout.focus,
4287            already_running: layout.already_running,
4288            pane_initial_contents: layout.pane_initial_contents,
4289            logical_position: layout.logical_position.map(|p| p as usize),
4290            borderless: layout.borderless,
4291            default_fg: layout.default_fg,
4292            default_bg: layout.default_bg,
4293        })
4294    }
4295}
4296
4297// SwapTiledLayout reverse conversion
4298impl TryFrom<crate::client_server_contract::client_server_contract::SwapTiledLayout>
4299    for crate::input::layout::SwapTiledLayout
4300{
4301    type Error = anyhow::Error;
4302
4303    fn try_from(
4304        layout: crate::client_server_contract::client_server_contract::SwapTiledLayout,
4305    ) -> Result<Self> {
4306        let constraint_map: Result<BTreeMap<_, _>> = layout
4307            .constraint_map
4308            .into_iter()
4309            .map(|pair| {
4310                Ok((
4311                    pair.constraint
4312                        .ok_or_else(|| anyhow!("Missing constraint"))?
4313                        .try_into()?,
4314                    pair.layout
4315                        .ok_or_else(|| anyhow!("Missing layout"))?
4316                        .try_into()?,
4317                ))
4318            })
4319            .collect();
4320        Ok((constraint_map?, layout.name))
4321    }
4322}
4323
4324// SwapFloatingLayout reverse conversion
4325impl TryFrom<crate::client_server_contract::client_server_contract::SwapFloatingLayout>
4326    for crate::input::layout::SwapFloatingLayout
4327{
4328    type Error = anyhow::Error;
4329
4330    fn try_from(
4331        layout: crate::client_server_contract::client_server_contract::SwapFloatingLayout,
4332    ) -> Result<Self> {
4333        let constraint_map: Result<BTreeMap<_, _>> = layout
4334            .constraint_map
4335            .into_iter()
4336            .map(|pair| {
4337                let floating_layouts: Result<Vec<_>> =
4338                    pair.layouts.into_iter().map(|l| l.try_into()).collect();
4339                Ok((
4340                    pair.constraint
4341                        .ok_or_else(|| anyhow!("Missing constraint"))?
4342                        .try_into()?,
4343                    floating_layouts?,
4344                ))
4345            })
4346            .collect();
4347
4348        Ok((constraint_map?, layout.name))
4349    }
4350}
4351
4352// PluginUserConfiguration reverse conversion
4353impl TryFrom<crate::client_server_contract::client_server_contract::PluginUserConfiguration>
4354    for crate::input::layout::PluginUserConfiguration
4355{
4356    type Error = anyhow::Error;
4357
4358    fn try_from(
4359        config: crate::client_server_contract::client_server_contract::PluginUserConfiguration,
4360    ) -> Result<Self> {
4361        let btree_map: BTreeMap<String, String> = config.configuration.into_iter().collect();
4362        Ok(crate::input::layout::PluginUserConfiguration::new(
4363            btree_map,
4364        ))
4365    }
4366}
4367
4368// LayoutConstraint reverse conversion
4369impl TryFrom<crate::client_server_contract::client_server_contract::LayoutConstraintWithValue>
4370    for crate::input::layout::LayoutConstraint
4371{
4372    type Error = anyhow::Error;
4373
4374    fn try_from(
4375        constraint: crate::client_server_contract::client_server_contract::LayoutConstraintWithValue,
4376    ) -> Result<Self> {
4377        use crate::client_server_contract::client_server_contract::LayoutConstraint as ProtoLayoutConstraint;
4378        match constraint.constraint_type {
4379            x if x == ProtoLayoutConstraint::MaxPanes as i32 => {
4380                let value = constraint
4381                    .value
4382                    .ok_or_else(|| anyhow!("MaxPanes constraint missing value"))?
4383                    as usize;
4384                Ok(crate::input::layout::LayoutConstraint::MaxPanes(value))
4385            },
4386            x if x == ProtoLayoutConstraint::MinPanes as i32 => {
4387                let value = constraint
4388                    .value
4389                    .ok_or_else(|| anyhow!("MinPanes constraint missing value"))?
4390                    as usize;
4391                Ok(crate::input::layout::LayoutConstraint::MinPanes(value))
4392            },
4393            x if x == ProtoLayoutConstraint::ExactPanes as i32 => {
4394                let value = constraint
4395                    .value
4396                    .ok_or_else(|| anyhow!("ExactPanes constraint missing value"))?
4397                    as usize;
4398                Ok(crate::input::layout::LayoutConstraint::ExactPanes(value))
4399            },
4400            x if x == ProtoLayoutConstraint::NoConstraint as i32 => {
4401                Ok(crate::input::layout::LayoutConstraint::NoConstraint)
4402            },
4403            _ => Err(anyhow!(
4404                "Invalid LayoutConstraint type: {}",
4405                constraint.constraint_type
4406            )),
4407        }
4408    }
4409}
4410
4411// RunPlugin reverse conversion
4412impl TryFrom<crate::client_server_contract::client_server_contract::RunPlugin>
4413    for crate::input::layout::RunPlugin
4414{
4415    type Error = anyhow::Error;
4416
4417    fn try_from(
4418        plugin: crate::client_server_contract::client_server_contract::RunPlugin,
4419    ) -> Result<Self> {
4420        let location = plugin
4421            .location
4422            .ok_or_else(|| anyhow!("RunPlugin missing location"))?
4423            .try_into()?;
4424        let configuration = plugin
4425            .configuration
4426            .ok_or_else(|| anyhow!("RunPlugin missing configuration"))?
4427            .try_into()?;
4428        let initial_cwd = plugin.initial_cwd.map(std::path::PathBuf::from);
4429
4430        Ok(crate::input::layout::RunPlugin {
4431            _allow_exec_host_cmd: plugin.allow_exec_host_cmd,
4432            location,
4433            configuration,
4434            initial_cwd,
4435        })
4436    }
4437}
4438
4439// PluginAlias reverse conversion
4440impl TryFrom<crate::client_server_contract::client_server_contract::PluginAlias>
4441    for crate::input::layout::PluginAlias
4442{
4443    type Error = anyhow::Error;
4444
4445    fn try_from(
4446        plugin_alias: crate::client_server_contract::client_server_contract::PluginAlias,
4447    ) -> Result<Self> {
4448        let run_plugin = plugin_alias.run_plugin.and_then(|r| r.try_into().ok());
4449        let configuration = plugin_alias.configuration.and_then(|c| c.try_into().ok());
4450        let initial_cwd = plugin_alias.initial_cwd.map(std::path::PathBuf::from);
4451        Ok(crate::input::layout::PluginAlias {
4452            name: plugin_alias.name,
4453            configuration,
4454            initial_cwd,
4455            run_plugin,
4456        })
4457    }
4458}
4459
4460// RunPluginLocation reverse conversion
4461impl TryFrom<crate::client_server_contract::client_server_contract::RunPluginLocationData>
4462    for crate::input::layout::RunPluginLocation
4463{
4464    type Error = anyhow::Error;
4465
4466    fn try_from(
4467        location: crate::client_server_contract::client_server_contract::RunPluginLocationData,
4468    ) -> Result<Self> {
4469        use crate::client_server_contract::client_server_contract::{
4470            run_plugin_location_data::LocationData, RunPluginLocation as ProtoRunPluginLocation,
4471        };
4472
4473        let location_data = location
4474            .location_data
4475            .ok_or_else(|| anyhow!("RunPluginLocationData missing location_data"))?;
4476        match location.location_type {
4477            x if x == ProtoRunPluginLocation::File as i32 => {
4478                if let LocationData::FilePath(path) = location_data {
4479                    Ok(crate::input::layout::RunPluginLocation::File(
4480                        std::path::PathBuf::from(path),
4481                    ))
4482                } else {
4483                    Err(anyhow!("File location type but wrong data variant"))
4484                }
4485            },
4486            x if x == ProtoRunPluginLocation::Zellij as i32 => {
4487                if let LocationData::ZellijTag(tag) = location_data {
4488                    Ok(crate::input::layout::RunPluginLocation::Zellij(
4489                        crate::data::PluginTag::new(tag.tag),
4490                    ))
4491                } else {
4492                    Err(anyhow!("Zellij location type but wrong data variant"))
4493                }
4494            },
4495            x if x == ProtoRunPluginLocation::Remote as i32 => {
4496                if let LocationData::RemoteUrl(url) = location_data {
4497                    Ok(crate::input::layout::RunPluginLocation::Remote(url))
4498                } else {
4499                    Err(anyhow!("Remote location type but wrong data variant"))
4500                }
4501            },
4502            _ => Err(anyhow!(
4503                "Invalid RunPluginLocation type: {}",
4504                location.location_type
4505            )),
4506        }
4507    }
4508}
4509
4510// RunPluginOrAlias reverse conversion
4511impl TryFrom<crate::client_server_contract::client_server_contract::RunPluginOrAlias>
4512    for crate::input::layout::RunPluginOrAlias
4513{
4514    type Error = anyhow::Error;
4515
4516    fn try_from(
4517        plugin: crate::client_server_contract::client_server_contract::RunPluginOrAlias,
4518    ) -> Result<Self> {
4519        use crate::client_server_contract::client_server_contract::run_plugin_or_alias::PluginType;
4520
4521        let plugin_type = plugin
4522            .plugin_type
4523            .ok_or_else(|| anyhow!("RunPluginOrAlias missing plugin_type"))?;
4524        match plugin_type {
4525            PluginType::Plugin(run_plugin) => Ok(
4526                crate::input::layout::RunPluginOrAlias::RunPlugin(run_plugin.try_into()?),
4527            ),
4528            PluginType::Alias(plugin_alias) => Ok(crate::input::layout::RunPluginOrAlias::Alias(
4529                plugin_alias.try_into()?,
4530            )),
4531        }
4532    }
4533}