1use crate::{
3 data::{ClientId, ConnectToSession, HostTerminalThemeMode, KeyWithModifier, PaneId, Style},
4 errors::{prelude::*, ErrorContext},
5 input::{actions::Action, cli_assets::CliAssets},
6 pane_size::{Size, SizeInPixels},
7};
8use interprocess::local_socket::Stream as LocalSocketStream;
9use log::warn;
10use serde::{Deserialize, Serialize};
11use std::{
12 fmt::{Display, Error, Formatter},
13 io::{self, Read, Write},
14 marker::PhantomData,
15};
16
17use crate::client_server_contract::client_server_contract::{
19 ClientToServerMsg as ProtoClientToServerMsg, ServerToClientMsg as ProtoServerToClientMsg,
20};
21use prost::Message;
22
23mod enum_conversions;
24mod protobuf_conversion;
25
26#[cfg(test)]
27mod tests;
28
29type SessionId = u64;
30
31pub trait IpcStream: Read + Write + Send + 'static {
33 fn try_clone_stream(&self) -> io::Result<Box<dyn IpcStream>>;
34}
35
36impl IpcStream for LocalSocketStream {
37 fn try_clone_stream(&self) -> io::Result<Box<dyn IpcStream>> {
38 use interprocess::TryClone;
39 Ok(Box::new(self.try_clone()?))
40 }
41}
42
43#[derive(PartialEq, Eq, Serialize, Deserialize, Hash)]
44pub struct Session {
45 id: SessionId,
47 conn_name: String,
49 alias: String,
51}
52
53#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55pub enum ClientType {
56 Reader,
57 Writer,
58}
59
60#[derive(Default, Serialize, Deserialize, Debug, Clone)]
61pub struct ClientAttributes {
62 pub size: Size,
63 pub style: Style,
64}
65
66#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
67pub struct PixelDimensions {
68 pub text_area_size: Option<SizeInPixels>,
69 pub character_cell_size: Option<SizeInPixels>,
70}
71
72#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
73pub struct PaneReference {
74 pub pane_id: u32,
75 pub is_plugin: bool,
76}
77
78#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
79pub struct ColorRegister {
80 pub index: usize,
81 pub color: String,
82}
83
84impl PixelDimensions {
85 pub fn merge(&mut self, other: PixelDimensions) {
86 if let Some(text_area_size) = other.text_area_size {
87 self.text_area_size = Some(text_area_size);
88 }
89 if let Some(character_cell_size) = other.character_cell_size {
90 self.character_cell_size = Some(character_cell_size);
91 }
92 }
93}
94
95#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
96pub struct MobileSizePayload {
97 pub cols: usize,
98 pub rows: usize,
99}
100
101#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
102pub struct MobileActivePanePayload {
103 pub pane_id: u32,
104 pub is_plugin: bool,
105 pub tab_position: usize,
106}
107
108#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
109pub struct MobileTabPayload {
110 pub position: usize,
111 pub name: String,
112 pub active: bool,
113}
114
115#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
116pub struct MobilePanePayload {
117 pub tab_position: usize,
118 pub pane_id: u32,
119 pub is_plugin: bool,
120 pub title: String,
121 pub is_floating: bool,
122 pub last_activity_secs_ago: u64,
123}
124
125#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
126pub struct MobileSessionPayload {
127 pub name: String,
128 pub web_clients_allowed: bool,
129 pub tab_count: usize,
130 pub pane_count: usize,
131 pub connected_clients: usize,
132 pub creation_secs_ago: u64,
133}
134
135#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
136pub struct MobileRenderPrefsPayload {
137 pub single_pane: bool,
138 pub fit: bool,
139 pub active_pane_is_fullscreen: bool,
140}
141
142#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
143pub struct MobileStatePayload {
144 pub session_name: String,
145 pub now_secs: u64,
146 pub is_welcome_screen: bool,
147 pub desktop_client_connected: bool,
148 pub desktop_size: Option<MobileSizePayload>,
149 pub active_pane: Option<MobileActivePanePayload>,
150 pub tabs: Vec<MobileTabPayload>,
151 pub panes: Vec<MobilePanePayload>,
152 pub sessions: Vec<MobileSessionPayload>,
153 pub render_prefs: MobileRenderPrefsPayload,
154}
155
156#[allow(clippy::large_enum_variant)]
158#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
159pub enum ClientToServerMsg {
160 DetachSession {
161 client_ids: Vec<ClientId>,
162 },
163 TerminalPixelDimensions {
164 pixel_dimensions: PixelDimensions,
165 },
166 BackgroundColor {
167 color: String,
168 },
169 ForegroundColor {
170 color: String,
171 },
172 ColorRegisters {
173 color_registers: Vec<ColorRegister>,
174 },
175 TerminalResize {
176 new_size: Size,
177 },
178 FirstClientConnected {
179 cli_assets: CliAssets,
180 is_web_client: bool,
181 },
182 AttachClient {
183 cli_assets: CliAssets,
184 tab_position_to_focus: Option<usize>,
185 pane_to_focus: Option<PaneReference>,
186 is_web_client: bool,
187 },
188 AttachWatcherClient {
189 terminal_size: Size,
190 is_web_client: bool,
191 },
192 Action {
193 action: Action,
194 terminal_id: Option<u32>,
195 client_id: Option<ClientId>,
196 is_cli_client: bool,
197 },
198 Key {
199 key: KeyWithModifier,
200 raw_bytes: Vec<u8>,
201 is_kitty_keyboard_protocol: bool,
202 },
203 ClientExited,
204 KillSession,
205 ConnStatus,
206 WebServerStarted {
207 base_url: String,
208 },
209 FailedToStartWebServer {
210 error: String,
211 },
212 SubscribeToPaneRenders {
213 pane_ids: Vec<PaneId>,
214 scrollback: Option<usize>,
215 ansi: bool,
216 },
217 DesktopNotificationResponse {
218 raw_bytes: Vec<u8>,
219 },
220 ForwardedReplyFromHost {
221 token: u32,
222 reply_bytes: Vec<u8>,
223 },
224 HostTerminalThemeChanged {
225 mode: HostTerminalThemeMode,
226 },
227 SoftKeyboardVisibilityChanged {
228 visible: bool,
229 },
230 NestedSessionFrameFromHost {
231 payload_bytes: Vec<u8>,
232 },
233 KittyGraphicsSupport {
234 supported: bool,
235 },
236 SixelSupport {
237 supported: bool,
238 },
239 RequestSessionList,
240 SetMobileRenderPreferences {
241 single_pane: bool,
242 fit: bool,
243 },
244 HostTerminalFocusChanged {
245 focused: bool,
246 },
247}
248
249#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
251pub enum ServerToClientMsg {
252 Render {
253 content: String,
254 },
255 UnblockInputThread,
256 Exit {
257 exit_reason: ExitReason,
258 },
259 Connected,
260 Log {
261 lines: Vec<String>,
262 },
263 LogError {
264 lines: Vec<String>,
265 },
266 SwitchSession {
267 connect_to_session: ConnectToSession,
268 },
269 UnblockCliPipeInput {
270 pipe_name: String,
271 },
272 CliPipeOutput {
273 pipe_name: String,
274 output: String,
275 },
276 QueryTerminalSize,
277 SetSoftKeyboard {
278 on: bool,
279 },
280 StartWebServer,
281 RenamedSession {
282 name: String,
283 },
284 ConfigFileUpdated,
285 PaneRenderUpdate {
286 pane_id: PaneId,
287 viewport: Vec<String>,
288 scrollback: Option<Vec<String>>,
289 is_initial: bool,
290 },
291 SubscribedPaneClosed {
292 pane_id: PaneId,
293 },
294 ForwardQueryToHost {
295 token: u32,
296 query_bytes: Vec<u8>,
297 resolve_async: bool,
298 },
299 EmitNestedSessionFrame {
300 payload_bytes: Vec<u8>,
301 },
302 MobileState {
303 payload: MobileStatePayload,
304 },
305}
306
307#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
308pub enum ExitReason {
309 Normal,
310 NormalDetached,
311 ForceDetached,
312 CannotAttach,
313 Disconnect,
314 WebClientsForbidden,
315 KickedByHost,
316 CustomExitStatus(i32),
317 Error(String),
318}
319
320impl Display for ExitReason {
321 fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
322 match self {
323 Self::Normal => write!(f, "Bye from Zellij!"),
324 Self::NormalDetached => write!(f, "Session detached"),
325 Self::ForceDetached => write!(
326 f,
327 "Session was detached from this client (possibly because another client connected)"
328 ),
329 Self::CannotAttach => write!(
330 f,
331 "Session attached to another client. Use --force flag to force connect."
332 ),
333 Self::WebClientsForbidden => write!(
334 f,
335 "Web clients are not allowed in this session - cannot attach"
336 ),
337 Self::Disconnect => {
338 let session_tip = match crate::envs::get_session_name() {
339 Ok(name) => format!("`zellij attach {}`", name),
340 Err(_) => "see `zellij ls` and `zellij attach`".to_string(),
341 };
342 write!(
343 f,
344 "
345Your zellij client lost connection to the zellij server.
346
347As a safety measure, you have been disconnected from the current zellij session.
348However, the session should still exist and none of your data should be lost.
349
350This usually means that your terminal didn't process server messages quick
351enough. Maybe your system is currently under high load, or your terminal
352isn't performant enough.
353
354There are a few things you can try now:
355 - Reattach to your previous session and see if it works out better this
356 time: {session_tip}
357 - Try using a faster (maybe GPU-accelerated) terminal emulator
358 "
359 )
360 },
361 Self::KickedByHost => write!(f, "Disconnected by host"),
362 Self::CustomExitStatus(exit_status) => write!(f, "Exit {}", exit_status),
363 Self::Error(e) => write!(f, "Error occurred in server:\n{}", e),
364 }
365 }
366}
367
368pub struct IpcSenderWithContext<T: Serialize> {
370 sender: io::BufWriter<Box<dyn IpcStream>>,
371 _phantom: PhantomData<T>,
372}
373
374impl<T: Serialize> IpcSenderWithContext<T> {
375 pub fn new(sender: LocalSocketStream) -> Self {
377 Self {
378 sender: io::BufWriter::new(Box::new(sender)),
379 _phantom: PhantomData,
380 }
381 }
382
383 fn from_boxed(sender: Box<dyn IpcStream>) -> Self {
384 Self {
385 sender: io::BufWriter::new(sender),
386 _phantom: PhantomData,
387 }
388 }
389
390 pub fn send_client_msg(&mut self, msg: ClientToServerMsg) -> Result<()> {
391 let proto_msg: ProtoClientToServerMsg = msg.into();
392 write_protobuf_message(&mut self.sender, &proto_msg)?;
393 let _ = self.sender.flush();
394 Ok(())
395 }
396
397 pub fn send_server_msg(&mut self, msg: ServerToClientMsg) -> Result<()> {
398 let proto_msg: ProtoServerToClientMsg = msg.into();
399 write_protobuf_message(&mut self.sender, &proto_msg)?;
400 let _ = self.sender.flush();
401 Ok(())
402 }
403
404 pub fn get_receiver<F>(&self) -> IpcReceiverWithContext<F>
406 where
407 F: for<'de> Deserialize<'de> + Serialize,
408 {
409 let socket = self.sender.get_ref().try_clone_stream().unwrap();
410 IpcReceiverWithContext::from_boxed(socket)
411 }
412}
413
414pub struct IpcReceiverWithContext<T> {
416 receiver: io::BufReader<Box<dyn IpcStream>>,
417 _phantom: PhantomData<T>,
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub enum IpcReceiveError {
422 Disconnected,
423 Undecodable,
424}
425
426impl Display for IpcReceiveError {
427 fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> {
428 match self {
429 IpcReceiveError::Disconnected => write!(f, "the peer closed the connection"),
430 IpcReceiveError::Undecodable => write!(f, "received a message that could not be read"),
431 }
432 }
433}
434
435impl<T> IpcReceiverWithContext<T>
436where
437 T: for<'de> Deserialize<'de> + Serialize,
438{
439 pub fn new(receiver: LocalSocketStream) -> Self {
441 Self {
442 receiver: io::BufReader::new(Box::new(receiver)),
443 _phantom: PhantomData,
444 }
445 }
446
447 fn from_boxed(receiver: Box<dyn IpcStream>) -> Self {
448 Self {
449 receiver: io::BufReader::new(receiver),
450 _phantom: PhantomData,
451 }
452 }
453
454 pub fn recv_client_msg(&mut self) -> Option<(ClientToServerMsg, ErrorContext)> {
455 self.try_recv_client_msg().ok()
456 }
457
458 pub fn recv_server_msg(&mut self) -> Option<(ServerToClientMsg, ErrorContext)> {
459 self.try_recv_server_msg().ok()
460 }
461
462 pub fn try_recv_client_msg(
463 &mut self,
464 ) -> std::result::Result<(ClientToServerMsg, ErrorContext), IpcReceiveError> {
465 let proto_msg = read_protobuf_message::<ProtoClientToServerMsg>(&mut self.receiver)?;
466 match proto_msg.try_into() {
467 Ok(rust_msg) => Ok((rust_msg, ErrorContext::default())),
468 Err(e) => {
469 warn!("Error converting protobuf to ClientToServerMsg: {:?}", e);
470 Err(IpcReceiveError::Undecodable)
471 },
472 }
473 }
474
475 pub fn try_recv_server_msg(
476 &mut self,
477 ) -> std::result::Result<(ServerToClientMsg, ErrorContext), IpcReceiveError> {
478 let proto_msg = read_protobuf_message::<ProtoServerToClientMsg>(&mut self.receiver)?;
479 match proto_msg.try_into() {
480 Ok(rust_msg) => Ok((rust_msg, ErrorContext::default())),
481 Err(e) => {
482 warn!("Error converting protobuf to ServerToClientMsg: {:?}", e);
483 Err(IpcReceiveError::Undecodable)
484 },
485 }
486 }
487
488 pub fn get_sender<F: Serialize>(&self) -> IpcSenderWithContext<F> {
490 let socket = self.receiver.get_ref().try_clone_stream().unwrap();
491 IpcSenderWithContext::from_boxed(socket)
492 }
493}
494
495fn read_protobuf_message<T: Message + Default>(
497 reader: &mut impl Read,
498) -> std::result::Result<T, IpcReceiveError> {
499 let mut len_bytes = [0u8; 4];
501 reader
502 .read_exact(&mut len_bytes)
503 .map_err(|_| IpcReceiveError::Disconnected)?;
504 let len = u32::from_le_bytes(len_bytes) as usize;
505
506 let mut buf = vec![0u8; len];
507 reader
508 .read_exact(&mut buf)
509 .map_err(|_| IpcReceiveError::Disconnected)?;
510
511 T::decode(&buf[..]).map_err(|_| IpcReceiveError::Undecodable)
512}
513
514fn write_protobuf_message<T: Message>(writer: &mut impl Write, msg: &T) -> Result<()> {
515 let encoded = msg.encode_to_vec();
516 let len = encoded.len() as u32;
517
518 writer.write_all(&len.to_le_bytes())?;
523 writer.write_all(&encoded)?;
524 Ok(())
525}
526
527pub fn send_protobuf_client_to_server(
529 sender: &mut IpcSenderWithContext<ClientToServerMsg>,
530 msg: ClientToServerMsg,
531) -> Result<()> {
532 let proto_msg: ProtoClientToServerMsg = msg.into();
533 write_protobuf_message(&mut sender.sender, &proto_msg)?;
534 let _ = sender.sender.flush();
535 Ok(())
536}
537
538pub fn send_protobuf_server_to_client(
539 sender: &mut IpcSenderWithContext<ServerToClientMsg>,
540 msg: ServerToClientMsg,
541) -> Result<()> {
542 let proto_msg: ProtoServerToClientMsg = msg.into();
543 write_protobuf_message(&mut sender.sender, &proto_msg)?;
544 let _ = sender.sender.flush();
545 Ok(())
546}
547
548pub fn recv_protobuf_client_to_server(
549 receiver: &mut IpcReceiverWithContext<ClientToServerMsg>,
550) -> Option<(ClientToServerMsg, ErrorContext)> {
551 receiver.try_recv_client_msg().ok()
552}
553
554pub fn recv_protobuf_server_to_client(
555 receiver: &mut IpcReceiverWithContext<ServerToClientMsg>,
556) -> Option<(ServerToClientMsg, ErrorContext)> {
557 receiver.try_recv_server_msg().ok()
558}
559
560#[cfg(unix)]
571pub async fn async_send_kill_and_await(path: &std::path::Path) -> io::Result<()> {
572 use interprocess::local_socket::traits::tokio::Stream as _;
573 use interprocess::local_socket::{prelude::*, GenericFilePath};
574 use tokio::io::{AsyncReadExt, AsyncWriteExt};
575
576 let fs_name = path.to_fs_name::<GenericFilePath>()?;
577 let mut stream: interprocess::local_socket::tokio::Stream =
578 interprocess::local_socket::tokio::Stream::connect(fs_name).await?;
579
580 let proto_msg: ProtoClientToServerMsg = crate::ipc::ClientToServerMsg::KillSession.into();
581 let encoded = proto_msg.encode_to_vec();
582 let len_bytes = (encoded.len() as u32).to_le_bytes();
583
584 stream.write_all(&len_bytes).await?;
585 stream.write_all(&encoded).await?;
586 let _ = stream.flush().await;
588
589 let mut len_buf = [0u8; 4];
595 let _ = stream.read_exact(&mut len_buf).await;
596 Ok(())
597}
598
599#[cfg(windows)]
600pub async fn async_send_kill_and_await(path: &std::path::Path) -> io::Result<()> {
601 let path = path.to_path_buf();
602 tokio::task::spawn_blocking(move || {
603 use crate::consts::{ipc_connect, ipc_connect_reply};
604 let stream = ipc_connect(&path)?;
605 let reply = ipc_connect_reply(&path);
606 let mut sender = IpcSenderWithContext::<ClientToServerMsg>::new(stream);
607 sender
608 .send_client_msg(ClientToServerMsg::KillSession)
609 .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
610 if let Ok(reply_stream) = reply {
611 let mut receiver: IpcReceiverWithContext<ServerToClientMsg> =
612 IpcReceiverWithContext::new(reply_stream);
613 let _ = receiver.recv_server_msg();
614 }
615 Ok::<(), io::Error>(())
616 })
617 .await
618 .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?
619}