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";
57const ENABLE_HOST_THEME_NOTIFY: &str = "\u{1b}[?2031h";
61const DISABLE_HOST_THEME_NOTIFY: &str = "\u{1b}[?2031l";
64const QUERY_HOST_THEME: &str = "\u{1b}[?996n";
68
69pub(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#[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(()), CliPipeOutput((), ()), QueryTerminalSize,
179 StartWebServer,
180 #[allow(dead_code)] RenamedSession(String), ConfigFileUpdated,
183 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 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 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#[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#[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#[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>), Resurrect(String, PathBuf, bool, Option<PathBuf>), Watch(String, Options), }
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), #[allow(dead_code)] MouseEvent(zellij_utils::input::mouse::MouseEvent),
432 AnsiStdinInstructions(Vec<AnsiStdinInstruction>),
433 DesktopNotificationResponse(Vec<u8>),
434 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 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 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 break;
495 }
496 Err(e) => {
497 log::error!("Error reading from stdin: {}", e);
498 break;
499 }
500 }
501 }
502
503 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 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 }
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 }
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, config_options: Options, info: ClientInfo,
726 tab_position_to_focus: Option<usize>,
727 pane_id_to_focus: Option<(u32, bool)>, 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 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 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 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 stdin_ansi_parser.lock().unwrap().open_forward(token);
1221 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 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 }, 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 }, 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;