Skip to main content

zellij_utils/ipc/
protobuf_conversion.rs

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