diff options
| author | Aram Drevekenin <aram@poor.dev> | 2025-08-28 17:48:34 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-28 17:48:34 +0200 |
| commit | 3a26df6314cf5966ffb3065b97d4183b480d1441 (patch) | |
| tree | 0afc4bc62272f22c397ccab260a46bbfed4ac24f | |
| parent | fix(web): avoid sending single meta keypresses, cleanup (#4376) (diff) | |
| download | sl0ppij-3a26df6314cf5966ffb3065b97d4183b480d1441.tar.gz sl0ppij-3a26df6314cf5966ffb3065b97d4183b480d1441.tar.xz sl0ppij-3a26df6314cf5966ffb3065b97d4183b480d1441.zip | |
refactor(client-server-contract): config + layout (#4383)
* commented out ConfigWrittenToDisk instructions
* commented out WriteConfigToDisk instructions
* commented out FailedToWriteConfigToDisk and WriteConfigToDisk instructions
* pass cli assets from client to server instead of parsed config - kind of working, lots of stuff still broken
* move ClientAttributes to the server side
* move ClientAttributes to the server side in Attach instruction as well
* move CliArgs and runtime options from NewClient -> move them to the server
* moved plugin_aliases to the server side
* moved initial config writing to the server side
* moved layout parsing to the server
* some cleanups
* move config file watching and live reload to the server
* move config file writing to the server side
* use saved config when attaching
* get web server to work with the new config roles
* some cleanups
* moar cleanups
* even moar cleanups
* yet more cleanups
* fix tests
* style(fmt): rustfmt
* style(fmt): rustfmt
* fix e2e tests
* docs(changelog): add PR
28 files changed, 882 insertions, 878 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f31d71a..1aa388aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] * fix: meta key handling in web client (https://github.com/zellij-org/zellij/pull/4376) +* refactor: move logical structures from client to server (https://github.com/zellij-org/zellij/pull/4383) ## [0.43.1] - 2025-08-08 * fix: pane rename backspace regression (https://github.com/zellij-org/zellij/pull/4346) @@ -5853,6 +5853,7 @@ dependencies = [ "sixel-tokenizer", "sysinfo", "tempfile", + "tokio", "typetag", "unicode-width 0.1.10", "url", diff --git a/src/commands.rs b/src/commands.rs index e25bce6d..01df6612 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -21,6 +21,8 @@ use zellij_utils::sessions::{ SessionNameMatch, }; +use zellij_utils::consts::session_layout_cache_file_name; + #[cfg(feature = "web_server_capability")] use zellij_client::web_client::start_web_client as start_web_client_impl; @@ -36,15 +38,14 @@ use miette::{Report, Result}; use zellij_server::{os_input_output::get_server_os_input, start_server as start_server_impl}; use zellij_utils::{ cli::{CliArgs, Command, SessionCommand, Sessions}, - data::{ConnectToSession, LayoutInfo}, + data::ConnectToSession, envs, input::{ actions::Action, config::{Config, ConfigError}, - layout::Layout, options::Options, }, - setup::{find_default_config_dir, get_layout_dir, Setup}, + setup::Setup, }; pub(crate) use zellij_utils::sessions::list_sessions; @@ -213,7 +214,7 @@ pub(crate) fn start_web_server( } fn create_new_client() -> ClientInfo { - ClientInfo::New(generate_unique_session_name_or_exit()) + ClientInfo::New(generate_unique_session_name_or_exit(), None, None) } #[cfg(feature = "web_server_capability")] @@ -534,7 +535,7 @@ fn attach_with_session_name( if session_exists(session).unwrap() { ClientInfo::Attach(session_name.unwrap(), config_options) } else { - ClientInfo::New(session_name.unwrap()) + ClientInfo::New(session_name.unwrap(), None, None) } }, Some(prefix) => match match_session_name(prefix).unwrap() { @@ -583,7 +584,7 @@ pub(crate) fn start_client(opts: CliArgs) { convert_old_yaml_files(&opts); let ( config, - layout, + _layout, config_options, mut config_without_layout, mut config_options_without_layout, @@ -599,19 +600,18 @@ pub(crate) fn start_client(opts: CliArgs) { process::exit(1); }, }; - let layout_is_welcome_screen = opts.layout == Some(PathBuf::from("welcome")) - || config.options.default_layout == Some(PathBuf::from("welcome")); let mut reconnect_to_session: Option<ConnectToSession> = None; let os_input = get_os_input(get_client_os_input); loop { let os_input = os_input.clone(); - let mut config = config.clone(); - let mut layout = layout.clone(); + let config = config.clone(); let mut config_options = config_options.clone(); let mut opts = opts.clone(); let mut is_a_reconnect = false; let mut should_create_detached = false; + let mut layout_info = None; + let mut new_session_cwd = None; if let Some(reconnect_to_session) = &reconnect_to_session { // this is integration code to make session reconnects work with this existing, @@ -639,53 +639,11 @@ pub(crate) fn start_client(opts: CliArgs) { } if let Some(reconnect_layout) = &reconnect_to_session.layout { - let layout_dir = config.options.layout_dir.clone().or_else(|| { - get_layout_dir(opts.config_dir.clone().or_else(find_default_config_dir)) - }); - let new_session_layout = match reconnect_layout { - LayoutInfo::BuiltIn(layout_name) => Layout::from_default_assets( - &PathBuf::from(layout_name), - layout_dir.clone(), - config_without_layout.clone(), - ), - LayoutInfo::File(layout_name) => Layout::from_path_or_default( - Some(&PathBuf::from(layout_name)), - layout_dir.clone(), - config_without_layout.clone(), - ), - LayoutInfo::Url(url) => Layout::from_url(&url, config_without_layout.clone()), - LayoutInfo::Stringified(stringified_layout) => Layout::from_stringified_layout( - &stringified_layout, - config_without_layout.clone(), - ), - }; - match new_session_layout { - Ok(new_session_layout) => { - // here we make sure to override both the layout and the config, but we do - // this with an instance of the config before it was merged with the - // layout configuration of the previous iteration of the loop, since we do - // not want it to mix with the config of this session - let (new_layout, new_layout_config) = new_session_layout; - layout = new_layout; - if let Some(cwd) = reconnect_to_session.cwd.as_ref() { - layout.add_cwd_to_layout(cwd); - } - let mut new_config = config_without_layout.clone(); - let _ = new_config.merge(new_layout_config.clone()); - config = new_config; - config_options = - config_options_without_layout.merge(new_layout_config.options); - }, - Err(e) => { - log::error!("Failed to parse new session layout: {:?}", e); - }, - } - } else { - if let Some(cwd) = reconnect_to_session.cwd.as_ref() { - config_options.default_cwd = Some(cwd.clone()); - } + layout_info = Some(reconnect_layout.clone()); + } + if let Some(cwd) = &reconnect_to_session.cwd { + new_session_cwd = Some(cwd.clone()); } - is_a_reconnect = true; } @@ -710,7 +668,7 @@ pub(crate) fn start_client(opts: CliArgs) { }; should_create_detached = create_background; - let client = if let Some(idx) = index { + let mut client = if let Some(idx) = index { attach_with_session_index( config_options.clone(), idx, @@ -742,7 +700,12 @@ pub(crate) fn start_client(opts: CliArgs) { if force_run_commands { resurrection_layout.recursively_add_start_suspended(Some(false)); } - ClientInfo::Resurrect(session_name.clone(), resurrection_layout) + ClientInfo::Resurrect( + session_name.clone(), + session_layout_cache_file_name(session_name.as_ref()), + force_run_commands, + new_session_cwd.clone(), + ) }, _ => attach_with_session_name( session_name, @@ -758,13 +721,13 @@ pub(crate) fn start_client(opts: CliArgs) { } } - let attach_layout = match &client { - ClientInfo::Attach(_, _) => None, - ClientInfo::New(_) => Some(layout), - ClientInfo::Resurrect(_session_name, layout_to_resurrect) => { - Some(layout_to_resurrect.clone()) - }, - }; + if let Some(layout_info) = layout_info { + client.set_layout_info(layout_info); + } + + if let Some(new_session_cwd) = new_session_cwd { + client.set_cwd(new_session_cwd); + } let tab_position_to_focus = reconnect_to_session .as_ref() @@ -778,12 +741,10 @@ pub(crate) fn start_client(opts: CliArgs) { config, config_options, client, - attach_layout, tab_position_to_focus, pane_id_to_focus, is_a_reconnect, should_create_detached, - layout_is_welcome_screen, ); } else { if let Some(session_name) = opts.session.clone() { @@ -793,13 +754,11 @@ pub(crate) fn start_client(opts: CliArgs) { opts, config, config_options, - ClientInfo::New(session_name), - Some(layout), + ClientInfo::New(session_name, layout_info, new_session_cwd), None, None, is_a_reconnect, should_create_detached, - layout_is_welcome_screen, ); } else { if let Some(session_name) = config_options.session_name.as_ref() { @@ -823,25 +782,16 @@ pub(crate) fn start_client(opts: CliArgs) { config_options.clone(), true, ); - let attach_layout = match &client { - ClientInfo::Attach(_, _) => None, - ClientInfo::New(_) => Some(layout), - ClientInfo::Resurrect(_, resurrection_layout) => { - Some(resurrection_layout.clone()) - }, - }; reconnect_to_session = start_client_impl( Box::new(os_input), opts, config, config_options, client, - attach_layout, None, None, is_a_reconnect, should_create_detached, - layout_is_welcome_screen, ); }, _ => { @@ -851,13 +801,11 @@ pub(crate) fn start_client(opts: CliArgs) { opts, config, config_options.clone(), - ClientInfo::New(session_name.clone()), - Some(layout), + ClientInfo::New(session_name.clone(), layout_info, new_session_cwd), None, None, is_a_reconnect, should_create_detached, - layout_is_welcome_screen, ); }, } @@ -876,13 +824,11 @@ pub(crate) fn start_client(opts: CliArgs) { opts, config, config_options, - ClientInfo::New(session_name), - Some(layout), + ClientInfo::New(session_name, layout_info, new_session_cwd), None, None, is_a_reconnect, should_create_detached, - layout_is_welcome_screen, ); } } diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index b9035002..54b6c468 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -185,7 +185,7 @@ fn start_zellij_without_frames(channel: &mut ssh2::Channel) { channel .write_all( format!( - "{} {} --session {} --data-dir {} options --show-release-notes false --show-startup-tips false --no-pane-frames\n", + "{} {} --session {} --data-dir {} options --show-release-notes false --show-startup-tips false --pane-frames false\n", SET_ENV_VARIABLES, ZELLIJ_EXECUTABLE_LOCATION, SESSION_NAME, ZELLIJ_DATA_DIR ) .as_bytes(), diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 0f326f8f..f06663d0 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -14,7 +14,7 @@ use log::info; use std::env::current_exe; use std::io::{self, Write}; use std::net::{IpAddr, Ipv4Addr}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::{Arc, Mutex}; use std::thread; @@ -27,20 +27,17 @@ use crate::{ os_input_output::ClientOsApi, stdin_handler::stdin_loop, }; use termwiz::input::InputEvent; +use zellij_utils::cli::CliArgs; use zellij_utils::{ channels::{self, ChannelWithContext, SenderWithContext}, consts::{set_permissions, ZELLIJ_SOCK_DIR}, - data::{ClientId, ConnectToSession, KeyWithModifier, Style}, + data::{ClientId, ConnectToSession, KeyWithModifier, LayoutInfo}, envs, errors::{ClientContext, ContextType, ErrorInstruction}, - input::{ - config::{watch_config_file_changes, Config}, - options::Options, - }, - ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg}, + input::{cli_assets::CliAssets, config::Config, options::Options}, + ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg}, pane_size::Size, }; -use zellij_utils::{cli::CliArgs, input::layout::Layout}; /// Instructions related to the client-side application #[derive(Debug, Clone)] @@ -59,12 +56,10 @@ pub(crate) enum ClientInstruction { UnblockCliPipeInput(()), // String -> pipe name CliPipeOutput((), ()), // String -> pipe name, String -> output QueryTerminalSize, - WriteConfigToDisk { - config: String, - }, StartWebServer, #[allow(dead_code)] // we need the session name here even though we're not currently using it RenamedSession(String), // String -> new session name + ConfigFileUpdated, } impl From<ServerToClientMsg> for ClientInstruction { @@ -86,11 +81,9 @@ impl From<ServerToClientMsg> for ClientInstruction { ClientInstruction::CliPipeOutput((), ()) }, ServerToClientMsg::QueryTerminalSize => ClientInstruction::QueryTerminalSize, - ServerToClientMsg::WriteConfigToDisk { config } => { - ClientInstruction::WriteConfigToDisk { config } - }, ServerToClientMsg::StartWebServer => ClientInstruction::StartWebServer, ServerToClientMsg::RenamedSession(name) => ClientInstruction::RenamedSession(name), + ServerToClientMsg::ConfigFileUpdated => ClientInstruction::ConfigFileUpdated, } } } @@ -112,9 +105,9 @@ impl From<&ClientInstruction> for ClientContext { ClientInstruction::UnblockCliPipeInput(..) => ClientContext::UnblockCliPipeInput, ClientInstruction::CliPipeOutput(..) => ClientContext::CliPipeOutput, ClientInstruction::QueryTerminalSize => ClientContext::QueryTerminalSize, - ClientInstruction::WriteConfigToDisk { .. } => ClientContext::WriteConfigToDisk, ClientInstruction::StartWebServer => ClientContext::StartWebServer, ClientInstruction::RenamedSession(..) => ClientContext::RenamedSession, + ClientInstruction::ConfigFileUpdated => ClientContext::ConfigFileUpdated, } } } @@ -126,9 +119,9 @@ impl ErrorInstruction for ClientInstruction { } #[cfg(feature = "web_server_capability")] -fn spawn_web_server(opts: &CliArgs) -> Result<String, String> { +fn spawn_web_server(cli_args: &CliArgs) -> Result<String, String> { let mut cmd = Command::new(current_exe().map_err(|e| e.to_string())?); - if let Some(config_file_path) = Config::config_file_path(opts) { + if let Some(config_file_path) = Config::config_file_path(cli_args) { let config_file_path_exists = Path::new(&config_file_path).exists(); if !config_file_path_exists { return Err(format!( @@ -157,7 +150,7 @@ fn spawn_web_server(opts: &CliArgs) -> Result<String, String> { } #[cfg(not(feature = "web_server_capability"))] -fn spawn_web_server(_opts: &CliArgs) -> Result<String, String> { +fn spawn_web_server(_cli_args: &CliArgs) -> Result<String, String> { log::error!( "This version of Zellij was compiled without web server support, cannot run web server!" ); @@ -188,16 +181,29 @@ pub fn spawn_server(socket_path: &Path, debug: bool) -> io::Result<()> { #[derive(Debug, Clone)] pub enum ClientInfo { Attach(String, Options), - New(String), - Resurrect(String, Layout), + New(String, Option<LayoutInfo>, Option<PathBuf>), // PathBuf -> explicit cwd + Resurrect(String, PathBuf, bool, Option<PathBuf>), // (name, path_to_layout, force_run_commands, cwd) } impl ClientInfo { pub fn get_session_name(&self) -> &str { match self { Self::Attach(ref name, _) => name, - Self::New(ref name) => name, - Self::Resurrect(ref name, _) => name, + Self::New(ref name, _layout_info, _layout_cwd) => name, + Self::Resurrect(ref name, _, _, _) => name, + } + } + pub fn set_layout_info(&mut self, new_layout_info: LayoutInfo) { + match self { + ClientInfo::New(_, layout_info, _) => *layout_info = Some(new_layout_info), + _ => {}, + } + } + pub fn set_cwd(&mut self, new_cwd: PathBuf) { + match self { + ClientInfo::New(_, _, cwd) => *cwd = Some(new_cwd), + ClientInfo::Resurrect(_, _, _, cwd) => *cwd = Some(new_cwd), + _ => {}, } } } @@ -214,19 +220,17 @@ pub(crate) enum InputInstruction { pub fn start_client( mut os_input: Box<dyn ClientOsApi>, - opts: CliArgs, + cli_args: CliArgs, config: Config, // saved to disk (or default?) config_options: Options, // CLI options merged into (getting priority over) saved config options info: ClientInfo, - layout: Option<Layout>, tab_position_to_focus: Option<usize>, pane_id_to_focus: Option<(u32, bool)>, // (pane_id, is_plugin) is_a_reconnect: bool, start_detached_and_exit: bool, - layout_is_welcome_screen: bool, ) -> Option<ConnectToSession> { if start_detached_and_exit { - start_server_detached(os_input, opts, config, config_options, info, layout); + start_server_detached(os_input, cli_args, config, config_options, info); return None; } info!("Starting Zellij client!"); @@ -265,19 +269,8 @@ pub fn start_client( envs::set_zellij("0".to_string()); config.env.set_vars(); - let palette = config - .theme_config(config_options.theme.as_ref()) - .unwrap_or_else(|| os_input.load_palette().into()); - let full_screen_ws = os_input.get_terminal_size_using_fd(0); - let client_attributes = ClientAttributes { - size: full_screen_ws, - style: Style { - colors: palette, - rounded_corners: config.ui.pane_frames.rounded_corners, - hide_session_name: config.ui.pane_frames.hide_session_name, - }, - }; + let web_server_ip = config_options .web_server_ip .unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); @@ -301,11 +294,33 @@ pub fn start_client( let ipc_pipe = create_ipc_pipe(); let is_web_client = false; + let cli_assets = CliAssets { + config_file_path: Config::config_file_path(&cli_args), + config_dir: cli_args.config_dir.clone(), + should_ignore_config: cli_args.is_setup_clean(), + configuration_options: Some(config_options.clone()), + layout: cli_args + .layout + .as_ref() + .and_then(|l| { + LayoutInfo::from_config(&config_options.layout_dir, &Some(l.clone())) + }) + .or_else(|| { + LayoutInfo::from_config( + &config_options.layout_dir, + &config_options.default_layout, + ) + }), + terminal_window_size: full_screen_ws, + data_dir: cli_args.data_dir.clone(), + is_debug: cli_args.debug, + max_panes: cli_args.max_panes, + force_run_layout_commands: false, + cwd: None, + }; ( ClientToServerMsg::AttachClient( - client_attributes, - config.clone(), - config_options.clone(), + cli_assets, tab_position_to_focus, pane_id_to_focus, is_web_client, @@ -313,41 +328,84 @@ pub fn start_client( ipc_pipe, ) }, - ClientInfo::New(name) | ClientInfo::Resurrect(name, _) => { + ClientInfo::Resurrect(name, path_to_layout, force_run_commands, cwd) => { envs::set_session_name(name.clone()); + + let cli_assets = CliAssets { + config_file_path: Config::config_file_path(&cli_args), + config_dir: cli_args.config_dir.clone(), + should_ignore_config: cli_args.is_setup_clean(), + configuration_options: Some(config_options.clone()), + layout: Some(LayoutInfo::File(path_to_layout.display().to_string())), + terminal_window_size: full_screen_ws, + data_dir: cli_args.data_dir.clone(), + is_debug: cli_args.debug, + max_panes: cli_args.max_panes, + force_run_layout_commands: force_run_commands, + cwd, + }; + os_input.update_session_name(name); let ipc_pipe = create_ipc_pipe(); - spawn_server(&*ipc_pipe, opts.debug).unwrap(); + spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); if should_start_web_server { - if let Err(e) = spawn_web_server(&opts) { + if let Err(e) = spawn_web_server(&cli_args) { log::error!("Failed to start web server: {}", e); } } - let successfully_written_config = - Config::write_config_to_disk_if_it_does_not_exist(config.to_string(true), &opts); - // if we successfully wrote the config to disk, it means two things: - // 1. It did not exist beforehand - // 2. The config folder is writeable - // - // If these two are true, we should launch the setup wizard, if even one of them is - // false, we should never launch it. - let should_launch_setup_wizard = successfully_written_config; let is_web_client = false; ( - ClientToServerMsg::NewClient( - client_attributes, - Box::new(opts.clone()), - Box::new(config.clone()), - Box::new(config_options.clone()), - Box::new(layout.unwrap()), - Box::new(config.plugins.clone()), - is_web_client, - should_launch_setup_wizard, - layout_is_welcome_screen, - ), + ClientToServerMsg::FirstClientConnected(cli_assets, is_web_client), + ipc_pipe, + ) + }, + ClientInfo::New(name, layout_info, layout_cwd) => { + envs::set_session_name(name.clone()); + + let cli_assets = CliAssets { + config_file_path: Config::config_file_path(&cli_args), + config_dir: cli_args.config_dir.clone(), + should_ignore_config: cli_args.is_setup_clean(), + configuration_options: Some(config_options.clone()), + layout: layout_info.or_else(|| { + cli_args + .layout + .as_ref() + .and_then(|l| { + LayoutInfo::from_config(&config_options.layout_dir, &Some(l.clone())) + }) + .or_else(|| { + LayoutInfo::from_config( + &config_options.layout_dir, + &config_options.default_layout, + ) + }) + }), + terminal_window_size: full_screen_ws, + data_dir: cli_args.data_dir.clone(), + is_debug: cli_args.debug, + max_panes: cli_args.max_panes, + force_run_layout_commands: false, + cwd: layout_cwd, + }; + + os_input.update_session_name(name); + let ipc_pipe = create_ipc_pipe(); + + spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); + if should_start_web_server { + if let Err(e) = spawn_web_server(&cli_args) { + log::error!("Failed to start web server: {}", e); + } + } + + let is_web_client = false; + + ( + ClientToServerMsg::FirstClientConnected(cli_assets, is_web_client), ipc_pipe, ) }, @@ -428,9 +486,7 @@ pub fn start_client( .name("signal_listener".to_string()) .spawn({ let os_input = os_input.clone(); - let opts = opts.clone(); move || { - report_changes_in_config_file(&opts, &os_input); os_input.handle_signals( Box::new({ let os_api = os_input.clone(); @@ -612,23 +668,6 @@ pub fn start_client( os_input.get_terminal_size_using_fd(0), )); }, - ClientInstruction::WriteConfigToDisk { config } => { - match Config::write_config_to_disk(config, &opts) { - Ok(written_config) => { - let _ = os_input - .send_to_server(ClientToServerMsg::ConfigWrittenToDisk(written_config)); - }, - Err(e) => { - let error_path = e - .as_ref() - .map(|p| p.display().to_string()) - .unwrap_or_else(String::new); - log::error!("Failed to write config to disk: {}", error_path); - let _ = os_input - .send_to_server(ClientToServerMsg::FailedToWriteConfigToDisk(e)); - }, - } - }, ClientInstruction::StartWebServer => { let web_server_base_url = web_server_base_url( web_server_ip, @@ -636,7 +675,7 @@ pub fn start_client( has_certificate, enforce_https_for_localhost, ); - match spawn_web_server(&opts) { + match spawn_web_server(&cli_args) { Ok(_) => { let _ = os_input.send_to_server(ClientToServerMsg::WebServerStarted( web_server_base_url, @@ -690,31 +729,16 @@ pub fn start_client( pub fn start_server_detached( mut os_input: Box<dyn ClientOsApi>, - opts: CliArgs, + cli_args: CliArgs, config: Config, config_options: Options, info: ClientInfo, - layout: Option<Layout>, ) { envs::set_zellij("0".to_string()); config.env.set_vars(); let should_start_web_server = config_options.web_server.map(|w| w).unwrap_or(false); - let palette = config - .theme_config(config_options.theme.as_ref()) - .unwrap_or_else(|| os_input.load_palette().into()); - - let client_attributes = ClientAttributes { - size: Size { rows: 50, cols: 50 }, // just so size is not 0, it doesn't matter because we - // immediately detach - style: Style { - colors: palette, - rounded_corners: config.ui.pane_frames.rounded_corners, - hide_session_name: config.ui.pane_frames.hide_session_name, - }, - }; - let create_ipc_pipe = || -> std::path::PathBuf { let mut sock_dir = ZELLIJ_SOCK_DIR.clone(); std::fs::create_dir_all(&sock_dir).unwrap(); @@ -724,33 +748,85 @@ pub fn start_server_detached( }; let (first_msg, ipc_pipe) = match info { - ClientInfo::New(name) | ClientInfo::Resurrect(name, _) => { + ClientInfo::Resurrect(name, path_to_layout, force_run_commands, cwd) => { envs::set_session_name(name.clone()); + + let cli_assets = CliAssets { + config_file_path: Config::config_file_path(&cli_args), + config_dir: cli_args.config_dir.clone(), + should_ignore_config: cli_args.is_setup_clean(), + configuration_options: Some(config_options.clone()), + layout: Some(LayoutInfo::File(path_to_layout.display().to_string())), + terminal_window_size: Size { cols: 50, rows: 50 }, // static number until a + // client connects + data_dir: cli_args.data_dir.clone(), + is_debug: cli_args.debug, + max_panes: cli_args.max_panes, + force_run_layout_commands: force_run_commands, + cwd, + }; + os_input.update_session_name(name); let ipc_pipe = create_ipc_pipe(); - spawn_server(&*ipc_pipe, opts.debug).unwrap(); + spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); if should_start_web_server { - if let Err(e) = spawn_web_server(&opts) { + if let Err(e) = spawn_web_server(&cli_args) { log::error!("Failed to start web server: {}", e); } } - let should_launch_setup_wizard = false; // no setup wizard when starting a detached - // server + let is_web_client = false; ( - ClientToServerMsg::NewClient( - client_attributes, - Box::new(opts), - Box::new(config.clone()), - Box::new(config_options.clone()), - Box::new(layout.unwrap()), - Box::new(config.plugins.clone()), - is_web_client, - should_launch_setup_wizard, - false, - ), + ClientToServerMsg::FirstClientConnected(cli_assets, is_web_client), + ipc_pipe, + ) + }, + ClientInfo::New(name, layout_info, layout_cwd) => { + envs::set_session_name(name.clone()); + + let cli_assets = CliAssets { + config_file_path: Config::config_file_path(&cli_args), + config_dir: cli_args.config_dir.clone(), + should_ignore_config: cli_args.is_setup_clean(), + configuration_options: cli_args.options(), + layout: layout_info.or_else(|| { + cli_args + .layout + .as_ref() + .and_then(|l| { + LayoutInfo::from_config(&config_options.layout_dir, &Some(l.clone())) + }) + .or_else(|| { + LayoutInfo::from_config( + &config_options.layout_dir, + &config_options.default_layout, + ) + }) + }), + terminal_window_size: Size { cols: 50, rows: 50 }, // static number until a + // client connects + data_dir: cli_args.data_dir.clone(), + is_debug: cli_args.debug, + max_panes: cli_args.max_panes, + force_run_layout_commands: false, + cwd: layout_cwd, + }; + + os_input.update_session_name(name); + let ipc_pipe = create_ipc_pipe(); + + spawn_server(&*ipc_pipe, cli_args.debug).unwrap(); + if should_start_web_server { + if let Err(e) = spawn_web_server(&cli_args) { + log::error!("Failed to start web server: {}", e); + } + } + let is_web_client = false; + + ( + ClientToServerMsg::FirstClientConnected(cli_assets, is_web_client), ipc_pipe, ) }, @@ -763,21 +839,3 @@ pub fn start_server_detached( os_input.connect_to_server(&*ipc_pipe); os_input.send_to_server(first_msg); } - -pub fn report_changes_in_config_file(opts: &CliArgs, os_input: &Box<dyn ClientOsApi>) { - if let Some(config_file_path) = Config::config_file_path(&opts) { - let os_input = os_input.clone(); - std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async move { - watch_config_file_changes(config_file_path, move |new_config| { - let os_input = os_input.clone(); - async move { - os_input.send_to_server(ClientToServerMsg::ConfigWrittenToDisk(new_config)); - } - }) - .await; - }); - }); - } -} diff --git a/zellij-client/src/web_client/ipc_listener.rs b/zellij-client/src/web_client/ipc_listener.rs index 801be8fc..766124ad 100644 --- a/zellij-client/src/web_client/ipc_listener.rs +++ b/zellij-client/src/web_client/ipc_listener.rs @@ -1,11 +1,7 @@ -use crate::web_client::types::AppState; - -use super::control_message::{SetConfigPayload, WebServerToWebClientControlMessage}; use axum_server::Handle; use tokio::io::AsyncReadExt; use tokio::net::{UnixListener, UnixStream}; use zellij_utils::consts::WEBSERVER_SOCKET_PATH; -use zellij_utils::ipc::ClientToServerMsg; use zellij_utils::web_server_commands::InstructionForWebServer; pub async fn create_webserver_receiver( @@ -33,7 +29,7 @@ pub async fn receive_webserver_instruction( .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) } -pub async fn listen_to_web_server_instructions(server_handle: Handle, state: AppState, id: &str) { +pub async fn listen_to_web_server_instructions(server_handle: Handle, id: &str) { loop { let receiver = create_webserver_receiver(id).await; match receiver { @@ -44,64 +40,6 @@ pub async fn listen_to_web_server_instructions(server_handle: Handle, state: App server_handle.shutdown(); break; }, - InstructionForWebServer::ConfigWrittenToDisk(new_config) => { - let set_config_payload = SetConfigPayload::from(&new_config); - - let mut config = state.config.lock().unwrap(); - *config = new_config.clone(); - - let client_ids: Vec<String> = { - let connection_table_lock = state.connection_table.lock().unwrap(); - connection_table_lock - .client_id_to_channels - .keys() - .cloned() - .collect() - }; - - let config_message = - WebServerToWebClientControlMessage::SetConfig(set_config_payload); - let config_msg_json = match serde_json::to_string(&config_message) { - Ok(json) => json, - Err(e) => { - log::error!("Failed to serialize config message: {}", e); - continue; - }, - }; - - for client_id in client_ids { - if let Some(control_tx) = state - .connection_table - .lock() - .unwrap() - .get_client_control_tx(&client_id) - { - let ws_message = config_msg_json.clone(); - match control_tx.send(ws_message.into()) { - Ok(_) => {}, // no-op - Err(e) => { - log::error!( - "Failed to send config update to client {}: {}", - client_id, - e - ); - }, - } - } - if let Some(os_input) = state - .connection_table - .lock() - .unwrap() - .get_client_os_api(&client_id) - { - // notify the zellij server of the config change - os_input.send_to_server( - ClientToServerMsg::ConfigWrittenToDisk(new_config.clone()), - ); - } - } - // Continue loop to recreate receiver for next message - }, }, Err(e) => { log::error!("Failed to process web server instruction: {}", e); diff --git a/zellij-client/src/web_client/mod.rs b/zellij-client/src/web_client/mod.rs index d7ef7ec0..9d46ed51 100644 --- a/zellij-client/src/web_client/mod.rs +++ b/zellij-client/src/web_client/mod.rs @@ -33,10 +33,7 @@ use nix::sys::stat::{umask, Mode}; use interprocess::unnamed_pipe::pipe; use std::io::{prelude::*, BufRead, BufReader}; use tokio::runtime::Runtime; -use zellij_utils::input::{ - config::{watch_config_file_changes, Config}, - options::Options, -}; +use zellij_utils::input::{config::Config, options::Options}; use authentication::auth_middleware; use http_handlers::{ @@ -52,13 +49,6 @@ use utils::should_use_https; use uuid::Uuid; use websocket_handlers::{ws_handler_control, ws_handler_terminal}; -use zellij_utils::{ - consts::WEBSERVER_SOCKET_PATH, - web_server_commands::{ - create_webserver_sender, send_webserver_instruction, InstructionForWebServer, - }, -}; - pub fn start_web_client( config: Config, config_options: Options, @@ -170,25 +160,6 @@ pub fn start_web_client( )); } -#[allow(dead_code)] -async fn listen_to_config_file_changes(config_file_path: PathBuf, instance_id: &str) { - let socket_path = WEBSERVER_SOCKET_PATH.join(instance_id); - - watch_config_file_changes(config_file_path, move |new_config| { - let socket_path = socket_path.clone(); - async move { - if let Ok(mut sender) = create_webserver_sender(&socket_path.to_string_lossy()) { - let _ = send_webserver_instruction( - &mut sender, - InstructionForWebServer::ConfigWrittenToDisk(new_config), - ); - drop(sender); - } - } - }) - .await; -} - pub async fn serve_web_client( config: Config, config_options: Options, @@ -219,15 +190,6 @@ pub async fn serve_web_client( .take(5) .collect(); - #[cfg(not(test))] - tokio::spawn({ - let config_file_path = config_file_path.clone(); - let id_string = format!("{}", id); - async move { - listen_to_config_file_changes(config_file_path, &id_string).await; - } - }); - let is_https = rustls_config.is_some(); let state = AppState { connection_table: connection_table.clone(), @@ -241,9 +203,8 @@ pub async fn serve_web_client( tokio::spawn({ let server_handle = server_handle.clone(); - let state = state.clone(); async move { - listen_to_web_server_instructions(server_handle, state, &format!("{}", id)).await; + listen_to_web_server_instructions(server_handle, &format!("{}", id)).await; } }); diff --git a/zellij-client/src/web_client/server_listener.rs b/zellij-client/src/web_client/server_listener.rs index 04ad6e04..1a19c327 100644 --- a/zellij-client/src/web_client/server_listener.rs +++ b/zellij-client/src/web_client/server_listener.rs @@ -1,5 +1,5 @@ use crate::os_input_output::ClientOsApi; -use crate::web_client::control_message::WebServerToWebClientControlMessage; +use crate::web_client::control_message::{SetConfigPayload, WebServerToWebClientControlMessage}; use crate::web_client::session_management::build_initial_connection; use crate::web_client::types::{ClientConnectionBus, ConnectionTable, SessionManager}; use crate::web_client::utils::terminal_init_messages; @@ -33,7 +33,7 @@ pub fn zellij_server_listener( move || { let mut client_connection_bus = ClientConnectionBus::new(&web_client_id, &connection_table); - let (mut reconnect_to_session, is_welcome_screen) = + let mut reconnect_to_session = match build_initial_connection(session_name, &config) { Ok(initial_session_connection) => initial_session_connection, Err(e) => { @@ -81,17 +81,13 @@ pub fn zellij_server_listener( .unwrap() .to_owned(); - let is_web_client = true; let (first_message, zellij_ipc_pipe) = session_manager.spawn_session_if_needed( &session_name, - path, client_attributes, - &config, + config_file_path.clone(), &config_options, - is_web_client, os_input.clone(), reconnect_info.as_ref().and_then(|r| r.layout.clone()), - is_welcome_screen, ); os_input.connect_to_server(&zellij_ipc_pipe); @@ -124,9 +120,6 @@ pub fn zellij_server_listener( reconnect_to_session = Some(connect_to_session); continue 'reconnect_loop; }, - Some((ServerToClientMsg::WriteConfigToDisk { config }, _)) => { - handle_config_write(&os_input, config); - }, Some((ServerToClientMsg::QueryTerminalSize, _)) => { client_connection_bus.send_control( WebServerToWebClientControlMessage::QueryTerminalSize, @@ -149,6 +142,53 @@ pub fn zellij_server_listener( }, ); }, + Some((ServerToClientMsg::ConfigFileUpdated, _)) => { + + if let Some(config_file_path) = &config_file_path { + if let Ok(new_config) = Config::from_path(&config_file_path, Some(config.clone())) { + let set_config_payload = SetConfigPayload::from(&new_config); + + let client_ids: Vec<String> = { + let connection_table_lock = connection_table.lock().unwrap(); + connection_table_lock + .client_id_to_channels + .keys() + .cloned() + .collect() + }; + + let config_message = + WebServerToWebClientControlMessage::SetConfig(set_config_payload); + let config_msg_json = match serde_json::to_string(&config_message) { + Ok(json) => json, + Err(e) => { + log::error!("Failed to serialize config message: {}", e); + continue; + }, + }; + + for client_id in client_ids { + if let Some(control_tx) = connection_table + .lock() + .unwrap() + .get_client_control_tx(&client_id) + { + let ws_message = config_msg_json.clone(); + match control_tx.send(ws_message.into()) { + Ok(_) => {}, // no-op + Err(e) => { + log::error!( + "Failed to send config update to client {}: {}", + client_id, + e + ); + }, + } + } + } + } + } + }, _ => { // server disconnected, stop trying to listen otherwise we retry // indefinitely and get 100% CPU @@ -189,22 +229,6 @@ fn handle_exit_reason(client_connection_bus: &mut ClientConnectionBus, exit_reas client_connection_bus.close_connection(); } -fn handle_config_write(os_input: &Box<dyn ClientOsApi>, config: String) { - match Config::write_config_to_disk(config, &CliArgs::default()) { - Ok(written_config) => { - let _ = os_input.send_to_server(ClientToServerMsg::ConfigWrittenToDisk(written_config)); - }, - Err(e) => { - let error_path = e - .as_ref() - .map(|p| p.display().to_string()) - .unwrap_or_else(String::new); - log::error!("Failed to write config to disk: {}", error_path); - let _ = os_input.send_to_server(ClientToServerMsg::FailedToWriteConfigToDisk(e)); - }, - } -} - fn reload_config_from_disk( config_without_layout: &mut Config, config_options_without_layout: &mut Options, diff --git a/zellij-client/src/web_client/session_management.rs b/zellij-client/src/web_client/session_management.rs index 6e765597..d0dc9c13 100644 --- a/zellij-client/src/web_client/session_management.rs +++ b/zellij-client/src/web_client/session_management.rs @@ -3,24 +3,18 @@ use crate::spawn_server; use std::{fs, path::PathBuf}; use zellij_utils::{ - cli::CliArgs, + consts::session_layout_cache_file_name, data::{ConnectToSession, LayoutInfo, WebSharing}, envs, - input::{ - config::{Config, ConfigError}, - layout::Layout, - options::Options, - }, + input::{cli_assets::CliAssets, config::Config, options::Options}, ipc::{ClientAttributes, ClientToServerMsg}, sessions::{generate_unique_session_name, resurrection_layout, session_exists}, - setup::{find_default_config_dir, get_layout_dir}, }; pub fn build_initial_connection( session_name: Option<String>, config: &Config, -) -> Result<(Option<ConnectToSession>, bool), &'static str> { - // bool -> is_welcome_screen +) -> Result<Option<ConnectToSession>, &'static str> { let should_start_with_welcome_screen = session_name.is_none(); let default_layout_from_config = LayoutInfo::from_config(&config.options.layout_dir, &config.options.default_layout); @@ -29,123 +23,69 @@ pub fn build_initial_connection( else { return Err("Failed to generate unique session name, bailing."); }; - Ok(( - Some(ConnectToSession { - name: Some(initial_session_name.clone()), - layout: Some(LayoutInfo::BuiltIn("welcome".to_owned())), - ..Default::default() - }), - should_start_with_welcome_screen, - )) + Ok(Some(ConnectToSession { + name: Some(initial_session_name.clone()), + layout: Some(LayoutInfo::BuiltIn("welcome".to_owned())), + ..Default::default() + })) } else if let Some(session_name) = session_name { - Ok(( - Some(ConnectToSession { - name: Some(session_name.clone()), - layout: default_layout_from_config, - ..Default::default() - }), - should_start_with_welcome_screen, - )) + Ok(Some(ConnectToSession { + name: Some(session_name.clone()), + layout: default_layout_from_config, + ..Default::default() + })) } else if default_layout_from_config.is_some() { - Ok(( - Some(ConnectToSession { - layout: default_layout_from_config, - ..Default::default() - }), - should_start_with_welcome_screen, - )) + Ok(Some(ConnectToSession { + layout: default_layout_from_config, + ..Default::default() + })) } else { - Ok((None, should_start_with_welcome_screen)) - } -} - -fn layout_for_new_session( - config: &Config, - requested_layout: Option<LayoutInfo>, -) -> Result<(Layout, Config), ConfigError> { - let layout_dir = config - .options - .layout_dir - .clone() - .or_else(|| get_layout_dir(find_default_config_dir())); - match requested_layout { - Some(LayoutInfo::BuiltIn(layout_name)) => Layout::from_default_assets( - &PathBuf::from(layout_name), - layout_dir.clone(), - config.clone(), - ), - Some(LayoutInfo::File(layout_name)) => Layout::from_path_or_default( - Some(&PathBuf::from(layout_name)), - layout_dir.clone(), - config.clone(), - ), - Some(LayoutInfo::Url(url)) => Layout::from_url(&url, config.clone()), - Some(LayoutInfo::Stringified(stringified_layout)) => { - Layout::from_stringified_layout(&stringified_layout, config.clone()) - }, - None => Layout::from_default_assets( - &PathBuf::from("default"), - layout_dir.clone(), - config.clone(), - ), + Ok(None) } } pub fn spawn_session_if_needed( session_name: &str, - path: String, client_attributes: ClientAttributes, - config: &Config, + config_file_path: Option<PathBuf>, config_options: &Options, - is_web_client: bool, os_input: Box<dyn ClientOsApi>, requested_layout: Option<LayoutInfo>, - is_welcome_screen: bool, ) -> (ClientToServerMsg, PathBuf) { if session_exists(&session_name).unwrap_or(false) { ipc_pipe_and_first_message_for_existing_session( - path, + session_name, client_attributes, - &config, - &config_options, - is_web_client, + config_file_path, + config_options.clone(), ) } else { - let force_run_commands = false; - let resurrection_layout = - resurrection_layout(&session_name) - .ok() - .flatten() - .map(|mut resurrection_layout| { - if force_run_commands { - resurrection_layout.recursively_add_start_suspended(Some(false)); - } - resurrection_layout - }); + // we still parse the resurrection layout here even though we don't use it just in case + // it's corrupted + let resurrection_layout = resurrection_layout(&session_name).ok().flatten(); - match resurrection_layout { - Some(resurrection_layout) => spawn_new_session( + if resurrection_layout.is_some() { + spawn_new_session( &session_name, os_input.clone(), - config.clone(), + config_file_path, config_options.clone(), - Some(resurrection_layout), + Some(LayoutInfo::File( + session_layout_cache_file_name(&session_name) + .display() + .to_string(), + )), client_attributes, - is_welcome_screen, - ), - None => { - let new_session_layout = layout_for_new_session(&config, requested_layout); - - spawn_new_session( - &session_name, - os_input.clone(), - config.clone(), - config_options.clone(), - new_session_layout.ok().map(|(l, _c)| l), - client_attributes, - is_welcome_screen, - ) - }, + ) + } else { + spawn_new_session( + &session_name, + os_input.clone(), + config_file_path, + config_options.clone(), + requested_layout, + client_attributes, + ) } } } @@ -153,11 +93,10 @@ pub fn spawn_session_if_needed( fn spawn_new_session( name: &str, mut os_input: Box<dyn ClientOsApi>, - mut config: Config, - config_opts: Options, - layout: Option<Layout>, + config_file_path: Option<PathBuf>, + mut config_opts: Options, + layout_info: Option<LayoutInfo>, client_attributes: ClientAttributes, - is_welcome_screen: bool, ) -> (ClientToServerMsg, PathBuf) { let debug = false; envs::set_session_name(name.to_owned()); @@ -173,38 +112,34 @@ fn spawn_new_session( spawn_server(&*zellij_ipc_pipe, debug).unwrap(); - let successfully_written_config = Config::write_config_to_disk_if_it_does_not_exist( - config.to_string(true), - &Default::default(), - ); - let should_launch_setup_wizard = successfully_written_config; - let cli_args = CliArgs::default(); - config.options.web_server = Some(true); - config.options.web_sharing = Some(WebSharing::On); + config_opts.web_server = Some(true); + config_opts.web_sharing = Some(WebSharing::On); + let cli_assets = CliAssets { + config_file_path, + config_dir: None, + should_ignore_config: false, + configuration_options: Some(config_opts), + layout: layout_info, + terminal_window_size: client_attributes.size, + data_dir: None, + is_debug: false, + max_panes: None, + force_run_layout_commands: false, + cwd: None, + }; let is_web_client = true; ( - ClientToServerMsg::NewClient( - client_attributes, - Box::new(cli_args), - Box::new(config.clone()), - Box::new(config_opts.clone()), - Box::new(layout.unwrap()), - Box::new(config.plugins.clone()), - should_launch_setup_wizard, - is_web_client, - is_welcome_screen, - ), + ClientToServerMsg::FirstClientConnected(cli_assets, is_web_client), zellij_ipc_pipe, ) } fn ipc_pipe_and_first_message_for_existing_session( - session_name: String, + session_name: &str, client_attributes: ClientAttributes, - config: &Config, - config_options: &Options, - is_web_client: bool, + config_file_path: Option<PathBuf>, + mut config_opts: Options, ) -> (ClientToServerMsg, PathBuf) { let zellij_ipc_pipe: PathBuf = { let mut sock_dir = zellij_utils::consts::ZELLIJ_SOCK_DIR.clone(); @@ -213,13 +148,24 @@ fn ipc_pipe_and_first_message_for_existing_session( sock_dir.push(session_name); sock_dir }; - let first_message = ClientToServerMsg::AttachClient( - client_attributes, - config.clone(), - config_options.clone(), - None, - None, - is_web_client, - ); + + config_opts.web_server = Some(true); + config_opts.web_sharing = Some(WebSharing::On); + let cli_assets = CliAssets { + config_file_path, + config_dir: None, + should_ignore_config: false, + configuration_options: Some(config_opts), + layout: None, + terminal_window_size: client_attributes.size, + data_dir: None, + is_debug: false, + max_panes: None, + force_run_layout_commands: false, + cwd: None, + }; + let is_web_client = true; + + let first_message = ClientToServerMsg::AttachClient(cli_assets, None, None, is_web_client); (first_message, zellij_ipc_pipe) } diff --git a/zellij-client/src/web_client/types.rs b/zellij-client/src/web_client/types.rs index c751199b..201a0cf4 100644 --- a/zellij-client/src/web_client/types.rs +++ b/zellij-client/src/web_client/types.rs @@ -37,14 +37,11 @@ pub trait SessionManager: Send + Sync + std::fmt::Debug { fn spawn_session_if_needed( &self, session_name: &str, - path: String, client_attributes: ClientAttributes, - config: &Config, + config_file_path: Option<PathBuf>, config_options: &Options, - is_web_client: bool, os_input: Box<dyn ClientOsApi>, requested_layout: Option<LayoutInfo>, - is_welcome_screen: bool, ) -> (ClientToServerMsg, PathBuf); } @@ -69,25 +66,19 @@ impl SessionManager for RealSessionManager { fn spawn_session_if_needed( &self, session_name: &str, - path: String, client_attributes: ClientAttributes, - config: &Config, + config_file_path: Option<PathBuf>, config_options: &Options, - is_web_client: bool, os_input: Box<dyn ClientOsApi>, requested_layout: Option<LayoutInfo>, - is_welcome_screen: bool, ) -> (ClientToServerMsg, PathBuf) { crate::web_client::session_management::spawn_session_if_needed( session_name, - path, client_attributes, - config, + config_file_path, config_options, - is_web_client, os_input, requested_layout, - is_welcome_screen, ) } } diff --git a/zellij-client/src/web_client/unit/web_client_tests.rs b/zellij-client/src/web_client/unit/web_client_tests.rs index c6967886..e0fa0516 100644 --- a/zellij-client/src/web_client/unit/web_client_tests.rs +++ b/zellij-client/src/web_client/unit/web_client_tests.rs @@ -8,6 +8,7 @@ use std::sync::{Arc, Mutex}; use tokio::time::timeout; use tokio_tungstenite::tungstenite::http::Request; use tokio_tungstenite::{connect_async, tungstenite::Message}; +use zellij_utils::input::cli_assets::CliAssets; use zellij_utils::input::layout::Layout; use zellij_utils::{consts::VERSION, input::config::Config, input::options::Options}; @@ -1290,25 +1291,28 @@ impl SessionManager for MockSessionManager { fn spawn_session_if_needed( &self, session_name: &str, - _path: String, client_attributes: ClientAttributes, - config: &Config, + config_file_path: Option<PathBuf>, config_options: &Options, - is_web_client: bool, _os_input: Box<dyn ClientOsApi>, _requested_layout: Option<LayoutInfo>, - _is_welcome_screen: bool, ) -> (ClientToServerMsg, PathBuf) { let mock_ipc_path = PathBuf::from(format!("/tmp/mock_zellij_{}", session_name)); - let first_message = ClientToServerMsg::AttachClient( - client_attributes, - config.clone(), - config_options.clone(), - None, - None, - is_web_client, - ); + let cli_assets = CliAssets { + config_file_path, + config_dir: None, + should_ignore_config: false, + configuration_options: Some(config_options.clone()), + layout: None, + terminal_window_size: client_attributes.size, + data_dir: None, + is_debug: false, + max_panes: None, + force_run_layout_commands: false, + cwd: None, + }; + let first_message = ClientToServerMsg::AttachClient(cli_assets, None, None, true); (first_message, mock_ipc_path) } diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml index 9085d371..a497ca5d 100644 --- a/zellij-server/Cargo.toml +++ b/zellij-server/Cargo.toml @@ -47,6 +47,7 @@ uuid = { workspace = true } vte = { workspace = true } wasmtime-wasi = { version = "29.0.1", default-features = false, features = ["preview1"] } # Keep in sync with wasmtime zellij-utils = { workspace = true } +tokio = { workspace = true } [dependencies.wasmtime] version = "29.0.1" # Keep in sync with wasmtime-wasi diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index f5004eb8..87536b43 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -32,6 +32,8 @@ use std::{ use zellij_utils::envs; use zellij_utils::pane_size::Size; +use zellij_utils::input::cli_assets::CliAssets; + use wasmtime::{Config as WasmtimeConfig, Engine, Strategy}; use crate::{ @@ -44,17 +46,19 @@ use crate::{ use route::route_thread_main; use zellij_utils::{ channels::{self, ChannelWithContext, SenderWithContext}, - cli::CliArgs, consts::{ DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE, ZELLIJ_SEEN_RELEASE_NOTES_CACHE_FILE, }, - data::{ConnectToSession, Event, InputMode, KeyWithModifier, PluginCapabilities, WebSharing}, + data::{ + ConnectToSession, Event, InputMode, KeyWithModifier, LayoutInfo, PluginCapabilities, Style, + WebSharing, + }, errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext}, home::{default_layout_dir, get_default_data_dir}, input::{ actions::Action, command::{RunCommand, TerminalAction}, - config::Config, + config::{watch_config_file_changes, Config}, get_mode_info, keybinds::Keybinds, layout::{FloatingPaneLayout, Layout, PluginAlias, Run, RunPluginOrAlias}, @@ -70,16 +74,9 @@ pub type ClientId = u16; /// Instructions related to server-side application #[derive(Debug, Clone)] pub enum ServerInstruction { - NewClient( - ClientAttributes, - Box<CliArgs>, - Box<Config>, // represents the saved config - Box<Options>, // represents the runtime configuration options - Box<Layout>, - Box<PluginAliases>, - bool, // should launch setup wizard + FirstClientConnected( + CliAssets, bool, // is_web_client - bool, // layout_is_welcome_screen ClientId, ), Render(Option<HashMap<ClientId, String>>), @@ -90,9 +87,7 @@ pub enum ServerInstruction { KillSession, DetachSession(Vec<ClientId>), AttachClient( - ClientAttributes, - Config, // represents the saved config - Options, // represents the runtime configuration options + CliAssets, Option<usize>, // tab position to focus Option<(u32, bool)>, // (pane_id, is_plugin) => pane_id to focus bool, // is_web_client @@ -116,7 +111,7 @@ pub enum ServerInstruction { config: String, write_config_to_disk: bool, }, - ConfigWrittenToDisk(ClientId, Config), + ConfigWrittenToDisk(Config), FailedToWriteConfigToDisk(ClientId, Option<PathBuf>), // Pathbuf - file we failed to write RebindKeys { client_id: ClientId, @@ -135,7 +130,7 @@ pub enum ServerInstruction { impl From<&ServerInstruction> for ServerContext { fn from(server_instruction: &ServerInstruction) -> Self { match *server_instruction { - ServerInstruction::NewClient(..) => ServerContext::NewClient, + ServerInstruction::FirstClientConnected(..) => ServerContext::NewClient, ServerInstruction::Render(..) => ServerContext::Render, ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread, ServerInstruction::ClientExit(..) => ServerContext::ClientExit, @@ -161,7 +156,6 @@ impl From<&ServerInstruction> for ServerContext { ServerContext::ChangeModeForAllClients }, ServerInstruction::Reconfigure { .. } => ServerContext::Reconfigure, - ServerInstruction::ConfigWrittenToDisk(..) => ServerContext::ConfigWrittenToDisk, ServerInstruction::FailedToWriteConfigToDisk(..) => { ServerContext::FailedToWriteConfigToDisk }, @@ -173,6 +167,7 @@ impl From<&ServerInstruction> for ServerContext { }, ServerInstruction::WebServerStarted(..) => ServerContext::WebServerStarted, ServerInstruction::FailedToStartWebServer(..) => ServerContext::FailedToStartWebServer, + ServerInstruction::ConfigWrittenToDisk(..) => ServerContext::ConfigWrittenToDisk, ServerInstruction::SendWebClientsForbidden(..) => { ServerContext::SendWebClientsForbidden }, @@ -189,20 +184,15 @@ impl ErrorInstruction for ServerInstruction { #[derive(Debug, Clone, Default)] pub(crate) struct SessionConfiguration { runtime_config: HashMap<ClientId, Config>, // if present, overrides the saved_config - saved_config: HashMap<ClientId, Config>, // the config as it is on disk (not guaranteed), + saved_config: Config, // the config as it is on disk (not guaranteed), // when changed, this resets the runtime config to // be identical to it and override any previous // changes } impl SessionConfiguration { - pub fn new_saved_config( - &mut self, - client_id: ClientId, - new_saved_config: Config, - ) -> Vec<(ClientId, Config)> { - self.saved_config - .insert(client_id, new_saved_config.clone()); + pub fn change_saved_config(&mut self, new_saved_config: Config) -> Vec<(ClientId, Config)> { + self.saved_config = new_saved_config.clone(); let mut config_changes = vec![]; for (client_id, current_runtime_config) in self.runtime_config.iter_mut() { @@ -213,8 +203,8 @@ impl SessionConfiguration { } config_changes } - pub fn set_client_saved_configuration(&mut self, client_id: ClientId, client_config: Config) { - self.saved_config.insert(client_id, client_config); + pub fn set_saved_configuration(&mut self, config: Config) { + self.saved_config = config; } pub fn set_client_runtime_configuration(&mut self, client_id: ClientId, client_config: Config) { self.runtime_config.insert(client_id, client_config); @@ -222,21 +212,21 @@ impl SessionConfiguration { pub fn get_client_keybinds(&self, client_id: &ClientId) -> Keybinds { self.runtime_config .get(client_id) - .or_else(|| self.saved_config.get(client_id)) + .or_else(|| Some(&self.saved_config)) .map(|c| c.keybinds.clone()) .unwrap_or_default() } pub fn get_client_default_input_mode(&self, client_id: &ClientId) -> InputMode { self.runtime_config .get(client_id) - .or_else(|| self.saved_config.get(client_id)) + .or_else(|| Some(&self.saved_config)) .and_then(|c| c.options.default_mode.clone()) .unwrap_or_default() } pub fn get_client_configuration(&self, client_id: &ClientId) -> Config { self.runtime_config .get(client_id) - .or_else(|| self.saved_config.get(client_id)) + .or_else(|| Some(&self.saved_config)) .cloned() .unwrap_or_default() } @@ -274,9 +264,8 @@ impl SessionConfiguration { let mut config_changed = false; if self.runtime_config.get(client_id).is_none() { - if let Some(saved_config) = self.saved_config.get(client_id) { - self.runtime_config.insert(*client_id, saved_config.clone()); - } + self.runtime_config + .insert(*client_id, self.saved_config.clone()); } match self.runtime_config.get_mut(client_id) { Some(config) => { @@ -334,6 +323,7 @@ pub(crate) struct SessionMetaData { plugin_thread: Option<thread::JoinHandle<()>>, pty_writer_thread: Option<thread::JoinHandle<()>>, background_jobs_thread: Option<thread::JoinHandle<()>>, + config_file_path: Option<PathBuf>, } impl SessionMetaData { @@ -659,40 +649,61 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { let (instruction, mut err_ctx) = server_receiver.recv().unwrap(); err_ctx.add_call(ContextType::IPCServer((&instruction).into())); match instruction { - ServerInstruction::NewClient( - // TODO: rename to FirstClientConnected? - client_attributes, - opts, - config, - runtime_config_options, - layout, - plugin_aliases, - should_launch_setup_wizard, - is_web_client, - layout_is_welcome_screen, - client_id, - ) => { + ServerInstruction::FirstClientConnected(cli_assets, is_web_client, client_id) => { + let (config, layout) = cli_assets.load_config_and_layout(); + let layout_is_welcome_screen = cli_assets.layout + == Some(LayoutInfo::BuiltIn("welcome".to_owned())) + || config.options.default_layout == Some(PathBuf::from("welcome")); + + let successfully_written_config = Config::write_config_to_disk_if_it_does_not_exist( + config.to_string(true), + &cli_assets.config_file_path, + ); + // if we successfully wrote the config to disk, it means two things: + // 1. It did not exist beforehand + // 2. The config folder is writeable + // + // If these two are true, we should launch the setup wizard, if even one of them is + // false, we should never launch it. + let should_launch_setup_wizard = successfully_written_config; + + let runtime_config_options = match &cli_assets.configuration_options { + Some(configuration_options) => { + config.options.merge(configuration_options.clone()) + }, + None => config.options.clone(), + }; + + let client_attributes = ClientAttributes { + size: cli_assets.terminal_window_size, + style: Style { + colors: config + .theme_config(runtime_config_options.theme.as_ref()) + .unwrap_or_else(|| default_palette().into()), + rounded_corners: config.ui.pane_frames.rounded_corners, + hide_session_name: config.ui.pane_frames.hide_session_name, + }, + }; + let mut session = init_session( os_input.clone(), to_server.clone(), client_attributes.clone(), - SessionOptions { - opts, - layout: layout.clone(), - config_options: runtime_config_options.clone(), - }, - *config.clone(), - plugin_aliases, + Box::new(runtime_config_options.clone()), // TODO: no box + Box::new(layout.clone()), // TODO: no box + cli_assets.clone(), + config.clone(), + config.plugins.clone(), client_id, ); let mut runtime_configuration = config.clone(); - runtime_configuration.options = *runtime_config_options.clone(); + runtime_configuration.options = runtime_config_options.clone(); session .session_configuration - .set_client_saved_configuration(client_id, *config.clone()); + .set_saved_configuration(config.clone()); session .session_configuration - .set_client_runtime_configuration(client_id, *runtime_configuration); + .set_client_runtime_configuration(client_id, runtime_configuration); let default_input_mode = runtime_config_options.default_mode.unwrap_or_default(); session .current_input_modes @@ -713,7 +724,9 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { ..Default::default() }) }); - let cwd = runtime_config_options.default_cwd; + let cwd = cli_assets + .cwd + .or_else(|| runtime_config_options.default_cwd); let spawn_tabs = |tab_layout, floating_panes_layout, @@ -798,9 +811,7 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { .unwrap(); }, ServerInstruction::AttachClient( - attrs, - config, - runtime_config_options, + cli_assets, tab_position_to_focus, pane_id_to_focus, is_web_client, @@ -808,14 +819,27 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { ) => { let mut rlock = session_data.write().unwrap(); let session_data = rlock.as_mut().unwrap(); + let config = session_data.session_configuration.saved_config.clone(); + let runtime_config_options = match cli_assets.configuration_options { + Some(configuration_options) => config.options.merge(configuration_options), + None => config.options.clone(), + }; + + let client_attributes = ClientAttributes { + size: cli_assets.terminal_window_size, + style: Style { + colors: config + .theme_config(runtime_config_options.theme.as_ref()) + .unwrap_or_else(|| default_palette().into()), + rounded_corners: config.ui.pane_frames.rounded_corners, + hide_session_name: config.ui.pane_frames.hide_session_name, + }, + }; let mut runtime_configuration = config.clone(); runtime_configuration.options = runtime_config_options.clone(); session_data .session_configuration - .set_client_saved_configuration(client_id, config.clone()); - session_data - .session_configuration .set_client_runtime_configuration(client_id, runtime_configuration); let default_input_mode = config.options.default_mode.unwrap_or_default(); @@ -825,7 +849,7 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { session_state.write().unwrap().set_client_data( client_id, - attrs.size, + client_attributes.size, is_web_client, ); let min_size = session_state @@ -853,7 +877,7 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { let default_mode = config.options.default_mode.unwrap_or_default(); let mode_info = get_mode_info( default_mode, - &attrs, + &client_attributes, session_data.capabilities, &session_data .session_configuration @@ -1229,42 +1253,22 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { .unwrap() .session_configuration .reconfigure_runtime_config(&client_id, config); - - if let Some(new_config) = new_config { - if write_config_to_disk { - let clear_defaults = true; - send_to_client!( - client_id, - os_input, - ServerToClientMsg::WriteConfigToDisk { - config: new_config.to_string(clear_defaults) - }, - session_state - ); - } - - if runtime_config_changed { - let config_was_written_to_disk = false; - session_data - .write() - .unwrap() - .as_mut() - .unwrap() - .propagate_configuration_changes( - vec![(client_id, new_config)], - config_was_written_to_disk, - ); - } - } + update_new_saved_config( + new_config, + write_config_to_disk, + runtime_config_changed, + &session_data, + client_id, + ); }, - ServerInstruction::ConfigWrittenToDisk(client_id, new_config) => { + ServerInstruction::ConfigWrittenToDisk(new_config) => { let changes = session_data .write() .unwrap() .as_mut() .unwrap() .session_configuration - .new_saved_config(client_id, new_config); + .change_saved_config(new_config); let config_was_written_to_disk = true; session_data .write() @@ -1272,6 +1276,15 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { .as_mut() .unwrap() .propagate_configuration_changes(changes, config_was_written_to_disk); + let client_ids = session_state.read().unwrap().client_ids(); + for client_id in client_ids { + send_to_client!( + client_id, + os_input, + ServerToClientMsg::ConfigFileUpdated, + session_state + ); + } }, ServerInstruction::FailedToWriteConfigToDisk(_client_id, file_path) => { session_data @@ -1296,32 +1309,14 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { .unwrap() .session_configuration .rebind_keys(&client_id, keys_to_rebind, keys_to_unbind); - if let Some(new_config) = new_config { - if write_config_to_disk { - let clear_defaults = true; - send_to_client!( - client_id, - os_input, - ServerToClientMsg::WriteConfigToDisk { - config: new_config.to_string(clear_defaults) - }, - session_state - ); - } - if runtime_config_changed { - let config_was_written_to_disk = false; - session_data - .write() - .unwrap() - .as_mut() - .unwrap() - .propagate_configuration_changes( - vec![(client_id, new_config)], - config_was_written_to_disk, - ); - } - } + update_new_saved_config( + new_config, + write_config_to_disk, + runtime_config_changed, + &session_data, + client_id, + ); }, ServerInstruction::StartWebServer(client_id) => { if cfg!(feature = "web_server_capability") { @@ -1424,26 +1419,17 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { drop(std::fs::remove_file(&socket_path)); } -pub struct SessionOptions { - pub opts: Box<CliArgs>, - pub config_options: Box<Options>, - pub layout: Box<Layout>, -} - fn init_session( os_input: Box<dyn ServerOsApi>, to_server: SenderWithContext<ServerInstruction>, client_attributes: ClientAttributes, - options: SessionOptions, + config_options: Box<Options>, + layout: Box<Layout>, + cli_assets: CliAssets, mut config: Config, - plugin_aliases: Box<PluginAliases>, + plugin_aliases: PluginAliases, client_id: ClientId, ) -> SessionMetaData { - let SessionOptions { - opts, - config_options, - layout, - } = options; config.options = config.options.merge(*config_options.clone()); let _ = SCROLL_BUFFER_SIZE.set( @@ -1473,7 +1459,7 @@ fn init_session( let to_background_jobs = SenderWithContext::new(to_background_jobs); // Determine and initialize the data directory - let data_dir = opts.data_dir.unwrap_or_else(get_default_data_dir); + let data_dir = cli_assets.data_dir.unwrap_or_else(get_default_data_dir); let capabilities = PluginCapabilities { arrow_fonts: config_options.simplified_ui.unwrap_or_default(), @@ -1519,7 +1505,7 @@ fn init_session( Some(&to_background_jobs), Some(os_input.clone()), ), - opts.debug, + cli_assets.is_debug, config_options.scrollback_editor.clone(), config_options.post_command_discovery_hook.clone(), ); @@ -1542,10 +1528,10 @@ fn init_session( Some(&to_background_jobs), Some(os_input.clone()), ); - let max_panes = opts.max_panes; + let max_panes = cli_assets.max_panes; let client_attributes_clone = client_attributes.clone(); - let debug = opts.debug; + let debug = cli_assets.is_debug; let layout = layout.clone(); let config = config.clone(); move || { @@ -1654,6 +1640,9 @@ fn init_session( } }) .unwrap(); + if let Some(config_file_path) = cli_assets.config_file_path.clone() { + report_changes_in_config_file(config_file_path, to_server.clone()); + } SessionMetaData { senders: ThreadSenders { @@ -1680,6 +1669,7 @@ fn init_session( web_sharing: config.options.web_sharing.unwrap_or(WebSharing::Off), #[cfg(not(feature = "web_server_capability"))] web_sharing: WebSharing::Disabled, + config_file_path: cli_assets.config_file_path, } } @@ -1756,6 +1746,110 @@ fn should_show_startup_tip( } } +fn report_changes_in_config_file( + config_file_path: PathBuf, + to_server: SenderWithContext<ServerInstruction>, +) { + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async move { + let to_server = to_server.clone(); + watch_config_file_changes(config_file_path, move |new_config| { + let to_server = to_server.clone(); + async move { + let _ = to_server.send(ServerInstruction::ConfigWrittenToDisk(new_config)); + } + }) + .await; + }); + }); +} + +fn update_new_saved_config( + new_config: Option<Config>, + write_config_to_disk: bool, + runtime_config_changed: bool, + session_data: &Arc<RwLock<Option<SessionMetaData>>>, + client_id: ClientId, +) { + if let Some(new_config) = new_config { + if write_config_to_disk { + let clear_defaults = true; + let config_file_path = session_data + .read() + .unwrap() + .as_ref() + .unwrap() + .config_file_path + .clone(); + + let Some(config_file_path) = config_file_path.as_ref() else { + log::error!("No config file path found."); + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_plugin(PluginInstruction::FailedToWriteConfigToDisk { + file_path: None, + }) + .unwrap(); + return; + }; + match Config::write_config_to_disk( + new_config.to_string(clear_defaults), + &config_file_path, + ) { + Ok(written_config) => { + let changes = session_data + .write() + .unwrap() + .as_mut() + .unwrap() + .session_configuration + .change_saved_config(written_config); + let config_was_written_to_disk = true; + session_data + .write() + .unwrap() + .as_mut() + .unwrap() + .propagate_configuration_changes(changes, config_was_written_to_disk); + }, + Err(e) => { + let error_path = e + .as_ref() + .map(|p| p.display().to_string()) + .unwrap_or_else(String::new); + log::error!("Failed to write config to disk: {}", error_path); + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_plugin(PluginInstruction::FailedToWriteConfigToDisk { + file_path: e, + }) + .unwrap(); + }, + } + } else if runtime_config_changed { + let config_was_written_to_disk = false; + session_data + .write() + .unwrap() + .as_mut() + .unwrap() + .propagate_configuration_changes( + vec![(client_id, new_config)], + config_was_written_to_disk, + ); + } + } +} + #[cfg(not(feature = "singlepass"))] fn get_engine() -> Engine { log::info!("Compiling plugins using Cranelift"); diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 1c77df5e..6149ac97 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -230,7 +230,7 @@ pub(crate) fn plugin_thread_main( capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option<TerminalAction>, - plugin_aliases: Box<PluginAliases>, + plugin_aliases: PluginAliases, default_mode: InputMode, default_keybinds: Keybinds, background_plugins: HashSet<RunPluginOrAlias>, diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 700011c8..da725443 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -365,7 +365,7 @@ fn create_plugin_thread( plugin_capabilities, client_attributes, default_shell_action, - Box::new(plugin_aliases), + plugin_aliases, InputMode::Normal, Keybinds::default(), Default::default(), @@ -449,7 +449,7 @@ fn create_plugin_thread_with_server_receiver( plugin_capabilities, client_attributes, default_shell_action, - Box::new(PluginAliases::default()), + PluginAliases::default(), InputMode::Normal, Keybinds::default(), Default::default(), @@ -539,7 +539,7 @@ fn create_plugin_thread_with_pty_receiver( plugin_capabilities, client_attributes, default_shell_action, - Box::new(PluginAliases::default()), + PluginAliases::default(), InputMode::Normal, Keybinds::default(), Default::default(), @@ -624,7 +624,7 @@ fn create_plugin_thread_with_background_jobs_receiver( plugin_capabilities, client_attributes, default_shell_action, - Box::new(PluginAliases::default()), + PluginAliases::default(), InputMode::Normal, Keybinds::default(), Default::default(), diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 915dbfa1..65f4d1dd 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -1104,27 +1104,10 @@ pub(crate) fn route_thread_main( ) .with_context(err_context)?; }, - ClientToServerMsg::NewClient( - client_attributes, - cli_args, - config, - runtime_config_options, - layout, - plugin_aliases, - should_launch_setup_wizard, - is_web_client, - layout_is_welcome_screen, - ) => { - let new_client_instruction = ServerInstruction::NewClient( - client_attributes, - cli_args, - config, - runtime_config_options, - layout, - plugin_aliases, - should_launch_setup_wizard, + ClientToServerMsg::FirstClientConnected(cli_assets, is_web_client) => { + let new_client_instruction = ServerInstruction::FirstClientConnected( + cli_assets, is_web_client, - layout_is_welcome_screen, client_id, ); to_server @@ -1132,9 +1115,7 @@ pub(crate) fn route_thread_main( .with_context(err_context)?; }, ClientToServerMsg::AttachClient( - client_attributes, - config, - runtime_config_options, + cli_assets, tab_position_to_focus, pane_id_to_focus, is_web_client, @@ -1148,9 +1129,7 @@ pub(crate) fn route_thread_main( let should_allow_connection = !is_web_client || allow_web_connections; if should_allow_connection { let attach_client_instruction = ServerInstruction::AttachClient( - client_attributes, - config, - runtime_config_options, + cli_assets, tab_position_to_focus, pane_id_to_focus, is_web_client, @@ -1188,16 +1167,6 @@ pub(crate) fn route_thread_main( let _ = to_server.send(ServerInstruction::DetachSession(client_id)); should_break = true; }, - ClientToServerMsg::ConfigWrittenToDisk(config) => { - let _ = to_server - .send(ServerInstruction::ConfigWrittenToDisk(client_id, config)); - }, - ClientToServerMsg::FailedToWriteConfigToDisk(failed_path) => { - let _ = to_server.send(ServerInstruction::FailedToWriteConfigToDisk( - client_id, - failed_path, - )); - }, ClientToServerMsg::WebServerStarted(base_url) => { let _ = to_server.send(ServerInstruction::WebServerStarted(base_url)); }, diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index a1164864..845cf0e2 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -562,6 +562,7 @@ impl MockScreen { layout, current_input_modes: self.session_metadata.current_input_modes.clone(), web_sharing: WebSharing::Off, + config_file_path: self.session_metadata.config_file_path.clone(), } } } @@ -621,6 +622,7 @@ impl MockScreen { session_configuration: Default::default(), current_input_modes: HashMap::new(), web_sharing: WebSharing::Off, + config_file_path: None, }; let os_input = FakeInputOutput::default(); diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 55e2a76e..1fcba3b4 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -2,7 +2,7 @@ use crate::data::{Direction, InputMode, Resize}; use crate::setup::Setup; use crate::{ consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV}, - input::{layout::PluginUserConfiguration, options::CliOptions}, + input::{layout::PluginUserConfiguration, options::Options}, }; use clap::{Args, Parser, Subcommand}; use serde::{Deserialize, Serialize}; @@ -80,11 +80,28 @@ pub struct CliArgs { pub debug: bool, } +impl CliArgs { + pub fn is_setup_clean(&self) -> bool { + if let Some(Command::Setup(ref setup)) = &self.command { + if setup.clean { + return true; + } + } + false + } + pub fn options(&self) -> Option<Options> { + if let Some(Command::Options(options)) = &self.command { + return Some(options.clone()); + } + None + } +} + #[derive(Debug, Subcommand, Clone, Serialize, Deserialize)] pub enum Command { /// Change the behaviour of zellij #[clap(name = "options", value_parser)] - Options(CliOptions), + Options(Options), /// Setup zellij and check its configuration #[clap(name = "setup", value_parser)] @@ -191,7 +208,7 @@ impl WebCli { pub enum SessionCommand { /// Change the behaviour of zellij #[clap(name = "options")] - Options(CliOptions), + Options(Options), } #[derive(Debug, Subcommand, Clone, Serialize, Deserialize)] diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 8712f454..6cdac7f1 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -1675,11 +1675,11 @@ impl LayoutInfo { } pub fn from_config( layout_dir: &Option<PathBuf>, - default_layout: &Option<PathBuf>, + layout_path: &Option<PathBuf>, ) -> Option<Self> { - match default_layout { - Some(default_layout) => { - if default_layout.extension().is_some() || default_layout.components().count() > 1 { + match layout_path { + Some(layout_path) => { + if layout_path.extension().is_some() || layout_path.components().count() > 1 { let Some(layout_dir) = layout_dir .as_ref() .map(|l| l.clone()) @@ -1688,14 +1688,13 @@ impl LayoutInfo { return None; }; Some(LayoutInfo::File( - layout_dir.join(default_layout).display().to_string(), + layout_dir.join(layout_path).display().to_string(), )) - } else if default_layout.starts_with("http://") - || default_layout.starts_with("https://") + } else if layout_path.starts_with("http://") || layout_path.starts_with("https://") { - Some(LayoutInfo::Url(default_layout.display().to_string())) + Some(LayoutInfo::Url(layout_path.display().to_string())) } else { - Some(LayoutInfo::BuiltIn(default_layout.display().to_string())) + Some(LayoutInfo::BuiltIn(layout_path.display().to_string())) } }, None => None, diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 10eed658..c93b5775 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -479,6 +479,7 @@ pub enum ClientContext { WriteConfigToDisk, StartWebServer, RenamedSession, + ConfigFileUpdated, } /// Stack call representations corresponding to the different types of [`ServerInstruction`]s. diff --git a/zellij-utils/src/input/cli_assets.rs b/zellij-utils/src/input/cli_assets.rs new file mode 100644 index 00000000..0db1395b --- /dev/null +++ b/zellij-utils/src/input/cli_assets.rs @@ -0,0 +1,86 @@ +use crate::data::LayoutInfo; +use crate::input::options::Options; +use crate::pane_size::Size; +use crate::{ + home::find_default_config_dir, + input::{config::Config, layout::Layout, theme::Themes}, + setup::{get_default_themes, get_theme_dir}, +}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct CliAssets { + pub config_file_path: Option<PathBuf>, + pub config_dir: Option<PathBuf>, + pub should_ignore_config: bool, + pub configuration_options: Option<Options>, // merged from everywhere: there are the source of truth + pub layout: Option<LayoutInfo>, + pub terminal_window_size: Size, + pub data_dir: Option<PathBuf>, + pub is_debug: bool, + pub max_panes: Option<usize>, + pub force_run_layout_commands: bool, + pub cwd: Option<PathBuf>, +} + +impl CliAssets { + pub fn load_config_and_layout(&self) -> (Config, Layout) { + let config = { + if self.should_ignore_config { + Config::from_default_assets().unwrap_or_else(|_| Default::default()) + } else if let Some(ref path) = self.config_file_path { + let default_config = + Config::from_default_assets().unwrap_or_else(|_| Default::default()); + Config::from_path(path, Some(default_config.clone())) + .unwrap_or_else(|_| default_config) + } else { + Config::from_default_assets().unwrap_or_else(|_| Default::default()) + } + }; + + let (mut layout, mut config_with_merged_layout_opts) = { + let layout_dir = self + .configuration_options + .as_ref() + .and_then(|e| e.layout_dir.clone()) + .or_else(|| config.options.layout_dir.clone()) + .or_else(|| { + self.config_dir + .clone() + .or_else(find_default_config_dir) + .map(|dir| dir.join("layouts")) + }); + self.layout.as_ref().and_then(|layout_info| { + Layout::from_layout_info_with_config(&layout_dir, layout_info, Some(config.clone())) + .ok() + }) + } + .map(|(layout, config)| (layout, config)) + .unwrap_or_else(|| (Layout::default_layout_asset(), config)); + + if self.force_run_layout_commands { + layout.recursively_add_start_suspended(Some(false)); + } + + config_with_merged_layout_opts.themes = config_with_merged_layout_opts + .themes + .merge(get_default_themes()); + + let user_theme_dir = self + .configuration_options + .as_ref() + .and_then(|o| o.theme_dir.clone()) + .or_else(|| { + get_theme_dir(config_with_merged_layout_opts.options.theme_dir.clone()) + .or_else(find_default_config_dir) + .filter(|dir| dir.exists()) + }); + if let Some(themes) = user_theme_dir.and_then(|u| Themes::from_dir(u).ok()) { + config_with_merged_layout_opts.themes = + config_with_merged_layout_opts.themes.merge(themes); + } + + (config_with_merged_layout_opts, layout) + } +} diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index 287d0e5f..1f407621 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -19,7 +19,7 @@ use crate::cli::{CliArgs, Command}; use crate::envs::EnvironmentVariables; use crate::{home, setup}; -const DEFAULT_CONFIG_FILE_NAME: &str = "config.kdl"; +pub const DEFAULT_CONFIG_FILE_NAME: &str = "config.kdl"; type ConfigResult = Result<Config, ConfigError>; @@ -243,26 +243,29 @@ impl Config { opts.config.clone().or_else(|| { opts.config_dir .clone() - .or_else(home::find_default_config_dir) + .or_else(|| { + home::try_create_home_config_dir(); + home::find_default_config_dir() + }) .map(|config_dir| config_dir.join(DEFAULT_CONFIG_FILE_NAME)) }) } pub fn default_config_file_path() -> Option<PathBuf> { home::find_default_config_dir().map(|config_dir| config_dir.join(DEFAULT_CONFIG_FILE_NAME)) } - pub fn write_config_to_disk(config: String, opts: &CliArgs) -> Result<Config, Option<PathBuf>> { + pub fn write_config_to_disk( + config: String, + config_file_path: &PathBuf, + ) -> Result<Config, Option<PathBuf>> { // if we fail, try to return the PathBuf of the file we were not able to write to + let config_file_path = config_file_path.clone(); Config::from_kdl(&config, None) .map_err(|e| { log::error!("Failed to parse config: {}", e); None }) .and_then(|parsed_config| { - let backed_up_file_name = Config::backup_current_config(&opts)?; - let config_file_path = Config::config_file_path(&opts).ok_or_else(|| { - log::error!("Config file path not found"); - None - })?; + let backed_up_file_name = Config::backup_current_config(&config_file_path)?; let config = match backed_up_file_name { Some(backed_up_file_name) => { format!( @@ -295,31 +298,28 @@ impl Config { }) } // returns true if the config was not previouly written to disk and we successfully wrote it - pub fn write_config_to_disk_if_it_does_not_exist(config: String, opts: &CliArgs) -> bool { - if opts.config.is_none() { - // if a config file path wasn't explicitly specified, we try to create the default - // config folder - home::try_create_home_config_dir(); - } - match Config::config_file_path(opts) { - Some(config_file_path) => { - if config_file_path.exists() { + pub fn write_config_to_disk_if_it_does_not_exist( + config: String, + config_file_path: &Option<PathBuf>, + ) -> bool { + let Some(config_file_path) = config_file_path.clone() else { + log::error!("Could not find file path to write config"); + return false; + }; + if config_file_path.exists() { + false + } else { + if let Err(e) = std::fs::write(&config_file_path, config.as_bytes()) { + log::error!("Failed to write config to disk: {}", e); + return false; + } + match std::fs::read_to_string(&config_file_path) { + Ok(written_config) => written_config == config, + Err(e) => { + log::error!("Failed to read written config: {}", e); false - } else { - if let Err(e) = std::fs::write(&config_file_path, config.as_bytes()) { - log::error!("Failed to write config to disk: {}", e); - return false; - } - match std::fs::read_to_string(&config_file_path) { - Ok(written_config) => written_config == config, - Err(e) => { - log::error!("Failed to read written config: {}", e); - false - }, - } - } - }, - None => false, + }, + } } } fn find_free_backup_file_name(config_file_path: &PathBuf) -> Option<PathBuf> { @@ -361,47 +361,45 @@ impl Config { }, } } - fn backup_current_config(opts: &CliArgs) -> Result<Option<PathBuf>, Option<PathBuf>> { + fn backup_current_config( + config_file_path: &PathBuf, + ) -> Result<Option<PathBuf>, Option<PathBuf>> { // if we fail, try to return the PathBuf of the file we were not able to write to - if let Some(config_file_path) = Config::config_file_path(&opts) { - match std::fs::read_to_string(&config_file_path) { - Ok(current_config) => { - let Some(backup_config_path) = - Config::find_free_backup_file_name(&config_file_path) - else { - log::error!("Failed to find a file name to back up the configuration to, ran out of files."); - return Err(None); - }; - if Config::backup_config_with_written_content_confirmation( - ¤t_config, - &config_file_path, - &backup_config_path, - ) { - Ok(Some(backup_config_path)) - } else { - log::error!( - "Failed to back up config file: {}", - backup_config_path.display() - ); - Err(Some(backup_config_path)) - } - }, - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - Ok(None) - } else { - log::error!( - "Failed to read current config {}: {}", - config_file_path.display(), - e - ); - Err(Some(config_file_path)) - } - }, - } - } else { - log::error!("No config file path found?"); - Err(None) + // if let Some(config_file_path) = Config::config_file_path(&opts) { + match std::fs::read_to_string(&config_file_path) { + Ok(current_config) => { + let Some(backup_config_path) = + Config::find_free_backup_file_name(&config_file_path) + else { + log::error!("Failed to find a file name to back up the configuration to, ran out of files."); + return Err(None); + }; + if Config::backup_config_with_written_content_confirmation( + ¤t_config, + &config_file_path, + &backup_config_path, + ) { + Ok(Some(backup_config_path)) + } else { + log::error!( + "Failed to back up config file: {}", + backup_config_path.display() + ); + Err(Some(backup_config_path)) + } + }, + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + Ok(None) + } else { + log::error!( + "Failed to read current config {}: {}", + config_file_path.display(), + e + ); + Err(Some(config_file_path.clone())) + } + }, } } fn autogen_config_message(backed_up_file_name: PathBuf) -> String { diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index e29ad94d..83704439 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -1248,6 +1248,50 @@ impl Layout { } layout } + pub fn from_layout_info_with_config( + layout_dir: &Option<PathBuf>, + layout_info: &LayoutInfo, + config: Option<Config>, + ) -> Result<(Layout, Config), ConfigError> { + let mut should_start_layout_commands_suspended = false; + let (path_to_raw_layout, raw_layout, raw_swap_layouts) = match layout_info { + LayoutInfo::File(layout_name_without_extension) => { + let layout_dir = layout_dir.clone().or_else(|| default_layout_dir()); + let (path_to_layout, stringified_layout, swap_layouts) = + Self::stringified_from_dir( + &PathBuf::from(layout_name_without_extension), + layout_dir.as_ref(), + )?; + (Some(path_to_layout), stringified_layout, swap_layouts) + }, + LayoutInfo::BuiltIn(layout_name) => { + let (path_to_layout, stringified_layout, swap_layouts) = + Self::stringified_from_default_assets(&PathBuf::from(layout_name))?; + (Some(path_to_layout), stringified_layout, swap_layouts) + }, + LayoutInfo::Url(url) => { + should_start_layout_commands_suspended = true; + (Some(url.clone()), Self::stringified_from_url(&url)?, None) + }, + LayoutInfo::Stringified(stringified_layout) => (None, stringified_layout.clone(), None), + }; + let mut layout = Layout::from_kdl( + &raw_layout, + path_to_raw_layout, + raw_swap_layouts + .as_ref() + .map(|(r, f)| (r.as_str(), f.as_str())), + None, + ); + if should_start_layout_commands_suspended { + layout + .iter_mut() + .next() + .map(|l| l.recursively_add_start_suspended_including_template(Some(true))); + } + let config = Config::from_kdl(&raw_layout, config)?; // this merges the two config, with + layout.map(|l| (l, config)) + } pub fn stringified_from_path_or_default( layout_path: Option<&PathBuf>, layout_dir: Option<PathBuf>, @@ -1369,6 +1413,20 @@ impl Layout { let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with Ok((layout, config)) } + pub fn default_layout_asset() -> Layout { + Layout::from_kdl( + &Self::stringified_default_from_assets().unwrap(), + None, + Some(( + "", + Self::stringified_default_swap_from_assets() + .unwrap() + .as_str(), + )), + None, + ) + .unwrap() + } pub fn from_str( raw: &str, path_to_raw_layout: String, diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 2ff8c206..69520fe2 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -1,4 +1,5 @@ pub mod actions; +pub mod cli_assets; pub mod command; pub mod config; pub mod keybinds; diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs index 72b4e63b..e9035dc1 100644 --- a/zellij-utils/src/input/options.rs +++ b/zellij-utils/src/input/options.rs @@ -496,73 +496,3 @@ impl Options { } } } - -#[derive(Clone, Default, Debug, PartialEq, Args, Serialize, Deserialize)] -/// Options that can be set through cli flags -/// boolean flags end up toggling boolean options in `Options` -pub struct CliOptions { - /// Disable handling of mouse events - #[clap(long, conflicts_with("mouse-mode"), value_parser)] - pub disable_mouse_mode: bool, - /// Disable display of pane frames - #[clap(long, conflicts_with("pane-frames"), value_parser)] - pub no_pane_frames: bool, - #[clap(flatten)] - pub options: Options, -} - -impl From<CliOptions> for Options { - fn from(cli_options: CliOptions) -> Self { - let mut opts = cli_options.options; - - // TODO: what? - if cli_options.no_pane_frames { - opts.pane_frames = Some(false); - } - if cli_options.disable_mouse_mode { - opts.mouse_mode = Some(false); - } - - Self { - simplified_ui: opts.simplified_ui, - theme: opts.theme, - default_mode: opts.default_mode, - default_shell: opts.default_shell, - default_cwd: opts.default_cwd, - default_layout: opts.default_layout, - layout_dir: opts.layout_dir, - theme_dir: opts.theme_dir, - mouse_mode: opts.mouse_mode, - pane_frames: opts.pane_frames, - mirror_session: opts.mirror_session, - on_force_close: opts.on_force_close, - scroll_buffer_size: opts.scroll_buffer_size, - copy_command: opts.copy_command, - copy_clipboard: opts.copy_clipboard, - copy_on_select: opts.copy_on_select, - scrollback_editor: opts.scrollback_editor, - session_name: opts.session_name, - attach_to_session: opts.attach_to_session, - auto_layout: opts.auto_layout, - session_serialization: opts.session_serialization, - serialize_pane_viewport: opts.serialize_pane_viewport, - scrollback_lines_to_serialize: opts.scrollback_lines_to_serialize, - styled_underlines: opts.styled_underlines, - serialization_interval: opts.serialization_interval, - support_kitty_keyboard_protocol: opts.support_kitty_keyboard_protocol, - web_server: opts.web_server, - web_sharing: opts.web_sharing, - stacked_resize: opts.stacked_resize, - show_startup_tips: opts.show_startup_tips, - show_release_notes: opts.show_release_notes, - advanced_mouse_actions: opts.advanced_mouse_actions, - web_server_ip: opts.web_server_ip, - web_server_port: opts.web_server_port, - web_server_cert: opts.web_server_cert, - web_server_key: opts.web_server_key, - enforce_https_for_localhost: opts.enforce_https_for_localhost, - post_command_discovery_hook: opts.post_command_discovery_hook, - ..Default::default() - } - } -} diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index 2ec6d8d5..57a31cd0 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -1,10 +1,8 @@ //! IPC stuff for starting to split things into a client and server model. use crate::{ - cli::CliArgs, data::{ClientId, ConnectToSession, KeyWithModifier, Style}, errors::{get_current_ctx, prelude::*, ErrorContext}, - input::config::Config, - input::{actions::Action, layout::Layout, options::Options, plugins::PluginAliases}, + input::{actions::Action, cli_assets::CliAssets}, pane_size::{Size, SizeInPixels}, }; use interprocess::local_socket::LocalSocketStream; @@ -16,7 +14,6 @@ use std::{ io::{self, Write}, marker::PhantomData, os::unix::io::{AsRawFd, FromRawFd}, - path::PathBuf, }; type SessionId = u64; @@ -71,21 +68,12 @@ pub enum ClientToServerMsg { ForegroundColor(String), ColorRegisters(Vec<(usize, String)>), TerminalResize(Size), - NewClient( - ClientAttributes, - Box<CliArgs>, - Box<Config>, // represents the saved configuration - Box<Options>, // represents the runtime configuration - Box<Layout>, - Box<PluginAliases>, - bool, // should launch setup wizard + FirstClientConnected( + CliAssets, bool, // is_web_client - bool, // layout_is_welcome_screen ), AttachClient( - ClientAttributes, - Config, // represents the saved configuration - Options, // represents the runtime configuration + CliAssets, Option<usize>, // tab position to focus Option<(u32, bool)>, // (pane_id, is_plugin) => pane id to focus bool, // is_web_client @@ -95,8 +83,6 @@ pub enum ClientToServerMsg { ClientExited, KillSession, ConnStatus, - ConfigWrittenToDisk(Config), - FailedToWriteConfigToDisk(Option<PathBuf>), WebServerStarted(String), // String -> base_url FailedToStartWebServer(String), } @@ -114,9 +100,9 @@ pub enum ServerToClientMsg { UnblockCliPipeInput(String), // String -> pipe name CliPipeOutput(String, String), // String -> pipe name, String -> Output QueryTerminalSize, - WriteConfigToDisk { config: String }, StartWebServer, RenamedSession(String), // String -> new session name + ConfigFileUpdated, } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 0093cbfa..2be6cdef 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -71,7 +71,7 @@ pub fn get_default_data_dir() -> PathBuf { } #[cfg(not(test))] -fn get_default_themes() -> Themes { +pub fn get_default_themes() -> Themes { let mut themes = Themes::default(); for file in ZELLIJ_DEFAULT_THEMES.files() { if let Some(content) = file.contents_utf8() { @@ -86,7 +86,7 @@ fn get_default_themes() -> Themes { } #[cfg(test)] -fn get_default_themes() -> Themes { +pub fn get_default_themes() -> Themes { Themes::default() } @@ -744,7 +744,7 @@ fn merge_attach_command_options( mod setup_test { use super::Setup; use crate::cli::{CliArgs, Command}; - use crate::input::options::{CliOptions, Options}; + use crate::input::options::Options; use insta::assert_snapshot; use std::path::PathBuf; @@ -759,11 +759,8 @@ mod setup_test { #[test] fn cli_arguments_override_config_options() { let mut cli_args = CliArgs::default(); - cli_args.command = Some(Command::Options(CliOptions { - options: Options { - simplified_ui: Some(true), - ..Default::default() - }, + cli_args.command = Some(Command::Options(Options { + simplified_ui: Some(true), ..Default::default() })); let (_config, _layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap(); @@ -787,11 +784,8 @@ mod setup_test { "{}/src/test-fixtures/layout-with-options.kdl", env!("CARGO_MANIFEST_DIR") ))); - cli_args.command = Some(Command::Options(CliOptions { - options: Options { - pane_frames: Some(true), - ..Default::default() - }, + cli_args.command = Some(Command::Options(Options { + pane_frames: Some(true), ..Default::default() })); let (_config, layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap(); diff --git a/zellij-utils/src/web_server_commands.rs b/zellij-utils/src/web_server_commands.rs index e2125eac..0b020f6e 100644 --- a/zellij-utils/src/web_server_commands.rs +++ b/zellij-utils/src/web_server_commands.rs @@ -1,6 +1,5 @@ use crate::consts::WEBSERVER_SOCKET_PATH; use crate::errors::prelude::*; -use crate::input::config::Config; use interprocess::local_socket::LocalSocketStream; use serde::{Deserialize, Serialize}; use std::fs; @@ -41,7 +40,6 @@ pub fn shutdown_all_webserver_instances() -> Result<()> { #[derive(Serialize, Deserialize, Debug, Clone)] pub enum InstructionForWebServer { ShutdownWebServer, - ConfigWrittenToDisk(Config), } pub fn create_webserver_sender(path: &str) -> Result<BufWriter<LocalSocketStream>> { |
