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