zellij_client/
lib.rs

1pub mod os_input_output;
2
3pub mod cli_client;
4mod command_is_executing;
5mod input_handler;
6mod keyboard_parser;
7pub mod old_config_converter;
8mod stdin_ansi_parser;
9mod stdin_handler;
10#[cfg(feature = "web_server_capability")]
11pub mod web_client;
12
13use log::info;
14use std::env::current_exe;
15use std::io::{self, Write};
16use std::net::{IpAddr, Ipv4Addr};
17use std::path::Path;
18use std::process::Command;
19use std::sync::{Arc, Mutex};
20use std::thread;
21use zellij_utils::errors::FatalError;
22use zellij_utils::shared::web_server_base_url;
23
24use crate::stdin_ansi_parser::{AnsiStdinInstruction, StdinAnsiParser, SyncOutput};
25use crate::{
26    command_is_executing::CommandIsExecuting, input_handler::input_loop,
27    os_input_output::ClientOsApi, stdin_handler::stdin_loop,
28};
29use termwiz::input::InputEvent;
30use zellij_utils::{
31    channels::{self, ChannelWithContext, SenderWithContext},
32    consts::{set_permissions, ZELLIJ_SOCK_DIR},
33    data::{ClientId, ConnectToSession, KeyWithModifier, Style},
34    envs,
35    errors::{ClientContext, ContextType, ErrorInstruction},
36    input::{
37        config::{watch_config_file_changes, Config},
38        options::Options,
39    },
40    ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg},
41    pane_size::Size,
42};
43use zellij_utils::{cli::CliArgs, input::layout::Layout};
44
45/// Instructions related to the client-side application
46#[derive(Debug, Clone)]
47pub(crate) enum ClientInstruction {
48    Error(String),
49    Render(String),
50    UnblockInputThread,
51    Exit(ExitReason),
52    Connected,
53    StartedParsingStdinQuery,
54    DoneParsingStdinQuery,
55    Log(Vec<String>),
56    LogError(Vec<String>),
57    SwitchSession(ConnectToSession),
58    SetSynchronizedOutput(Option<SyncOutput>),
59    UnblockCliPipeInput(()), // String -> pipe name
60    CliPipeOutput((), ()),   // String -> pipe name, String -> output
61    QueryTerminalSize,
62    WriteConfigToDisk {
63        config: String,
64    },
65    StartWebServer,
66    #[allow(dead_code)] // we need the session name here even though we're not currently using it
67    RenamedSession(String), // String -> new session name
68}
69
70impl From<ServerToClientMsg> for ClientInstruction {
71    fn from(instruction: ServerToClientMsg) -> Self {
72        match instruction {
73            ServerToClientMsg::Exit(e) => ClientInstruction::Exit(e),
74            ServerToClientMsg::Render(buffer) => ClientInstruction::Render(buffer),
75            ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread,
76            ServerToClientMsg::Connected => ClientInstruction::Connected,
77            ServerToClientMsg::Log(log_lines) => ClientInstruction::Log(log_lines),
78            ServerToClientMsg::LogError(log_lines) => ClientInstruction::LogError(log_lines),
79            ServerToClientMsg::SwitchSession(connect_to_session) => {
80                ClientInstruction::SwitchSession(connect_to_session)
81            },
82            ServerToClientMsg::UnblockCliPipeInput(_pipe_name) => {
83                ClientInstruction::UnblockCliPipeInput(())
84            },
85            ServerToClientMsg::CliPipeOutput(_pipe_name, _output) => {
86                ClientInstruction::CliPipeOutput((), ())
87            },
88            ServerToClientMsg::QueryTerminalSize => ClientInstruction::QueryTerminalSize,
89            ServerToClientMsg::WriteConfigToDisk { config } => {
90                ClientInstruction::WriteConfigToDisk { config }
91            },
92            ServerToClientMsg::StartWebServer => ClientInstruction::StartWebServer,
93            ServerToClientMsg::RenamedSession(name) => ClientInstruction::RenamedSession(name),
94        }
95    }
96}
97
98impl From<&ClientInstruction> for ClientContext {
99    fn from(client_instruction: &ClientInstruction) -> Self {
100        match *client_instruction {
101            ClientInstruction::Exit(_) => ClientContext::Exit,
102            ClientInstruction::Error(_) => ClientContext::Error,
103            ClientInstruction::Render(_) => ClientContext::Render,
104            ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread,
105            ClientInstruction::Connected => ClientContext::Connected,
106            ClientInstruction::Log(_) => ClientContext::Log,
107            ClientInstruction::LogError(_) => ClientContext::LogError,
108            ClientInstruction::StartedParsingStdinQuery => ClientContext::StartedParsingStdinQuery,
109            ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery,
110            ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession,
111            ClientInstruction::SetSynchronizedOutput(..) => ClientContext::SetSynchronisedOutput,
112            ClientInstruction::UnblockCliPipeInput(..) => ClientContext::UnblockCliPipeInput,
113            ClientInstruction::CliPipeOutput(..) => ClientContext::CliPipeOutput,
114            ClientInstruction::QueryTerminalSize => ClientContext::QueryTerminalSize,
115            ClientInstruction::WriteConfigToDisk { .. } => ClientContext::WriteConfigToDisk,
116            ClientInstruction::StartWebServer => ClientContext::StartWebServer,
117            ClientInstruction::RenamedSession(..) => ClientContext::RenamedSession,
118        }
119    }
120}
121
122impl ErrorInstruction for ClientInstruction {
123    fn error(err: String) -> Self {
124        ClientInstruction::Error(err)
125    }
126}
127
128#[cfg(feature = "web_server_capability")]
129fn spawn_web_server(opts: &CliArgs) -> Result<String, String> {
130    let mut cmd = Command::new(current_exe().map_err(|e| e.to_string())?);
131    if let Some(config_file_path) = Config::config_file_path(opts) {
132        let config_file_path_exists = Path::new(&config_file_path).exists();
133        if !config_file_path_exists {
134            return Err(format!(
135                "Config file: {} does not exist",
136                config_file_path.display()
137            ));
138        }
139        // this is so that if Zellij itself was started with a different config file, we'll use it
140        // to start the webserver
141        cmd.arg("--config");
142        cmd.arg(format!("{}", config_file_path.display()));
143    }
144    cmd.arg("web");
145    cmd.arg("-d");
146    let output = cmd.output();
147    match output {
148        Ok(output) => {
149            if output.status.success() {
150                Ok(String::from_utf8_lossy(&output.stdout).to_string())
151            } else {
152                Err(String::from_utf8_lossy(&output.stderr).to_string())
153            }
154        },
155        Err(e) => Err(e.to_string()),
156    }
157}
158
159#[cfg(not(feature = "web_server_capability"))]
160fn spawn_web_server(_opts: &CliArgs) -> Result<String, String> {
161    log::error!(
162        "This version of Zellij was compiled without web server support, cannot run web server!"
163    );
164    Ok("".to_owned())
165}
166
167pub fn spawn_server(socket_path: &Path, debug: bool) -> io::Result<()> {
168    let mut cmd = Command::new(current_exe()?);
169    cmd.arg("--server");
170    cmd.arg(socket_path);
171    if debug {
172        cmd.arg("--debug");
173    }
174    let status = cmd.status()?;
175
176    if status.success() {
177        Ok(())
178    } else {
179        let msg = "Process returned non-zero exit code";
180        let err_msg = match status.code() {
181            Some(c) => format!("{}: {}", msg, c),
182            None => msg.to_string(),
183        };
184        Err(io::Error::new(io::ErrorKind::Other, err_msg))
185    }
186}
187
188#[derive(Debug, Clone)]
189pub enum ClientInfo {
190    Attach(String, Options),
191    New(String),
192    Resurrect(String, Layout),
193}
194
195impl ClientInfo {
196    pub fn get_session_name(&self) -> &str {
197        match self {
198            Self::Attach(ref name, _) => name,
199            Self::New(ref name) => name,
200            Self::Resurrect(ref name, _) => name,
201        }
202    }
203}
204
205#[derive(Debug, Clone)]
206pub(crate) enum InputInstruction {
207    KeyEvent(InputEvent, Vec<u8>),
208    KeyWithModifierEvent(KeyWithModifier, Vec<u8>),
209    AnsiStdinInstructions(Vec<AnsiStdinInstruction>),
210    StartedParsing,
211    DoneParsing,
212    Exit,
213}
214
215pub fn start_client(
216    mut os_input: Box<dyn ClientOsApi>,
217    opts: CliArgs,
218    config: Config,          // saved to disk (or default?)
219    config_options: Options, // CLI options merged into (getting priority over) saved config options
220    info: ClientInfo,
221    layout: Option<Layout>,
222    tab_position_to_focus: Option<usize>,
223    pane_id_to_focus: Option<(u32, bool)>, // (pane_id, is_plugin)
224    is_a_reconnect: bool,
225    start_detached_and_exit: bool,
226    layout_is_welcome_screen: bool,
227) -> Option<ConnectToSession> {
228    if start_detached_and_exit {
229        start_server_detached(os_input, opts, config, config_options, info, layout);
230        return None;
231    }
232    info!("Starting Zellij client!");
233
234    let explicitly_disable_kitty_keyboard_protocol = config_options
235        .support_kitty_keyboard_protocol
236        .map(|e| !e)
237        .unwrap_or(false);
238    let should_start_web_server = config_options.web_server.map(|w| w).unwrap_or(false);
239    let mut reconnect_to_session = None;
240    let clear_client_terminal_attributes = "\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";
241    let take_snapshot = "\u{1b}[?1049h";
242    let bracketed_paste = "\u{1b}[?2004h";
243    let enter_kitty_keyboard_mode = "\u{1b}[>1u";
244    os_input.unset_raw_mode(0).unwrap();
245
246    if !is_a_reconnect {
247        // we don't do this for a reconnect because our controlling terminal already has the
248        // attributes we want from it, and some terminals don't treat these atomically (looking at
249        // you Windows Terminal...)
250        let _ = os_input
251            .get_stdout_writer()
252            .write(take_snapshot.as_bytes())
253            .unwrap();
254        let _ = os_input
255            .get_stdout_writer()
256            .write(clear_client_terminal_attributes.as_bytes())
257            .unwrap();
258        if !explicitly_disable_kitty_keyboard_protocol {
259            let _ = os_input
260                .get_stdout_writer()
261                .write(enter_kitty_keyboard_mode.as_bytes())
262                .unwrap();
263        }
264    }
265    envs::set_zellij("0".to_string());
266    config.env.set_vars();
267
268    let palette = config
269        .theme_config(config_options.theme.as_ref())
270        .unwrap_or_else(|| os_input.load_palette().into());
271
272    let full_screen_ws = os_input.get_terminal_size_using_fd(0);
273    let client_attributes = ClientAttributes {
274        size: full_screen_ws,
275        style: Style {
276            colors: palette,
277            rounded_corners: config.ui.pane_frames.rounded_corners,
278            hide_session_name: config.ui.pane_frames.hide_session_name,
279        },
280    };
281    let web_server_ip = config_options
282        .web_server_ip
283        .unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
284    let web_server_port = config_options.web_server_port.unwrap_or_else(|| 8082);
285    let has_certificate =
286        config_options.web_server_cert.is_some() && config_options.web_server_key.is_some();
287    let enforce_https_for_localhost = config_options.enforce_https_for_localhost.unwrap_or(false);
288
289    let create_ipc_pipe = || -> std::path::PathBuf {
290        let mut sock_dir = ZELLIJ_SOCK_DIR.clone();
291        std::fs::create_dir_all(&sock_dir).unwrap();
292        set_permissions(&sock_dir, 0o700).unwrap();
293        sock_dir.push(envs::get_session_name().unwrap());
294        sock_dir
295    };
296
297    let (first_msg, ipc_pipe) = match info {
298        ClientInfo::Attach(name, config_options) => {
299            envs::set_session_name(name.clone());
300            os_input.update_session_name(name);
301            let ipc_pipe = create_ipc_pipe();
302            let is_web_client = false;
303
304            (
305                ClientToServerMsg::AttachClient(
306                    client_attributes,
307                    config.clone(),
308                    config_options.clone(),
309                    tab_position_to_focus,
310                    pane_id_to_focus,
311                    is_web_client,
312                ),
313                ipc_pipe,
314            )
315        },
316        ClientInfo::New(name) | ClientInfo::Resurrect(name, _) => {
317            envs::set_session_name(name.clone());
318            os_input.update_session_name(name);
319            let ipc_pipe = create_ipc_pipe();
320
321            spawn_server(&*ipc_pipe, opts.debug).unwrap();
322            if should_start_web_server {
323                if let Err(e) = spawn_web_server(&opts) {
324                    log::error!("Failed to start web server: {}", e);
325                }
326            }
327
328            let successfully_written_config =
329                Config::write_config_to_disk_if_it_does_not_exist(config.to_string(true), &opts);
330            // if we successfully wrote the config to disk, it means two things:
331            // 1. It did not exist beforehand
332            // 2. The config folder is writeable
333            //
334            // If these two are true, we should launch the setup wizard, if even one of them is
335            // false, we should never launch it.
336            let should_launch_setup_wizard = successfully_written_config;
337            let is_web_client = false;
338
339            (
340                ClientToServerMsg::NewClient(
341                    client_attributes,
342                    Box::new(opts.clone()),
343                    Box::new(config.clone()),
344                    Box::new(config_options.clone()),
345                    Box::new(layout.unwrap()),
346                    Box::new(config.plugins.clone()),
347                    is_web_client,
348                    should_launch_setup_wizard,
349                    layout_is_welcome_screen,
350                ),
351                ipc_pipe,
352            )
353        },
354    };
355
356    os_input.connect_to_server(&*ipc_pipe);
357    os_input.send_to_server(first_msg);
358
359    let mut command_is_executing = CommandIsExecuting::new();
360
361    os_input.set_raw_mode(0);
362    let _ = os_input
363        .get_stdout_writer()
364        .write(bracketed_paste.as_bytes())
365        .unwrap();
366
367    let (send_client_instructions, receive_client_instructions): ChannelWithContext<
368        ClientInstruction,
369    > = channels::bounded(50);
370    let send_client_instructions = SenderWithContext::new(send_client_instructions);
371
372    let (send_input_instructions, receive_input_instructions): ChannelWithContext<
373        InputInstruction,
374    > = channels::bounded(50);
375    let send_input_instructions = SenderWithContext::new(send_input_instructions);
376
377    std::panic::set_hook({
378        use zellij_utils::errors::handle_panic;
379        let send_client_instructions = send_client_instructions.clone();
380        let os_input = os_input.clone();
381        Box::new(move |info| {
382            if let Ok(()) = os_input.unset_raw_mode(0) {
383                handle_panic(info, &send_client_instructions);
384            }
385        })
386    });
387
388    let on_force_close = config_options.on_force_close.unwrap_or_default();
389    let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new()));
390
391    let _stdin_thread = thread::Builder::new()
392        .name("stdin_handler".to_string())
393        .spawn({
394            let os_input = os_input.clone();
395            let send_input_instructions = send_input_instructions.clone();
396            let stdin_ansi_parser = stdin_ansi_parser.clone();
397            move || {
398                stdin_loop(
399                    os_input,
400                    send_input_instructions,
401                    stdin_ansi_parser,
402                    explicitly_disable_kitty_keyboard_protocol,
403                )
404            }
405        });
406
407    let _input_thread = thread::Builder::new()
408        .name("input_handler".to_string())
409        .spawn({
410            let send_client_instructions = send_client_instructions.clone();
411            let command_is_executing = command_is_executing.clone();
412            let os_input = os_input.clone();
413            let default_mode = config_options.default_mode.unwrap_or_default();
414            move || {
415                input_loop(
416                    os_input,
417                    config,
418                    config_options,
419                    command_is_executing,
420                    send_client_instructions,
421                    default_mode,
422                    receive_input_instructions,
423                )
424            }
425        });
426
427    let _signal_thread = thread::Builder::new()
428        .name("signal_listener".to_string())
429        .spawn({
430            let os_input = os_input.clone();
431            let opts = opts.clone();
432            move || {
433                report_changes_in_config_file(&opts, &os_input);
434                os_input.handle_signals(
435                    Box::new({
436                        let os_api = os_input.clone();
437                        move || {
438                            os_api.send_to_server(ClientToServerMsg::TerminalResize(
439                                os_api.get_terminal_size_using_fd(0),
440                            ));
441                        }
442                    }),
443                    Box::new({
444                        let os_api = os_input.clone();
445                        move || {
446                            os_api.send_to_server(ClientToServerMsg::Action(
447                                on_force_close.into(),
448                                None,
449                                None,
450                            ));
451                        }
452                    }),
453                );
454            }
455        })
456        .unwrap();
457
458    let router_thread = thread::Builder::new()
459        .name("router".to_string())
460        .spawn({
461            let os_input = os_input.clone();
462            let mut should_break = false;
463            move || loop {
464                match os_input.recv_from_server() {
465                    Some((instruction, err_ctx)) => {
466                        err_ctx.update_thread_ctx();
467                        if let ServerToClientMsg::Exit(_) = instruction {
468                            should_break = true;
469                        }
470                        send_client_instructions.send(instruction.into()).unwrap();
471                        if should_break {
472                            break;
473                        }
474                    },
475                    None => {
476                        send_client_instructions
477                            .send(ClientInstruction::UnblockInputThread)
478                            .unwrap();
479                        log::error!("Received empty message from server");
480                        send_client_instructions
481                            .send(ClientInstruction::Error(
482                                "Received empty message from server".to_string(),
483                            ))
484                            .unwrap();
485                        break;
486                    },
487                }
488            }
489        })
490        .unwrap();
491
492    let handle_error = |backtrace: String| {
493        os_input.unset_raw_mode(0).unwrap();
494        let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1);
495        let restore_snapshot = "\u{1b}[?1049l";
496        os_input.disable_mouse().non_fatal();
497        let error = format!(
498            "{}\n{}{}\n",
499            restore_snapshot, goto_start_of_last_line, backtrace
500        );
501        let _ = os_input
502            .get_stdout_writer()
503            .write(error.as_bytes())
504            .unwrap();
505        let _ = os_input.get_stdout_writer().flush().unwrap();
506        std::process::exit(1);
507    };
508
509    let mut exit_msg = String::new();
510    let mut loading = true;
511    let mut pending_instructions = vec![];
512    let mut synchronised_output = match os_input.env_variable("TERM").as_deref() {
513        Some("alacritty") => Some(SyncOutput::DCS),
514        _ => None,
515    };
516
517    let mut stdout = os_input.get_stdout_writer();
518    stdout
519        .write_all("\u{1b}[1m\u{1b}[HLoading Zellij\u{1b}[m\n\r".as_bytes())
520        .expect("cannot write to stdout");
521    stdout.flush().expect("could not flush");
522
523    loop {
524        let (client_instruction, mut err_ctx) = if !loading && !pending_instructions.is_empty() {
525            // there are buffered instructions, we need to go through them before processing the
526            // new ones
527            pending_instructions.remove(0)
528        } else {
529            receive_client_instructions
530                .recv()
531                .expect("failed to receive app instruction on channel")
532        };
533
534        if loading {
535            // when the app is still loading, we buffer instructions and show a loading screen
536            match client_instruction {
537                ClientInstruction::StartedParsingStdinQuery => {
538                    stdout
539                        .write_all("Querying terminal emulator for \u{1b}[32;1mdefault colors\u{1b}[m and \u{1b}[32;1mpixel/cell\u{1b}[m ratio...".as_bytes())
540                        .expect("cannot write to stdout");
541                    stdout.flush().expect("could not flush");
542                },
543                ClientInstruction::DoneParsingStdinQuery => {
544                    stdout
545                        .write_all("done".as_bytes())
546                        .expect("cannot write to stdout");
547                    stdout.flush().expect("could not flush");
548                    loading = false;
549                },
550                instruction => {
551                    pending_instructions.push((instruction, err_ctx));
552                },
553            }
554            continue;
555        }
556
557        err_ctx.add_call(ContextType::Client((&client_instruction).into()));
558
559        match client_instruction {
560            ClientInstruction::Exit(reason) => {
561                os_input.send_to_server(ClientToServerMsg::ClientExited);
562
563                if let ExitReason::Error(_) = reason {
564                    handle_error(reason.to_string());
565                }
566                exit_msg = reason.to_string();
567                break;
568            },
569            ClientInstruction::Error(backtrace) => {
570                handle_error(backtrace);
571            },
572            ClientInstruction::Render(output) => {
573                let mut stdout = os_input.get_stdout_writer();
574                if let Some(sync) = synchronised_output {
575                    stdout
576                        .write_all(sync.start_seq())
577                        .expect("cannot write to stdout");
578                }
579                stdout
580                    .write_all(output.as_bytes())
581                    .expect("cannot write to stdout");
582                if let Some(sync) = synchronised_output {
583                    stdout
584                        .write_all(sync.end_seq())
585                        .expect("cannot write to stdout");
586                }
587                stdout.flush().expect("could not flush");
588            },
589            ClientInstruction::UnblockInputThread => {
590                command_is_executing.unblock_input_thread();
591            },
592            ClientInstruction::Log(lines_to_log) => {
593                for line in lines_to_log {
594                    log::info!("{line}");
595                }
596            },
597            ClientInstruction::LogError(lines_to_log) => {
598                for line in lines_to_log {
599                    log::error!("{line}");
600                }
601            },
602            ClientInstruction::SwitchSession(connect_to_session) => {
603                reconnect_to_session = Some(connect_to_session);
604                os_input.send_to_server(ClientToServerMsg::ClientExited);
605                break;
606            },
607            ClientInstruction::SetSynchronizedOutput(enabled) => {
608                synchronised_output = enabled;
609            },
610            ClientInstruction::QueryTerminalSize => {
611                os_input.send_to_server(ClientToServerMsg::TerminalResize(
612                    os_input.get_terminal_size_using_fd(0),
613                ));
614            },
615            ClientInstruction::WriteConfigToDisk { config } => {
616                match Config::write_config_to_disk(config, &opts) {
617                    Ok(written_config) => {
618                        let _ = os_input
619                            .send_to_server(ClientToServerMsg::ConfigWrittenToDisk(written_config));
620                    },
621                    Err(e) => {
622                        let error_path = e
623                            .as_ref()
624                            .map(|p| p.display().to_string())
625                            .unwrap_or_else(String::new);
626                        log::error!("Failed to write config to disk: {}", error_path);
627                        let _ = os_input
628                            .send_to_server(ClientToServerMsg::FailedToWriteConfigToDisk(e));
629                    },
630                }
631            },
632            ClientInstruction::StartWebServer => {
633                let web_server_base_url = web_server_base_url(
634                    web_server_ip,
635                    web_server_port,
636                    has_certificate,
637                    enforce_https_for_localhost,
638                );
639                match spawn_web_server(&opts) {
640                    Ok(_) => {
641                        let _ = os_input.send_to_server(ClientToServerMsg::WebServerStarted(
642                            web_server_base_url,
643                        ));
644                    },
645                    Err(e) => {
646                        log::error!("Failed to start web_server: {}", e);
647                        let _ =
648                            os_input.send_to_server(ClientToServerMsg::FailedToStartWebServer(e));
649                    },
650                }
651            },
652            _ => {},
653        }
654    }
655
656    router_thread.join().unwrap();
657
658    if reconnect_to_session.is_none() {
659        let reset_style = "\u{1b}[m";
660        let show_cursor = "\u{1b}[?25h";
661        let restore_snapshot = "\u{1b}[?1049l";
662        let goto_start_of_last_line = format!("\u{1b}[{};{}H", full_screen_ws.rows, 1);
663        let goodbye_message = format!(
664            "{}\n{}{}{}{}\n",
665            goto_start_of_last_line, restore_snapshot, reset_style, show_cursor, exit_msg
666        );
667
668        os_input.disable_mouse().non_fatal();
669        info!("{}", exit_msg);
670        os_input.unset_raw_mode(0).unwrap();
671        let mut stdout = os_input.get_stdout_writer();
672        let exit_kitty_keyboard_mode = "\u{1b}[<1u";
673        if !explicitly_disable_kitty_keyboard_protocol {
674            let _ = stdout.write(exit_kitty_keyboard_mode.as_bytes()).unwrap();
675            stdout.flush().unwrap();
676        }
677        let _ = stdout.write(goodbye_message.as_bytes()).unwrap();
678        stdout.flush().unwrap();
679    } else {
680        let clear_screen = "\u{1b}[2J";
681        let mut stdout = os_input.get_stdout_writer();
682        let _ = stdout.write(clear_screen.as_bytes()).unwrap();
683        stdout.flush().unwrap();
684    }
685
686    let _ = send_input_instructions.send(InputInstruction::Exit);
687
688    reconnect_to_session
689}
690
691pub fn start_server_detached(
692    mut os_input: Box<dyn ClientOsApi>,
693    opts: CliArgs,
694    config: Config,
695    config_options: Options,
696    info: ClientInfo,
697    layout: Option<Layout>,
698) {
699    envs::set_zellij("0".to_string());
700    config.env.set_vars();
701
702    let should_start_web_server = config_options.web_server.map(|w| w).unwrap_or(false);
703
704    let palette = config
705        .theme_config(config_options.theme.as_ref())
706        .unwrap_or_else(|| os_input.load_palette().into());
707
708    let client_attributes = ClientAttributes {
709        size: Size { rows: 50, cols: 50 }, // just so size is not 0, it doesn't matter because we
710        // immediately detach
711        style: Style {
712            colors: palette,
713            rounded_corners: config.ui.pane_frames.rounded_corners,
714            hide_session_name: config.ui.pane_frames.hide_session_name,
715        },
716    };
717
718    let create_ipc_pipe = || -> std::path::PathBuf {
719        let mut sock_dir = ZELLIJ_SOCK_DIR.clone();
720        std::fs::create_dir_all(&sock_dir).unwrap();
721        set_permissions(&sock_dir, 0o700).unwrap();
722        sock_dir.push(envs::get_session_name().unwrap());
723        sock_dir
724    };
725
726    let (first_msg, ipc_pipe) = match info {
727        ClientInfo::New(name) | ClientInfo::Resurrect(name, _) => {
728            envs::set_session_name(name.clone());
729            os_input.update_session_name(name);
730            let ipc_pipe = create_ipc_pipe();
731
732            spawn_server(&*ipc_pipe, opts.debug).unwrap();
733            if should_start_web_server {
734                if let Err(e) = spawn_web_server(&opts) {
735                    log::error!("Failed to start web server: {}", e);
736                }
737            }
738            let should_launch_setup_wizard = false; // no setup wizard when starting a detached
739                                                    // server
740            let is_web_client = false;
741
742            (
743                ClientToServerMsg::NewClient(
744                    client_attributes,
745                    Box::new(opts),
746                    Box::new(config.clone()),
747                    Box::new(config_options.clone()),
748                    Box::new(layout.unwrap()),
749                    Box::new(config.plugins.clone()),
750                    is_web_client,
751                    should_launch_setup_wizard,
752                    false,
753                ),
754                ipc_pipe,
755            )
756        },
757        _ => {
758            eprintln!("Session already exists");
759            std::process::exit(1);
760        },
761    };
762
763    os_input.connect_to_server(&*ipc_pipe);
764    os_input.send_to_server(first_msg);
765}
766
767pub fn report_changes_in_config_file(opts: &CliArgs, os_input: &Box<dyn ClientOsApi>) {
768    if let Some(config_file_path) = Config::config_file_path(&opts) {
769        let os_input = os_input.clone();
770        std::thread::spawn(move || {
771            let rt = tokio::runtime::Runtime::new().unwrap();
772            rt.block_on(async move {
773                watch_config_file_changes(config_file_path, move |new_config| {
774                    let os_input = os_input.clone();
775                    async move {
776                        os_input.send_to_server(ClientToServerMsg::ConfigWrittenToDisk(new_config));
777                    }
778                })
779                .await;
780            });
781        });
782    }
783}