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