Skip to main content

zellij_client/
lib.rs

1pub mod os_input_output;
2
3#[cfg(not(windows))]
4#[path = "os_input_output_unix.rs"]
5mod os_input_output_unix;
6#[cfg(windows)]
7#[path = "os_input_output_windows.rs"]
8mod os_input_output_windows;
9
10pub mod cli_client;
11mod command_is_executing;
12mod input_handler;
13mod keyboard_parser;
14pub mod old_config_converter;
15#[cfg(feature = "web_server_capability")]
16pub mod remote_attach;
17mod stdin_ansi_parser;
18mod stdin_handler;
19#[cfg(windows)]
20mod stdin_handler_windows;
21#[cfg(feature = "web_server_capability")]
22pub mod web_client;
23
24use log::info;
25use std::env::current_exe;
26use std::io::{self, Write};
27use std::net::{IpAddr, Ipv4Addr};
28use std::path::{Path, PathBuf};
29use std::process::Command;
30use std::sync::{Arc, Mutex};
31use std::thread;
32use zellij_utils::errors::FatalError;
33use zellij_utils::shared::web_server_base_url;
34
35#[cfg(feature = "web_server_capability")]
36use futures_util::{SinkExt, StreamExt};
37#[cfg(feature = "web_server_capability")]
38use tokio_tungstenite::tungstenite::Message;
39
40#[cfg(feature = "web_server_capability")]
41use crate::web_client::control_message::{
42    WebClientToWebServerControlMessage, WebClientToWebServerControlMessagePayload,
43    WebServerToWebClientControlMessage,
44};
45
46static ASYNC_RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
47use std::sync::OnceLock;
48
49const ENTER_ALTERNATE_SCREEN: &str = "\u{1b}[?1049h";
50const EXIT_ALTERNATE_SCREEN: &str = "\u{1b}[?1049l";
51const ENABLE_BRACKETED_PASTE: &str = "\u{1b}[?2004h";
52const RESET_STYLE: &str = "\u{1b}[m";
53const SHOW_CURSOR: &str = "\u{1b}[?25h";
54const ENTER_KITTY_KEYBOARD_MODE: &str = "\u{1b}[>1u";
55const EXIT_KITTY_KEYBOARD_MODE: &str = "\u{1b}[<1u";
56const CLEAR_CLIENT_TERMINAL_ATTRIBUTES: &str = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l";
57/// Subscribe to host color-palette theme notifications (CSI 2031). Hosts
58/// that support it begin emitting unsolicited DSR 997 reports on theme
59/// change after this is sent.
60const ENABLE_HOST_THEME_NOTIFY: &str = "\u{1b}[?2031h";
61/// Cancel the CSI 2031 subscription (sent on detach / shutdown so we
62/// don't leave the host emitting DSR 997s into nothing).
63const DISABLE_HOST_THEME_NOTIFY: &str = "\u{1b}[?2031l";
64/// Actively query the current host theme (DSR 996). Reply arrives in the
65/// same `CSI ? 997 ; {1|2} n` form as unsolicited notifications, so the
66/// stdin parser handles both uniformly.
67const QUERY_HOST_THEME: &str = "\u{1b}[?996n";
68
69/// Spawn an async runtime for this client instance.
70///
71/// The number of workers can be configured to any nonzero value. Passing zero or `None` will spawn
72/// one worker per physical CPU on the current machine.
73pub(crate) fn async_runtime(maybe_number_of_workers: Option<usize>) -> tokio::runtime::Handle {
74    match tokio::runtime::Handle::try_current() {
75        Ok(handle) => handle.clone(),
76        _ => {
77            let number_of_workers = match maybe_number_of_workers {
78                Some(value) if value > 0 => {
79                    log::debug!(
80                        "Creating client async runtime with {} tasks based on user request",
81                        value
82                    );
83                    value
84                },
85                _ => {
86                    let cpus = num_cpus::get_physical();
87                    log::debug!(
88                        "Creating client async runtime with {} tasks based on CPU count",
89                        cpus
90                    );
91                    cpus
92                },
93            };
94            let runtime = ASYNC_RUNTIME.get_or_init(|| {
95                tokio::runtime::Builder::new_multi_thread()
96                    .worker_threads(number_of_workers)
97                    .thread_name("zellij client async-runtime")
98                    .enable_all()
99                    .build()
100                    .expect("Failed to create tokio runtime")
101            });
102            runtime.handle().clone()
103        },
104    }
105}
106
107#[derive(Debug)]
108pub enum RemoteClientError {
109    InvalidAuthToken,
110    SessionTokenExpired,
111    Unauthorized,
112    ConnectionFailed(String),
113    UrlParseError(url::ParseError),
114    IoError(std::io::Error),
115    Other(Box<dyn std::error::Error + Send + Sync>),
116}
117
118impl std::fmt::Display for RemoteClientError {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        match self {
121            RemoteClientError::InvalidAuthToken => write!(f, "Invalid authentication token"),
122            RemoteClientError::SessionTokenExpired => write!(f, "Session token expired"),
123            RemoteClientError::Unauthorized => write!(f, "Unauthorized"),
124            RemoteClientError::ConnectionFailed(msg) => write!(f, "Connection failed: {}", msg),
125            RemoteClientError::UrlParseError(e) => write!(f, "Invalid URL: {}", e),
126            RemoteClientError::IoError(e) => write!(f, "IO error: {}", e),
127            RemoteClientError::Other(e) => write!(f, "{}", e),
128        }
129    }
130}
131
132impl std::error::Error for RemoteClientError {}
133
134impl From<url::ParseError> for RemoteClientError {
135    fn from(error: url::ParseError) -> Self {
136        RemoteClientError::UrlParseError(error)
137    }
138}
139
140impl From<std::io::Error> for RemoteClientError {
141    fn from(error: std::io::Error) -> Self {
142        RemoteClientError::IoError(error)
143    }
144}
145
146use crate::stdin_ansi_parser::{AnsiStdinInstruction, StdinAnsiParser, SyncOutput};
147use crate::{
148    command_is_executing::CommandIsExecuting, input_handler::input_loop,
149    os_input_output::ClientOsApi, stdin_handler::stdin_loop,
150};
151use zellij_utils::cli::CliArgs;
152use zellij_utils::{
153    channels::{self, ChannelWithContext, SenderWithContext},
154    consts::{set_permissions, ZELLIJ_SOCK_DIR},
155    data::{ClientId, ConnectToSession, KeyWithModifier, LayoutInfo, LayoutMetadata},
156    envs,
157    errors::{ClientContext, ContextType, ErrorInstruction},
158    input::{cli_assets::CliAssets, config::Config, options::Options},
159    ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg},
160    pane_size::Size,
161    vendored::termwiz::input::InputEvent,
162};
163
164/// Instructions related to the client-side application
165#[derive(Debug, Clone)]
166pub(crate) enum ClientInstruction {
167    Error(String),
168    Render(String),
169    UnblockInputThread,
170    Exit(ExitReason),
171    Connected,
172    Log(Vec<String>),
173    LogError(Vec<String>),
174    SwitchSession(ConnectToSession),
175    SetSynchronizedOutput(Option<SyncOutput>),
176    UnblockCliPipeInput(()), // String -> pipe name
177    CliPipeOutput((), ()),   // String -> pipe name, String -> output
178    QueryTerminalSize,
179    StartWebServer,
180    #[allow(dead_code)] // we need the session name here even though we're not currently using it
181    RenamedSession(String), // String -> new session name
182    ConfigFileUpdated,
183    /// Server asked us to forward `query_bytes` to the host terminal and
184    /// collect the reply bytes into the window identified by `token`.
185    ForwardQueryToHost {
186        token: u32,
187        query_bytes: Vec<u8>,
188    },
189}
190
191impl From<ServerToClientMsg> for ClientInstruction {
192    fn from(instruction: ServerToClientMsg) -> Self {
193        match instruction {
194            ServerToClientMsg::Exit { exit_reason } => ClientInstruction::Exit(exit_reason),
195            ServerToClientMsg::Render { content } => ClientInstruction::Render(content),
196            ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread,
197            ServerToClientMsg::Connected => ClientInstruction::Connected,
198            ServerToClientMsg::Log { lines } => ClientInstruction::Log(lines),
199            ServerToClientMsg::LogError { lines } => ClientInstruction::LogError(lines),
200            ServerToClientMsg::SwitchSession { connect_to_session } => {
201                ClientInstruction::SwitchSession(connect_to_session)
202            },
203            ServerToClientMsg::UnblockCliPipeInput { .. } => {
204                ClientInstruction::UnblockCliPipeInput(())
205            },
206            ServerToClientMsg::CliPipeOutput { .. } => ClientInstruction::CliPipeOutput((), ()),
207            ServerToClientMsg::QueryTerminalSize => ClientInstruction::QueryTerminalSize,
208            ServerToClientMsg::StartWebServer => ClientInstruction::StartWebServer,
209            ServerToClientMsg::RenamedSession { name } => ClientInstruction::RenamedSession(name),
210            ServerToClientMsg::ConfigFileUpdated => ClientInstruction::ConfigFileUpdated,
211            ServerToClientMsg::ForwardQueryToHost { token, query_bytes } => {
212                ClientInstruction::ForwardQueryToHost { token, query_bytes }
213            },
214            // Subscribe-only messages — not handled by regular interactive clients
215            ServerToClientMsg::PaneRenderUpdate { .. } => ClientInstruction::UnblockInputThread,
216            ServerToClientMsg::SubscribedPaneClosed { .. } => ClientInstruction::UnblockInputThread,
217        }
218    }
219}
220
221impl From<&ClientInstruction> for ClientContext {
222    fn from(client_instruction: &ClientInstruction) -> Self {
223        match *client_instruction {
224            ClientInstruction::Exit(_) => ClientContext::Exit,
225            ClientInstruction::Error(_) => ClientContext::Error,
226            ClientInstruction::Render(_) => ClientContext::Render,
227            ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread,
228            ClientInstruction::Connected => ClientContext::Connected,
229            ClientInstruction::Log(_) => ClientContext::Log,
230            ClientInstruction::LogError(_) => ClientContext::LogError,
231            ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession,
232            ClientInstruction::SetSynchronizedOutput(..) => ClientContext::SetSynchronisedOutput,
233            ClientInstruction::UnblockCliPipeInput(..) => ClientContext::UnblockCliPipeInput,
234            ClientInstruction::CliPipeOutput(..) => ClientContext::CliPipeOutput,
235            ClientInstruction::QueryTerminalSize => ClientContext::QueryTerminalSize,
236            ClientInstruction::StartWebServer => ClientContext::StartWebServer,
237            ClientInstruction::RenamedSession(..) => ClientContext::RenamedSession,
238            ClientInstruction::ConfigFileUpdated => ClientContext::ConfigFileUpdated,
239            ClientInstruction::ForwardQueryToHost { .. } => ClientContext::ForwardQueryToHost,
240        }
241    }
242}
243
244impl ErrorInstruction for ClientInstruction {
245    fn error(err: String) -> Self {
246        ClientInstruction::Error(err)
247    }
248}
249
250#[cfg(all(feature = "web_server_capability", not(windows)))]
251fn spawn_web_server(cli_args: &CliArgs) -> Result<String, String> {
252    let mut cmd = Command::new(current_exe().map_err(|e| e.to_string())?);
253    if let Some(config_file_path) = Config::config_file_path(cli_args) {
254        let config_file_path_exists = Path::new(&config_file_path).exists();
255        if !config_file_path_exists {
256            return Err(format!(
257                "Config file: {} does not exist",
258                config_file_path.display()
259            ));
260        }
261        // this is so that if Zellij itself was started with a different config file, we'll use it
262        // to start the webserver
263        cmd.arg("--config");
264        cmd.arg(format!("{}", config_file_path.display()));
265    }
266    cmd.arg("web");
267    cmd.arg("-d");
268    let output = cmd.output();
269    match output {
270        Ok(output) => {
271            if output.status.success() {
272                Ok(String::from_utf8_lossy(&output.stdout).to_string())
273            } else {
274                Err(String::from_utf8_lossy(&output.stderr).to_string())
275            }
276        },
277        Err(e) => Err(e.to_string()),
278    }
279}
280
281/// On Windows, cmd.output() creates pipe handles for stdout/stderr. The child
282/// (zellij web -d) spawns a grandchild (the web server) which inherits these
283/// pipe handles. cmd.output() waits for EOF on the pipes, but the long-lived
284/// grandchild keeps them open — hanging forever.
285///
286/// Redirecting the grandchild's stdio to null is not sufficient: on Windows,
287/// CreateProcess with bInheritHandles=TRUE inherits ALL inheritable handles,
288/// not just the stdio handles specified in STARTUPINFO. The pipe handles leak
289/// through regardless of the grandchild's stdio configuration.
290///
291/// Use cmd.status() instead: no pipes are created, so nothing to hang on.
292#[cfg(all(feature = "web_server_capability", windows))]
293fn spawn_web_server(cli_args: &CliArgs) -> Result<String, String> {
294    let mut cmd = Command::new(current_exe().map_err(|e| e.to_string())?);
295    if let Some(config_file_path) = Config::config_file_path(cli_args) {
296        let config_file_path_exists = Path::new(&config_file_path).exists();
297        if !config_file_path_exists {
298            return Err(format!(
299                "Config file: {} does not exist",
300                config_file_path.display()
301            ));
302        }
303        cmd.arg("--config");
304        cmd.arg(format!("{}", config_file_path.display()));
305    }
306    cmd.arg("web");
307    cmd.arg("-d");
308    match cmd.status() {
309        Ok(status) => {
310            if status.success() {
311                Ok(String::new())
312            } else {
313                Err(format!(
314                    "Web server process exited with code: {}",
315                    status.code().unwrap_or(-1)
316                ))
317            }
318        },
319        Err(e) => Err(e.to_string()),
320    }
321}
322
323#[cfg(not(feature = "web_server_capability"))]
324fn spawn_web_server(_cli_args: &CliArgs) -> Result<String, String> {
325    log::error!(
326        "This version of Zellij was compiled without web server support, cannot run web server!"
327    );
328    Ok("".to_owned())
329}
330
331fn check_ipc_pipe_length(ipc_pipe: &Path) {
332    use zellij_utils::consts::ZELLIJ_SOCK_MAX_LENGTH;
333    let path_len = ipc_pipe.as_os_str().len();
334    if path_len >= ZELLIJ_SOCK_MAX_LENGTH {
335        eprintln!(
336            "Error: the IPC socket path is too long ({} bytes, max {}):\n  {}\n\n\
337             This is usually caused by a long $TMPDIR path.\n\
338             To fix this, set a shorter socket directory, eg.:\n  \
339             ZELLIJ_SOCKET_DIR=/tmp/zellij zellij",
340            path_len,
341            ZELLIJ_SOCK_MAX_LENGTH - 1,
342            ipc_pipe.display()
343        );
344        std::process::exit(1);
345    }
346}
347
348/// Spawn the Zellij server process.
349///
350/// On Unix the server daemonizes (double-fork) inside start_server(), so
351/// the intermediate child exits immediately and `cmd.status()` returns.
352#[cfg(not(windows))]
353pub fn spawn_server(socket_path: &Path, debug: bool) -> io::Result<()> {
354    let mut cmd = Command::new(current_exe()?);
355    cmd.arg("--server").arg(socket_path);
356    if debug {
357        cmd.arg("--debug");
358    }
359    let status = cmd.status()?;
360    if status.success() {
361        Ok(())
362    } else {
363        let msg = "Process returned non-zero exit code";
364        let err_msg = match status.code() {
365            Some(c) => format!("{}: {}", msg, c),
366            None => msg.to_string(),
367        };
368        Err(io::Error::new(io::ErrorKind::Other, err_msg))
369    }
370}
371
372/// Spawn the Zellij server process.
373///
374/// On Windows there is no daemonize — we launch the server as a background
375/// process with a hidden console.  We use CREATE_NO_WINDOW (not
376/// DETACHED_PROCESS) so the server gets valid standard handles;
377/// DETACHED_PROCESS leaves stdin/stdout/stderr as NULL, which breaks PTY
378/// creation, WASM plugin loading, and logging.
379#[cfg(windows)]
380pub fn spawn_server(socket_path: &Path, debug: bool) -> io::Result<()> {
381    use std::os::windows::process::CommandExt;
382    let mut cmd = Command::new(current_exe()?);
383    cmd.arg("--server").arg(socket_path);
384    if debug {
385        cmd.arg("--debug");
386    }
387    const CREATE_NO_WINDOW: u32 = 0x08000000;
388    const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
389    cmd.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP);
390    cmd.spawn()?;
391    Ok(())
392}
393
394#[derive(Debug, Clone)]
395pub enum ClientInfo {
396    Attach(String, Options),
397    New(String, Option<LayoutInfo>, Option<PathBuf>), // PathBuf -> explicit cwd
398    Resurrect(String, PathBuf, bool, Option<PathBuf>), // (name, path_to_layout, force_run_commands, cwd)
399    Watch(String, Options),                            // Watch mode (read-only)
400}
401
402impl ClientInfo {
403    pub fn get_session_name(&self) -> &str {
404        match self {
405            Self::Attach(ref name, _) => name,
406            Self::New(ref name, _layout_info, _layout_cwd) => name,
407            Self::Resurrect(ref name, _, _, _) => name,
408            Self::Watch(ref name, _) => name,
409        }
410    }
411    pub fn set_layout_info(&mut self, new_layout_info: LayoutInfo) {
412        match self {
413            ClientInfo::New(_, layout_info, _) => *layout_info = Some(new_layout_info),
414            _ => {},
415        }
416    }
417    pub fn set_cwd(&mut self, new_cwd: PathBuf) {
418        match self {
419            ClientInfo::New(_, _, cwd) => *cwd = Some(new_cwd),
420            ClientInfo::Resurrect(_, _, _, cwd) => *cwd = Some(new_cwd),
421            _ => {},
422        }
423    }
424}
425
426#[derive(Debug, Clone)]
427pub(crate) enum InputInstruction {
428    KeyEvent(InputEvent, Vec<u8>),
429    KeyWithModifierEvent(KeyWithModifier, Vec<u8>, bool), // bool = is_kitty_keyboard_protocol
430    #[allow(dead_code)] // constructed in stdin_handler_windows.rs (Windows-only)
431    MouseEvent(zellij_utils::input::mouse::MouseEvent),
432    AnsiStdinInstructions(Vec<AnsiStdinInstruction>),
433    DesktopNotificationResponse(Vec<u8>),
434    /// The continuous host-reply parser closed a forwarding window (barrier
435    /// reply seen or timeout fired). Payload is the accumulated raw bytes
436    /// to ship to the server.
437    ForwardedReplyFromHostComplete {
438        token: u32,
439        reply_bytes: Vec<u8>,
440    },
441    Exit,
442}
443
444#[cfg(feature = "web_server_capability")]
445pub async fn run_remote_client_terminal_loop(
446    os_input: Box<dyn ClientOsApi>,
447    mut connections: remote_attach::WebSocketConnections,
448) -> Result<Option<ConnectToSession>, RemoteClientError> {
449    use crate::os_input_output::{AsyncSignals, AsyncStdin};
450
451    let synchronised_output = match os_input.env_variable("TERM").as_deref() {
452        Some("alacritty") => Some(SyncOutput::DCS),
453        _ => None,
454    };
455
456    let mut async_stdin: Box<dyn AsyncStdin> = os_input.get_async_stdin_reader();
457    let mut async_signals: Box<dyn AsyncSignals> = os_input
458        .get_async_signal_listener()
459        .map_err(|e| RemoteClientError::IoError(e))?;
460
461    let create_resize_message = |size: Size| {
462        Message::Text(
463            serde_json::to_string(&WebClientToWebServerControlMessage {
464                web_client_id: connections.web_client_id.clone(),
465                payload: WebClientToWebServerControlMessagePayload::TerminalResize(size),
466            })
467            .unwrap(),
468        )
469    };
470
471    // send size on startup
472    let new_size = os_input.get_terminal_size();
473    if let Err(e) = connections
474        .control_ws
475        .send(create_resize_message(new_size))
476        .await
477    {
478        log::error!("Failed to send resize message: {}", e);
479    }
480
481    loop {
482        tokio::select! {
483            // Handle stdin input
484            result = async_stdin.read() => {
485                match result {
486                    Ok(buf) if !buf.is_empty() => {
487                        if let Err(e) = connections.terminal_ws.send(Message::Binary(buf)).await {
488                            log::error!("Failed to send stdin to terminal WebSocket: {}", e);
489                            break;
490                        }
491                    }
492                    Ok(_) => {
493                        // Empty buffer means EOF
494                        break;
495                    }
496                    Err(e) => {
497                        log::error!("Error reading from stdin: {}", e);
498                        break;
499                    }
500                }
501            }
502
503            // Handle signals
504            Some(signal) = async_signals.recv() => {
505                match signal {
506                    crate::os_input_output::SignalEvent::Resize => {
507                        let new_size = os_input.get_terminal_size();
508                        if let Err(e) = connections.control_ws.send(create_resize_message(new_size)).await {
509                            log::error!("Failed to send resize message: {}", e);
510                            break;
511                        }
512                    }
513                    crate::os_input_output::SignalEvent::Quit => {
514                        break;
515                    }
516                }
517            }
518
519            // Handle terminal messages
520            terminal_msg = connections.terminal_ws.next() => {
521                match terminal_msg {
522                    Some(Ok(Message::Text(text))) => {
523                        let mut stdout = os_input.get_stdout_writer();
524                        if let Some(sync) = synchronised_output {
525                            stdout
526                                .write_all(sync.start_seq())
527                                .expect("cannot write to stdout");
528                        }
529                        stdout
530                            .write_all(text.as_bytes())
531                            .expect("cannot write to stdout");
532                        if let Some(sync) = synchronised_output {
533                            stdout
534                                .write_all(sync.end_seq())
535                                .expect("cannot write to stdout");
536                        }
537                        stdout.flush().expect("could not flush");
538                    }
539                    Some(Ok(Message::Binary(data))) => {
540                        let mut stdout = os_input.get_stdout_writer();
541                        if let Some(sync) = synchronised_output {
542                            stdout
543                                .write_all(sync.start_seq())
544                                .expect("cannot write to stdout");
545                        }
546                        stdout
547                            .write_all(&data)
548                            .expect("cannot write to stdout");
549                        if let Some(sync) = synchronised_output {
550                            stdout
551                                .write_all(sync.end_seq())
552                                .expect("cannot write to stdout");
553                        }
554                        stdout.flush().expect("could not flush");
555                    }
556                    Some(Ok(Message::Close(_))) => {
557                        break;
558                    }
559                    Some(Err(e)) => {
560                        log::error!("Error: {}", e);
561                        break;
562                    }
563                    None => {
564                        log::error!("Received empty message from web server");
565                        break;
566                    }
567                    _ => {}
568                }
569            }
570
571            control_msg = connections.control_ws.next() => {
572                match control_msg {
573                    Some(Ok(Message::Text(msg))) => {
574                        let deserialized_msg: Result<WebServerToWebClientControlMessage, _> =
575                            serde_json::from_str(&msg);
576                        match deserialized_msg {
577                            Ok(WebServerToWebClientControlMessage::SetConfig(..)) => {
578                                // no-op
579                            }
580                            Ok(WebServerToWebClientControlMessage::QueryTerminalSize) => {
581                                let new_size = os_input.get_terminal_size();
582                                if let Err(e) = connections.control_ws.send(create_resize_message(new_size)).await {
583                                    log::error!("Failed to send resize message: {}", e);
584                                }
585                            }
586                            Ok(WebServerToWebClientControlMessage::Log { lines }) => {
587                                for line in lines {
588                                    log::info!("{}", line);
589                                }
590                            }
591                            Ok(WebServerToWebClientControlMessage::LogError { lines }) => {
592                                for line in lines {
593                                    log::error!("{}", line);
594                                }
595                            }
596                            Ok(WebServerToWebClientControlMessage::SwitchedSession{ .. }) => {
597                                // no-op
598                            }
599                            Err(e) => {
600                                log::error!("Failed to deserialize control message: {}", e);
601                            }
602                        }
603
604                    }
605                    Some(Ok(Message::Close(_))) => {
606                        break;
607                    }
608                    Some(Err(e)) => {
609                        log::error!("{}", e);
610                        break;
611                    }
612                    None => break,
613                    _ => {}
614                }
615            }
616
617        }
618    }
619
620    Ok(None)
621}
622
623#[cfg(feature = "web_server_capability")]
624pub fn start_remote_client(
625    mut os_input: Box<dyn ClientOsApi>,
626    remote_session_url: &str,
627    token: Option<String>,
628    remember: bool,
629    forget: bool,
630    ca_cert: Option<std::path::PathBuf>,
631    insecure: bool,
632    async_worker_tasks: Option<usize>,
633) -> Result<Option<ConnectToSession>, RemoteClientError> {
634    info!("Starting Zellij client!");
635
636    let runtime = crate::async_runtime(async_worker_tasks);
637
638    let connections = remote_attach::attach_to_remote_session(
639        runtime.clone(),
640        os_input.clone(),
641        remote_session_url,
642        token,
643        remember,
644        forget,
645        ca_cert.as_deref(),
646        insecure,
647    )?;
648
649    let reconnect_to_session = None;
650    os_input.unset_raw_mode().unwrap();
651
652    let mut stdout = os_input.get_stdout_writer();
653    stdout.write_all(ENTER_ALTERNATE_SCREEN.as_bytes()).unwrap();
654    stdout
655        .write_all(CLEAR_CLIENT_TERMINAL_ATTRIBUTES.as_bytes())
656        .unwrap();
657    stdout
658        .write_all(ENTER_KITTY_KEYBOARD_MODE.as_bytes())
659        .unwrap();
660    stdout
661        .write_all(ENABLE_HOST_THEME_NOTIFY.as_bytes())
662        .unwrap();
663    stdout.write_all(QUERY_HOST_THEME.as_bytes()).unwrap();
664
665    envs::set_zellij("0".to_string());
666
667    let full_screen_ws = os_input.get_terminal_size();
668
669    os_input.set_raw_mode();
670    stdout.write_all(ENABLE_BRACKETED_PASTE.as_bytes()).unwrap();
671
672    std::panic::set_hook({
673        use zellij_utils::errors::handle_panic;
674        let os_input = os_input.clone();
675        Box::new(move |info| {
676            os_input.disable_mouse().non_fatal();
677            os_input.restore_console_mode();
678            if let Ok(()) = os_input.unset_raw_mode() {
679                handle_panic::<ClientInstruction>(info, None);
680            }
681        })
682    });
683
684    let reset_controlling_terminal_state = |e: String, exit_status: i32| {
685        os_input.disable_mouse().non_fatal();
686        os_input.unset_raw_mode().unwrap();
687        os_input.restore_console_mode();
688        let error = terminal_teardown_message(&e, full_screen_ws.rows, true);
689        let mut stdout = os_input.get_stdout_writer();
690        stdout.write_all(error.as_bytes()).unwrap();
691        stdout.flush().unwrap();
692        if exit_status == 0 {
693            log::info!("{}", e);
694        } else {
695            log::error!("{}", e);
696        };
697        std::process::exit(exit_status);
698    };
699
700    runtime.block_on(run_remote_client_terminal_loop(
701        os_input.clone(),
702        connections,
703    ))?;
704
705    let exit_msg = String::from("Bye from Zellij!");
706
707    if reconnect_to_session.is_none() {
708        reset_controlling_terminal_state(exit_msg, 0);
709        std::process::exit(0);
710    } else {
711        let clear_screen = "\u{1b}[2J";
712        let mut stdout = os_input.get_stdout_writer();
713        stdout.write_all(clear_screen.as_bytes()).unwrap();
714        stdout.flush().unwrap();
715    }
716
717    Ok(reconnect_to_session)
718}
719
720pub fn start_client(
721    mut os_input: Box<dyn ClientOsApi>,
722    cli_args: CliArgs,
723    config: Config,          // saved to disk (or default?)
724    config_options: Options, // CLI options merged into (getting priority over) saved config options
725    info: ClientInfo,
726    tab_position_to_focus: Option<usize>,
727    pane_id_to_focus: Option<(u32, bool)>, // (pane_id, is_plugin)
728    is_a_reconnect: bool,
729    start_detached_and_exit: bool,
730) -> Option<ConnectToSession> {
731    if start_detached_and_exit {
732        start_server_detached(os_input, cli_args, config, config_options, info);
733        return None;
734    }
735    info!("Starting Zellij client!");
736
737    let explicitly_disable_kitty_keyboard_protocol = config_options
738        .support_kitty_keyboard_protocol
739        .map(|e| !e)
740        .unwrap_or(false);
741    let should_start_web_server = config_options.web_server.map(|w| w).unwrap_or(false);
742    let mut reconnect_to_session = None;
743    os_input.unset_raw_mode().unwrap();
744
745    if !is_a_reconnect {
746        // we don't do this for a reconnect because our controlling terminal already has the
747        // attributes we want from it, and some terminals don't treat these atomically (looking at
748        // you Windows Terminal...)
749        let mut stdout = os_input.get_stdout_writer();
750        stdout.write_all(ENTER_ALTERNATE_SCREEN.as_bytes()).unwrap();
751        stdout
752            .write_all(CLEAR_CLIENT_TERMINAL_ATTRIBUTES.as_bytes())
753            .unwrap();
754        if !explicitly_disable_kitty_keyboard_protocol {
755            stdout
756                .write_all(ENTER_KITTY_KEYBOARD_MODE.as_bytes())
757                .unwrap();
758        }
759        // Subscribe to host CSI 2031 theme notifications and query the
760        // current mode. Sent right after CLEAR_CLIENT_TERMINAL_ATTRIBUTES
761        // so there's no window in which the host is unsubscribed.
762        // Hosts that don't support 2031 ignore both sequences.
763        stdout
764            .write_all(ENABLE_HOST_THEME_NOTIFY.as_bytes())
765            .unwrap();
766        stdout.write_all(QUERY_HOST_THEME.as_bytes()).unwrap();
767    }
768    envs::set_zellij("0".to_string());
769    config.env.set_vars();
770
771    let full_screen_ws = os_input.get_terminal_size();
772
773    let web_server_ip = config_options
774        .web_server_ip
775        .unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
776    let web_server_port = config_options.web_server_port.unwrap_or_else(|| 8082);
777    let has_certificate =
778        config_options.web_server_cert.is_some() && config_options.web_server_key.is_some();
779    let enforce_https_for_localhost = config_options.enforce_https_for_localhost.unwrap_or(false);
780
781    let create_ipc_pipe = || -> std::path::PathBuf {
782        let mut sock_dir = ZELLIJ_SOCK_DIR.clone();
783        std::fs::create_dir_all(&sock_dir).unwrap();
784        set_permissions(&sock_dir, 0o700).unwrap();
785        sock_dir.push(envs::get_session_name().unwrap());
786        check_ipc_pipe_length(&sock_dir);
787        sock_dir
788    };
789
790    let (first_msg, ipc_pipe) = match info {
791        ClientInfo::Attach(name, config_options) => {
792            envs::set_session_name(name.clone());
793            os_input.update_session_name(name);
794            let ipc_pipe = create_ipc_pipe();
795            let is_web_client = false;
796
797            let cli_assets = CliAssets {
798                config_file_path: Config::config_file_path(&cli_args),
799                config_dir: cli_args.config_dir.clone(),
800                should_ignore_config: cli_args.is_setup_clean(),
801                configuration_options: Some(config_options.clone()),
802                layout: if let Some(layout_string) = &cli_args.layout_string {
803                    Some(LayoutInfo::Stringified(layout_string.clone()))
804                } else {
805                    cli_args
806                        .layout
807                        .as_ref()
808                        .and_then(|l| {
809                            LayoutInfo::from_cli(
810                                &config_options.layout_dir,
811                                &Some(l.clone()),
812                                std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
813                            )
814                        })
815                        .or_else(|| {
816                            LayoutInfo::from_config(
817                                &config_options.layout_dir,
818                                &config_options.default_layout,
819                            )
820                        })
821                },
822                terminal_window_size: full_screen_ws,
823                data_dir: cli_args.data_dir.clone(),
824                is_debug: cli_args.debug,
825                max_panes: cli_args.max_panes,
826                force_run_layout_commands: false,
827                cwd: None,
828            };
829            (
830                ClientToServerMsg::AttachClient {
831                    cli_assets,
832                    tab_position_to_focus,
833                    pane_to_focus: pane_id_to_focus.map(|(pane_id, is_plugin)| {
834                        zellij_utils::ipc::PaneReference { pane_id, is_plugin }
835                    }),
836                    is_web_client,
837                },
838                ipc_pipe,
839            )
840        },
841        ClientInfo::Watch(name, _config_options) => {
842            envs::set_session_name(name.clone());
843            os_input.update_session_name(name);
844            let ipc_pipe = create_ipc_pipe();
845            let is_web_client = false;
846
847            (
848                ClientToServerMsg::AttachWatcherClient {
849                    terminal_size: full_screen_ws,
850                    is_web_client,
851                },
852                ipc_pipe,
853            )
854        },
855        ClientInfo::Resurrect(name, path_to_layout, force_run_commands, cwd) => {
856            envs::set_session_name(name.clone());
857
858            let cli_assets = CliAssets {
859                config_file_path: Config::config_file_path(&cli_args),
860                config_dir: cli_args.config_dir.clone(),
861                should_ignore_config: cli_args.is_setup_clean(),
862                configuration_options: Some(config_options.clone()),
863                layout: Some(LayoutInfo::File(
864                    path_to_layout.display().to_string(),
865                    LayoutMetadata::default(),
866                )),
867                terminal_window_size: full_screen_ws,
868                data_dir: cli_args.data_dir.clone(),
869                is_debug: cli_args.debug,
870                max_panes: cli_args.max_panes,
871                force_run_layout_commands: force_run_commands,
872                cwd,
873            };
874
875            os_input.update_session_name(name);
876            let ipc_pipe = create_ipc_pipe();
877
878            spawn_server(&*ipc_pipe, cli_args.debug).unwrap();
879            if should_start_web_server {
880                if let Err(e) = spawn_web_server(&cli_args) {
881                    log::error!("Failed to start web server: {}", e);
882                }
883            }
884
885            let is_web_client = false;
886
887            (
888                ClientToServerMsg::FirstClientConnected {
889                    cli_assets,
890                    is_web_client,
891                },
892                ipc_pipe,
893            )
894        },
895        ClientInfo::New(name, layout_info, layout_cwd) => {
896            envs::set_session_name(name.clone());
897
898            let cli_assets = CliAssets {
899                config_file_path: Config::config_file_path(&cli_args),
900                config_dir: cli_args.config_dir.clone(),
901                should_ignore_config: cli_args.is_setup_clean(),
902                configuration_options: Some(config_options.clone()),
903                layout: layout_info.or_else(|| {
904                    cli_args
905                        .layout
906                        .as_ref()
907                        .and_then(|l| {
908                            LayoutInfo::from_cli(
909                                &config_options.layout_dir,
910                                &Some(l.clone()),
911                                std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
912                            )
913                        })
914                        .or_else(|| {
915                            LayoutInfo::from_config(
916                                &config_options.layout_dir,
917                                &config_options.default_layout,
918                            )
919                        })
920                }),
921                terminal_window_size: full_screen_ws,
922                data_dir: cli_args.data_dir.clone(),
923                is_debug: cli_args.debug,
924                max_panes: cli_args.max_panes,
925                force_run_layout_commands: false,
926                cwd: layout_cwd,
927            };
928
929            os_input.update_session_name(name);
930            let ipc_pipe = create_ipc_pipe();
931
932            spawn_server(&*ipc_pipe, cli_args.debug).unwrap();
933            if should_start_web_server {
934                if let Err(e) = spawn_web_server(&cli_args) {
935                    log::error!("Failed to start web server: {}", e);
936                }
937            }
938
939            let is_web_client = false;
940
941            (
942                ClientToServerMsg::FirstClientConnected {
943                    cli_assets,
944                    is_web_client,
945                },
946                ipc_pipe,
947            )
948        },
949    };
950
951    os_input.connect_to_server(&*ipc_pipe);
952    os_input.send_to_server(first_msg);
953
954    let mut command_is_executing = CommandIsExecuting::new();
955
956    os_input.set_raw_mode();
957    let mut stdout = os_input.get_stdout_writer();
958    stdout.write_all(ENABLE_BRACKETED_PASTE.as_bytes()).unwrap();
959
960    let (send_client_instructions, receive_client_instructions): ChannelWithContext<
961        ClientInstruction,
962    > = channels::bounded(50);
963    let send_client_instructions = SenderWithContext::new(send_client_instructions);
964
965    let (send_input_instructions, receive_input_instructions): ChannelWithContext<
966        InputInstruction,
967    > = channels::bounded(50);
968    let send_input_instructions = SenderWithContext::new(send_input_instructions);
969
970    std::panic::set_hook({
971        use zellij_utils::errors::handle_panic;
972        let send_client_instructions = send_client_instructions.clone();
973        let os_input = os_input.clone();
974        Box::new(move |info| {
975            os_input.disable_mouse().non_fatal();
976            os_input.restore_console_mode();
977            if let Ok(()) = os_input.unset_raw_mode() {
978                handle_panic(info, Some(&send_client_instructions));
979            }
980        })
981    });
982
983    let on_force_close = config_options.on_force_close.unwrap_or_default();
984    let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new()));
985
986    let (resize_sender, resize_receiver) = std::sync::mpsc::channel::<()>();
987
988    let _stdin_thread = thread::Builder::new()
989        .name("stdin_handler".to_string())
990        .spawn({
991            let os_input = os_input.clone();
992            let send_input_instructions = send_input_instructions.clone();
993            let stdin_ansi_parser = stdin_ansi_parser.clone();
994            move || {
995                stdin_loop(
996                    os_input,
997                    send_input_instructions,
998                    stdin_ansi_parser,
999                    explicitly_disable_kitty_keyboard_protocol,
1000                    Some(resize_sender),
1001                )
1002            }
1003        });
1004
1005    // Apps running inside Zellij panes can issue a whitelisted set
1006    // of queries to the host terminal (bg/fg colour, palette
1007    // registers, window pixel dimensions). Each query opens a
1008    // "forward slot" on the client: we write the query + a
1009    // Primary-DA barrier to stdout, then collect any reply bytes
1010    // that arrive on stdin until the barrier reply closes the slot.
1011    // The pane that asked gets the captured bytes piped to its pty.
1012    //
1013    // If the host never answers, we must close the slot anyway so
1014    // the server can dispatch the next queued forward. A per-slot
1015    // timer task enforces that deadline: opening a forward spawns
1016    // an async sleep on `forward_timeout_runtime()`; on wake it
1017    // tries to close the slot for that specific token. If the
1018    // barrier (or a later forward) closed the slot first, the
1019    // timer's close call is a no-op — the token-guard makes
1020    // cancellation implicit. Spawn site: the
1021    // `ClientInstruction::ForwardQueryToHost` handler below.
1022
1023    let _input_thread = thread::Builder::new()
1024        .name("input_handler".to_string())
1025        .spawn({
1026            let send_client_instructions = send_client_instructions.clone();
1027            let command_is_executing = command_is_executing.clone();
1028            let os_input = os_input.clone();
1029            let default_mode = config_options.default_mode.unwrap_or_default();
1030            move || {
1031                input_loop(
1032                    os_input,
1033                    config,
1034                    config_options,
1035                    command_is_executing,
1036                    send_client_instructions,
1037                    default_mode,
1038                    receive_input_instructions,
1039                )
1040            }
1041        });
1042
1043    let _signal_thread = thread::Builder::new()
1044        .name("signal_listener".to_string())
1045        .spawn({
1046            let os_input = os_input.clone();
1047            move || {
1048                os_input.handle_signals(
1049                    Box::new({
1050                        let os_api = os_input.clone();
1051                        move || {
1052                            os_api.send_to_server(ClientToServerMsg::TerminalResize {
1053                                new_size: os_api.get_terminal_size(),
1054                            });
1055                        }
1056                    }),
1057                    Box::new({
1058                        let os_api = os_input.clone();
1059                        move || {
1060                            os_api.send_to_server(ClientToServerMsg::Action {
1061                                action: on_force_close.into(),
1062                                terminal_id: None,
1063                                client_id: None,
1064                                is_cli_client: false,
1065                            });
1066                        }
1067                    }),
1068                    Some(resize_receiver),
1069                );
1070            }
1071        })
1072        .unwrap();
1073
1074    let router_thread = thread::Builder::new()
1075        .name("router".to_string())
1076        .spawn({
1077            let os_input = os_input.clone();
1078            let mut should_break = false;
1079            let mut consecutive_unknown_messages_received = 0;
1080            move || loop {
1081                match os_input.recv_from_server() {
1082                    Some((instruction, err_ctx)) => {
1083                        consecutive_unknown_messages_received = 0;
1084                        err_ctx.update_thread_ctx();
1085                        if let ServerToClientMsg::Exit { .. } = instruction {
1086                            should_break = true;
1087                        }
1088                        send_client_instructions.send(instruction.into()).unwrap();
1089                        if should_break {
1090                            break;
1091                        }
1092                    },
1093                    None => {
1094                        consecutive_unknown_messages_received += 1;
1095                        send_client_instructions
1096                            .send(ClientInstruction::UnblockInputThread)
1097                            .unwrap();
1098                        log::error!("Received unknown message from server");
1099                        if consecutive_unknown_messages_received >= 1000 {
1100                            send_client_instructions
1101                                .send(ClientInstruction::Error(
1102                                    "Received empty unknown from server".to_string(),
1103                                ))
1104                                .unwrap();
1105                            break;
1106                        }
1107                    },
1108                }
1109            }
1110        })
1111        .unwrap();
1112
1113    let handle_error = |backtrace: String| {
1114        os_input.disable_mouse().non_fatal();
1115        os_input.unset_raw_mode().unwrap();
1116        os_input.restore_console_mode();
1117        let error = terminal_teardown_message(
1118            &backtrace,
1119            full_screen_ws.rows,
1120            !explicitly_disable_kitty_keyboard_protocol,
1121        );
1122        let mut stdout = os_input.get_stdout_writer();
1123        stdout.write_all(error.as_bytes()).unwrap();
1124        stdout.flush().unwrap();
1125        std::process::exit(1);
1126    };
1127
1128    let mut exit_msg = String::new();
1129    let mut synchronised_output = match os_input.env_variable("TERM").as_deref() {
1130        Some("alacritty") => Some(SyncOutput::DCS),
1131        _ => None,
1132    };
1133
1134    loop {
1135        let (client_instruction, mut err_ctx) = receive_client_instructions
1136            .recv()
1137            .expect("failed to receive app instruction on channel");
1138
1139        err_ctx.add_call(ContextType::Client((&client_instruction).into()));
1140
1141        match client_instruction {
1142            ClientInstruction::Exit(reason) => {
1143                os_input.send_to_server(ClientToServerMsg::ClientExited);
1144
1145                if let ExitReason::Error(_) = reason {
1146                    handle_error(reason.to_string());
1147                }
1148                exit_msg = reason.to_string();
1149                break;
1150            },
1151            ClientInstruction::Error(backtrace) => {
1152                handle_error(backtrace);
1153            },
1154            ClientInstruction::Render(output) => {
1155                let mut stdout = os_input.get_stdout_writer();
1156                if let Some(sync) = synchronised_output {
1157                    stdout
1158                        .write_all(sync.start_seq())
1159                        .expect("cannot write to stdout");
1160                }
1161                stdout
1162                    .write_all(output.as_bytes())
1163                    .expect("cannot write to stdout");
1164                if let Some(sync) = synchronised_output {
1165                    stdout
1166                        .write_all(sync.end_seq())
1167                        .expect("cannot write to stdout");
1168                }
1169                stdout.flush().expect("could not flush");
1170            },
1171            ClientInstruction::UnblockInputThread => {
1172                command_is_executing.unblock_input_thread();
1173            },
1174            ClientInstruction::Log(lines_to_log) => {
1175                for line in lines_to_log {
1176                    log::info!("{line}");
1177                }
1178            },
1179            ClientInstruction::LogError(lines_to_log) => {
1180                for line in lines_to_log {
1181                    log::error!("{line}");
1182                }
1183            },
1184            ClientInstruction::SwitchSession(connect_to_session) => {
1185                reconnect_to_session = Some(connect_to_session);
1186                os_input.send_to_server(ClientToServerMsg::ClientExited);
1187                break;
1188            },
1189            ClientInstruction::SetSynchronizedOutput(enabled) => {
1190                synchronised_output = enabled;
1191            },
1192            ClientInstruction::QueryTerminalSize => {
1193                os_input.send_to_server(ClientToServerMsg::TerminalResize {
1194                    new_size: os_input.get_terminal_size(),
1195                });
1196            },
1197            ClientInstruction::StartWebServer => {
1198                let web_server_base_url = web_server_base_url(
1199                    web_server_ip,
1200                    web_server_port,
1201                    has_certificate,
1202                    enforce_https_for_localhost,
1203                );
1204                match spawn_web_server(&cli_args) {
1205                    Ok(_) => {
1206                        let _ = os_input.send_to_server(ClientToServerMsg::WebServerStarted {
1207                            base_url: web_server_base_url,
1208                        });
1209                    },
1210                    Err(e) => {
1211                        log::error!("Failed to start web_server: {}", e);
1212                        let _ = os_input
1213                            .send_to_server(ClientToServerMsg::FailedToStartWebServer { error: e });
1214                    },
1215                }
1216            },
1217            ClientInstruction::ForwardQueryToHost { token, query_bytes } => {
1218                // 1. Open a forwarding window on the parser so any reply
1219                //    events that arrive before the barrier are captured.
1220                stdin_ansi_parser.lock().unwrap().open_forward(token);
1221                // 2. Spawn a per-forward timer on the dedicated async
1222                //    runtime. When the deadline fires, the task closes
1223                //    the slot (if it's still open for this token) and
1224                //    relays `ForwardedReplyFromHostComplete` so the
1225                //    server releases `forward_in_flight` and dispatches
1226                //    the next queued forward.
1227                let runtime = stdin_ansi_parser::forward_timeout_runtime();
1228                let parser_for_timer = stdin_ansi_parser.clone();
1229                let sender_for_timer = send_input_instructions.clone();
1230                stdin_ansi_parser::schedule_forward_timeout(
1231                    runtime.handle(),
1232                    parser_for_timer,
1233                    token,
1234                    std::time::Duration::from_millis(500),
1235                    move |token, reply_bytes| {
1236                        let _ = sender_for_timer.send(
1237                            InputInstruction::ForwardedReplyFromHostComplete { token, reply_bytes },
1238                        );
1239                    },
1240                );
1241                // 3. Write the query + Primary-DA barrier in a single
1242                //    write_all. The barrier closes the window on the
1243                //    parser side when its reply arrives — the timer
1244                //    task's eventual wake-up finds an empty slot for
1245                //    this token and no-ops.
1246                let mut blob = query_bytes;
1247                blob.extend_from_slice(b"\x1b[c");
1248                let mut out = os_input.get_stdout_writer();
1249                let _ = out.write_all(&blob);
1250                let _ = out.flush();
1251            },
1252            _ => {},
1253        }
1254    }
1255
1256    router_thread.join().unwrap();
1257
1258    if reconnect_to_session.is_none() {
1259        let goodbye_message = terminal_teardown_message(
1260            &exit_msg,
1261            full_screen_ws.rows,
1262            !explicitly_disable_kitty_keyboard_protocol,
1263        );
1264
1265        os_input.disable_mouse().non_fatal();
1266        info!("{}", exit_msg);
1267        os_input.unset_raw_mode().unwrap();
1268        os_input.restore_console_mode();
1269        let mut stdout = os_input.get_stdout_writer();
1270        stdout.write_all(goodbye_message.as_bytes()).unwrap();
1271        stdout.flush().unwrap();
1272    } else {
1273        let clear_screen = "\u{1b}[2J";
1274        let mut stdout = os_input.get_stdout_writer();
1275        stdout.write_all(clear_screen.as_bytes()).unwrap();
1276        stdout.flush().unwrap();
1277    }
1278
1279    let _ = send_input_instructions.send(InputInstruction::Exit);
1280
1281    reconnect_to_session
1282}
1283
1284pub fn start_server_detached(
1285    mut os_input: Box<dyn ClientOsApi>,
1286    cli_args: CliArgs,
1287    config: Config,
1288    config_options: Options,
1289    info: ClientInfo,
1290) {
1291    envs::set_zellij("0".to_string());
1292    config.env.set_vars();
1293
1294    let should_start_web_server = config_options.web_server.map(|w| w).unwrap_or(false);
1295
1296    let create_ipc_pipe = || -> std::path::PathBuf {
1297        let mut sock_dir = ZELLIJ_SOCK_DIR.clone();
1298        std::fs::create_dir_all(&sock_dir).unwrap();
1299        set_permissions(&sock_dir, 0o700).unwrap();
1300        sock_dir.push(envs::get_session_name().unwrap());
1301        check_ipc_pipe_length(&sock_dir);
1302        sock_dir
1303    };
1304
1305    let (first_msg, ipc_pipe) = match info {
1306        ClientInfo::Resurrect(name, path_to_layout, force_run_commands, cwd) => {
1307            envs::set_session_name(name.clone());
1308
1309            let cli_assets = CliAssets {
1310                config_file_path: Config::config_file_path(&cli_args),
1311                config_dir: cli_args.config_dir.clone(),
1312                should_ignore_config: cli_args.is_setup_clean(),
1313                configuration_options: Some(config_options.clone()),
1314                layout: Some(LayoutInfo::File(
1315                    path_to_layout.display().to_string(),
1316                    LayoutMetadata::default(),
1317                )),
1318                terminal_window_size: Size { cols: 50, rows: 50 }, // static number until a
1319                // client connects
1320                data_dir: cli_args.data_dir.clone(),
1321                is_debug: cli_args.debug,
1322                max_panes: cli_args.max_panes,
1323                force_run_layout_commands: force_run_commands,
1324                cwd,
1325            };
1326
1327            os_input.update_session_name(name);
1328            let ipc_pipe = create_ipc_pipe();
1329
1330            spawn_server(&*ipc_pipe, cli_args.debug).unwrap();
1331            if should_start_web_server {
1332                if let Err(e) = spawn_web_server(&cli_args) {
1333                    log::error!("Failed to start web server: {}", e);
1334                }
1335            }
1336
1337            let is_web_client = false;
1338
1339            (
1340                ClientToServerMsg::FirstClientConnected {
1341                    cli_assets,
1342                    is_web_client,
1343                },
1344                ipc_pipe,
1345            )
1346        },
1347        ClientInfo::New(name, layout_info, layout_cwd) => {
1348            envs::set_session_name(name.clone());
1349
1350            let cli_assets = CliAssets {
1351                config_file_path: Config::config_file_path(&cli_args),
1352                config_dir: cli_args.config_dir.clone(),
1353                should_ignore_config: cli_args.is_setup_clean(),
1354                configuration_options: cli_args.options(),
1355                layout: layout_info.or_else(|| {
1356                    cli_args
1357                        .layout
1358                        .as_ref()
1359                        .and_then(|l| {
1360                            LayoutInfo::from_cli(
1361                                &config_options.layout_dir,
1362                                &Some(l.clone()),
1363                                std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
1364                            )
1365                        })
1366                        .or_else(|| {
1367                            LayoutInfo::from_config(
1368                                &config_options.layout_dir,
1369                                &config_options.default_layout,
1370                            )
1371                        })
1372                }),
1373                terminal_window_size: Size { cols: 50, rows: 50 }, // static number until a
1374                // client connects
1375                data_dir: cli_args.data_dir.clone(),
1376                is_debug: cli_args.debug,
1377                max_panes: cli_args.max_panes,
1378                force_run_layout_commands: false,
1379                cwd: layout_cwd,
1380            };
1381
1382            os_input.update_session_name(name);
1383            let ipc_pipe = create_ipc_pipe();
1384
1385            spawn_server(&*ipc_pipe, cli_args.debug).unwrap();
1386            if should_start_web_server {
1387                if let Err(e) = spawn_web_server(&cli_args) {
1388                    log::error!("Failed to start web server: {}", e);
1389                }
1390            }
1391            let is_web_client = false;
1392
1393            (
1394                ClientToServerMsg::FirstClientConnected {
1395                    cli_assets,
1396                    is_web_client,
1397                },
1398                ipc_pipe,
1399            )
1400        },
1401        _ => {
1402            eprintln!("Session already exists");
1403            std::process::exit(1);
1404        },
1405    };
1406
1407    os_input.connect_to_server(&*ipc_pipe);
1408    os_input.send_to_server(first_msg);
1409}
1410
1411fn terminal_teardown_message(message: &str, rows: usize, include_kitty_exit: bool) -> String {
1412    let goto_start_of_last_line = format!("\u{1b}[{};{}H", rows, 1);
1413    let kitty_exit = if include_kitty_exit {
1414        EXIT_KITTY_KEYBOARD_MODE
1415    } else {
1416        ""
1417    };
1418    format!(
1419        "{}{}{}{}{}{}{}\n",
1420        kitty_exit,
1421        DISABLE_HOST_THEME_NOTIFY,
1422        EXIT_ALTERNATE_SCREEN,
1423        RESET_STYLE,
1424        SHOW_CURSOR,
1425        goto_start_of_last_line,
1426        message
1427    )
1428}
1429
1430#[cfg(test)]
1431mod unit;