diff --git a/default-plugins/configuration/src/main.rs b/default-plugins/configuration/src/main.rs index db0219c56b..bcdc4d6352 100644 --- a/default-plugins/configuration/src/main.rs +++ b/default-plugins/configuration/src/main.rs @@ -2,6 +2,8 @@ use zellij_tile::prelude::*; use std::collections::{BTreeMap, BTreeSet}; +static UI_SIZE: usize = 15; + struct State { userspace_configuration: BTreeMap, selected_index: Option, @@ -16,6 +18,9 @@ struct State { preset_color_index: usize, primary_leader_key_color_index: usize, secondary_leader_key_color_index: usize, + notification: Option, + is_setup_wizard: bool, + ui_size: usize, } impl Default for State { @@ -43,6 +48,9 @@ impl Default for State { secondary_leader_key_color_index: 0, mode_color_index: 2, preset_color_index: 1, + notification: None, + is_setup_wizard: false, + ui_size: UI_SIZE, } } } @@ -51,6 +59,10 @@ register_plugin!(State); impl ZellijPlugin for State { fn load(&mut self, configuration: BTreeMap) { + self.is_setup_wizard = configuration + .get("is_setup_wizard") + .map(|v| v == "true") + .unwrap_or(false); self.userspace_configuration = configuration; // we need the ReadApplicationState permission to receive the ModeUpdate and TabUpdate // events @@ -64,12 +76,19 @@ impl ZellijPlugin for State { PermissionType::ChangeApplicationState, ]); subscribe(&[ - EventType::ModeUpdate, - EventType::TabUpdate, - EventType::Key, - EventType::Timer, EventType::PermissionRequestResult, + EventType::Key, + EventType::FailedToWriteConfigToDisk, ]); + if self.is_setup_wizard { + self.ui_size = 18; + self.selected_index = Some(0); + let own_plugin_id = get_plugin_ids().plugin_id; + rename_plugin_pane(own_plugin_id, "First Run Setup Wizard (Step 1/1)"); + resize_focused_pane(Resize::Increase); + resize_focused_pane(Resize::Increase); + resize_focused_pane(Resize::Increase); + } } fn update(&mut self, event: Event) -> bool { let mut should_render = false; @@ -80,10 +99,26 @@ impl ZellijPlugin for State { Event::Key(key) => { if self.remapping_leaders { should_render = self.handle_remapping_screen_key(key); + } else if self.is_setup_wizard { + should_render = self.handle_setup_wizard_key(key); } else { should_render = self.handle_main_screen_key(key); } }, + Event::FailedToWriteConfigToDisk(config_file_path) => { + match config_file_path { + Some(failed_path) => { + self.notification = Some(format!( + "Failed to write configuration file: {}", + failed_path + )); + }, + None => { + self.notification = Some(format!("Failed to write configuration file.")); + }, + } + should_render = true; + }, _ => (), }; should_render @@ -91,6 +126,8 @@ impl ZellijPlugin for State { fn render(&mut self, rows: usize, cols: usize) { if self.remapping_leaders { self.render_remapping_leaders_screen(rows, cols); + } else if self.is_setup_wizard { + self.render_setup_wizard_screen(rows, cols); } else { self.render_main_screen(rows, cols); } @@ -185,7 +222,10 @@ impl State { } fn handle_main_screen_key(&mut self, key: KeyWithModifier) -> bool { let mut should_render = false; - if key.bare_key == BareKey::Down && key.has_no_modifiers() { + if self.notification.is_some() { + self.notification = None; + should_render = true; + } else if key.bare_key == BareKey::Down && key.has_no_modifiers() { if self.selected_index.is_none() { self.selected_index = Some(0); } else if self.selected_index < Some(1) { @@ -205,45 +245,70 @@ impl State { should_render = true; } else if key.bare_key == BareKey::Enter && key.has_no_modifiers() { if let Some(selected) = self.selected_index.take() { - if selected == 0 { - // TODO: these should be part of a "transaction" when they are - // implemented - reconfigure(default_keybinds( - self.primary_modifier - .iter() - .map(|m| m.to_string()) - .collect::>() - .join(" "), - self.secondary_modifier - .iter() - .map(|m| m.to_string()) - .collect::>() - .join(" "), - )); - switch_to_input_mode(&InputMode::Normal); - } else if selected == 1 { - // TODO: these should be part of a "transaction" when they are - // implemented - reconfigure(unlock_first_keybinds( - self.primary_modifier - .iter() - .map(|m| m.to_string()) - .collect::>() - .join(" "), - self.secondary_modifier - .iter() - .map(|m| m.to_string()) - .collect::>() - .join(" "), - )); - switch_to_input_mode(&InputMode::Locked); - } + let write_to_disk = false; + self.reconfigure(selected, write_to_disk); + self.notification = Some("Configuration applied to current session.".to_owned()); + should_render = true; + } else { + self.selected_index = Some(0); + should_render = true; + } + } else if key.bare_key == BareKey::Char(' ') && key.has_no_modifiers() { + if let Some(selected) = self.selected_index.take() { + let write_to_disk = true; + self.reconfigure(selected, write_to_disk); + self.notification = Some("Configuration applied and saved to disk.".to_owned()); should_render = true; } } else if key.bare_key == BareKey::Char('l') && key.has_no_modifiers() { self.remapping_leaders = true; should_render = true; - } else if key.bare_key == BareKey::Esc && key.has_no_modifiers() { + } else if (key.bare_key == BareKey::Esc && key.has_no_modifiers()) + || key.is_key_with_ctrl_modifier(BareKey::Char('c')) + { + close_self(); + should_render = true; + } + should_render + } + fn handle_setup_wizard_key(&mut self, key: KeyWithModifier) -> bool { + let mut should_render = false; + if self.notification.is_some() { + self.notification = None; + should_render = true; + } else if key.bare_key == BareKey::Down && key.has_no_modifiers() { + if self.selected_index.is_none() { + self.selected_index = Some(0); + } else if self.selected_index < Some(1) { + self.selected_index = Some(1); + } else { + self.selected_index = None; + } + should_render = true; + } else if key.bare_key == BareKey::Up && key.has_no_modifiers() { + if self.selected_index.is_none() { + self.selected_index = Some(1); + } else if self.selected_index == Some(1) { + self.selected_index = Some(0); + } else { + self.selected_index = None; + } + should_render = true; + } else if key.bare_key == BareKey::Enter && key.has_no_modifiers() { + if let Some(selected) = self.selected_index.take() { + let write_to_disk = true; + self.reconfigure(selected, write_to_disk); + close_self(); + } else { + self.selected_index = Some(0); + should_render = true; + } + } else if key.bare_key == BareKey::Char('l') && key.has_no_modifiers() { + self.remapping_leaders = true; + should_render = true; + } else if (key.bare_key == BareKey::Esc && key.has_no_modifiers()) + || key.is_key_with_ctrl_modifier(BareKey::Char('c')) + { close_self(); should_render = true; } @@ -412,7 +477,7 @@ impl State { print_text_with_coordinates( Text::new(title_text).color_range(2, ..), left_padding, - rows.saturating_sub(15) / 2, + rows.saturating_sub(self.ui_size) / 2, None, None, ); @@ -426,7 +491,91 @@ impl State { print_text_with_coordinates( Text::new(title_text).color_range(2, ..), left_padding, - rows.saturating_sub(15) / 2, + rows.saturating_sub(self.ui_size) / 2, + None, + None, + ); + } + } + fn render_setup_wizard_title(&self, rows: usize, cols: usize, primary_modifier_key_text: &str) { + let widths = self.main_screen_widths(primary_modifier_key_text); + if cols >= widths.0 { + let title_text_1 = "Hi there! How would you like to interact with Zellij?"; + let title_text_2 = "Not sure? Press to choose Default."; + let title_text_3 = "Everything can always be changed later."; + let title_text_4 = "Tips appear on screen - you don't need to remember anything."; + let left_padding = cols.saturating_sub(widths.0) / 2; + let first_row_coords = (rows.saturating_sub(self.ui_size) / 2).saturating_sub(1); + print_text_with_coordinates( + Text::new(title_text_1).color_range(2, ..), + left_padding, + first_row_coords, + None, + None, + ); + print_text_with_coordinates( + Text::new(title_text_2) + .color_range(0, ..10) + .color_range(2, 16..23) + .color_range(self.preset_color_index, 34..41), + left_padding, + first_row_coords + 2, + None, + None, + ); + print_text_with_coordinates( + Text::new(title_text_3), + left_padding, + first_row_coords + 4, + None, + None, + ); + print_text_with_coordinates( + Text::new(title_text_4), + left_padding, + first_row_coords + 5, + None, + None, + ); + } else { + let title_text_1 = "Hi there! Which do you prefer?"; + let title_text_2 = "Not sure? Press "; + let title_text_3 = "Can be changed later. Tips appear"; + let title_text_4 = "on screen - no need to remember"; + let left_padding = if cols >= widths.1 { + cols.saturating_sub(widths.1) / 2 + } else { + cols.saturating_sub(widths.2) / 2 + }; + let first_row_coords = (rows.saturating_sub(self.ui_size) / 2).saturating_sub(1); + print_text_with_coordinates( + Text::new(title_text_1).color_range(2, ..), + left_padding, + first_row_coords, + None, + None, + ); + print_text_with_coordinates( + Text::new(title_text_2) + .color_range(0, ..10) + .color_range(2, 16..23) + .color_range(self.preset_color_index, 40..49), + left_padding, + first_row_coords + 2, + None, + None, + ); + print_text_with_coordinates( + Text::new(title_text_3), + left_padding, + first_row_coords + 4, + None, + None, + ); + print_text_with_coordinates( + Text::new(title_text_4), + left_padding, + first_row_coords + 5, None, None, ); @@ -455,7 +604,7 @@ impl State { ) .indent(1), NestedListItem::new(format!( - "{} t - to enter TAB mode.", + "{} t - to enter TAB mode", primary_modifier_key_text )) .color_range( @@ -488,7 +637,7 @@ impl State { primary_modifier_key_text_len + 14..primary_modifier_key_text_len + 18, ), NestedListItem::new(format!( - "{} t - to enter TAB mode.", + "{} t - to enter TAB mode", primary_modifier_key_text )) .indent(1) @@ -517,7 +666,7 @@ impl State { primary_modifier_key_text_len + 5..primary_modifier_key_text_len + 10, ) .indent(1), - NestedListItem::new(format!("{} t - TAB mode.", primary_modifier_key_text)) + NestedListItem::new(format!("{} t - TAB mode", primary_modifier_key_text)) .color_range( self.primary_leader_key_color_index, ..primary_modifier_key_text_len + 3, @@ -536,9 +685,9 @@ impl State { } let left_padding = cols.saturating_sub(max_width) / 2; let top_coordinates = if rows > 14 { - (rows.saturating_sub(15) / 2) + 2 + (rows.saturating_sub(self.ui_size) / 2) + 2 } else { - (rows.saturating_sub(15) / 2) + 1 + (rows.saturating_sub(self.ui_size) / 2) + 1 }; print_nested_list_with_coordinates( list_items, @@ -578,7 +727,7 @@ impl State { primary_modifier_key_text_len + 16..primary_modifier_key_text_len + 21, ), NestedListItem::new(format!( - "{} g + t to enter TAB mode.", + "{} g + t to enter TAB mode", primary_modifier_key_text )) .indent(1) @@ -623,7 +772,7 @@ impl State { ) .indent(1), NestedListItem::new(format!( - "{} g + t to enter TAB mode.", + "{} g + t to enter TAB mode", primary_modifier_key_text )) .color_range( @@ -664,7 +813,7 @@ impl State { primary_modifier_key_text_len + 7..primary_modifier_key_text_len + 11, ) .indent(1), - NestedListItem::new(format!("{} g + t TAB mode.", primary_modifier_key_text)) + NestedListItem::new(format!("{} g + t TAB mode", primary_modifier_key_text)) .color_range( self.primary_leader_key_color_index, ..primary_modifier_key_text_len + 3, @@ -687,9 +836,9 @@ impl State { } let left_padding = cols.saturating_sub(max_width) / 2; let top_coordinates = if rows > 14 { - (rows.saturating_sub(15) / 2) + 7 + (rows.saturating_sub(self.ui_size) / 2) + 7 } else { - (rows.saturating_sub(15) / 2) + 5 + (rows.saturating_sub(self.ui_size) / 2) + 5 }; print_nested_list_with_coordinates( list_items, @@ -710,14 +859,14 @@ impl State { let primary_modifier_key_text_len = primary_modifier_key_text.chars().count(); let secondary_modifier_key_text_len = secondary_modifier_key_text.chars().count(); let top_coordinates = if rows > 14 { - (rows.saturating_sub(15) / 2) + 12 + (rows.saturating_sub(self.ui_size) / 2) + 12 } else { - (rows.saturating_sub(15) / 2) + 9 + (rows.saturating_sub(self.ui_size) / 2) + 9 }; if cols >= widths.0 { let leader_key_text = format!( - "Leader keys: {} - modes, {} - quicknav and shortcuts.", + "Leader keys: {} - modes, {} - quicknav and shortcuts", primary_modifier_key_text, secondary_modifier_key_text ); let left_padding = cols.saturating_sub(widths.0) / 2; @@ -767,12 +916,12 @@ impl State { ) }; } - fn render_warning_if_needed(&self, rows: usize, cols: usize, primary_modifier_key_text: &str) { + fn render_info_line(&self, rows: usize, cols: usize, primary_modifier_key_text: &str) { let widths = self.main_screen_widths(primary_modifier_key_text); let top_coordinates = if rows > 14 { - (rows.saturating_sub(15) / 2) + 14 + (rows.saturating_sub(self.ui_size) / 2) + 14 } else { - (rows.saturating_sub(15) / 2) + 10 + (rows.saturating_sub(self.ui_size) / 2) + 10 }; let left_padding = if cols >= widths.0 { cols.saturating_sub(widths.0) / 2 @@ -781,7 +930,15 @@ impl State { } else { cols.saturating_sub(widths.2) / 2 }; - if let Some(warning_text) = self.warning_text(cols) { + if let Some(notification) = &self.notification { + print_text_with_coordinates( + Text::new(notification).color_range(3, ..), + left_padding, + top_coordinates, + None, + None, + ); + } else if let Some(warning_text) = self.warning_text(cols) { print_text_with_coordinates( Text::new(warning_text).color_range(3, ..), left_padding, @@ -791,24 +948,56 @@ impl State { ); } } - fn render_help_text_main(&self, rows: usize, cols: usize, primary_modifier_key_text: &str) { - let widths = self.main_screen_widths(primary_modifier_key_text); - if cols >= widths.0 { - let help_text = "Help: <↓↑/ENTER> - navigate/select, - leaders, - close"; + fn render_help_text_main(&self, rows: usize, cols: usize) { + let full_help_text = "Help: <↓↑> - navigate, - apply, - apply & save, - leaders, - close"; + let short_help_text = "Help: <↓↑> / / / / "; + if cols >= full_help_text.chars().count() { print_text_with_coordinates( - Text::new(help_text) - .color_range(2, 6..16) - .color_range(2, 36..39) - .color_range(2, 51..56), + Text::new(full_help_text) + .color_range(2, 6..10) + .color_range(2, 23..30) + .color_range(2, 40..47) + .color_range(2, 64..67) + .color_range(2, 79..84), 0, rows, None, None, ); } else { - let help_text = "Help: <↓↑> / / / "; print_text_with_coordinates( - Text::new(help_text) + Text::new(short_help_text) + .color_range(2, 6..10) + .color_range(2, 13..20) + .color_range(2, 23..30) + .color_range(2, 33..36) + .color_range(2, 39..44), + 0, + rows, + None, + None, + ); + } + } + fn render_help_text_setup_wizard(&self, rows: usize, cols: usize) { + let full_help_text = + "Help: <↓↑> - navigate, - apply & save, - change leaders, - close"; + let short_help_text = "Help: <↓↑> / / / "; + if cols >= full_help_text.chars().count() { + print_text_with_coordinates( + Text::new(full_help_text) + .color_range(2, 6..10) + .color_range(2, 23..30) + .color_range(2, 47..50) + .color_range(2, 69..74), + 0, + rows, + None, + None, + ); + } else { + print_text_with_coordinates( + Text::new(short_help_text) .color_range(2, 6..10) .color_range(2, 13..20) .color_range(2, 23..26) @@ -892,8 +1081,23 @@ impl State { &primary_modifier_key_text, &secondary_modifier_key_text, ); - self.render_warning_if_needed(rows, cols, &primary_modifier_key_text); - self.render_help_text_main(rows, cols, &primary_modifier_key_text); + self.render_info_line(rows, cols, &primary_modifier_key_text); + self.render_help_text_main(rows, cols); + } + fn render_setup_wizard_screen(&mut self, rows: usize, cols: usize) { + let primary_modifier_key_text = self.primary_modifier_text(); + let secondary_modifier_key_text = self.secondary_modifier_text(); + self.render_setup_wizard_title(rows, cols, &primary_modifier_key_text); + self.render_first_bulletin(rows + 8, cols, &primary_modifier_key_text); + self.render_second_bulletin(rows + 8, cols, &primary_modifier_key_text); + self.render_leader_keys_indication( + rows + 8, + cols, + &primary_modifier_key_text, + &secondary_modifier_key_text, + ); + self.render_info_line(rows + 8, cols, &primary_modifier_key_text); + self.render_help_text_setup_wizard(rows + 8, cols); } fn warning_text(&self, max_width: usize) -> Option { if self.needs_kitty_support() { @@ -921,6 +1125,47 @@ impl State { || self.primary_modifier.contains(&KeyModifier::Super) || self.secondary_modifier.contains(&KeyModifier::Super) } + fn reconfigure(&self, selected: usize, write_to_disk: bool) { + if selected == 0 { + // TODO: these should be part of a "transaction" when they are + // implemented + reconfigure( + default_keybinds( + self.primary_modifier + .iter() + .map(|m| m.to_string()) + .collect::>() + .join(" "), + self.secondary_modifier + .iter() + .map(|m| m.to_string()) + .collect::>() + .join(" "), + ), + write_to_disk, + ); + switch_to_input_mode(&InputMode::Normal); + } else if selected == 1 { + // TODO: these should be part of a "transaction" when they are + // implemented + reconfigure( + unlock_first_keybinds( + self.primary_modifier + .iter() + .map(|m| m.to_string()) + .collect::>() + .join(" "), + self.secondary_modifier + .iter() + .map(|m| m.to_string()) + .collect::>() + .join(" "), + ), + write_to_disk, + ); + switch_to_input_mode(&InputMode::Locked); + } + } } fn unlock_first_keybinds(primary_modifier: String, secondary_modifier: String) -> String { diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 52b2d7e092..b0915f2927 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -332,6 +332,7 @@ impl ZellijPlugin for State { ); }, BareKey::Char('0') if key.has_modifiers(&[KeyModifier::Ctrl]) => { + let write_to_disk = true; reconfigure( " keybinds { @@ -341,6 +342,7 @@ impl ZellijPlugin for State { } " .to_owned(), + write_to_disk, ); }, BareKey::Char('a') if key.has_modifiers(&[KeyModifier::Alt]) => { diff --git a/src/commands.rs b/src/commands.rs index f533999215..605abdf877 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -386,19 +386,25 @@ fn attach_with_session_name( pub(crate) fn start_client(opts: CliArgs) { // look for old YAML config/layout/theme files and convert them to KDL convert_old_yaml_files(&opts); - let (config, layout, config_options, config_without_layout, config_options_without_layout) = - match Setup::from_cli_args(&opts) { - Ok(results) => results, - Err(e) => { - if let ConfigError::KdlError(error) = e { - let report: Report = error.into(); - eprintln!("{:?}", report); - } else { - eprintln!("{}", e); - } - process::exit(1); - }, - }; + let ( + config, + layout, + config_options, + mut config_without_layout, + mut config_options_without_layout, + ) = match Setup::from_cli_args(&opts) { + Ok(results) => results, + Err(e) => { + if let ConfigError::KdlError(error) = e { + let report: Report = error.into(); + eprintln!("{:?}", report); + } else { + eprintln!("{}", e); + } + process::exit(1); + }, + }; + let mut reconnect_to_session: Option = None; let os_input = get_os_input(get_client_os_input); loop { @@ -415,6 +421,11 @@ pub(crate) fn start_client(opts: CliArgs) { // untested and pretty involved function // // ideally, we should write tests for this whole function and refctor it + reload_config_from_disk( + &mut config_without_layout, + &mut config_options_without_layout, + &opts, + ); if reconnect_to_session.name.is_some() { opts.command = Some(Command::Sessions(Sessions::Attach { session_name: reconnect_to_session.name.clone(), @@ -711,3 +722,19 @@ pub(crate) fn list_aliases(opts: CliArgs) { } process::exit(0); } + +fn reload_config_from_disk( + config_without_layout: &mut Config, + config_options_without_layout: &mut Options, + opts: &CliArgs, +) { + match Setup::from_cli_args(&opts) { + Ok((_, _, _, reloaded_config_without_layout, reloaded_config_options_without_layout)) => { + *config_without_layout = reloaded_config_without_layout; + *config_options_without_layout = reloaded_config_options_without_layout; + }, + Err(e) => { + log::error!("Failed to reload config: {}", e); + }, + }; +} diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 321b110621..eba3a62b3d 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -12,6 +12,7 @@ use log::info; use std::env::current_exe; use std::io::{self, Write}; use std::path::Path; +use std::path::PathBuf; use std::process::Command; use std::sync::{Arc, Mutex}; use std::thread; @@ -53,6 +54,7 @@ pub(crate) enum ClientInstruction { UnblockCliPipeInput(String), // String -> pipe name CliPipeOutput(String, String), // String -> pipe name, String -> output QueryTerminalSize, + WriteConfigToDisk { config: String }, } impl From for ClientInstruction { @@ -75,6 +77,9 @@ impl From for ClientInstruction { ClientInstruction::CliPipeOutput(pipe_name, output) }, ServerToClientMsg::QueryTerminalSize => ClientInstruction::QueryTerminalSize, + ServerToClientMsg::WriteConfigToDisk { config } => { + ClientInstruction::WriteConfigToDisk { config } + }, } } } @@ -97,6 +102,7 @@ impl From<&ClientInstruction> for ClientContext { ClientInstruction::UnblockCliPipeInput(..) => ClientContext::UnblockCliPipeInput, ClientInstruction::CliPipeOutput(..) => ClientContext::CliPipeOutput, ClientInstruction::QueryTerminalSize => ClientContext::QueryTerminalSize, + ClientInstruction::WriteConfigToDisk { .. } => ClientContext::WriteConfigToDisk, } } } @@ -158,8 +164,8 @@ pub(crate) enum InputInstruction { pub fn start_client( mut os_input: Box, opts: CliArgs, - config: Config, - config_options: Options, + config: Config, // saved to disk (or default?) + config_options: Options, // CLI options merged into (getting priority over) saved config options info: ClientInfo, layout: Option, tab_position_to_focus: Option, @@ -219,7 +225,6 @@ pub fn start_client( rounded_corners: config.ui.pane_frames.rounded_corners, hide_session_name: config.ui.pane_frames.hide_session_name, }, - keybinds: config.keybinds.clone(), }; let create_ipc_pipe = || -> std::path::PathBuf { @@ -239,7 +244,8 @@ pub fn start_client( ( ClientToServerMsg::AttachClient( client_attributes, - config_options, + config.clone(), + config_options.clone(), tab_position_to_focus, pane_id_to_focus, ), @@ -252,14 +258,25 @@ pub fn start_client( let ipc_pipe = create_ipc_pipe(); spawn_server(&*ipc_pipe, opts.debug).unwrap(); + 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; ( ClientToServerMsg::NewClient( client_attributes, - Box::new(opts), + Box::new(opts.clone()), + Box::new(config.clone()), Box::new(config_options.clone()), Box::new(layout.unwrap()), Box::new(config.plugins.clone()), + should_launch_setup_wizard, ), ipc_pipe, ) @@ -531,6 +548,23 @@ 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)); + }, + } + }, _ => {}, } } @@ -593,7 +627,6 @@ pub fn start_server_detached( rounded_corners: config.ui.pane_frames.rounded_corners, hide_session_name: config.ui.pane_frames.hide_session_name, }, - keybinds: config.keybinds.clone(), }; let create_ipc_pipe = || -> std::path::PathBuf { @@ -611,14 +644,18 @@ pub fn start_server_detached( let ipc_pipe = create_ipc_pipe(); spawn_server(&*ipc_pipe, opts.debug).unwrap(); + let should_launch_setup_wizard = false; // no setup wizard when starting a detached + // server ( 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()), + should_launch_setup_wizard, ), ipc_pipe, ) diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index fbde2f86d4..b9b23f6cfe 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -18,7 +18,7 @@ mod ui; use background_jobs::{background_jobs_main, BackgroundJob}; use log::info; use pty_writer::{pty_writer_main, PtyWriteInstruction}; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::{ path::PathBuf, sync::{Arc, RwLock}, @@ -28,7 +28,7 @@ use zellij_utils::envs; use zellij_utils::nix::sys::stat::{umask, Mode}; use zellij_utils::pane_size::Size; -use wasmtime::{Config, Engine, Strategy}; +use wasmtime::{Config as WasmtimeConfig, Engine, Strategy}; use crate::{ os_input_output::ServerOsApi, @@ -47,9 +47,10 @@ use zellij_utils::{ home::{default_layout_dir, get_default_data_dir}, input::{ command::{RunCommand, TerminalAction}, + config::Config, get_mode_info, keybinds::Keybinds, - layout::Layout, + layout::{FloatingPaneLayout, Layout, PercentOrFixed, PluginAlias, Run, RunPluginOrAlias}, options::Options, plugins::PluginAliases, }, @@ -64,9 +65,11 @@ pub enum ServerInstruction { NewClient( ClientAttributes, Box, - Box, + Box, // represents the saved config + Box, // represents the runtime configuration options Box, Box, + bool, // should launch setup wizard ClientId, ), Render(Option>), @@ -78,7 +81,8 @@ pub enum ServerInstruction { DetachSession(Vec), AttachClient( ClientAttributes, - Options, + Config, // represents the saved config + Options, // represents the runtime configuration options Option, // tab position to focus Option<(u32, bool)>, // (pane_id, is_plugin) => pane_id to focus ClientId, @@ -97,7 +101,13 @@ pub enum ServerInstruction { DisconnectAllClientsExcept(ClientId), ChangeMode(ClientId, InputMode), ChangeModeForAllClients(InputMode), - Reconfigure(ClientId, String), // String -> stringified configuration + Reconfigure { + client_id: ClientId, + config: String, + write_config_to_disk: bool, + }, + ConfigWrittenToDisk(ClientId, Config), + FailedToWriteConfigToDisk(ClientId, Option), // Pathbuf - file we failed to write } impl From<&ServerInstruction> for ServerContext { @@ -129,7 +139,11 @@ impl From<&ServerInstruction> for ServerContext { ServerInstruction::ChangeModeForAllClients(..) => { ServerContext::ChangeModeForAllClients }, - ServerInstruction::Reconfigure(..) => ServerContext::Reconfigure, + ServerInstruction::Reconfigure { .. } => ServerContext::Reconfigure, + ServerInstruction::ConfigWrittenToDisk(..) => ServerContext::ConfigWrittenToDisk, + ServerInstruction::FailedToWriteConfigToDisk(..) => { + ServerContext::FailedToWriteConfigToDisk + }, } } } @@ -140,16 +154,83 @@ impl ErrorInstruction for ServerInstruction { } } +#[derive(Debug, Clone, Default)] +pub(crate) struct SessionConfiguration { + runtime_config: HashMap, // if present, overrides the saved_config + saved_config: HashMap, // config guaranteed to have been saved to disk +} + +impl SessionConfiguration { + pub fn new_saved_config(&mut self, client_id: ClientId, mut new_saved_config: Config) { + self.saved_config + .insert(client_id, new_saved_config.clone()); + if let Some(runtime_config) = self.runtime_config.get_mut(&client_id) { + match new_saved_config.merge(runtime_config.clone()) { + Ok(_) => { + *runtime_config = new_saved_config; + }, + Err(e) => { + log::error!("Failed to update runtime config: {}", e); + }, + } + } + // TODO: handle change by propagating to all the relevant places + } + 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_client_runtime_configuration(&mut self, client_id: ClientId, client_config: Config) { + self.runtime_config.insert(client_id, client_config); + } + pub fn get_client_keybinds(&self, client_id: &ClientId) -> Keybinds { + self.runtime_config + .get(client_id) + .or_else(|| self.saved_config.get(client_id)) + .map(|c| c.keybinds.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)) + .cloned() + .unwrap_or_default() + } + pub fn reconfigure_runtime_config( + &mut self, + client_id: &ClientId, + stringified_config: String, + ) -> (Option, bool) { + // bool is whether the config changed + let mut full_reconfigured_config = None; + let mut config_changed = false; + let current_client_configuration = self.get_client_configuration(client_id); + match Config::from_kdl( + &stringified_config, + Some(current_client_configuration.clone()), + ) { + Ok(new_config) => { + config_changed = current_client_configuration != new_config; + full_reconfigured_config = Some(new_config.clone()); + self.runtime_config.insert(*client_id, new_config); + }, + Err(e) => { + log::error!("Failed to reconfigure runtime config: {}", e); + }, + } + (full_reconfigured_config, config_changed) + } +} + pub(crate) struct SessionMetaData { pub senders: ThreadSenders, pub capabilities: PluginCapabilities, pub client_attributes: ClientAttributes, pub default_shell: Option, pub layout: Box, - pub config_options: Box, - pub client_keybinds: HashMap, - pub client_input_modes: HashMap, - pub default_mode: HashMap, + pub current_input_modes: HashMap, + pub session_configuration: SessionConfiguration, + screen_thread: Option>, pty_thread: Option>, plugin_thread: Option>, @@ -158,52 +239,21 @@ pub(crate) struct SessionMetaData { } impl SessionMetaData { - pub fn set_client_keybinds(&mut self, client_id: ClientId, keybinds: Keybinds) { - self.client_keybinds.insert(client_id, keybinds); - self.client_input_modes.insert( - client_id, - self.config_options.default_mode.unwrap_or_default(), - ); - } pub fn get_client_keybinds_and_mode( &self, client_id: &ClientId, - ) -> Option<(&Keybinds, &InputMode)> { - match ( - self.client_keybinds.get(client_id), - self.client_input_modes.get(client_id), - ) { - (Some(client_keybinds), Some(client_input_mode)) => { - Some((client_keybinds, client_input_mode)) - }, + ) -> Option<(Keybinds, &InputMode)> { + let client_keybinds = self.session_configuration.get_client_keybinds(client_id); + match self.current_input_modes.get(client_id) { + Some(client_input_mode) => Some((client_keybinds, client_input_mode)), _ => None, } } pub fn change_mode_for_all_clients(&mut self, input_mode: InputMode) { - let all_clients: Vec = self.client_input_modes.keys().copied().collect(); + let all_clients: Vec = self.current_input_modes.keys().copied().collect(); for client_id in all_clients { - self.client_input_modes.insert(client_id, input_mode); - } - } - pub fn rebind_keys(&mut self, client_id: ClientId, new_keybinds: String) -> Option { - if let Some(current_keybinds) = self.client_keybinds.get_mut(&client_id) { - match Keybinds::from_string( - new_keybinds, - current_keybinds.clone(), - &self.config_options, - ) { - Ok(new_keybinds) => { - *current_keybinds = new_keybinds.clone(); - return Some(new_keybinds); - }, - Err(e) => { - log::error!("Failed to parse keybindings: {}", e); - }, - } - } else { - log::error!("Failed to bind keys for client: {client_id}"); + self.current_input_modes.insert(client_id, input_mode); } - None } } @@ -426,44 +476,55 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { err_ctx.add_call(ContextType::IPCServer((&instruction).into())); match instruction { ServerInstruction::NewClient( + // TODO: rename to FirstClientConnected? client_attributes, opts, - config_options, + config, + runtime_config_options, layout, plugin_aliases, + should_launch_setup_wizard, client_id, ) => { - let session = init_session( + let mut session = init_session( os_input.clone(), to_server.clone(), client_attributes.clone(), SessionOptions { opts, layout: layout.clone(), - config_options: config_options.clone(), + config_options: runtime_config_options.clone(), }, + *config.clone(), plugin_aliases, ); + let mut runtime_configuration = config.clone(); + runtime_configuration.options = *runtime_config_options.clone(); + session + .session_configuration + .set_client_saved_configuration(client_id, *config.clone()); + session + .session_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 + .insert(client_id, default_input_mode); + *session_data.write().unwrap() = Some(session); - session_data - .write() - .unwrap() - .as_mut() - .unwrap() - .set_client_keybinds(client_id, client_attributes.keybinds.clone()); session_state .write() .unwrap() .set_client_size(client_id, client_attributes.size); - let default_shell = config_options.default_shell.map(|shell| { + let default_shell = runtime_config_options.default_shell.map(|shell| { TerminalAction::RunCommand(RunCommand { command: shell, - cwd: config_options.default_cwd.clone(), + cwd: config.options.default_cwd.clone(), ..Default::default() }) }); - let cwd = config_options.default_cwd; + let cwd = runtime_config_options.default_cwd; let spawn_tabs = |tab_layout, floating_panes_layout, tab_name, swap_layouts| { session_data @@ -511,9 +572,17 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .unwrap(); } } else { + let mut floating_panes = + layout.template.map(|t| t.1).clone().unwrap_or_default(); + if should_launch_setup_wizard { + // we only do this here (and only once) because otherwise it will be + // intrusive + let setup_wizard = setup_wizard_floating_pane(); + floating_panes.push(setup_wizard); + } spawn_tabs( None, - layout.template.map(|t| t.1).clone().unwrap_or_default(), + floating_panes, None, ( layout.swap_tiled_layouts.clone(), @@ -532,14 +601,29 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { }, ServerInstruction::AttachClient( attrs, - options, + config, + runtime_config_options, tab_position_to_focus, pane_id_to_focus, client_id, ) => { let mut rlock = session_data.write().unwrap(); let session_data = rlock.as_mut().unwrap(); - session_data.set_client_keybinds(client_id, attrs.keybinds.clone()); + + 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(); + session_data + .current_input_modes + .insert(client_id, default_input_mode); + session_state .write() .unwrap() @@ -565,15 +649,14 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .senders .send_to_plugin(PluginInstruction::AddClient(client_id)) .unwrap(); - let default_mode = options.default_mode.unwrap_or_default(); + let default_mode = config.options.default_mode.unwrap_or_default(); let mode_info = get_mode_info( default_mode, &attrs, session_data.capabilities, - session_data - .client_keybinds - .get(&client_id) - .unwrap_or(&session_data.client_attributes.keybinds), + &session_data + .session_configuration + .get_client_keybinds(&client_id), Some(default_mode), ); session_data @@ -853,7 +936,11 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .read() .unwrap() .as_ref() - .and_then(|c| c.config_options.layout_dir.clone()) + .unwrap() + .session_configuration + .get_client_configuration(&client_id) + .options + .layout_dir .or_else(|| default_layout_dir()); if let Some(layout_dir) = layout_dir { connect_to_session.apply_layout_dir(&layout_dir); @@ -904,7 +991,7 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .unwrap() .as_mut() .unwrap() - .client_input_modes + .current_input_modes .insert(client_id, input_mode); }, ServerInstruction::ChangeModeForAllClients(input_mode) => { @@ -915,56 +1002,77 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .unwrap() .change_mode_for_all_clients(input_mode); }, - ServerInstruction::Reconfigure(client_id, new_config) => { - let mut new_default_mode = None; - match Options::from_string(&new_config) { - Ok(mut new_config_options) => { - if let Some(default_mode) = new_config_options.default_mode.take() { - new_default_mode = Some(default_mode); - session_data - .write() - .unwrap() - .as_mut() - .unwrap() - .default_mode - .insert(client_id, default_mode); - } - }, - Err(e) => { - log::error!("Failed to parse config: {}", e); - }, - } - - let new_keybinds = session_data + ServerInstruction::Reconfigure { + client_id, + config, + write_config_to_disk, + } => { + let (new_config, runtime_config_changed) = session_data .write() .unwrap() .as_mut() .unwrap() - .rebind_keys(client_id, new_config) - .clone(); + .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 { + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_screen(ScreenInstruction::Reconfigure { + client_id, + keybinds: Some(new_config.keybinds.clone()), + default_mode: new_config.options.default_mode, + }) + .unwrap(); + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_plugin(PluginInstruction::Reconfigure { + client_id, + keybinds: Some(new_config.keybinds), + default_mode: new_config.options.default_mode, + }) + .unwrap(); + } + } + }, + ServerInstruction::ConfigWrittenToDisk(client_id, new_config) => { session_data .write() .unwrap() - .as_ref() + .as_mut() .unwrap() - .senders - .send_to_screen(ScreenInstruction::Reconfigure { - client_id, - keybinds: new_keybinds.clone(), - default_mode: new_default_mode, - }) - .unwrap(); + .session_configuration + .new_saved_config(client_id, new_config); + }, + ServerInstruction::FailedToWriteConfigToDisk(client_id, file_path) => { session_data .write() .unwrap() .as_ref() .unwrap() .senders - .send_to_plugin(PluginInstruction::Reconfigure { - client_id, - keybinds: new_keybinds, - default_mode: new_default_mode, - }) + .send_to_plugin(PluginInstruction::FailedToWriteConfigToDisk { file_path }) .unwrap(); }, } @@ -987,6 +1095,7 @@ fn init_session( to_server: SenderWithContext, client_attributes: ClientAttributes, options: SessionOptions, + mut config: Config, plugin_aliases: Box, ) -> SessionMetaData { let SessionOptions { @@ -994,6 +1103,7 @@ fn init_session( config_options, layout, } = options; + config.options = config.options.merge(*config_options.clone()); let _ = SCROLL_BUFFER_SIZE.set( config_options @@ -1043,6 +1153,7 @@ fn init_session( .unwrap_or_else(|| get_default_shell()); let default_mode = config_options.default_mode.unwrap_or_default(); + let default_keybinds = config.keybinds.clone(); let pty_thread = thread::Builder::new() .name("pty".to_string()) @@ -1085,13 +1196,12 @@ fn init_session( let client_attributes_clone = client_attributes.clone(); let debug = opts.debug; let layout = layout.clone(); - let config_options = config_options.clone(); move || { screen_thread_main( screen_bus, max_panes, client_attributes_clone, - config_options, + config, debug, layout, ) @@ -1135,6 +1245,7 @@ fn init_session( default_shell, plugin_aliases, default_mode, + default_keybinds, ) .fatal() } @@ -1196,26 +1307,35 @@ fn init_session( default_shell, client_attributes, layout, - config_options: config_options.clone(), - client_keybinds: HashMap::new(), - client_input_modes: HashMap::new(), + session_configuration: Default::default(), + current_input_modes: HashMap::new(), screen_thread: Some(screen_thread), pty_thread: Some(pty_thread), plugin_thread: Some(plugin_thread), pty_writer_thread: Some(pty_writer_thread), background_jobs_thread: Some(background_jobs_thread), - default_mode: HashMap::new(), } } +fn setup_wizard_floating_pane() -> FloatingPaneLayout { + let mut setup_wizard_pane = FloatingPaneLayout::new(); + let configuration = BTreeMap::from_iter([("is_setup_wizard".to_owned(), "true".to_owned())]); + setup_wizard_pane.run = Some(Run::Plugin(RunPluginOrAlias::Alias(PluginAlias::new( + "configuration", + &Some(configuration), + None, + )))); + setup_wizard_pane +} + #[cfg(not(feature = "singlepass"))] fn get_engine() -> Engine { log::info!("Compiling plugins using Cranelift"); - Engine::new(Config::new().strategy(Strategy::Cranelift)).unwrap() + Engine::new(WasmtimeConfig::new().strategy(Strategy::Cranelift)).unwrap() } #[cfg(feature = "singlepass")] fn get_engine() -> Engine { log::info!("Compiling plugins using Singlepass"); - Engine::new(Config::new().strategy(Strategy::Winch)).unwrap() + Engine::new(WasmtimeConfig::new().strategy(Strategy::Winch)).unwrap() } diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 7e97f1d7b5..e1b807ce00 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -148,6 +148,9 @@ pub enum PluginInstruction { keybinds: Option, default_mode: Option, }, + FailedToWriteConfigToDisk { + file_path: Option, + }, WatchFilesystem, Exit, } @@ -189,6 +192,9 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::KeybindPipe { .. } => PluginContext::KeybindPipe, PluginInstruction::DumpLayoutToPlugin(..) => PluginContext::DumpLayoutToPlugin, PluginInstruction::Reconfigure { .. } => PluginContext::Reconfigure, + PluginInstruction::FailedToWriteConfigToDisk { .. } => { + PluginContext::FailedToWriteConfigToDisk + }, } } } @@ -206,6 +212,7 @@ pub(crate) fn plugin_thread_main( default_shell: Option, plugin_aliases: Box, default_mode: InputMode, + default_keybinds: Keybinds, ) -> Result<()> { info!("Wasm main thread starts"); let plugin_dir = data_dir.join("plugins/"); @@ -228,6 +235,7 @@ pub(crate) fn plugin_thread_main( layout.clone(), layout_dir, default_mode, + default_keybinds, ); loop { @@ -764,6 +772,16 @@ pub(crate) fn plugin_thread_main( .reconfigure(client_id, keybinds, default_mode) .non_fatal(); }, + PluginInstruction::FailedToWriteConfigToDisk { file_path } => { + let updates = vec![( + None, + None, + Event::FailedToWriteConfigToDisk(file_path.map(|f| f.display().to_string())), + )]; + wasm_bridge + .update_plugins(updates, shutdown_send.clone()) + .non_fatal(); + }, PluginInstruction::WatchFilesystem => { wasm_bridge.start_fs_watcher_if_not_started(); }, diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs index 51ca3e169a..7613da0997 100644 --- a/zellij-server/src/plugins/plugin_loader.rs +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -70,7 +70,7 @@ pub struct PluginLoader<'a> { default_layout: Box, layout_dir: Option, default_mode: InputMode, - keybinds: Option, + keybinds: Keybinds, } impl<'a> PluginLoader<'a> { @@ -90,7 +90,7 @@ impl<'a> PluginLoader<'a> { default_shell: Option, default_layout: Box, layout_dir: Option, - default_mode: InputMode, + base_modes: &HashMap, keybinds: &HashMap, ) -> Result<()> { let err_context = || format!("failed to reload plugin {plugin_id} from memory"); @@ -100,7 +100,11 @@ impl<'a> PluginLoader<'a> { return Err(anyhow!("No connected clients, cannot reload plugin")); } let first_client_id = connected_clients.remove(0); - let keybinds = keybinds.get(&first_client_id).cloned(); + let keybinds = keybinds.get(&first_client_id).cloned().unwrap_or_default(); + let default_mode = base_modes + .get(&first_client_id) + .cloned() + .unwrap_or_default(); let mut plugin_loader = PluginLoader::new_from_existing_plugin_attributes( &plugin_cache, @@ -157,7 +161,7 @@ impl<'a> PluginLoader<'a> { skip_cache: bool, layout_dir: Option, default_mode: InputMode, - keybinds: Option, + keybinds: Keybinds, ) -> Result<()> { let err_context = || format!("failed to start plugin {plugin_id} for client {client_id}"); let mut plugin_loader = PluginLoader::new( @@ -233,7 +237,7 @@ impl<'a> PluginLoader<'a> { default_layout: Box, layout_dir: Option, default_mode: InputMode, - keybinds: Option, + keybinds: Keybinds, ) -> Result<()> { let mut new_plugins = HashSet::new(); for plugin_id in plugin_map.lock().unwrap().plugin_ids() { @@ -286,7 +290,7 @@ impl<'a> PluginLoader<'a> { default_shell: Option, default_layout: Box, layout_dir: Option, - default_mode: InputMode, + base_modes: &HashMap, keybinds: &HashMap, ) -> Result<()> { let err_context = || format!("failed to reload plugin id {plugin_id}"); @@ -297,7 +301,11 @@ impl<'a> PluginLoader<'a> { return Err(anyhow!("No connected clients, cannot reload plugin")); } let first_client_id = connected_clients.remove(0); - let keybinds = keybinds.get(&first_client_id).cloned(); + let keybinds = keybinds.get(&first_client_id).cloned().unwrap_or_default(); + let default_mode = base_modes + .get(&first_client_id) + .cloned() + .unwrap_or_default(); let mut plugin_loader = PluginLoader::new_from_existing_plugin_attributes( &plugin_cache, @@ -350,7 +358,7 @@ impl<'a> PluginLoader<'a> { default_layout: Box, layout_dir: Option, default_mode: InputMode, - keybinds: Option, + keybinds: Keybinds, ) -> Result { let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR .join(Url::from(&plugin.location).to_string()) @@ -399,7 +407,7 @@ impl<'a> PluginLoader<'a> { default_layout: Box, layout_dir: Option, default_mode: InputMode, - keybinds: Option, + keybinds: Keybinds, ) -> Result { let err_context = || "Failed to find existing plugin"; let (running_plugin, _subscriptions, _workers) = { @@ -455,7 +463,7 @@ impl<'a> PluginLoader<'a> { default_layout: Box, layout_dir: Option, default_mode: InputMode, - keybinds: Option, + keybinds: Keybinds, ) -> Result { let err_context = || "Failed to find existing plugin"; let running_plugin = { @@ -851,11 +859,7 @@ impl<'a> PluginLoader<'a> { layout_dir: self.layout_dir.clone(), default_mode: self.default_mode.clone(), subscriptions: Arc::new(Mutex::new(HashSet::new())), - keybinds: self - .keybinds - .as_ref() - .unwrap_or_else(|| &self.client_attributes.keybinds) - .clone(), + keybinds: self.keybinds.clone(), stdin_pipe, stdout_pipe, }; diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 1ce8ae0b1f..18df615982 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -11,6 +11,7 @@ use zellij_utils::data::{ PluginCapabilities, }; use zellij_utils::errors::ErrorContext; +use zellij_utils::input::keybinds::Keybinds; use zellij_utils::input::layout::{ Layout, PluginAlias, PluginUserConfiguration, RunPlugin, RunPluginLocation, RunPluginOrAlias, }; @@ -58,6 +59,35 @@ macro_rules! log_actions_in_thread { }; } +macro_rules! log_actions_in_thread_struct { + ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr ) => { + std::thread::Builder::new() + .name("logger thread".to_string()) + .spawn({ + let log = $arc_mutex_log.clone(); + let mut exit_event_count = 0; + move || loop { + let (event, _err_ctx) = $receiver + .recv() + .expect("failed to receive event on channel"); + match event { + $exit_event { .. } => { + exit_event_count += 1; + log.lock().unwrap().push(event); + if exit_event_count == $exit_after_count { + break; + } + }, + _ => { + log.lock().unwrap().push(event); + }, + } + } + }) + .unwrap() + }; +} + macro_rules! grant_permissions_and_log_actions_in_thread { ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr, $permission_type:expr, $cache_path:expr, $plugin_thread_sender:expr, $client_id:expr ) => { std::thread::Builder::new() @@ -282,6 +312,7 @@ fn create_plugin_thread( default_shell_action, Box::new(plugin_aliases), InputMode::Normal, + Keybinds::default(), ) .expect("TEST") }) @@ -362,6 +393,7 @@ fn create_plugin_thread_with_server_receiver( default_shell_action, Box::new(PluginAliases::default()), InputMode::Normal, + Keybinds::default(), ) .expect("TEST"); }) @@ -448,6 +480,7 @@ fn create_plugin_thread_with_pty_receiver( default_shell_action, Box::new(PluginAliases::default()), InputMode::Normal, + Keybinds::default(), ) .expect("TEST") }) @@ -529,6 +562,7 @@ fn create_plugin_thread_with_background_jobs_receiver( default_shell_action, Box::new(PluginAliases::default()), InputMode::Normal, + Keybinds::default(), ) .expect("TEST") }) @@ -6525,7 +6559,7 @@ pub fn reconfigure_plugin_command() { client_id ); let received_server_instruction = Arc::new(Mutex::new(vec![])); - let server_thread = log_actions_in_thread!( + let server_thread = log_actions_in_thread_struct!( received_server_instruction, ServerInstruction::Reconfigure, server_receiver, @@ -6561,7 +6595,7 @@ pub fn reconfigure_plugin_command() { .iter() .rev() .find_map(|i| { - if let ServerInstruction::Reconfigure(..) = i { + if let ServerInstruction::Reconfigure { .. } = i { Some(i.clone()) } else { None diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__reconfigure_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__reconfigure_plugin_command.snap index 3f788d7d7d..74676eb1d1 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__reconfigure_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__reconfigure_plugin_command.snap @@ -1,11 +1,12 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 6571 +assertion_line: 6605 expression: "format!(\"{:#?}\", reconfigure_event)" --- Some( - Reconfigure( - 1, - "\n keybinds {\n locked {\n bind \"a\" { NewTab; }\n }\n }\n ", - ), + Reconfigure { + client_id: 1, + config: "\n keybinds {\n locked {\n bind \"a\" { NewTab; }\n }\n }\n ", + write_config_to_disk: true, + }, ) diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index cefa6c5049..59ebe33250 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -104,7 +104,9 @@ pub struct WasmBridge { pending_pipes: PendingPipes, layout_dir: Option, default_mode: InputMode, + default_keybinds: Keybinds, keybinds: HashMap, + base_modes: HashMap, } impl WasmBridge { @@ -120,6 +122,7 @@ impl WasmBridge { default_layout: Box, layout_dir: Option, default_mode: InputMode, + default_keybinds: Keybinds, ) -> Self { let plugin_map = Arc::new(Mutex::new(PluginMap::default())); let connected_clients: Arc>> = Arc::new(Mutex::new(vec![])); @@ -151,7 +154,9 @@ impl WasmBridge { pending_pipes: Default::default(), layout_dir, default_mode, + default_keybinds, keybinds: HashMap::new(), + base_modes: HashMap::new(), } } pub fn load_plugin( @@ -208,8 +213,16 @@ impl WasmBridge { let default_shell = self.default_shell.clone(); let default_layout = self.default_layout.clone(); let layout_dir = self.layout_dir.clone(); - let default_mode = self.default_mode; - let keybinds = self.keybinds.get(&client_id).cloned(); + let default_mode = self + .base_modes + .get(&client_id) + .copied() + .unwrap_or(self.default_mode); + let keybinds = self + .keybinds + .get(&client_id) + .cloned() + .unwrap_or_else(|| self.default_keybinds.clone()); async move { let _ = senders.send_to_background_jobs( BackgroundJob::AnimatePluginLoading(plugin_id), @@ -350,7 +363,7 @@ impl WasmBridge { let default_shell = self.default_shell.clone(); let default_layout = self.default_layout.clone(); let layout_dir = self.layout_dir.clone(); - let default_mode = self.default_mode; + let base_modes = self.base_modes.clone(); let keybinds = self.keybinds.clone(); async move { match PluginLoader::reload_plugin( @@ -369,7 +382,7 @@ impl WasmBridge { default_shell.clone(), default_layout.clone(), layout_dir.clone(), - default_mode, + &base_modes, &keybinds, ) { Ok(_) => { @@ -396,7 +409,7 @@ impl WasmBridge { default_shell.clone(), default_layout.clone(), layout_dir.clone(), - default_mode, + &base_modes, &keybinds, ) { Ok(_) => handle_plugin_successful_loading(&senders, *plugin_id), @@ -451,7 +464,10 @@ impl WasmBridge { self.default_layout.clone(), self.layout_dir.clone(), self.default_mode, - self.keybinds.get(&client_id).cloned(), + self.keybinds + .get(&client_id) + .cloned() + .unwrap_or_else(|| self.default_keybinds.clone()), ) { Ok(_) => { let _ = self @@ -827,7 +843,7 @@ impl WasmBridge { }) .collect(); if let Some(default_mode) = default_mode.as_ref() { - self.default_mode = *default_mode; + self.base_modes.insert(client_id, *default_mode); } if let Some(keybinds) = keybinds.as_ref() { self.keybinds.insert(client_id, keybinds.clone()); @@ -1332,6 +1348,7 @@ fn check_event_permission( | Event::PaneClosed(..) | Event::EditPaneOpened(..) | Event::EditPaneExited(..) + | Event::FailedToWriteConfigToDisk(..) | Event::CommandPaneReRun(..) | Event::InputReceived => PermissionType::ReadApplicationState, _ => return (PermissionStatus::Granted, None), diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 7b236fa4fa..26b6388a89 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -257,7 +257,9 @@ fn host_run_plugin_command(caller: Caller<'_, PluginEnv>) { PluginCommand::WatchFilesystem => watch_filesystem(env), PluginCommand::DumpSessionLayout => dump_session_layout(env), PluginCommand::CloseSelf => close_self(env), - PluginCommand::Reconfigure(new_config) => reconfigure(env, new_config)?, + PluginCommand::Reconfigure(new_config, write_config_to_disk) => { + reconfigure(env, new_config, write_config_to_disk)? + }, PluginCommand::HidePaneWithId(pane_id) => { hide_pane_with_id(env, pane_id.into())? }, @@ -894,11 +896,15 @@ fn close_self(env: &PluginEnv) { .non_fatal(); } -fn reconfigure(env: &PluginEnv, new_config: String) -> Result<()> { +fn reconfigure(env: &PluginEnv, new_config: String, write_config_to_disk: bool) -> Result<()> { let err_context = || "Failed to reconfigure"; let client_id = env.client_id; env.senders - .send_to_server(ServerInstruction::Reconfigure(client_id, new_config)) + .send_to_server(ServerInstruction::Reconfigure { + client_id, + config: new_config, + write_config_to_disk, + }) .with_context(err_context)?; Ok(()) } diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index a08ae99eec..f23f3176da 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -1045,19 +1045,13 @@ pub(crate) fn route_thread_main( rlocked_sessions.default_shell.clone(), rlocked_sessions.layout.clone(), Some(&mut seen_cli_pipes), + keybinds.clone(), rlocked_sessions - .client_keybinds - .get(&client_id) - .unwrap_or( - &rlocked_sessions - .client_attributes - .keybinds, - ) - .clone(), - rlocked_sessions + .session_configuration + .get_client_configuration(&client_id) + .options .default_mode - .get(&client_id) - .unwrap_or(&InputMode::Normal) + .unwrap_or(InputMode::Normal) .clone(), )? { should_break = true; @@ -1084,14 +1078,15 @@ pub(crate) fn route_thread_main( rlocked_sessions.layout.clone(), Some(&mut seen_cli_pipes), rlocked_sessions - .client_keybinds - .get(&client_id) - .unwrap_or(&rlocked_sessions.client_attributes.keybinds) + .session_configuration + .get_client_keybinds(&client_id) .clone(), rlocked_sessions + .session_configuration + .get_client_configuration(&client_id) + .options .default_mode - .get(&client_id) - .unwrap_or(&InputMode::Normal) + .unwrap_or(InputMode::Normal) .clone(), )? { should_break = true; @@ -1164,16 +1159,20 @@ pub(crate) fn route_thread_main( ClientToServerMsg::NewClient( client_attributes, cli_args, - opts, + config, + runtime_config_options, layout, plugin_aliases, + should_launch_setup_wizard, ) => { let new_client_instruction = ServerInstruction::NewClient( client_attributes, cli_args, - opts, + config, + runtime_config_options, layout, plugin_aliases, + should_launch_setup_wizard, client_id, ); to_server @@ -1182,13 +1181,15 @@ pub(crate) fn route_thread_main( }, ClientToServerMsg::AttachClient( client_attributes, - opts, + config, + runtime_config_options, tab_position_to_focus, pane_id_to_focus, ) => { let attach_client_instruction = ServerInstruction::AttachClient( client_attributes, - opts, + config, + runtime_config_options, tab_position_to_focus, pane_id_to_focus, client_id, @@ -1219,6 +1220,16 @@ pub(crate) fn route_thread_main( ClientToServerMsg::ListClients => { let _ = to_server.send(ServerInstruction::ActiveClients(client_id)); }, + 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, + )); + }, } Ok(should_break) }; diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index b67b68e7c1..ea1814bc70 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -13,6 +13,7 @@ use zellij_utils::data::{ }; use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; +use zellij_utils::input::config::Config; use zellij_utils::input::keybinds::Keybinds; use zellij_utils::input::options::Clipboard; use zellij_utils::pane_size::{Size, SizeInPixels}; @@ -2361,10 +2362,11 @@ pub(crate) fn screen_thread_main( bus: Bus, max_panes: Option, client_attributes: ClientAttributes, - config_options: Box, + config: Config, debug: bool, default_layout: Box, ) -> Result<()> { + let config_options = config.options; let arrow_fonts = !config_options.simplified_ui.unwrap_or_default(); let draw_pane_frames = config_options.pane_frames.unwrap_or(true); let auto_layout = config_options.auto_layout.unwrap_or(true); @@ -2403,7 +2405,7 @@ pub(crate) fn screen_thread_main( // ¯\_(ツ)_/¯ arrow_fonts: !arrow_fonts, }, - &client_attributes.keybinds, + &config.keybinds, config_options.default_mode, ), draw_pane_frames, diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 1d3c140522..1d8a7c818c 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -14,6 +14,7 @@ use zellij_utils::data::{Event, Resize, Style}; use zellij_utils::errors::{prelude::*, ErrorContext}; use zellij_utils::input::actions::Action; use zellij_utils::input::command::{RunCommand, TerminalAction}; +use zellij_utils::input::config::Config; use zellij_utils::input::layout::{ FloatingPaneLayout, Layout, PluginAlias, PluginUserConfiguration, Run, RunPlugin, RunPluginLocation, RunPluginOrAlias, SplitDirection, SplitSize, TiledPaneLayout, @@ -114,12 +115,20 @@ fn send_cli_action_to_server( let get_current_dir = || PathBuf::from("."); let actions = Action::actions_from_cli(cli_action, Box::new(get_current_dir), None).unwrap(); let senders = session_metadata.senders.clone(); - let client_keybinds = session_metadata.client_keybinds.clone(); - let default_mode = session_metadata.default_mode.clone(); let capabilities = PluginCapabilities::default(); let client_attributes = ClientAttributes::default(); let default_shell = None; let default_layout = Box::new(Layout::default()); + let default_mode = session_metadata + .session_configuration + .get_client_configuration(&client_id) + .options + .default_mode + .unwrap_or(InputMode::Normal); + let client_keybinds = session_metadata + .session_configuration + .get_client_keybinds(&client_id) + .clone(); for action in actions { route_action( action, @@ -131,14 +140,8 @@ fn send_cli_action_to_server( default_shell.clone(), default_layout.clone(), None, - client_keybinds - .get(&client_id) - .unwrap_or(&session_metadata.client_attributes.keybinds) - .clone(), - default_mode - .get(&client_id) - .unwrap_or(&InputMode::Normal) - .clone(), + client_keybinds.clone(), + default_mode, ) .unwrap(); } @@ -307,6 +310,7 @@ struct MockScreen { pub client_attributes: ClientAttributes, pub config_options: Options, pub session_metadata: SessionMetaData, + pub config: Config, last_opened_tab_index: Option, } @@ -316,7 +320,7 @@ impl MockScreen { initial_layout: Option, initial_floating_panes_layout: Vec, ) -> std::thread::JoinHandle<()> { - let config_options = self.config_options.clone(); + let config = self.config.clone(); let client_attributes = self.client_attributes.clone(); let screen_bus = Bus::new( vec![self.screen_receiver.take().unwrap()], @@ -338,7 +342,7 @@ impl MockScreen { screen_bus, None, client_attributes, - Box::new(config_options), + config, debug, Box::new(Layout::default()), ) @@ -391,7 +395,7 @@ impl MockScreen { initial_layout: Option, initial_floating_panes_layout: Vec, ) -> std::thread::JoinHandle<()> { - let config_options = self.config_options.clone(); + let config = self.config.clone(); let client_attributes = self.client_attributes.clone(); let screen_bus = Bus::new( vec![self.screen_receiver.take().unwrap()], @@ -413,7 +417,7 @@ impl MockScreen { screen_bus, None, client_attributes, - Box::new(config_options), + config, debug, Box::new(Layout::default()), ) @@ -522,11 +526,9 @@ impl MockScreen { plugin_thread: None, pty_writer_thread: None, background_jobs_thread: None, - config_options: Default::default(), + session_configuration: self.session_metadata.session_configuration.clone(), layout, - client_input_modes: HashMap::new(), - client_keybinds: HashMap::new(), - default_mode: self.session_metadata.default_mode.clone(), + current_input_modes: self.session_metadata.current_input_modes.clone(), } } } @@ -582,11 +584,9 @@ impl MockScreen { plugin_thread: None, pty_writer_thread: None, background_jobs_thread: None, - config_options: Default::default(), layout, - client_input_modes: HashMap::new(), - client_keybinds: HashMap::new(), - default_mode: HashMap::new(), + session_configuration: Default::default(), + current_input_modes: HashMap::new(), }; let os_input = FakeInputOutput::default(); @@ -611,6 +611,7 @@ impl MockScreen { config_options, session_metadata, last_opened_tab_index: None, + config: Config::default(), } } } diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index b3ccc3eb6d..2c5e858011 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -842,8 +842,8 @@ pub fn dump_session_layout() { } /// Rebind keys for the current user -pub fn reconfigure(new_config: String) { - let plugin_command = PluginCommand::Reconfigure(new_config); +pub fn reconfigure(new_config: String, save_configuration_file: bool) { + let plugin_command = PluginCommand::Reconfigure(new_config, save_configuration_file); let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); object_to_stdout(&protobuf_plugin_command.encode_to_vec()); unsafe { host_run_plugin_command() }; diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs index 11ab070bf6..2e9fafa723 100644 --- a/zellij-utils/assets/prost/api.event.rs +++ b/zellij-utils/assets/prost/api.event.rs @@ -11,7 +11,7 @@ pub struct Event { pub name: i32, #[prost( oneof = "event::Payload", - tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21" + tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22" )] pub payload: ::core::option::Option, } @@ -60,10 +60,18 @@ pub mod event { EditPaneExitedPayload(super::EditPaneExitedPayload), #[prost(message, tag = "21")] CommandPaneRerunPayload(super::CommandPaneReRunPayload), + #[prost(message, tag = "22")] + FailedToWriteConfigToDiskPayload(super::FailedToWriteConfigToDiskPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct FailedToWriteConfigToDiskPayload { + #[prost(string, optional, tag = "1")] + pub file_path: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct CommandPaneReRunPayload { #[prost(uint32, tag = "1")] pub terminal_pane_id: u32, @@ -426,6 +434,7 @@ pub enum EventType { EditPaneOpened = 22, EditPaneExited = 23, CommandPaneReRun = 24, + FailedToWriteConfigToDisk = 25, } impl EventType { /// String value of the enum field names used in the ProtoBuf definition. @@ -459,6 +468,7 @@ impl EventType { EventType::EditPaneOpened => "EditPaneOpened", EventType::EditPaneExited => "EditPaneExited", EventType::CommandPaneReRun => "CommandPaneReRun", + EventType::FailedToWriteConfigToDisk => "FailedToWriteConfigToDisk", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -489,6 +499,7 @@ impl EventType { "EditPaneOpened" => Some(Self::EditPaneOpened), "EditPaneExited" => Some(Self::EditPaneExited), "CommandPaneReRun" => Some(Self::CommandPaneReRun), + "FailedToWriteConfigToDisk" => Some(Self::FailedToWriteConfigToDisk), _ => None, } } diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index 641ffa3d07..2290aab45a 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -118,8 +118,8 @@ pub mod plugin_command { ScanHostFolderPayload(::prost::alloc::string::String), #[prost(message, tag = "62")] NewTabsWithLayoutInfoPayload(super::NewTabsWithLayoutInfoPayload), - #[prost(string, tag = "63")] - ReconfigurePayload(::prost::alloc::string::String), + #[prost(message, tag = "63")] + ReconfigurePayload(super::ReconfigurePayload), #[prost(message, tag = "64")] HidePaneWithIdPayload(super::HidePaneWithIdPayload), #[prost(message, tag = "65")] @@ -132,6 +132,14 @@ pub mod plugin_command { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReconfigurePayload { + #[prost(string, tag = "1")] + pub config: ::prost::alloc::string::String, + #[prost(bool, tag = "2")] + pub write_to_disk: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct RerunCommandPanePayload { #[prost(uint32, tag = "1")] pub terminal_pane_id: u32, diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index f03c01cf26..59653e7825 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -120,7 +120,7 @@ impl fmt::Display for KeyWithModifier { .iter() .map(|m| m.to_string()) .collect::>() - .join("-"), + .join(" "), self.bare_key ) } @@ -920,6 +920,7 @@ pub enum Event { EditPaneOpened(u32, Context), // u32 - terminal_pane_id EditPaneExited(u32, Option, Context), // u32 - terminal_pane_id, Option - exit code CommandPaneReRun(u32, Context), // u32 - terminal_pane_id, Option - + FailedToWriteConfigToDisk(Option), // String -> the file path we failed to write } #[derive( @@ -1799,7 +1800,8 @@ pub enum PluginCommand { DumpSessionLayout, CloseSelf, NewTabsWithLayoutInfo(LayoutInfo), - Reconfigure(String), // String -> stringified configuration + Reconfigure(String, bool), // String -> stringified configuration, bool -> save configuration + // file to disk HidePaneWithId(PaneId), ShowPaneWithId(PaneId, bool), // bool -> should_float_if_hidden OpenCommandPaneBackground(CommandToRun, Context), diff --git a/zellij-utils/src/envs.rs b/zellij-utils/src/envs.rs index c7aff980a6..bda884a9b6 100644 --- a/zellij-utils/src/envs.rs +++ b/zellij-utils/src/envs.rs @@ -64,4 +64,7 @@ impl EnvironmentVariables { set_var(k, v); } } + pub fn inner(&self) -> &HashMap { + &self.env + } } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 94417e00a6..21b9294e76 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -411,6 +411,7 @@ pub enum PluginContext { DumpLayoutToPlugin, ListClientsMetadata, Reconfigure, + FailedToWriteConfigToDisk, } /// Stack call representations corresponding to the different types of [`ClientInstruction`]s. @@ -434,6 +435,7 @@ pub enum ClientContext { UnblockCliPipeInput, CliPipeOutput, QueryTerminalSize, + WriteConfigToDisk, } /// Stack call representations corresponding to the different types of [`ServerInstruction`]s. @@ -460,6 +462,8 @@ pub enum ServerContext { ChangeMode, ChangeModeForAllClients, Reconfigure, + ConfigWrittenToDisk, + FailedToWriteConfigToDisk, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index 56c6f338af..92174eb22d 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -1,5 +1,6 @@ use crate::data::Palette; use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode}; +use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::{self, Read}; use std::path::PathBuf; @@ -20,7 +21,7 @@ const DEFAULT_CONFIG_FILE_NAME: &str = "config.kdl"; type ConfigResult = Result; /// Main configuration. -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] pub struct Config { pub keybinds: Keybinds, pub options: Options, @@ -233,6 +234,164 @@ impl Config { self.env = self.env.merge(other.env); Ok(()) } + fn config_file_path(opts: &CliArgs) -> Option { + opts.config_dir + .clone() + .or_else(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> { + // if we fail, try to return the PathBuf of the file we were not able to write to + 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 config = match backed_up_file_name { + Some(backed_up_file_name) => { + format!( + "{}{}", + Config::autogen_config_message(backed_up_file_name), + config + ) + }, + None => config, + }; + std::fs::write(&config_file_path, config.as_bytes()).map_err(|e| { + log::error!("Failed to write config: {}", e); + Some(config_file_path.clone()) + })?; + let written_config = std::fs::read_to_string(&config_file_path).map_err(|e| { + log::error!("Failed to read written config: {}", e); + Some(config_file_path.clone()) + })?; + let parsed_written_config = + Config::from_kdl(&written_config, None).map_err(|e| { + log::error!("Failed to parse written config: {}", e); + None + })?; + if parsed_written_config == parsed_config { + Ok(parsed_config) + } else { + log::error!("Configuration corrupted when writing to disk"); + Err(Some(config_file_path)) + } + }) + } + // 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 { + match Config::config_file_path(opts) { + Some(config_file_path) => { + 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 + }, + } + } + }, + None => false, + } + } + fn find_free_backup_file_name(config_file_path: &PathBuf) -> Option { + let mut backup_config_path = None; + let config_file_name = config_file_path + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or_else(|| DEFAULT_CONFIG_FILE_NAME); + for i in 0..100 { + let new_file_name = if i == 0 { + format!("{}.bak", config_file_name) + } else { + format!("{}.bak.{}", config_file_name, i) + }; + let mut potential_config_path = config_file_path.clone(); + potential_config_path.set_file_name(new_file_name); + if !potential_config_path.exists() { + backup_config_path = Some(potential_config_path); + break; + } + } + backup_config_path + } + fn backup_config_with_written_content_confirmation( + current_config: &str, + current_config_file_path: &PathBuf, + backup_config_path: &PathBuf, + ) -> bool { + let _ = std::fs::copy(current_config_file_path, &backup_config_path); + match std::fs::read_to_string(&backup_config_path) { + Ok(backed_up_config) => current_config == &backed_up_config, + Err(e) => { + log::error!( + "Failed to back up config file {}: {:?}", + backup_config_path.display(), + e + ); + false + }, + } + } + fn backup_current_config(opts: &CliArgs) -> Result, Option> { + // 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) + } + } + fn autogen_config_message(backed_up_file_name: PathBuf) -> String { + format!("//\n// THIS FILE WAS AUTOGENERATED BY ZELLIJ, THE PREVIOUS FILE AT THIS LOCATION WAS COPIED TO: {}\n//\n\n", backed_up_file_name.display()) + } } #[cfg(test)] @@ -463,6 +622,7 @@ mod config_test { white: PaletteColor::Rgb((255, 255, 255)), ..Default::default() }, + sourced_from_external_file: false, }, ); let expected_themes = Themes::from_data(expected_themes); @@ -520,6 +680,7 @@ mod config_test { white: PaletteColor::Rgb((255, 255, 255)), ..Default::default() }, + sourced_from_external_file: false, }, ); expected_themes.insert( @@ -539,6 +700,7 @@ mod config_test { orange: PaletteColor::Rgb((208, 135, 112)), ..Default::default() }, + sourced_from_external_file: false, }, ); let expected_themes = Themes::from_data(expected_themes); @@ -583,6 +745,7 @@ mod config_test { white: PaletteColor::EightBit(255), ..Default::default() }, + sourced_from_external_file: false, }, ); let expected_themes = Themes::from_data(expected_themes); diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 77411a5dea..8278f63893 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -504,6 +504,12 @@ impl PluginUserConfiguration { configuration.remove("direction"); configuration.remove("floating"); configuration.remove("move_to_focused_tab"); + configuration.remove("launch_new"); + configuration.remove("payload"); + configuration.remove("skip_cache"); + configuration.remove("title"); + configuration.remove("in_place"); + configuration.remove("skip_plugin_cache"); PluginUserConfiguration(configuration) } @@ -735,6 +741,19 @@ pub struct FloatingPaneLayout { } impl FloatingPaneLayout { + pub fn new() -> Self { + FloatingPaneLayout { + name: None, + height: None, + width: None, + x: None, + y: None, + run: None, + focus: None, + already_running: false, + pane_initial_contents: None, + } + } pub fn add_cwd_to_layout(&mut self, cwd: &PathBuf) { match self.run.as_mut() { Some(run) => run.add_cwd(cwd), diff --git a/zellij-utils/src/input/theme.rs b/zellij-utils/src/input/theme.rs index bf0edf232c..a7f212f0d7 100644 --- a/zellij-utils/src/input/theme.rs +++ b/zellij-utils/src/input/theme.rs @@ -37,7 +37,7 @@ impl FrameConfig { } } -#[derive(Clone, PartialEq, Default)] +#[derive(Clone, PartialEq, Default, Serialize, Deserialize)] pub struct Themes(HashMap); impl fmt::Debug for Themes { @@ -67,12 +67,16 @@ impl Themes { pub fn get_theme(&self, theme_name: &str) -> Option<&Theme> { self.0.get(theme_name) } + pub fn inner(&self) -> &HashMap { + &self.0 + } } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct Theme { #[serde(flatten)] pub palette: Palette, + pub sourced_from_external_file: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__theme__theme_test__dracula_theme_from_file.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__theme__theme_test__dracula_theme_from_file.snap index 315c6d50eb..957ddf0469 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__theme__theme_test__dracula_theme_from_file.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__theme__theme_test__dracula_theme_from_file.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/input/./unit/theme_test.rs +assertion_line: 15 expression: "format!(\"{:#?}\", theme)" --- { @@ -103,5 +104,6 @@ expression: "format!(\"{:#?}\", theme)" 0, ), }, + sourced_from_external_file: true, }, } diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index 2713ca781f..85064f76a8 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -3,6 +3,7 @@ use crate::{ cli::CliArgs, data::{ClientId, ConnectToSession, KeyWithModifier, Style}, errors::{get_current_ctx, prelude::*, ErrorContext}, + input::config::Config, input::keybinds::Keybinds, input::{actions::Action, layout::Layout, options::Options, plugins::PluginAliases}, pane_size::{Size, SizeInPixels}, @@ -16,6 +17,7 @@ use std::{ io::{self, Write}, marker::PhantomData, os::unix::io::{AsRawFd, FromRawFd}, + path::PathBuf, }; type SessionId = u64; @@ -41,7 +43,6 @@ pub enum ClientType { pub struct ClientAttributes { pub size: Size, pub style: Style, - pub keybinds: Keybinds, } #[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] @@ -74,13 +75,16 @@ pub enum ClientToServerMsg { NewClient( ClientAttributes, Box, - Box, + Box, // represents the saved configuration + Box, // represents the runtime configuration Box, Box, + bool, // should launch setup wizard ), AttachClient( ClientAttributes, - Options, + Config, // represents the saved configuration + Options, // represents the runtime configuration Option, // tab position to focus Option<(u32, bool)>, // (pane_id, is_plugin) => pane id to focus ), @@ -90,6 +94,8 @@ pub enum ClientToServerMsg { KillSession, ConnStatus, ListClients, + ConfigWrittenToDisk(Config), + FailedToWriteConfigToDisk(Option), } // Types of messages sent from the server to the client @@ -106,6 +112,7 @@ pub enum ServerToClientMsg { UnblockCliPipeInput(String), // String -> pipe name CliPipeOutput(String, String), // String -> pipe name, String -> Output QueryTerminalSize, + WriteConfigToDisk { config: String }, } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 37d02081f4..bfb01c7824 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -1,25 +1,27 @@ mod kdl_layout_parser; use crate::data::{ - Direction, FloatingPaneCoordinates, InputMode, KeyWithModifier, LayoutInfo, Palette, + BareKey, Direction, FloatingPaneCoordinates, InputMode, KeyWithModifier, LayoutInfo, Palette, PaletteColor, PaneInfo, PaneManifest, PermissionType, Resize, SessionInfo, TabInfo, }; use crate::envs::EnvironmentVariables; use crate::home::{find_default_config_dir, get_layout_dir}; use crate::input::config::{Config, ConfigError, KdlError}; use crate::input::keybinds::Keybinds; -use crate::input::layout::{Layout, RunPlugin, RunPluginOrAlias}; +use crate::input::layout::{ + Layout, PluginUserConfiguration, RunPlugin, RunPluginOrAlias, SplitSize, +}; use crate::input::options::{Clipboard, OnForceClose, Options}; use crate::input::permission::{GrantedPermission, PermissionCache}; use crate::input::plugins::PluginAliases; use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig}; use kdl_layout_parser::KdlLayoutParser; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use strum::IntoEnumIterator; use uuid::Uuid; use miette::NamedSource; -use kdl::{KdlDocument, KdlEntry, KdlNode}; +use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue}; use std::path::PathBuf; use std::str::FromStr; @@ -548,6 +550,526 @@ impl Action { )), } } + pub fn to_kdl(&self) -> Option { + match self { + Action::Quit => Some(KdlNode::new("Quit")), + Action::Write(_key, bytes, _is_kitty) => { + let mut node = KdlNode::new("Write"); + for byte in bytes { + node.push(KdlValue::Base10(*byte as i64)); + } + Some(node) + }, + Action::WriteChars(string) => { + let mut node = KdlNode::new("WriteChars"); + node.push(string.clone()); + Some(node) + }, + Action::SwitchToMode(input_mode) => { + let mut node = KdlNode::new("SwitchToMode"); + node.push(format!("{:?}", input_mode).to_lowercase()); + Some(node) + }, + Action::Resize(resize, resize_direction) => { + let mut node = KdlNode::new("Resize"); + let resize = match resize { + Resize::Increase => "Increase", + Resize::Decrease => "Decrease", + }; + if let Some(resize_direction) = resize_direction { + let resize_direction = match resize_direction { + Direction::Left => "left", + Direction::Right => "right", + Direction::Up => "up", + Direction::Down => "down", + }; + node.push(format!("{} {}", resize, resize_direction)); + } else { + node.push(format!("{}", resize)); + } + Some(node) + }, + Action::FocusNextPane => Some(KdlNode::new("FocusNextPane")), + Action::FocusPreviousPane => Some(KdlNode::new("FocusPreviousPane")), + Action::SwitchFocus => Some(KdlNode::new("SwitchFocus")), + Action::MoveFocus(direction) => { + let mut node = KdlNode::new("MoveFocus"); + let direction = match direction { + Direction::Left => "left", + Direction::Right => "right", + Direction::Up => "up", + Direction::Down => "down", + }; + node.push(direction); + Some(node) + }, + Action::MoveFocusOrTab(direction) => { + let mut node = KdlNode::new("MoveFocusOrTab"); + let direction = match direction { + Direction::Left => "left", + Direction::Right => "right", + Direction::Up => "up", + Direction::Down => "down", + }; + node.push(direction); + Some(node) + }, + Action::MovePane(direction) => { + let mut node = KdlNode::new("MovePane"); + if let Some(direction) = direction { + let direction = match direction { + Direction::Left => "left", + Direction::Right => "right", + Direction::Up => "up", + Direction::Down => "down", + }; + node.push(direction); + } + Some(node) + }, + Action::MovePaneBackwards => Some(KdlNode::new("MovePaneBackwards")), + Action::DumpScreen(file, _) => { + let mut node = KdlNode::new("DumpScreen"); + node.push(file.clone()); + Some(node) + }, + Action::DumpLayout => Some(KdlNode::new("DumpLayout")), + Action::EditScrollback => Some(KdlNode::new("EditScrollback")), + Action::ScrollUp => Some(KdlNode::new("ScrollUp")), + Action::ScrollDown => Some(KdlNode::new("ScrollDown")), + Action::ScrollToBottom => Some(KdlNode::new("ScrollToBottom")), + Action::ScrollToTop => Some(KdlNode::new("ScrollToTop")), + Action::PageScrollUp => Some(KdlNode::new("PageScrollUp")), + Action::PageScrollDown => Some(KdlNode::new("PageScrollDown")), + Action::HalfPageScrollUp => Some(KdlNode::new("HalfPageScrollUp")), + Action::HalfPageScrollDown => Some(KdlNode::new("HalfPageScrollDown")), + Action::ToggleFocusFullscreen => Some(KdlNode::new("ToggleFocusFullscreen")), + Action::TogglePaneFrames => Some(KdlNode::new("TogglePaneFrames")), + Action::ToggleActiveSyncTab => Some(KdlNode::new("ToggleActiveSyncTab")), + Action::NewPane(direction, _, _) => { + let mut node = KdlNode::new("NewPane"); + if let Some(direction) = direction { + let direction = match direction { + Direction::Left => "left", + Direction::Right => "right", + Direction::Up => "up", + Direction::Down => "down", + }; + node.push(direction); + } + Some(node) + }, + Action::TogglePaneEmbedOrFloating => Some(KdlNode::new("TogglePaneEmbedOrFloating")), + Action::ToggleFloatingPanes => Some(KdlNode::new("ToggleFloatingPanes")), + Action::CloseFocus => Some(KdlNode::new("CloseFocus")), + Action::PaneNameInput(bytes) => { + let mut node = KdlNode::new("PaneNameInput"); + for byte in bytes { + node.push(KdlValue::Base10(*byte as i64)); + } + Some(node) + }, + Action::UndoRenamePane => Some(KdlNode::new("UndoRenamePane")), + Action::NewTab(_, _, _, _, name) => { + log::warn!("Converting new tab action without arguments, original action saved to .bak.kdl file"); + let mut node = KdlNode::new("NewTab"); + if let Some(name) = name { + let mut children = KdlDocument::new(); + let mut name_node = KdlNode::new("name"); + name_node.push(name.clone()); + children.nodes_mut().push(name_node); + node.set_children(children); + } + Some(node) + }, + Action::GoToNextTab => Some(KdlNode::new("GoToNextTab")), + Action::GoToPreviousTab => Some(KdlNode::new("GoToPreviousTab")), + Action::CloseTab => Some(KdlNode::new("CloseTab")), + Action::GoToTab(index) => { + let mut node = KdlNode::new("GoToTab"); + node.push(KdlValue::Base10(*index as i64)); + Some(node) + }, + Action::ToggleTab => Some(KdlNode::new("ToggleTab")), + Action::TabNameInput(bytes) => { + let mut node = KdlNode::new("TabNameInput"); + for byte in bytes { + node.push(KdlValue::Base10(*byte as i64)); + } + Some(node) + }, + Action::UndoRenameTab => Some(KdlNode::new("UndoRenameTab")), + Action::MoveTab(direction) => { + let mut node = KdlNode::new("MoveTab"); + let direction = match direction { + Direction::Left => "left", + Direction::Right => "right", + Direction::Up => "up", + Direction::Down => "down", + }; + node.push(direction); + Some(node) + }, + Action::NewTiledPane(direction, run_command_action, name) => { + let mut node = KdlNode::new("Run"); + let mut node_children = KdlDocument::new(); + if let Some(run_command_action) = run_command_action { + node.push(run_command_action.command.display().to_string()); + for arg in &run_command_action.args { + node.push(arg.clone()); + } + if let Some(cwd) = &run_command_action.cwd { + let mut cwd_node = KdlNode::new("cwd"); + cwd_node.push(cwd.display().to_string()); + node_children.nodes_mut().push(cwd_node); + } + if run_command_action.hold_on_start { + let mut hos_node = KdlNode::new("hold_on_start"); + hos_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(hos_node); + } + if !run_command_action.hold_on_close { + let mut hoc_node = KdlNode::new("hold_on_close"); + hoc_node.push(KdlValue::Bool(false)); + node_children.nodes_mut().push(hoc_node); + } + } + if let Some(name) = name { + let mut name_node = KdlNode::new("name"); + name_node.push(name.clone()); + node_children.nodes_mut().push(name_node); + } + if let Some(direction) = direction { + let mut direction_node = KdlNode::new("direction"); + let direction = match direction { + Direction::Left => "left", + Direction::Right => "right", + Direction::Up => "up", + Direction::Down => "down", + }; + direction_node.push(direction); + node_children.nodes_mut().push(direction_node); + } + if !node_children.nodes().is_empty() { + node.set_children(node_children); + } + Some(node) + }, + Action::NewFloatingPane(run_command_action, name, floating_pane_coordinates) => { + let mut node = KdlNode::new("Run"); + let mut node_children = KdlDocument::new(); + let mut floating_pane = KdlNode::new("floating"); + floating_pane.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(floating_pane); + if let Some(run_command_action) = run_command_action { + node.push(run_command_action.command.display().to_string()); + for arg in &run_command_action.args { + node.push(arg.clone()); + } + if let Some(cwd) = &run_command_action.cwd { + let mut cwd_node = KdlNode::new("cwd"); + cwd_node.push(cwd.display().to_string()); + node_children.nodes_mut().push(cwd_node); + } + if run_command_action.hold_on_start { + let mut hos_node = KdlNode::new("hold_on_start"); + hos_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(hos_node); + } + if !run_command_action.hold_on_close { + let mut hoc_node = KdlNode::new("hold_on_close"); + hoc_node.push(KdlValue::Bool(false)); + node_children.nodes_mut().push(hoc_node); + } + } + if let Some(floating_pane_coordinates) = floating_pane_coordinates { + if let Some(x) = floating_pane_coordinates.x { + let mut x_node = KdlNode::new("x"); + match x { + SplitSize::Percent(x) => { + x_node.push(format!("{}%", x)); + }, + SplitSize::Fixed(x) => { + x_node.push(KdlValue::Base10(x as i64)); + }, + }; + node_children.nodes_mut().push(x_node); + } + if let Some(y) = floating_pane_coordinates.y { + let mut y_node = KdlNode::new("y"); + match y { + SplitSize::Percent(y) => { + y_node.push(format!("{}%", y)); + }, + SplitSize::Fixed(y) => { + y_node.push(KdlValue::Base10(y as i64)); + }, + }; + node_children.nodes_mut().push(y_node); + } + if let Some(width) = floating_pane_coordinates.width { + let mut width_node = KdlNode::new("width"); + match width { + SplitSize::Percent(width) => { + width_node.push(format!("{}%", width)); + }, + SplitSize::Fixed(width) => { + width_node.push(KdlValue::Base10(width as i64)); + }, + }; + node_children.nodes_mut().push(width_node); + } + if let Some(height) = floating_pane_coordinates.height { + let mut height_node = KdlNode::new("height"); + match height { + SplitSize::Percent(height) => { + height_node.push(format!("{}%", height)); + }, + SplitSize::Fixed(height) => { + height_node.push(KdlValue::Base10(height as i64)); + }, + }; + node_children.nodes_mut().push(height_node); + } + } + if let Some(name) = name { + let mut name_node = KdlNode::new("name"); + name_node.push(name.clone()); + node_children.nodes_mut().push(name_node); + } + if !node_children.nodes().is_empty() { + node.set_children(node_children); + } + Some(node) + }, + Action::NewInPlacePane(run_command_action, name) => { + let mut node = KdlNode::new("Run"); + let mut node_children = KdlDocument::new(); + if let Some(run_command_action) = run_command_action { + node.push(run_command_action.command.display().to_string()); + for arg in &run_command_action.args { + node.push(arg.clone()); + } + let mut in_place_node = KdlNode::new("in_place"); + in_place_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(in_place_node); + if let Some(cwd) = &run_command_action.cwd { + let mut cwd_node = KdlNode::new("cwd"); + cwd_node.push(cwd.display().to_string()); + node_children.nodes_mut().push(cwd_node); + } + if run_command_action.hold_on_start { + let mut hos_node = KdlNode::new("hold_on_start"); + hos_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(hos_node); + } + if !run_command_action.hold_on_close { + let mut hoc_node = KdlNode::new("hold_on_close"); + hoc_node.push(KdlValue::Bool(false)); + node_children.nodes_mut().push(hoc_node); + } + } + if let Some(name) = name { + let mut name_node = KdlNode::new("name"); + name_node.push(name.clone()); + node_children.nodes_mut().push(name_node); + } + if !node_children.nodes().is_empty() { + node.set_children(node_children); + } + Some(node) + }, + Action::Detach => Some(KdlNode::new("Detach")), + Action::LaunchOrFocusPlugin( + run_plugin_or_alias, + should_float, + move_to_focused_tab, + should_open_in_place, + skip_plugin_cache, + ) => { + let mut node = KdlNode::new("LaunchOrFocusPlugin"); + let mut node_children = KdlDocument::new(); + let location = run_plugin_or_alias.location_string(); + node.push(location); + if *should_float { + let mut should_float_node = KdlNode::new("floating"); + should_float_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(should_float_node); + } + if *move_to_focused_tab { + let mut move_to_focused_tab_node = KdlNode::new("move_to_focused_tab"); + move_to_focused_tab_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(move_to_focused_tab_node); + } + if *should_open_in_place { + let mut should_open_in_place_node = KdlNode::new("in_place"); + should_open_in_place_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(should_open_in_place_node); + } + if *skip_plugin_cache { + let mut skip_plugin_cache_node = KdlNode::new("skip_plugin_cache"); + skip_plugin_cache_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(skip_plugin_cache_node); + } + if let Some(configuration) = run_plugin_or_alias.get_configuration() { + for (config_key, config_value) in configuration.inner().iter() { + let mut node = KdlNode::new(config_key.clone()); + node.push(config_value.clone()); + node_children.nodes_mut().push(node); + } + } + if !node_children.nodes().is_empty() { + node.set_children(node_children); + } + Some(node) + }, + Action::LaunchPlugin( + run_plugin_or_alias, + should_float, + should_open_in_place, + skip_plugin_cache, + cwd, + ) => { + let mut node = KdlNode::new("LaunchPlugin"); + let mut node_children = KdlDocument::new(); + let location = run_plugin_or_alias.location_string(); + node.push(location); + if *should_float { + let mut should_float_node = KdlNode::new("floating"); + should_float_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(should_float_node); + } + if *should_open_in_place { + let mut should_open_in_place_node = KdlNode::new("in_place"); + should_open_in_place_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(should_open_in_place_node); + } + if *skip_plugin_cache { + let mut skip_plugin_cache_node = KdlNode::new("skip_plugin_cache"); + skip_plugin_cache_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(skip_plugin_cache_node); + } + if let Some(cwd) = &cwd { + let mut cwd_node = KdlNode::new("cwd"); + cwd_node.push(cwd.display().to_string()); + node_children.nodes_mut().push(cwd_node); + } else if let Some(cwd) = run_plugin_or_alias.get_initial_cwd() { + let mut cwd_node = KdlNode::new("cwd"); + cwd_node.push(cwd.display().to_string()); + node_children.nodes_mut().push(cwd_node); + } + if let Some(configuration) = run_plugin_or_alias.get_configuration() { + for (config_key, config_value) in configuration.inner().iter() { + let mut node = KdlNode::new(config_key.clone()); + node.push(config_value.clone()); + node_children.nodes_mut().push(node); + } + } + if !node_children.nodes().is_empty() { + node.set_children(node_children); + } + Some(node) + }, + Action::Copy => Some(KdlNode::new("Copy")), + Action::SearchInput(bytes) => { + let mut node = KdlNode::new("SearchInput"); + for byte in bytes { + node.push(KdlValue::Base10(*byte as i64)); + } + Some(node) + }, + Action::Search(search_direction) => { + let mut node = KdlNode::new("Search"); + let direction = match search_direction { + SearchDirection::Down => "down", + SearchDirection::Up => "up", + }; + node.push(direction); + Some(node) + }, + Action::SearchToggleOption(search_toggle_option) => { + let mut node = KdlNode::new("SearchToggleOption"); + node.push(format!("{:?}", search_toggle_option)); + Some(node) + }, + Action::ToggleMouseMode => Some(KdlNode::new("ToggleMouseMode")), + Action::PreviousSwapLayout => Some(KdlNode::new("PreviousSwapLayout")), + Action::NextSwapLayout => Some(KdlNode::new("NextSwapLayout")), + Action::BreakPane => Some(KdlNode::new("BreakPane")), + Action::BreakPaneRight => Some(KdlNode::new("BreakPaneRight")), + Action::BreakPaneLeft => Some(KdlNode::new("BreakPaneLeft")), + Action::KeybindPipe { + name, + payload, + args, + plugin, + configuration, + launch_new, + skip_cache, + floating, + in_place, + cwd, + pane_title, + } => { + let mut node = KdlNode::new("MessagePlugin"); + let mut node_children = KdlDocument::new(); + if let Some(plugin) = plugin { + node.push(plugin.clone()); + } + if let Some(name) = name { + let mut name_node = KdlNode::new("name"); + name_node.push(name.clone()); + node_children.nodes_mut().push(name_node); + } + if let Some(cwd) = cwd { + let mut cwd_node = KdlNode::new("cwd"); + cwd_node.push(cwd.display().to_string()); + node_children.nodes_mut().push(cwd_node); + } + if let Some(payload) = payload { + let mut payload_node = KdlNode::new("payload"); + payload_node.push(payload.clone()); + node_children.nodes_mut().push(payload_node); + } + if *launch_new { + let mut launch_new_node = KdlNode::new("launch_new"); + launch_new_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(launch_new_node); + } + if *skip_cache { + let mut skip_cache_node = KdlNode::new("skip_cache"); + skip_cache_node.push(KdlValue::Bool(true)); + node_children.nodes_mut().push(skip_cache_node); + } + if let Some(floating) = floating { + let mut floating_node = KdlNode::new("floating"); + floating_node.push(KdlValue::Bool(*floating)); + node_children.nodes_mut().push(floating_node); + } + if let Some(title) = pane_title { + let mut title_node = KdlNode::new("title"); + title_node.push(title.clone()); + node_children.nodes_mut().push(title_node); + } + if let Some(configuration) = configuration { + // we do this because the constructor removes the relevant config fields from + // above, otherwise we would have duplicates + let configuration = PluginUserConfiguration::new(configuration.clone()); + let configuration = configuration.inner(); + for (config_key, config_value) in configuration.iter() { + let mut node = KdlNode::new(config_key.clone()); + node.push(config_value.clone()); + node_children.nodes_mut().push(node); + } + } + if !node_children.nodes().is_empty() { + node.set_children(node_children); + } + Some(node) + }, + _ => None, + } + } } impl TryFrom<(&str, &KdlDocument)> for PaletteColor { @@ -667,6 +1189,23 @@ impl TryFrom<(&str, &KdlDocument)> for PaletteColor { } } +impl PaletteColor { + pub fn to_kdl(&self, color_name: &str) -> KdlNode { + let mut node = KdlNode::new(color_name); + match self { + PaletteColor::Rgb((r, g, b)) => { + node.push(KdlValue::Base10(*r as i64)); + node.push(KdlValue::Base10(*g as i64)); + node.push(KdlValue::Base10(*b as i64)); + }, + PaletteColor::EightBit(color_index) => { + node.push(KdlValue::Base10(*color_index as i64)); + }, + } + node + } +} + impl TryFrom<(&KdlNode, &Options)> for Action { type Error = ConfigError; fn try_from((kdl_action, config_options): (&KdlNode, &Options)) -> Result { @@ -1654,6 +2193,876 @@ impl Options { let document: KdlDocument = stringified_keybindings.parse()?; Options::from_kdl(&document) } + fn simplified_ui_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Use a simplified UI without special fonts (arrow glyphs)", + "// Options:", + "// - true", + "// - false (Default)", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("simplified_ui"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(simplified_ui) = self.simplified_ui { + let mut node = create_node(simplified_ui); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(true); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn theme_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}", + " ", + "// Choose the theme that is specified in the themes section.", + "// Default: default", + "// ", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("theme"); + node.push(node_value.to_owned()); + node + }; + if let Some(theme) = &self.theme { + let mut node = create_node(theme); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("dracula"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn default_mode_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}", + " ", "// Choose the base input mode of zellij.", "// Default: normal", "// " + ); + + let create_node = |default_mode: &InputMode| -> KdlNode { + let mut node = KdlNode::new("default_mode"); + node.push(format!("{:?}", default_mode).to_lowercase()); + node + }; + if let Some(default_mode) = &self.default_mode { + let mut node = create_node(default_mode); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(&InputMode::Locked); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn default_shell_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = + format!("{}\n{}\n{}\n{}", + " ", + "// Choose the path to the default shell that zellij will use for opening new panes", + "// Default: $SHELL", + "// ", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("default_shell"); + node.push(node_value.to_owned()); + node + }; + if let Some(default_shell) = &self.default_shell { + let mut node = create_node(&default_shell.display().to_string()); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("fish"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn default_cwd_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}", + " ", + "// Choose the path to override cwd that zellij will use for opening new panes", + "// ", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("default_cwd"); + node.push(node_value.to_owned()); + node + }; + if let Some(default_cwd) = &self.default_cwd { + let mut node = create_node(&default_cwd.display().to_string()); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("/tmp"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn default_layout_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}", + " ", + "// The name of the default layout to load on startup", + "// Default: \"default\"", + "// ", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("default_layout"); + node.push(node_value.to_owned()); + node + }; + if let Some(default_layout) = &self.default_layout { + let mut node = create_node(&default_layout.display().to_string()); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("compact"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn layout_dir_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}", + " ", "// The folder in which Zellij will look for layouts", "// ", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("layout_dir"); + node.push(node_value.to_owned()); + node + }; + if let Some(layout_dir) = &self.layout_dir { + let mut node = create_node(&layout_dir.display().to_string()); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("/tmp"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn theme_dir_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}", + " ", "// The folder in which Zellij will look for themes", "// ", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("theme_dir"); + node.push(node_value.to_owned()); + node + }; + if let Some(theme_dir) = &self.theme_dir { + let mut node = create_node(&theme_dir.display().to_string()); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("/tmp"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn mouse_mode_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Toggle enabling the mouse mode.", + "// On certain configurations, or terminals this could", + "// potentially interfere with copying text.", + "// Options:", + "// - true (default)", + "// - false", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("mouse_mode"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(mouse_mode) = self.mouse_mode { + let mut node = create_node(mouse_mode); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(false); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn pane_frames_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Toggle having pane frames around the panes", + "// Options:", + "// - true (default, enabled)", + "// - false", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("pane_frames"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(pane_frames) = self.pane_frames { + let mut node = create_node(pane_frames); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(false); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn mirror_session_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// When attaching to an existing session with other users,", + "// should the session be mirrored (true)", + "// or should each user have their own cursor (false)", + "// Default: false", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("mirror_session"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(mirror_session) = self.mirror_session { + let mut node = create_node(mirror_session); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(true); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn on_force_close_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP", + "// eg. when terminal window with an active zellij session is closed", + "// Options:", + "// - detach (Default)", + "// - quit", + "// ", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("on_force_close"); + node.push(node_value.to_owned()); + node + }; + if let Some(on_force_close) = &self.on_force_close { + let mut node = match on_force_close { + OnForceClose::Detach => create_node("detach"), + OnForceClose::Quit => create_node("quit"), + }; + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("quit"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn scroll_buffer_size_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Configure the scroll back buffer size", + "// This is the number of lines zellij stores for each pane in the scroll back", + "// buffer. Excess number of lines are discarded in a FIFO fashion.", + "// Valid values: positive integers", + "// Default value: 10000", + "// ", + ); + + let create_node = |node_value: usize| -> KdlNode { + let mut node = KdlNode::new("scroll_buffer_size"); + node.push(KdlValue::Base10(node_value as i64)); + node + }; + if let Some(scroll_buffer_size) = self.scroll_buffer_size { + let mut node = create_node(scroll_buffer_size); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(10000); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn copy_command_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Provide a command to execute when copying text. The text will be piped to", + "// the stdin of the program to perform the copy. This can be used with", + "// terminal emulators which do not support the OSC 52 ANSI control sequence", + "// that will be used by default if this option is not set.", + "// Examples:", + "//", + "// copy_command \"xclip -selection clipboard\" // x11", + "// copy_command \"wl-copy\" // wayland", + "// copy_command \"pbcopy\" // osx", + "// ", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("copy_command"); + node.push(node_value.to_owned()); + node + }; + if let Some(copy_command) = &self.copy_command { + let mut node = create_node(copy_command); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("pbcopy"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn copy_clipboard_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Choose the destination for copied text", + "// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard.", + "// Does not apply when using copy_command.", + "// Options:", + "// - system (default)", + "// - primary", + "// ", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("copy_clipboard"); + node.push(node_value.to_owned()); + node + }; + if let Some(copy_clipboard) = &self.copy_clipboard { + let mut node = match copy_clipboard { + Clipboard::Primary => create_node("primary"), + Clipboard::System => create_node("system"), + }; + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("primary"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn copy_on_select_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}", + " ", + "// Enable automatic copying (and clearing) of selection when releasing mouse", + "// Default: true", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("copy_on_select"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(copy_on_select) = self.copy_on_select { + let mut node = create_node(copy_on_select); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(true); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn scrollback_editor_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}", + " ", + "// Path to the default editor to use to edit pane scrollbuffer", + "// Default: $EDITOR or $VISUAL", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("scrollback_editor"); + node.push(node_value.to_owned()); + node + }; + if let Some(scrollback_editor) = &self.scrollback_editor { + let mut node = create_node(&scrollback_editor.display().to_string()); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("/usr/bin/vim"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn session_name_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// A fixed name to always give the Zellij session.", + "// Consider also setting `attach_to_session true,`", + "// otherwise this will error if such a session exists.", + "// Default: ", + "// ", + ); + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("session_name"); + node.push(node_value.to_owned()); + node + }; + if let Some(session_name) = &self.session_name { + let mut node = create_node(&session_name); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("My singleton session"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn attach_to_session_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}", + " ", + "// When `session_name` is provided, attaches to that session", + "// if it is already running or creates it otherwise.", + "// Default: false", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("attach_to_session"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(attach_to_session) = self.attach_to_session { + let mut node = create_node(attach_to_session); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(true); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn auto_layout_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible", + "// Options:", + "// - true (default)", + "// - false", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("auto_layout"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(auto_layout) = self.auto_layout { + let mut node = create_node(auto_layout); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(false); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn session_serialization_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!("{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected", + "// Options:", + "// - true (default)", + "// - false", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("session_serialization"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(session_serialization) = self.session_serialization { + let mut node = create_node(session_serialization); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(false); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn serialize_pane_viewport_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Whether pane viewports are serialized along with the session, default is false", + "// Options:", + "// - true", + "// - false (default)", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("serialize_pane_viewport"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(serialize_pane_viewport) = self.serialize_pane_viewport { + let mut node = create_node(serialize_pane_viewport); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(false); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn scrollback_lines_to_serialize_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!("{}\n{}\n{}\n{}\n{}", + " ", + "// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0", + "// defaults to the scrollback size. If this number is higher than the scrollback size, it will", + "// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true.", + "// ", + ); + + let create_node = |node_value: usize| -> KdlNode { + let mut node = KdlNode::new("scrollback_lines_to_serialize"); + node.push(KdlValue::Base10(node_value as i64)); + node + }; + if let Some(scrollback_lines_to_serialize) = self.scrollback_lines_to_serialize { + let mut node = create_node(scrollback_lines_to_serialize); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(10000); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn styled_underlines_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}", + " ", + "// Enable or disable the rendering of styled and colored underlines (undercurl).", + "// May need to be disabled for certain unsupported terminals", + "// Default: true", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("styled_underlines"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(styled_underlines) = self.styled_underlines { + let mut node = create_node(styled_underlines); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(false); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn serialization_interval_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!( + "{}\n{}\n{}", + " ", "// How often in seconds sessions are serialized", "// ", + ); + + let create_node = |node_value: u64| -> KdlNode { + let mut node = KdlNode::new("serialization_interval"); + node.push(KdlValue::Base10(node_value as i64)); + node + }; + if let Some(serialization_interval) = self.serialization_interval { + let mut node = create_node(serialization_interval); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(10000); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn disable_session_metadata_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!("{}\n{}\n{}\n{}\n{}", + " ", + "// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know", + "// metadata info on this session)", + "// Default: false", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("disable_session_metadata"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(disable_session_metadata) = self.disable_session_metadata { + let mut node = create_node(disable_session_metadata); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(false); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + fn support_kitty_keyboard_protocol_to_kdl(&self, add_comments: bool) -> Option { + let comment_text = format!("{}\n{}\n{}\n{}", + " ", + "// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it)", + "// Default: true (if the host terminal supports it)", + "// ", + ); + + let create_node = |node_value: bool| -> KdlNode { + let mut node = KdlNode::new("support_kitty_keyboard_protocol"); + node.push(KdlValue::Bool(node_value)); + node + }; + if let Some(support_kitty_keyboard_protocol) = self.support_kitty_keyboard_protocol { + let mut node = create_node(support_kitty_keyboard_protocol); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node(false); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } + pub fn to_kdl(&self, add_comments: bool) -> Vec { + let mut nodes = vec![]; + if let Some(simplified_ui_node) = self.simplified_ui_to_kdl(add_comments) { + nodes.push(simplified_ui_node); + } + if let Some(theme_node) = self.theme_to_kdl(add_comments) { + nodes.push(theme_node); + } + if let Some(default_mode) = self.default_mode_to_kdl(add_comments) { + nodes.push(default_mode); + } + if let Some(default_shell) = self.default_shell_to_kdl(add_comments) { + nodes.push(default_shell); + } + if let Some(default_cwd) = self.default_cwd_to_kdl(add_comments) { + nodes.push(default_cwd); + } + if let Some(default_layout) = self.default_layout_to_kdl(add_comments) { + nodes.push(default_layout); + } + if let Some(layout_dir) = self.layout_dir_to_kdl(add_comments) { + nodes.push(layout_dir); + } + if let Some(theme_dir) = self.theme_dir_to_kdl(add_comments) { + nodes.push(theme_dir); + } + if let Some(mouse_mode) = self.mouse_mode_to_kdl(add_comments) { + nodes.push(mouse_mode); + } + if let Some(pane_frames) = self.pane_frames_to_kdl(add_comments) { + nodes.push(pane_frames); + } + if let Some(mirror_session) = self.mirror_session_to_kdl(add_comments) { + nodes.push(mirror_session); + } + if let Some(on_force_close) = self.on_force_close_to_kdl(add_comments) { + nodes.push(on_force_close); + } + if let Some(scroll_buffer_size) = self.scroll_buffer_size_to_kdl(add_comments) { + nodes.push(scroll_buffer_size); + } + if let Some(copy_command) = self.copy_command_to_kdl(add_comments) { + nodes.push(copy_command); + } + if let Some(copy_clipboard) = self.copy_clipboard_to_kdl(add_comments) { + nodes.push(copy_clipboard); + } + if let Some(copy_on_select) = self.copy_on_select_to_kdl(add_comments) { + nodes.push(copy_on_select); + } + if let Some(scrollback_editor) = self.scrollback_editor_to_kdl(add_comments) { + nodes.push(scrollback_editor); + } + if let Some(session_name) = self.session_name_to_kdl(add_comments) { + nodes.push(session_name); + } + if let Some(attach_to_session) = self.attach_to_session_to_kdl(add_comments) { + nodes.push(attach_to_session); + } + if let Some(auto_layout) = self.auto_layout_to_kdl(add_comments) { + nodes.push(auto_layout); + } + if let Some(session_serialization) = self.session_serialization_to_kdl(add_comments) { + nodes.push(session_serialization); + } + if let Some(serialize_pane_viewport) = self.serialize_pane_viewport_to_kdl(add_comments) { + nodes.push(serialize_pane_viewport); + } + if let Some(scrollback_lines_to_serialize) = + self.scrollback_lines_to_serialize_to_kdl(add_comments) + { + nodes.push(scrollback_lines_to_serialize); + } + if let Some(styled_underlines) = self.styled_underlines_to_kdl(add_comments) { + nodes.push(styled_underlines); + } + if let Some(serialization_interval) = self.serialization_interval_to_kdl(add_comments) { + nodes.push(serialization_interval); + } + if let Some(disable_session_metadata) = self.disable_session_metadata_to_kdl(add_comments) { + nodes.push(disable_session_metadata); + } + if let Some(support_kitty_keyboard_protocol) = + self.support_kitty_keyboard_protocol_to_kdl(add_comments) + { + nodes.push(support_kitty_keyboard_protocol); + } + nodes + } } impl Layout { @@ -1744,6 +3153,29 @@ impl EnvironmentVariables { } Ok(EnvironmentVariables::from_data(env)) } + pub fn to_kdl(&self) -> Option { + let mut has_env_vars = false; + let mut env = KdlNode::new("env"); + let mut env_vars = KdlDocument::new(); + + let mut stable_sorted = BTreeMap::new(); + for (env_var_name, env_var_value) in self.inner() { + stable_sorted.insert(env_var_name, env_var_value); + } + for (env_key, env_value) in stable_sorted { + has_env_vars = true; + let mut variable_key = KdlNode::new(env_key.to_owned()); + variable_key.push(env_value.to_owned()); + env_vars.nodes_mut().push(variable_key); + } + + if has_env_vars { + env.set_children(env_vars); + Some(env) + } else { + None + } + } } impl Keybinds { @@ -1904,6 +3336,184 @@ impl Keybinds { )) } } + // minimize keybind entries for serialization, so that duplicate entries will appear in + // "shared" nodes later rather than once per mode + fn minimize_entries( + &self, + ) -> BTreeMap, BTreeMap>> { + let mut minimized: BTreeMap, BTreeMap>> = + BTreeMap::new(); + let mut flattened: Vec>> = self + .0 + .iter() + .map(|(_input_mode, keybind)| keybind.clone().into_iter().collect()) + .collect(); + for keybind in flattened.drain(..) { + for (key, actions) in keybind.into_iter() { + let mut appears_in_modes: BTreeSet = BTreeSet::new(); + for (input_mode, keybinds) in self.0.iter() { + if keybinds.get(&key) == Some(&actions) { + appears_in_modes.insert(*input_mode); + } + } + minimized + .entry(appears_in_modes) + .or_insert_with(Default::default) + .insert(key, actions); + } + } + minimized + } + fn serialize_mode_title_node(&self, input_modes: &BTreeSet) -> KdlNode { + let all_modes: Vec = InputMode::iter().collect(); + let total_input_mode_count = all_modes.len(); + if input_modes.len() == 1 { + let input_mode_name = + format!("{:?}", input_modes.iter().next().unwrap()).to_lowercase(); + KdlNode::new(input_mode_name) + } else if input_modes.len() == total_input_mode_count { + KdlNode::new("shared") + } else if input_modes.len() < total_input_mode_count / 2 { + let mut node = KdlNode::new("shared_among"); + for input_mode in input_modes { + node.push(format!("{:?}", input_mode).to_lowercase()); + } + node + } else { + let mut node = KdlNode::new("shared_except"); + let mut modes = all_modes.clone(); + for input_mode in input_modes { + modes.retain(|m| m != input_mode) + } + for mode in modes { + node.push(format!("{:?}", mode).to_lowercase()); + } + node + } + } + fn serialize_mode_keybinds( + &self, + keybinds: &BTreeMap>, + ) -> KdlDocument { + let mut mode_keybinds = KdlDocument::new(); + for keybind in keybinds { + let mut keybind_node = KdlNode::new("bind"); + keybind_node.push(keybind.0.to_kdl()); + let mut actions = KdlDocument::new(); + let mut actions_have_children = false; + for action in keybind.1 { + if let Some(kdl_action) = action.to_kdl() { + if kdl_action.children().is_some() { + actions_have_children = true; + } + actions.nodes_mut().push(kdl_action); + } + } + if !actions_have_children { + for action in actions.nodes_mut() { + action.set_leading(""); + action.set_trailing("; "); + } + actions.set_leading(" "); + actions.set_trailing(""); + } + keybind_node.set_children(actions); + mode_keybinds.nodes_mut().push(keybind_node); + } + mode_keybinds + } + pub fn to_kdl(&self, should_clear_defaults: bool) -> KdlNode { + let mut keybinds_node = KdlNode::new("keybinds"); + if should_clear_defaults { + keybinds_node.insert("clear-defaults", true); + } + let mut minimized = self.minimize_entries(); + let mut keybinds_children = KdlDocument::new(); + + macro_rules! encode_single_input_mode { + ($mode_name:ident) => {{ + if let Some(keybinds) = minimized.remove(&BTreeSet::from([InputMode::$mode_name])) { + let mut mode_node = + KdlNode::new(format!("{:?}", InputMode::$mode_name).to_lowercase()); + let mode_keybinds = self.serialize_mode_keybinds(&keybinds); + mode_node.set_children(mode_keybinds); + keybinds_children.nodes_mut().push(mode_node); + } + }}; + } + // we do this explicitly so that the sorting order of modes in the config is more Human + // readable - this is actually less code (and clearer) than implementing Ord in this case + encode_single_input_mode!(Normal); + encode_single_input_mode!(Locked); + encode_single_input_mode!(Pane); + encode_single_input_mode!(Tab); + encode_single_input_mode!(Resize); + encode_single_input_mode!(Move); + encode_single_input_mode!(Scroll); + encode_single_input_mode!(Search); + encode_single_input_mode!(Session); + + for (input_modes, keybinds) in minimized { + if input_modes.is_empty() { + log::error!("invalid input mode for keybinds: {:#?}", keybinds); + continue; + } + let mut mode_node = self.serialize_mode_title_node(&input_modes); + let mode_keybinds = self.serialize_mode_keybinds(&keybinds); + mode_node.set_children(mode_keybinds); + keybinds_children.nodes_mut().push(mode_node); + } + keybinds_node.set_children(keybinds_children); + keybinds_node + } +} + +impl KeyWithModifier { + pub fn to_kdl(&self) -> String { + if self.key_modifiers.is_empty() { + self.bare_key.to_kdl() + } else { + format!( + "{} {}", + self.key_modifiers + .iter() + .map(|m| m.to_string()) + .collect::>() + .join(" "), + self.bare_key.to_kdl() + ) + } + } +} + +impl BareKey { + pub fn to_kdl(&self) -> String { + match self { + BareKey::PageDown => format!("PageDown"), + BareKey::PageUp => format!("PageUp"), + BareKey::Left => format!("left"), + BareKey::Down => format!("down"), + BareKey::Up => format!("up"), + BareKey::Right => format!("right"), + BareKey::Home => format!("home"), + BareKey::End => format!("end"), + BareKey::Backspace => format!("backspace"), + BareKey::Delete => format!("del"), + BareKey::Insert => format!("insert"), + BareKey::F(index) => format!("F{}", index), + BareKey::Char(' ') => format!("space"), + BareKey::Char(character) => format!("{}", character), + BareKey::Tab => format!("tab"), + BareKey::Esc => format!("esc"), + BareKey::Enter => format!("enter"), + BareKey::CapsLock => format!("capslock"), + BareKey::ScrollLock => format!("scrolllock"), + BareKey::NumLock => format!("numlock"), + BareKey::PrintScreen => format!("printscreen"), + BareKey::Pause => format!("pause"), + BareKey::Menu => format!("menu"), + } + } } impl Config { @@ -1920,7 +3530,8 @@ impl Config { config.keybinds = Keybinds::from_kdl(&kdl_keybinds, config.keybinds, &config.options)?; } if let Some(kdl_themes) = kdl_config.get("themes") { - let config_themes = Themes::from_kdl(kdl_themes)?; + let sourced_from_external_file = false; + let config_themes = Themes::from_kdl(kdl_themes, sourced_from_external_file)?; config.themes = config.themes.merge(config_themes); } if let Some(kdl_plugin_aliases) = kdl_config.get("plugins") { @@ -1937,6 +3548,34 @@ impl Config { } Ok(config) } + pub fn to_string(&self, add_comments: bool) -> String { + let mut document = KdlDocument::new(); + + let clear_defaults = true; + let keybinds = self.keybinds.to_kdl(clear_defaults); + document.nodes_mut().push(keybinds); + + if let Some(themes) = self.themes.to_kdl() { + document.nodes_mut().push(themes); + } + + let plugins = self.plugins.to_kdl(); + document.nodes_mut().push(plugins); + + if let Some(ui_config) = self.ui.to_kdl() { + document.nodes_mut().push(ui_config); + } + + if let Some(env) = self.env.to_kdl() { + document.nodes_mut().push(env); + } + + document + .nodes_mut() + .append(&mut self.options.to_kdl(add_comments)); + + document.to_string() + } } impl PluginAliases { @@ -1962,6 +3601,46 @@ impl PluginAliases { } Ok(PluginAliases { aliases }) } + pub fn to_kdl(&self) -> KdlNode { + let mut plugins = KdlNode::new("plugins"); + let mut plugins_children = KdlDocument::new(); + for (alias_name, plugin_alias) in self.aliases.iter() { + let mut plugin_alias_node = KdlNode::new(alias_name.clone()); + let mut plugin_alias_children = KdlDocument::new(); + let location_string = plugin_alias.location.display(); + + plugin_alias_node.insert("location", location_string); + let cwd = plugin_alias.initial_cwd.as_ref(); + let mut has_children = false; + if let Some(cwd) = cwd { + has_children = true; + let mut cwd_node = KdlNode::new("cwd"); + cwd_node.push(cwd.display().to_string()); + plugin_alias_children.nodes_mut().push(cwd_node); + } + let configuration = plugin_alias.configuration.inner(); + if !configuration.is_empty() { + has_children = true; + for (config_key, config_value) in configuration { + let mut node = KdlNode::new(config_key.to_owned()); + if config_value == "true" { + node.push(KdlValue::Bool(true)); + } else if config_value == "false" { + node.push(KdlValue::Bool(false)); + } else { + node.push(config_value.to_string()); + } + plugin_alias_children.nodes_mut().push(node); + } + } + if has_children { + plugin_alias_node.set_children(plugin_alias_children); + } + plugins_children.nodes_mut().push(plugin_alias_node); + } + plugins.set_children(plugins_children); + plugins + } } impl UiConfig { @@ -1981,10 +3660,40 @@ impl UiConfig { } Ok(ui_config) } + pub fn to_kdl(&self) -> Option { + let mut ui_config = KdlNode::new("ui"); + let mut ui_config_children = KdlDocument::new(); + let mut frame_config = KdlNode::new("pane_frames"); + let mut frame_config_children = KdlDocument::new(); + let mut has_ui_config = false; + if self.pane_frames.rounded_corners { + has_ui_config = true; + let mut rounded_corners = KdlNode::new("rounded_corners"); + rounded_corners.push(KdlValue::Bool(true)); + frame_config_children.nodes_mut().push(rounded_corners); + } + if self.pane_frames.hide_session_name { + has_ui_config = true; + let mut hide_session_name = KdlNode::new("hide_session_name"); + hide_session_name.push(KdlValue::Bool(true)); + frame_config_children.nodes_mut().push(hide_session_name); + } + if has_ui_config { + frame_config.set_children(frame_config_children); + ui_config_children.nodes_mut().push(frame_config); + ui_config.set_children(ui_config_children); + Some(ui_config) + } else { + None + } + } } impl Themes { - pub fn from_kdl(themes_from_kdl: &KdlNode) -> Result { + pub fn from_kdl( + themes_from_kdl: &KdlNode, + sourced_from_external_file: bool, + ) -> Result { let mut themes: HashMap = HashMap::new(); for theme_config in kdl_children_nodes_or_error!(themes_from_kdl, "no themes found") { let theme_name = kdl_name!(theme_config); @@ -2004,6 +3713,7 @@ impl Themes { white: PaletteColor::try_from(("white", theme_colors))?, ..Default::default() }, + sourced_from_external_file, }; themes.insert(theme_name.into(), theme); } @@ -2011,14 +3721,17 @@ impl Themes { Ok(themes) } - pub fn from_string(raw_string: &String) -> Result { + pub fn from_string( + raw_string: &String, + sourced_from_external_file: bool, + ) -> Result { let kdl_config: KdlDocument = raw_string.parse()?; let kdl_themes = kdl_config.get("themes").ok_or(ConfigError::new_kdl_error( "No theme node found in file".into(), kdl_config.span().offset(), kdl_config.span().len(), ))?; - let all_themes_in_file = Themes::from_kdl(kdl_themes)?; + let all_themes_in_file = Themes::from_kdl(kdl_themes, sourced_from_external_file)?; Ok(all_themes_in_file) } @@ -2026,7 +3739,8 @@ impl Themes { // String is the theme name let kdl_config = std::fs::read_to_string(&path_to_theme_file) .map_err(|e| ConfigError::IoPath(e, path_to_theme_file.clone()))?; - Themes::from_string(&kdl_config).map_err(|e| match e { + let sourced_from_external_file = true; + Themes::from_string(&kdl_config, sourced_from_external_file).map_err(|e| match e { ConfigError::KdlError(kdl_error) => ConfigError::KdlError( kdl_error.add_src(path_to_theme_file.display().to_string(), kdl_config), ), @@ -2049,6 +3763,63 @@ impl Themes { } Ok(themes) } + pub fn to_kdl(&self) -> Option { + let mut theme_node = KdlNode::new("themes"); + let mut themes = KdlDocument::new(); + let mut has_themes = false; + let sorted_themes: BTreeMap = self.inner().clone().into_iter().collect(); + for (theme_name, theme) in sorted_themes { + if theme.sourced_from_external_file { + // we do not serialize themes that have been defined in external files so as not to + // clog up the configuration file definitions + continue; + } + has_themes = true; + let mut current_theme_node = KdlNode::new(theme_name.clone()); + let mut current_theme_node_children = KdlDocument::new(); + current_theme_node_children + .nodes_mut() + .push(theme.palette.fg.to_kdl("fg")); + current_theme_node_children + .nodes_mut() + .push(theme.palette.bg.to_kdl("bg")); + current_theme_node_children + .nodes_mut() + .push(theme.palette.red.to_kdl("red")); + current_theme_node_children + .nodes_mut() + .push(theme.palette.green.to_kdl("green")); + current_theme_node_children + .nodes_mut() + .push(theme.palette.yellow.to_kdl("yellow")); + current_theme_node_children + .nodes_mut() + .push(theme.palette.blue.to_kdl("blue")); + current_theme_node_children + .nodes_mut() + .push(theme.palette.magenta.to_kdl("magenta")); + current_theme_node_children + .nodes_mut() + .push(theme.palette.orange.to_kdl("orange")); + current_theme_node_children + .nodes_mut() + .push(theme.palette.cyan.to_kdl("cyan")); + current_theme_node_children + .nodes_mut() + .push(theme.palette.black.to_kdl("black")); + current_theme_node_children + .nodes_mut() + .push(theme.palette.white.to_kdl("white")); + current_theme_node.set_children(current_theme_node_children); + themes.nodes_mut().push(current_theme_node); + } + if has_themes { + theme_node.set_children(themes); + Some(theme_node) + } else { + None + } + } } impl PermissionCache { @@ -2197,7 +3968,7 @@ impl SessionInfo { LayoutInfo::File(name) => (name.clone(), "file"), LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"), LayoutInfo::Url(url) => (url.clone(), "url"), - LayoutInfo::Stringified(stringified) => ("stringified-layout".to_owned(), "N/A"), + LayoutInfo::Stringified(_stringified) => ("stringified-layout".to_owned(), "N/A"), }; let mut layout_node = KdlNode::new(format!("{}", layout_name)); let layout_source = KdlEntry::new_prop("source", layout_source); @@ -2707,3 +4478,879 @@ fn serialize_and_deserialize_session_info_with_data() { assert_eq!(session_info, deserealized); insta::assert_snapshot!(serialized); } + +#[test] +fn keybinds_to_string() { + let fake_config = r#" + keybinds { + normal { + bind "Ctrl g" { SwitchToMode "Locked"; } + } + }"#; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = Keybinds::from_kdl( + document.get("keybinds").unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + let clear_defaults = true; + let serialized = Keybinds::to_kdl(&deserialized, clear_defaults); + let deserialized_from_serialized = Keybinds::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("keybinds") + .unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + insta::assert_snapshot!(serialized.to_string()); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); +} + +#[test] +fn keybinds_to_string_without_clearing_defaults() { + let fake_config = r#" + keybinds { + normal { + bind "Ctrl g" { SwitchToMode "Locked"; } + } + }"#; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = Keybinds::from_kdl( + document.get("keybinds").unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + let clear_defaults = false; + let serialized = Keybinds::to_kdl(&deserialized, clear_defaults); + let deserialized_from_serialized = Keybinds::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("keybinds") + .unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + insta::assert_snapshot!(serialized.to_string()); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); +} + +#[test] +fn keybinds_to_string_with_multiple_actions() { + let fake_config = r#" + keybinds { + normal { + bind "Ctrl n" { NewPane; SwitchToMode "Locked"; } + } + }"#; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = Keybinds::from_kdl( + document.get("keybinds").unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + let clear_defaults = true; + let serialized = Keybinds::to_kdl(&deserialized, clear_defaults); + let deserialized_from_serialized = Keybinds::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("keybinds") + .unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn keybinds_to_string_with_all_actions() { + let fake_config = r#" + keybinds { + normal { + bind "Ctrl a" { Quit; } + bind "Ctrl b" { Write 102 111 111; } + bind "Ctrl c" { WriteChars "hi there!"; } + bind "Ctrl d" { SwitchToMode "Locked"; } + bind "Ctrl e" { Resize "Increase"; } + bind "Ctrl f" { FocusNextPane; } + bind "Ctrl g" { FocusPreviousPane; } + bind "Ctrl h" { SwitchFocus; } + bind "Ctrl i" { MoveFocus "Right"; } + bind "Ctrl j" { MoveFocusOrTab "Right"; } + bind "Ctrl k" { MovePane "Right"; } + bind "Ctrl l" { MovePaneBackwards; } + bind "Ctrl m" { Resize "Decrease Down"; } + bind "Ctrl n" { DumpScreen "/tmp/dumped"; } + bind "Ctrl o" { DumpLayout "/tmp/dumped-layout"; } + bind "Ctrl p" { EditScrollback; } + bind "Ctrl q" { ScrollUp; } + bind "Ctrl r" { ScrollDown; } + bind "Ctrl s" { ScrollToBottom; } + bind "Ctrl t" { ScrollToTop; } + bind "Ctrl u" { PageScrollUp; } + bind "Ctrl v" { PageScrollDown; } + bind "Ctrl w" { HalfPageScrollUp; } + bind "Ctrl x" { HalfPageScrollDown; } + bind "Ctrl y" { ToggleFocusFullscreen; } + bind "Ctrl z" { TogglePaneFrames; } + bind "Alt a" { ToggleActiveSyncTab; } + bind "Alt b" { NewPane "Right"; } + bind "Alt c" { TogglePaneEmbedOrFloating; } + bind "Alt d" { ToggleFloatingPanes; } + bind "Alt e" { CloseFocus; } + bind "Alt f" { PaneNameInput 0; } + bind "Alt g" { UndoRenamePane; } + bind "Alt h" { NewTab; } + bind "Alt i" { GoToNextTab; } + bind "Alt j" { GoToPreviousTab; } + bind "Alt k" { CloseTab; } + bind "Alt l" { GoToTab 1; } + bind "Alt m" { ToggleTab; } + bind "Alt n" { TabNameInput 0; } + bind "Alt o" { UndoRenameTab; } + bind "Alt p" { MoveTab "Right"; } + bind "Alt q" { + Run "ls" "-l" { + hold_on_start true; + hold_on_close false; + cwd "/tmp"; + name "my cool pane"; + }; + } + bind "Alt r" { + Run "ls" "-l" { + hold_on_start true; + hold_on_close false; + cwd "/tmp"; + name "my cool pane"; + floating true; + }; + } + bind "Alt s" { + Run "ls" "-l" { + hold_on_start true; + hold_on_close false; + cwd "/tmp"; + name "my cool pane"; + in_place true; + }; + } + bind "Alt t" { Detach; } + bind "Alt u" { + LaunchOrFocusPlugin "zellij:session-manager"{ + floating true; + move_to_focused_tab true; + skip_plugin_cache true; + config_key_1 "config_value_1"; + config_key_2 "config_value_2"; + }; + } + bind "Alt v" { + LaunchOrFocusPlugin "zellij:session-manager"{ + in_place true; + move_to_focused_tab true; + skip_plugin_cache true; + config_key_1 "config_value_1"; + config_key_2 "config_value_2"; + }; + } + bind "Alt w" { + LaunchPlugin "zellij:session-manager" { + floating true; + skip_plugin_cache true; + config_key_1 "config_value_1"; + config_key_2 "config_value_2"; + }; + } + bind "Alt x" { + LaunchPlugin "zellij:session-manager"{ + in_place true; + skip_plugin_cache true; + config_key_1 "config_value_1"; + config_key_2 "config_value_2"; + }; + } + bind "Alt y" { Copy; } + bind "Alt z" { SearchInput 0; } + bind "Ctrl Alt a" { Search "Up"; } + bind "Ctrl Alt b" { SearchToggleOption "CaseSensitivity"; } + bind "Ctrl Alt c" { ToggleMouseMode; } + bind "Ctrl Alt d" { PreviousSwapLayout; } + bind "Ctrl Alt e" { NextSwapLayout; } + bind "Ctrl Alt g" { BreakPane; } + bind "Ctrl Alt h" { BreakPaneRight; } + bind "Ctrl Alt i" { BreakPaneLeft; } + bind "Ctrl Alt i" { BreakPaneLeft; } + bind "Ctrl Alt j" { + MessagePlugin "zellij:session-manager"{ + name "message_name"; + payload "message_payload"; + cwd "/tmp"; + launch_new true; + skip_cache true; + floating true; + title "plugin_title"; + config_key_1 "config_value_1"; + config_key_2 "config_value_2"; + }; + } + } + }"#; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = Keybinds::from_kdl( + document.get("keybinds").unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + let clear_defaults = true; + let serialized = Keybinds::to_kdl(&deserialized, clear_defaults); + let deserialized_from_serialized = Keybinds::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("keybinds") + .unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + // uncomment the below lines for more easily debugging a failed assertion here + // for (input_mode, input_mode_keybinds) in deserialized.0 { + // if let Some(other_input_mode_keybinds) = deserialized_from_serialized.0.get(&input_mode) { + // for (keybind, action) in input_mode_keybinds { + // if let Some(other_action) = other_input_mode_keybinds.get(&keybind) { + // assert_eq!(&action, other_action); + // } else { + // eprintln!("keybind: {:?} not found in other", keybind); + // } + // } + // } + // } + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn keybinds_to_string_with_shared_modes() { + let fake_config = r#" + keybinds { + normal { + bind "Ctrl n" { NewPane; SwitchToMode "Locked"; } + } + locked { + bind "Ctrl n" { NewPane; SwitchToMode "Locked"; } + } + shared_except "locked" "pane" { + bind "Ctrl f" { TogglePaneEmbedOrFloating; } + } + shared_among "locked" "pane" { + bind "Ctrl p" { WriteChars "foo"; } + } + }"#; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = Keybinds::from_kdl( + document.get("keybinds").unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + let clear_defaults = true; + let serialized = Keybinds::to_kdl(&deserialized, clear_defaults); + let deserialized_from_serialized = Keybinds::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("keybinds") + .unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn keybinds_to_string_with_multiple_multiline_actions() { + let fake_config = r#" + keybinds { + shared { + bind "Ctrl n" { + NewPane + SwitchToMode "Locked" + MessagePlugin "zellij:session-manager"{ + name "message_name"; + payload "message_payload"; + cwd "/tmp"; + launch_new true; + skip_cache true; + floating true; + title "plugin_title"; + config_key_1 "config_value_1"; + config_key_2 "config_value_2"; + }; + } + } + }"#; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = Keybinds::from_kdl( + document.get("keybinds").unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + let clear_defaults = true; + let serialized = Keybinds::to_kdl(&deserialized, clear_defaults); + let deserialized_from_serialized = Keybinds::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("keybinds") + .unwrap(), + Default::default(), + &Default::default(), + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn themes_to_string() { + let fake_config = r#" + themes { + dracula { + fg 248 248 242 + bg 40 42 54 + black 0 0 0 + red 255 85 85 + green 80 250 123 + yellow 241 250 140 + blue 98 114 164 + magenta 255 121 198 + cyan 139 233 253 + white 255 255 255 + orange 255 184 108 + } + }"#; + let document: KdlDocument = fake_config.parse().unwrap(); + let sourced_from_external_file = false; + let deserialized = + Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap(); + let serialized = Themes::to_kdl(&deserialized).unwrap(); + let deserialized_from_serialized = Themes::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("themes") + .unwrap(), + sourced_from_external_file, + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn themes_to_string_with_hex_definitions() { + let fake_config = r##" + themes { + nord { + fg "#D8DEE9" + bg "#2E3440" + black "#3B4252" + red "#BF616A" + green "#A3BE8C" + yellow "#EBCB8B" + blue "#81A1C1" + magenta "#B48EAD" + cyan "#88C0D0" + white "#E5E9F0" + orange "#D08770" + } + }"##; + let document: KdlDocument = fake_config.parse().unwrap(); + let sourced_from_external_file = false; + let deserialized = + Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap(); + let serialized = Themes::to_kdl(&deserialized).unwrap(); + let deserialized_from_serialized = Themes::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("themes") + .unwrap(), + sourced_from_external_file, + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn themes_to_string_with_eight_bit_definitions() { + let fake_config = r##" + themes { + default { + fg 1 + bg 10 + black 20 + red 30 + green 40 + yellow 50 + blue 60 + magenta 70 + cyan 80 + white 90 + orange 254 + } + }"##; + let document: KdlDocument = fake_config.parse().unwrap(); + let sourced_from_external_file = false; + let deserialized = + Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap(); + let serialized = Themes::to_kdl(&deserialized).unwrap(); + let deserialized_from_serialized = Themes::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("themes") + .unwrap(), + sourced_from_external_file, + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn themes_to_string_with_combined_definitions() { + let fake_config = r##" + themes { + default { + fg 1 + bg 10 + black 20 + red 30 + green 40 + yellow 50 + blue 60 + magenta 70 + cyan 80 + white 255 255 255 + orange "#D08770" + } + }"##; + let document: KdlDocument = fake_config.parse().unwrap(); + let sourced_from_external_file = false; + let deserialized = + Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap(); + let serialized = Themes::to_kdl(&deserialized).unwrap(); + let deserialized_from_serialized = Themes::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("themes") + .unwrap(), + sourced_from_external_file, + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn themes_to_string_with_multiple_theme_definitions() { + let fake_config = r##" + themes { + nord { + fg "#D8DEE9" + bg "#2E3440" + black "#3B4252" + red "#BF616A" + green "#A3BE8C" + yellow "#EBCB8B" + blue "#81A1C1" + magenta "#B48EAD" + cyan "#88C0D0" + white "#E5E9F0" + orange "#D08770" + } + dracula { + fg 248 248 242 + bg 40 42 54 + black 0 0 0 + red 255 85 85 + green 80 250 123 + yellow 241 250 140 + blue 98 114 164 + magenta 255 121 198 + cyan 139 233 253 + white 255 255 255 + orange 255 184 108 + } + }"##; + let document: KdlDocument = fake_config.parse().unwrap(); + let sourced_from_external_file = false; + let deserialized = + Themes::from_kdl(document.get("themes").unwrap(), sourced_from_external_file).unwrap(); + let serialized = Themes::to_kdl(&deserialized).unwrap(); + let deserialized_from_serialized = Themes::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("themes") + .unwrap(), + sourced_from_external_file, + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn plugins_to_string() { + let fake_config = r##" + plugins { + tab-bar location="zellij:tab-bar" + status-bar location="zellij:status-bar" + strider location="zellij:strider" + compact-bar location="zellij:compact-bar" + session-manager location="zellij:session-manager" + welcome-screen location="zellij:session-manager" { + welcome_screen true + } + filepicker location="zellij:strider" { + cwd "/" + } + }"##; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = PluginAliases::from_kdl(document.get("plugins").unwrap()).unwrap(); + let serialized = PluginAliases::to_kdl(&deserialized); + let deserialized_from_serialized = PluginAliases::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("plugins") + .unwrap(), + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn plugins_to_string_with_file_and_web() { + let fake_config = r##" + plugins { + tab-bar location="https://foo.com/plugin.wasm" + filepicker location="file:/path/to/my/plugin.wasm" { + cwd "/" + } + }"##; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = PluginAliases::from_kdl(document.get("plugins").unwrap()).unwrap(); + let serialized = PluginAliases::to_kdl(&deserialized); + let deserialized_from_serialized = PluginAliases::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("plugins") + .unwrap(), + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn ui_config_to_string() { + let fake_config = r##" + ui { + pane_frames { + rounded_corners true + hide_session_name true + } + }"##; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = UiConfig::from_kdl(document.get("ui").unwrap()).unwrap(); + let serialized = UiConfig::to_kdl(&deserialized).unwrap(); + let deserialized_from_serialized = UiConfig::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("ui") + .unwrap(), + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn ui_config_to_string_with_no_ui_config() { + let fake_config = r##" + ui { + pane_frames { + } + }"##; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = UiConfig::from_kdl(document.get("ui").unwrap()).unwrap(); + assert_eq!(UiConfig::to_kdl(&deserialized), None); +} + +#[test] +fn env_vars_to_string() { + let fake_config = r##" + env { + foo "bar" + bar "foo" + thing 1 + baz "true" + }"##; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = EnvironmentVariables::from_kdl(document.get("env").unwrap()).unwrap(); + let serialized = EnvironmentVariables::to_kdl(&deserialized).unwrap(); + let deserialized_from_serialized = EnvironmentVariables::from_kdl( + serialized + .to_string() + .parse::() + .unwrap() + .get("env") + .unwrap(), + ) + .unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(serialized.to_string()); +} + +#[test] +fn env_vars_to_string_with_no_env_vars() { + let fake_config = r##" + env { + }"##; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = EnvironmentVariables::from_kdl(document.get("env").unwrap()).unwrap(); + assert_eq!(EnvironmentVariables::to_kdl(&deserialized), None); +} + +#[test] +fn config_options_to_string() { + let fake_config = r##" + simplified_ui true + theme "dracula" + default_mode "locked" + default_shell "fish" + default_cwd "/tmp/foo" + default_layout "compact" + layout_dir "/tmp/layouts" + theme_dir "/tmp/themes" + mouse_mode false + pane_frames false + mirror_session true + on_force_close "quit" + scroll_buffer_size 100 + copy_command "pbcopy" + copy_clipboard "system" + copy_on_select false + scrollback_editor "vim" + session_name "my_cool_session" + attach_to_session false + auto_layout false + session_serialization true + serialize_pane_viewport false + scrollback_lines_to_serialize 1000 + styled_underlines false + serialization_interval 1 + disable_session_metadata true + support_kitty_keyboard_protocol false + "##; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = Options::from_kdl(&document).unwrap(); + let mut serialized = Options::to_kdl(&deserialized, false); + let mut fake_document = KdlDocument::new(); + fake_document.nodes_mut().append(&mut serialized); + let deserialized_from_serialized = + Options::from_kdl(&fake_document.to_string().parse::().unwrap()).unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(fake_document.to_string()); +} + +#[test] +fn config_options_to_string_with_comments() { + let fake_config = r##" + simplified_ui true + theme "dracula" + default_mode "locked" + default_shell "fish" + default_cwd "/tmp/foo" + default_layout "compact" + layout_dir "/tmp/layouts" + theme_dir "/tmp/themes" + mouse_mode false + pane_frames false + mirror_session true + on_force_close "quit" + scroll_buffer_size 100 + copy_command "pbcopy" + copy_clipboard "system" + copy_on_select false + scrollback_editor "vim" + session_name "my_cool_session" + attach_to_session false + auto_layout false + session_serialization true + serialize_pane_viewport false + scrollback_lines_to_serialize 1000 + styled_underlines false + serialization_interval 1 + disable_session_metadata true + support_kitty_keyboard_protocol false + "##; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = Options::from_kdl(&document).unwrap(); + let mut serialized = Options::to_kdl(&deserialized, true); + let mut fake_document = KdlDocument::new(); + fake_document.nodes_mut().append(&mut serialized); + let deserialized_from_serialized = + Options::from_kdl(&fake_document.to_string().parse::().unwrap()).unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(fake_document.to_string()); +} + +#[test] +fn config_options_to_string_without_options() { + let fake_config = r##" + "##; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = Options::from_kdl(&document).unwrap(); + let mut serialized = Options::to_kdl(&deserialized, false); + let mut fake_document = KdlDocument::new(); + fake_document.nodes_mut().append(&mut serialized); + let deserialized_from_serialized = + Options::from_kdl(&fake_document.to_string().parse::().unwrap()).unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(fake_document.to_string()); +} + +#[test] +fn config_options_to_string_with_some_options() { + let fake_config = r##" + default_layout "compact" + "##; + let document: KdlDocument = fake_config.parse().unwrap(); + let deserialized = Options::from_kdl(&document).unwrap(); + let mut serialized = Options::to_kdl(&deserialized, false); + let mut fake_document = KdlDocument::new(); + fake_document.nodes_mut().append(&mut serialized); + let deserialized_from_serialized = + Options::from_kdl(&fake_document.to_string().parse::().unwrap()).unwrap(); + assert_eq!( + deserialized, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(fake_document.to_string()); +} + +#[test] +fn bare_config_from_default_assets_to_string() { + let fake_config = Config::from_default_assets().unwrap(); + let fake_config_stringified = fake_config.to_string(false); + let deserialized_from_serialized = Config::from_kdl(&fake_config_stringified, None).unwrap(); + assert_eq!( + fake_config, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(fake_config_stringified); +} + +#[test] +fn bare_config_from_default_assets_to_string_with_comments() { + let fake_config = Config::from_default_assets().unwrap(); + let fake_config_stringified = fake_config.to_string(true); + let deserialized_from_serialized = Config::from_kdl(&fake_config_stringified, None).unwrap(); + assert_eq!( + fake_config, deserialized_from_serialized, + "Deserialized serialized config equals original config" + ); + insta::assert_snapshot!(fake_config_stringified); +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap new file mode 100644 index 0000000000..6cd58d55bf --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap @@ -0,0 +1,240 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 5060 +expression: fake_config_stringified +--- +keybinds clear-defaults=true { + locked { + bind "Ctrl g" { SwitchToMode "normal"; } + } + pane { + bind "left" { MoveFocus "left"; } + bind "down" { MoveFocus "down"; } + bind "up" { MoveFocus "up"; } + bind "right" { MoveFocus "right"; } + bind "c" { SwitchToMode "renamepane"; PaneNameInput 0; } + bind "d" { NewPane "down"; SwitchToMode "normal"; } + bind "e" { TogglePaneEmbedOrFloating; SwitchToMode "normal"; } + bind "f" { ToggleFocusFullscreen; SwitchToMode "normal"; } + bind "h" { MoveFocus "left"; } + bind "j" { MoveFocus "down"; } + bind "k" { MoveFocus "up"; } + bind "l" { MoveFocus "right"; } + bind "n" { NewPane; SwitchToMode "normal"; } + bind "p" { SwitchFocus; } + bind "Ctrl p" { SwitchToMode "normal"; } + bind "r" { NewPane "right"; SwitchToMode "normal"; } + bind "w" { ToggleFloatingPanes; SwitchToMode "normal"; } + bind "z" { TogglePaneFrames; SwitchToMode "normal"; } + } + tab { + bind "left" { GoToPreviousTab; } + bind "down" { GoToNextTab; } + bind "up" { GoToPreviousTab; } + bind "right" { GoToNextTab; } + bind "1" { GoToTab 1; SwitchToMode "normal"; } + bind "2" { GoToTab 2; SwitchToMode "normal"; } + bind "3" { GoToTab 3; SwitchToMode "normal"; } + bind "4" { GoToTab 4; SwitchToMode "normal"; } + bind "5" { GoToTab 5; SwitchToMode "normal"; } + bind "6" { GoToTab 6; SwitchToMode "normal"; } + bind "7" { GoToTab 7; SwitchToMode "normal"; } + bind "8" { GoToTab 8; SwitchToMode "normal"; } + bind "9" { GoToTab 9; SwitchToMode "normal"; } + bind "[" { BreakPaneLeft; SwitchToMode "normal"; } + bind "]" { BreakPaneRight; SwitchToMode "normal"; } + bind "b" { BreakPane; SwitchToMode "normal"; } + bind "h" { GoToPreviousTab; } + bind "j" { GoToNextTab; } + bind "k" { GoToPreviousTab; } + bind "l" { GoToNextTab; } + bind "n" { NewTab; SwitchToMode "normal"; } + bind "r" { SwitchToMode "renametab"; TabNameInput 0; } + bind "s" { ToggleActiveSyncTab; SwitchToMode "normal"; } + bind "Ctrl t" { SwitchToMode "normal"; } + bind "x" { CloseTab; SwitchToMode "normal"; } + bind "tab" { ToggleTab; } + } + resize { + bind "left" { Resize "Increase left"; } + bind "down" { Resize "Increase down"; } + bind "up" { Resize "Increase up"; } + bind "right" { Resize "Increase right"; } + bind "+" { Resize "Increase"; } + bind "-" { Resize "Decrease"; } + bind "=" { Resize "Increase"; } + bind "H" { Resize "Decrease left"; } + bind "J" { Resize "Decrease down"; } + bind "K" { Resize "Decrease up"; } + bind "L" { Resize "Decrease right"; } + bind "h" { Resize "Increase left"; } + bind "j" { Resize "Increase down"; } + bind "k" { Resize "Increase up"; } + bind "l" { Resize "Increase right"; } + bind "Ctrl n" { SwitchToMode "normal"; } + } + move { + bind "left" { MovePane "left"; } + bind "down" { MovePane "down"; } + bind "up" { MovePane "up"; } + bind "right" { MovePane "right"; } + bind "h" { MovePane "left"; } + bind "Ctrl h" { SwitchToMode "normal"; } + bind "j" { MovePane "down"; } + bind "k" { MovePane "up"; } + bind "l" { MovePane "right"; } + bind "n" { MovePane; } + bind "p" { MovePaneBackwards; } + bind "tab" { MovePane; } + } + scroll { + bind "e" { EditScrollback; SwitchToMode "normal"; } + bind "s" { SwitchToMode "entersearch"; SearchInput 0; } + } + search { + bind "c" { SearchToggleOption "CaseSensitivity"; } + bind "n" { Search "down"; } + bind "o" { SearchToggleOption "WholeWord"; } + bind "p" { Search "up"; } + bind "w" { SearchToggleOption "Wrap"; } + } + session { + bind "c" { + LaunchOrFocusPlugin "configuration" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } + bind "Ctrl o" { SwitchToMode "normal"; } + bind "w" { + LaunchOrFocusPlugin "session-manager" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } + } + shared_except "locked" { + bind "Alt left" { MoveFocusOrTab "left"; } + bind "Alt down" { MoveFocus "down"; } + bind "Alt up" { MoveFocus "up"; } + bind "Alt right" { MoveFocusOrTab "right"; } + bind "Alt +" { Resize "Increase"; } + bind "Alt -" { Resize "Decrease"; } + bind "Alt =" { Resize "Increase"; } + bind "Alt [" { PreviousSwapLayout; } + bind "Alt ]" { NextSwapLayout; } + bind "Alt f" { ToggleFloatingPanes; } + bind "Ctrl g" { SwitchToMode "locked"; } + bind "Alt h" { MoveFocusOrTab "left"; } + bind "Alt i" { MoveTab "left"; } + bind "Alt j" { MoveFocus "down"; } + bind "Alt k" { MoveFocus "up"; } + bind "Alt l" { MoveFocusOrTab "right"; } + bind "Alt n" { NewPane; } + bind "Alt o" { MoveTab "right"; } + bind "Ctrl q" { Quit; } + } + shared_except "locked" "move" { + bind "Ctrl h" { SwitchToMode "move"; } + } + shared_except "locked" "session" { + bind "Ctrl o" { SwitchToMode "session"; } + } + shared_except "locked" "scroll" "search" "tmux" { + bind "Ctrl b" { SwitchToMode "tmux"; } + } + shared_except "locked" "scroll" "search" { + bind "Ctrl s" { SwitchToMode "scroll"; } + } + shared_except "locked" "tab" { + bind "Ctrl t" { SwitchToMode "tab"; } + } + shared_except "locked" "pane" { + bind "Ctrl p" { SwitchToMode "pane"; } + } + shared_except "locked" "resize" { + bind "Ctrl n" { SwitchToMode "resize"; } + } + shared_except "normal" "locked" "entersearch" { + bind "enter" { SwitchToMode "normal"; } + } + shared_except "normal" "locked" "entersearch" "renametab" "renamepane" { + bind "esc" { SwitchToMode "normal"; } + } + shared_among "pane" "tmux" { + bind "x" { CloseFocus; SwitchToMode "normal"; } + } + shared_among "scroll" "search" { + bind "PageDown" { PageScrollDown; } + bind "PageUp" { PageScrollUp; } + bind "left" { PageScrollUp; } + bind "down" { ScrollDown; } + bind "up" { ScrollUp; } + bind "right" { PageScrollDown; } + bind "Ctrl b" { PageScrollUp; } + bind "Ctrl c" { ScrollToBottom; SwitchToMode "normal"; } + bind "d" { HalfPageScrollDown; } + bind "Ctrl f" { PageScrollDown; } + bind "h" { PageScrollUp; } + bind "j" { ScrollDown; } + bind "k" { ScrollUp; } + bind "l" { PageScrollDown; } + bind "Ctrl s" { SwitchToMode "normal"; } + bind "u" { HalfPageScrollUp; } + } + entersearch { + bind "Ctrl c" { SwitchToMode "scroll"; } + bind "esc" { SwitchToMode "scroll"; } + bind "enter" { SwitchToMode "search"; } + } + renametab { + bind "esc" { UndoRenameTab; SwitchToMode "tab"; } + } + shared_among "renametab" "renamepane" { + bind "Ctrl c" { SwitchToMode "normal"; } + } + renamepane { + bind "esc" { UndoRenamePane; SwitchToMode "pane"; } + } + shared_among "session" "tmux" { + bind "d" { Detach; } + } + tmux { + bind "left" { MoveFocus "left"; SwitchToMode "normal"; } + bind "down" { MoveFocus "down"; SwitchToMode "normal"; } + bind "up" { MoveFocus "up"; SwitchToMode "normal"; } + bind "right" { MoveFocus "right"; SwitchToMode "normal"; } + bind "space" { NextSwapLayout; } + bind "\"" { NewPane "down"; SwitchToMode "normal"; } + bind "%" { NewPane "right"; SwitchToMode "normal"; } + bind "," { SwitchToMode "renametab"; } + bind "[" { SwitchToMode "scroll"; } + bind "Ctrl b" { Write 2; SwitchToMode "normal"; } + bind "c" { NewTab; SwitchToMode "normal"; } + bind "h" { MoveFocus "left"; SwitchToMode "normal"; } + bind "j" { MoveFocus "down"; SwitchToMode "normal"; } + bind "k" { MoveFocus "up"; SwitchToMode "normal"; } + bind "l" { MoveFocus "right"; SwitchToMode "normal"; } + bind "n" { GoToNextTab; SwitchToMode "normal"; } + bind "o" { FocusNextPane; } + bind "p" { GoToPreviousTab; SwitchToMode "normal"; } + bind "z" { ToggleFocusFullscreen; SwitchToMode "normal"; } + } +} +plugins { + compact-bar location="zellij:compact-bar" + configuration location="zellij:configuration" + filepicker location="zellij:strider" { + cwd "/" + } + session-manager location="zellij:session-manager" + status-bar location="zellij:status-bar" + strider location="zellij:strider" + tab-bar location="zellij:tab-bar" + welcome-screen location="zellij:session-manager" { + welcome_screen true + } +} + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap new file mode 100644 index 0000000000..443f101746 --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap @@ -0,0 +1,409 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 5069 +expression: fake_config_stringified +--- +keybinds clear-defaults=true { + locked { + bind "Ctrl g" { SwitchToMode "normal"; } + } + pane { + bind "left" { MoveFocus "left"; } + bind "down" { MoveFocus "down"; } + bind "up" { MoveFocus "up"; } + bind "right" { MoveFocus "right"; } + bind "c" { SwitchToMode "renamepane"; PaneNameInput 0; } + bind "d" { NewPane "down"; SwitchToMode "normal"; } + bind "e" { TogglePaneEmbedOrFloating; SwitchToMode "normal"; } + bind "f" { ToggleFocusFullscreen; SwitchToMode "normal"; } + bind "h" { MoveFocus "left"; } + bind "j" { MoveFocus "down"; } + bind "k" { MoveFocus "up"; } + bind "l" { MoveFocus "right"; } + bind "n" { NewPane; SwitchToMode "normal"; } + bind "p" { SwitchFocus; } + bind "Ctrl p" { SwitchToMode "normal"; } + bind "r" { NewPane "right"; SwitchToMode "normal"; } + bind "w" { ToggleFloatingPanes; SwitchToMode "normal"; } + bind "z" { TogglePaneFrames; SwitchToMode "normal"; } + } + tab { + bind "left" { GoToPreviousTab; } + bind "down" { GoToNextTab; } + bind "up" { GoToPreviousTab; } + bind "right" { GoToNextTab; } + bind "1" { GoToTab 1; SwitchToMode "normal"; } + bind "2" { GoToTab 2; SwitchToMode "normal"; } + bind "3" { GoToTab 3; SwitchToMode "normal"; } + bind "4" { GoToTab 4; SwitchToMode "normal"; } + bind "5" { GoToTab 5; SwitchToMode "normal"; } + bind "6" { GoToTab 6; SwitchToMode "normal"; } + bind "7" { GoToTab 7; SwitchToMode "normal"; } + bind "8" { GoToTab 8; SwitchToMode "normal"; } + bind "9" { GoToTab 9; SwitchToMode "normal"; } + bind "[" { BreakPaneLeft; SwitchToMode "normal"; } + bind "]" { BreakPaneRight; SwitchToMode "normal"; } + bind "b" { BreakPane; SwitchToMode "normal"; } + bind "h" { GoToPreviousTab; } + bind "j" { GoToNextTab; } + bind "k" { GoToPreviousTab; } + bind "l" { GoToNextTab; } + bind "n" { NewTab; SwitchToMode "normal"; } + bind "r" { SwitchToMode "renametab"; TabNameInput 0; } + bind "s" { ToggleActiveSyncTab; SwitchToMode "normal"; } + bind "Ctrl t" { SwitchToMode "normal"; } + bind "x" { CloseTab; SwitchToMode "normal"; } + bind "tab" { ToggleTab; } + } + resize { + bind "left" { Resize "Increase left"; } + bind "down" { Resize "Increase down"; } + bind "up" { Resize "Increase up"; } + bind "right" { Resize "Increase right"; } + bind "+" { Resize "Increase"; } + bind "-" { Resize "Decrease"; } + bind "=" { Resize "Increase"; } + bind "H" { Resize "Decrease left"; } + bind "J" { Resize "Decrease down"; } + bind "K" { Resize "Decrease up"; } + bind "L" { Resize "Decrease right"; } + bind "h" { Resize "Increase left"; } + bind "j" { Resize "Increase down"; } + bind "k" { Resize "Increase up"; } + bind "l" { Resize "Increase right"; } + bind "Ctrl n" { SwitchToMode "normal"; } + } + move { + bind "left" { MovePane "left"; } + bind "down" { MovePane "down"; } + bind "up" { MovePane "up"; } + bind "right" { MovePane "right"; } + bind "h" { MovePane "left"; } + bind "Ctrl h" { SwitchToMode "normal"; } + bind "j" { MovePane "down"; } + bind "k" { MovePane "up"; } + bind "l" { MovePane "right"; } + bind "n" { MovePane; } + bind "p" { MovePaneBackwards; } + bind "tab" { MovePane; } + } + scroll { + bind "e" { EditScrollback; SwitchToMode "normal"; } + bind "s" { SwitchToMode "entersearch"; SearchInput 0; } + } + search { + bind "c" { SearchToggleOption "CaseSensitivity"; } + bind "n" { Search "down"; } + bind "o" { SearchToggleOption "WholeWord"; } + bind "p" { Search "up"; } + bind "w" { SearchToggleOption "Wrap"; } + } + session { + bind "c" { + LaunchOrFocusPlugin "configuration" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } + bind "Ctrl o" { SwitchToMode "normal"; } + bind "w" { + LaunchOrFocusPlugin "session-manager" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } + } + shared_except "locked" { + bind "Alt left" { MoveFocusOrTab "left"; } + bind "Alt down" { MoveFocus "down"; } + bind "Alt up" { MoveFocus "up"; } + bind "Alt right" { MoveFocusOrTab "right"; } + bind "Alt +" { Resize "Increase"; } + bind "Alt -" { Resize "Decrease"; } + bind "Alt =" { Resize "Increase"; } + bind "Alt [" { PreviousSwapLayout; } + bind "Alt ]" { NextSwapLayout; } + bind "Alt f" { ToggleFloatingPanes; } + bind "Ctrl g" { SwitchToMode "locked"; } + bind "Alt h" { MoveFocusOrTab "left"; } + bind "Alt i" { MoveTab "left"; } + bind "Alt j" { MoveFocus "down"; } + bind "Alt k" { MoveFocus "up"; } + bind "Alt l" { MoveFocusOrTab "right"; } + bind "Alt n" { NewPane; } + bind "Alt o" { MoveTab "right"; } + bind "Ctrl q" { Quit; } + } + shared_except "locked" "move" { + bind "Ctrl h" { SwitchToMode "move"; } + } + shared_except "locked" "session" { + bind "Ctrl o" { SwitchToMode "session"; } + } + shared_except "locked" "scroll" "search" "tmux" { + bind "Ctrl b" { SwitchToMode "tmux"; } + } + shared_except "locked" "scroll" "search" { + bind "Ctrl s" { SwitchToMode "scroll"; } + } + shared_except "locked" "tab" { + bind "Ctrl t" { SwitchToMode "tab"; } + } + shared_except "locked" "pane" { + bind "Ctrl p" { SwitchToMode "pane"; } + } + shared_except "locked" "resize" { + bind "Ctrl n" { SwitchToMode "resize"; } + } + shared_except "normal" "locked" "entersearch" { + bind "enter" { SwitchToMode "normal"; } + } + shared_except "normal" "locked" "entersearch" "renametab" "renamepane" { + bind "esc" { SwitchToMode "normal"; } + } + shared_among "pane" "tmux" { + bind "x" { CloseFocus; SwitchToMode "normal"; } + } + shared_among "scroll" "search" { + bind "PageDown" { PageScrollDown; } + bind "PageUp" { PageScrollUp; } + bind "left" { PageScrollUp; } + bind "down" { ScrollDown; } + bind "up" { ScrollUp; } + bind "right" { PageScrollDown; } + bind "Ctrl b" { PageScrollUp; } + bind "Ctrl c" { ScrollToBottom; SwitchToMode "normal"; } + bind "d" { HalfPageScrollDown; } + bind "Ctrl f" { PageScrollDown; } + bind "h" { PageScrollUp; } + bind "j" { ScrollDown; } + bind "k" { ScrollUp; } + bind "l" { PageScrollDown; } + bind "Ctrl s" { SwitchToMode "normal"; } + bind "u" { HalfPageScrollUp; } + } + entersearch { + bind "Ctrl c" { SwitchToMode "scroll"; } + bind "esc" { SwitchToMode "scroll"; } + bind "enter" { SwitchToMode "search"; } + } + renametab { + bind "esc" { UndoRenameTab; SwitchToMode "tab"; } + } + shared_among "renametab" "renamepane" { + bind "Ctrl c" { SwitchToMode "normal"; } + } + renamepane { + bind "esc" { UndoRenamePane; SwitchToMode "pane"; } + } + shared_among "session" "tmux" { + bind "d" { Detach; } + } + tmux { + bind "left" { MoveFocus "left"; SwitchToMode "normal"; } + bind "down" { MoveFocus "down"; SwitchToMode "normal"; } + bind "up" { MoveFocus "up"; SwitchToMode "normal"; } + bind "right" { MoveFocus "right"; SwitchToMode "normal"; } + bind "space" { NextSwapLayout; } + bind "\"" { NewPane "down"; SwitchToMode "normal"; } + bind "%" { NewPane "right"; SwitchToMode "normal"; } + bind "," { SwitchToMode "renametab"; } + bind "[" { SwitchToMode "scroll"; } + bind "Ctrl b" { Write 2; SwitchToMode "normal"; } + bind "c" { NewTab; SwitchToMode "normal"; } + bind "h" { MoveFocus "left"; SwitchToMode "normal"; } + bind "j" { MoveFocus "down"; SwitchToMode "normal"; } + bind "k" { MoveFocus "up"; SwitchToMode "normal"; } + bind "l" { MoveFocus "right"; SwitchToMode "normal"; } + bind "n" { GoToNextTab; SwitchToMode "normal"; } + bind "o" { FocusNextPane; } + bind "p" { GoToPreviousTab; SwitchToMode "normal"; } + bind "z" { ToggleFocusFullscreen; SwitchToMode "normal"; } + } +} +plugins { + compact-bar location="zellij:compact-bar" + configuration location="zellij:configuration" + filepicker location="zellij:strider" { + cwd "/" + } + session-manager location="zellij:session-manager" + status-bar location="zellij:status-bar" + strider location="zellij:strider" + tab-bar location="zellij:tab-bar" + welcome-screen location="zellij:session-manager" { + welcome_screen true + } +} + +// Use a simplified UI without special fonts (arrow glyphs) +// Options: +// - true +// - false (Default) +// +// simplified_ui true + +// Choose the theme that is specified in the themes section. +// Default: default +// +// theme "dracula" + +// Choose the base input mode of zellij. +// Default: normal +// +// default_mode "locked" + +// Choose the path to the default shell that zellij will use for opening new panes +// Default: $SHELL +// +// default_shell "fish" + +// Choose the path to override cwd that zellij will use for opening new panes +// +// default_cwd "/tmp" + +// The name of the default layout to load on startup +// Default: "default" +// +// default_layout "compact" + +// The folder in which Zellij will look for layouts +// +// layout_dir "/tmp" + +// The folder in which Zellij will look for themes +// +// theme_dir "/tmp" + +// Toggle enabling the mouse mode. +// On certain configurations, or terminals this could +// potentially interfere with copying text. +// Options: +// - true (default) +// - false +// +// mouse_mode false + +// Toggle having pane frames around the panes +// Options: +// - true (default, enabled) +// - false +// +// pane_frames false + +// When attaching to an existing session with other users, +// should the session be mirrored (true) +// or should each user have their own cursor (false) +// Default: false +// +// mirror_session true + +// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP +// eg. when terminal window with an active zellij session is closed +// Options: +// - detach (Default) +// - quit +// +// on_force_close "quit" + +// Configure the scroll back buffer size +// This is the number of lines zellij stores for each pane in the scroll back +// buffer. Excess number of lines are discarded in a FIFO fashion. +// Valid values: positive integers +// Default value: 10000 +// +// scroll_buffer_size 10000 + +// Provide a command to execute when copying text. The text will be piped to +// the stdin of the program to perform the copy. This can be used with +// terminal emulators which do not support the OSC 52 ANSI control sequence +// that will be used by default if this option is not set. +// Examples: +// +// copy_command "xclip -selection clipboard" // x11 +// copy_command "wl-copy" // wayland +// copy_command "pbcopy" // osx +// +// copy_command "pbcopy" + +// Choose the destination for copied text +// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard. +// Does not apply when using copy_command. +// Options: +// - system (default) +// - primary +// +// copy_clipboard "primary" + +// Enable automatic copying (and clearing) of selection when releasing mouse +// Default: true +// +// copy_on_select true + +// Path to the default editor to use to edit pane scrollbuffer +// Default: $EDITOR or $VISUAL +// scrollback_editor "/usr/bin/vim" + +// A fixed name to always give the Zellij session. +// Consider also setting `attach_to_session true,` +// otherwise this will error if such a session exists. +// Default: +// +// session_name "My singleton session" + +// When `session_name` is provided, attaches to that session +// if it is already running or creates it otherwise. +// Default: false +// +// attach_to_session true + +// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible +// Options: +// - true (default) +// - false +// +// auto_layout false + +// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected +// Options: +// - true (default) +// - false +// +// session_serialization false + +// Whether pane viewports are serialized along with the session, default is false +// Options: +// - true +// - false (default) +// +// serialize_pane_viewport false + +// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0 +// defaults to the scrollback size. If this number is higher than the scrollback size, it will +// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true. +// +// scrollback_lines_to_serialize 10000 + +// Enable or disable the rendering of styled and colored underlines (undercurl). +// May need to be disabled for certain unsupported terminals +// Default: true +// +// styled_underlines false + +// How often in seconds sessions are serialized +// +// serialization_interval 10000 + +// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know +// metadata info on this session) +// Default: false +// +// disable_session_metadata false + +// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it) +// Default: true (if the host terminal supports it) +// +// support_kitty_keyboard_protocol false + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap new file mode 100644 index 0000000000..e7f95a87aa --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string.snap @@ -0,0 +1,33 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 4182 +expression: fake_document.to_string() +--- +simplified_ui true +theme "dracula" +default_mode "locked" +default_shell "fish" +default_cwd "/tmp/foo" +default_layout "compact" +layout_dir "/tmp/layouts" +theme_dir "/tmp/themes" +mouse_mode false +pane_frames false +mirror_session true +on_force_close "quit" +scroll_buffer_size 100 +copy_command "pbcopy" +copy_clipboard "system" +copy_on_select false +scrollback_editor "vim" +session_name "my_cool_session" +attach_to_session false +auto_layout false +session_serialization true +serialize_pane_viewport false +scrollback_lines_to_serialize 1000 +styled_underlines false +serialization_interval 1 +disable_session_metadata true +support_kitty_keyboard_protocol false + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap new file mode 100644 index 0000000000..3d0736e05c --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap @@ -0,0 +1,175 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 4987 +expression: fake_document.to_string() +--- + +// Use a simplified UI without special fonts (arrow glyphs) +// Options: +// - true +// - false (Default) +// +simplified_ui true + +// Choose the theme that is specified in the themes section. +// Default: default +// +theme "dracula" + +// Choose the base input mode of zellij. +// Default: normal +// +default_mode "locked" + +// Choose the path to the default shell that zellij will use for opening new panes +// Default: $SHELL +// +default_shell "fish" + +// Choose the path to override cwd that zellij will use for opening new panes +// +default_cwd "/tmp/foo" + +// The name of the default layout to load on startup +// Default: "default" +// +default_layout "compact" + +// The folder in which Zellij will look for layouts +// +layout_dir "/tmp/layouts" + +// The folder in which Zellij will look for themes +// +theme_dir "/tmp/themes" + +// Toggle enabling the mouse mode. +// On certain configurations, or terminals this could +// potentially interfere with copying text. +// Options: +// - true (default) +// - false +// +mouse_mode false + +// Toggle having pane frames around the panes +// Options: +// - true (default, enabled) +// - false +// +pane_frames false + +// When attaching to an existing session with other users, +// should the session be mirrored (true) +// or should each user have their own cursor (false) +// Default: false +// +mirror_session true + +// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP +// eg. when terminal window with an active zellij session is closed +// Options: +// - detach (Default) +// - quit +// +on_force_close "quit" + +// Configure the scroll back buffer size +// This is the number of lines zellij stores for each pane in the scroll back +// buffer. Excess number of lines are discarded in a FIFO fashion. +// Valid values: positive integers +// Default value: 10000 +// +scroll_buffer_size 100 + +// Provide a command to execute when copying text. The text will be piped to +// the stdin of the program to perform the copy. This can be used with +// terminal emulators which do not support the OSC 52 ANSI control sequence +// that will be used by default if this option is not set. +// Examples: +// +// copy_command "xclip -selection clipboard" // x11 +// copy_command "wl-copy" // wayland +// copy_command "pbcopy" // osx +// +copy_command "pbcopy" + +// Choose the destination for copied text +// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard. +// Does not apply when using copy_command. +// Options: +// - system (default) +// - primary +// +copy_clipboard "system" + +// Enable automatic copying (and clearing) of selection when releasing mouse +// Default: true +// +copy_on_select false + +// Path to the default editor to use to edit pane scrollbuffer +// Default: $EDITOR or $VISUAL +scrollback_editor "vim" + +// A fixed name to always give the Zellij session. +// Consider also setting `attach_to_session true,` +// otherwise this will error if such a session exists. +// Default: +// +session_name "my_cool_session" + +// When `session_name` is provided, attaches to that session +// if it is already running or creates it otherwise. +// Default: false +// +attach_to_session false + +// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible +// Options: +// - true (default) +// - false +// +auto_layout false + +// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected +// Options: +// - true (default) +// - false +// +session_serialization true + +// Whether pane viewports are serialized along with the session, default is false +// Options: +// - true +// - false (default) +// +serialize_pane_viewport false + +// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0 +// defaults to the scrollback size. If this number is higher than the scrollback size, it will +// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true. +// +scrollback_lines_to_serialize 1000 + +// Enable or disable the rendering of styled and colored underlines (undercurl). +// May need to be disabled for certain unsupported terminals +// Default: true +// +styled_underlines false + +// How often in seconds sessions are serialized +// +serialization_interval 1 + +// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know +// metadata info on this session) +// Default: false +// +disable_session_metadata true + +// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it) +// Default: true (if the host terminal supports it) +// +support_kitty_keyboard_protocol false + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_some_options.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_some_options.snap new file mode 100644 index 0000000000..2cf4ba5bf1 --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_some_options.snap @@ -0,0 +1,7 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 4211 +expression: fake_document.to_string() +--- +default_layout "compact" + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_without_options.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_without_options.snap new file mode 100644 index 0000000000..82eaf8e5af --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_without_options.snap @@ -0,0 +1,6 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 4196 +expression: fake_document.to_string() +--- + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__env_vars_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__env_vars_to_string.snap new file mode 100644 index 0000000000..609ed8968a --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__env_vars_to_string.snap @@ -0,0 +1,11 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 3981 +expression: serialized.to_string() +--- +env { + bar "foo" + baz "true" + foo "bar" + thing "1" +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string.snap new file mode 100644 index 0000000000..3055ea875e --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string.snap @@ -0,0 +1,11 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 2558 +expression: serialized.to_string() +--- +keybinds clear-defaults=true { + normal { + bind "Ctrl g" { SwitchToMode "locked"; } + } +} + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_all_actions.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_all_actions.snap new file mode 100644 index 0000000000..43e85faa5c --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_all_actions.snap @@ -0,0 +1,130 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 2731 +expression: serialized.to_string() +--- +keybinds clear-defaults=true { + normal { + bind "Ctrl a" { Quit; } + bind "Ctrl Alt a" { Search "up"; } + bind "Alt a" { ToggleActiveSyncTab; } + bind "Ctrl b" { Write 102 111 111; } + bind "Ctrl Alt b" { SearchToggleOption "CaseSensitivity"; } + bind "Alt b" { NewPane "right"; } + bind "Ctrl c" { WriteChars "hi there!"; } + bind "Ctrl Alt c" { ToggleMouseMode; } + bind "Alt c" { TogglePaneEmbedOrFloating; } + bind "Ctrl d" { SwitchToMode "locked"; } + bind "Ctrl Alt d" { PreviousSwapLayout; } + bind "Alt d" { ToggleFloatingPanes; } + bind "Ctrl e" { Resize "Increase"; } + bind "Ctrl Alt e" { NextSwapLayout; } + bind "Alt e" { CloseFocus; } + bind "Ctrl f" { FocusNextPane; } + bind "Alt f" { PaneNameInput 0; } + bind "Ctrl g" { FocusPreviousPane; } + bind "Ctrl Alt g" { BreakPane; } + bind "Alt g" { UndoRenamePane; } + bind "Ctrl h" { SwitchFocus; } + bind "Ctrl Alt h" { BreakPaneRight; } + bind "Alt h" { NewTab; } + bind "Ctrl i" { MoveFocus "right"; } + bind "Ctrl Alt i" { BreakPaneLeft; } + bind "Alt i" { GoToNextTab; } + bind "Ctrl j" { MoveFocusOrTab "right"; } + bind "Ctrl Alt j" { + MessagePlugin "zellij:session-manager" { + name "message_name" + cwd "/tmp" + payload "message_payload" + launch_new true + skip_cache true + floating true + title "plugin_title" + config_key_1 "config_value_1" + config_key_2 "config_value_2" + } + } + bind "Alt j" { GoToPreviousTab; } + bind "Ctrl k" { MovePane "right"; } + bind "Alt k" { CloseTab; } + bind "Ctrl l" { MovePaneBackwards; } + bind "Alt l" { GoToTab 1; } + bind "Ctrl m" { Resize "Decrease down"; } + bind "Alt m" { ToggleTab; } + bind "Ctrl n" { DumpScreen "/tmp/dumped"; } + bind "Alt n" { TabNameInput 0; } + bind "Ctrl o" { DumpLayout; } + bind "Alt o" { UndoRenameTab; } + bind "Ctrl p" { EditScrollback; } + bind "Alt p" { MoveTab "right"; } + bind "Ctrl q" { ScrollUp; } + bind "Alt q" { + Run "ls" "-l" { + cwd "/tmp" + name "my cool pane" + } + } + bind "Ctrl r" { ScrollDown; } + bind "Alt r" { + Run "ls" "-l" { + floating true + cwd "/tmp" + name "my cool pane" + } + } + bind "Ctrl s" { ScrollToBottom; } + bind "Alt s" { + Run "ls" "-l" { + in_place true + cwd "/tmp" + name "my cool pane" + } + } + bind "Ctrl t" { ScrollToTop; } + bind "Alt t" { Detach; } + bind "Ctrl u" { PageScrollUp; } + bind "Alt u" { + LaunchOrFocusPlugin "zellij:session-manager" { + floating true + move_to_focused_tab true + skip_plugin_cache true + config_key_1 "config_value_1" + config_key_2 "config_value_2" + } + } + bind "Ctrl v" { PageScrollDown; } + bind "Alt v" { + LaunchOrFocusPlugin "zellij:session-manager" { + move_to_focused_tab true + in_place true + skip_plugin_cache true + config_key_1 "config_value_1" + config_key_2 "config_value_2" + } + } + bind "Ctrl w" { HalfPageScrollUp; } + bind "Alt w" { + LaunchPlugin "zellij:session-manager" { + floating true + skip_plugin_cache true + config_key_1 "config_value_1" + config_key_2 "config_value_2" + } + } + bind "Ctrl x" { HalfPageScrollDown; } + bind "Alt x" { + LaunchPlugin "zellij:session-manager" { + in_place true + skip_plugin_cache true + config_key_1 "config_value_1" + config_key_2 "config_value_2" + } + } + bind "Ctrl y" { ToggleFocusFullscreen; } + bind "Alt y" { Copy; } + bind "Ctrl z" { TogglePaneFrames; } + bind "Alt z" { SearchInput 0; } + } +} + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_multiple_actions.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_multiple_actions.snap new file mode 100644 index 0000000000..ddaa4c0d6d --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_multiple_actions.snap @@ -0,0 +1,11 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 2576 +expression: serialized.to_string() +--- +keybinds clear-defaults=true { + normal { + bind "Ctrl n" { NewPane; SwitchToMode "locked"; } + } +} + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_multiple_multiline_actions.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_multiple_multiline_actions.snap new file mode 100644 index 0000000000..d6a1792ca0 --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_multiple_multiline_actions.snap @@ -0,0 +1,25 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 2788 +expression: serialized.to_string() +--- +keybinds clear-defaults=true { + shared { + bind "Ctrl n" { + NewPane + SwitchToMode "locked" + MessagePlugin "zellij:session-manager" { + name "message_name" + cwd "/tmp" + payload "message_payload" + launch_new true + skip_cache true + floating true + title "plugin_title" + config_key_1 "config_value_1" + config_key_2 "config_value_2" + } + } + } +} + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_shared_modes.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_shared_modes.snap new file mode 100644 index 0000000000..d7ca3ad27a --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_shared_modes.snap @@ -0,0 +1,17 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 2757 +expression: serialized.to_string() +--- +keybinds clear-defaults=true { + shared_among "normal" "locked" { + bind "Ctrl n" { NewPane; SwitchToMode "locked"; } + } + shared_except "locked" "pane" { + bind "Ctrl f" { TogglePaneEmbedOrFloating; } + } + shared_among "locked" "pane" { + bind "Ctrl p" { WriteChars "foo"; } + } +} + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_without_clearing_defaults.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_without_clearing_defaults.snap new file mode 100644 index 0000000000..143bb1efeb --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_without_clearing_defaults.snap @@ -0,0 +1,11 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 2575 +expression: serialized.to_string() +--- +keybinds { + normal { + bind "Ctrl g" { SwitchToMode "locked"; } + } +} + diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__plugins_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__plugins_to_string.snap new file mode 100644 index 0000000000..694f2c15cf --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__plugins_to_string.snap @@ -0,0 +1,18 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 3874 +expression: serialized.to_string() +--- +plugins { + compact-bar location="zellij:compact-bar" + filepicker location="zellij:strider" { + cwd "/" + } + session-manager location="zellij:session-manager" + status-bar location="zellij:status-bar" + strider location="zellij:strider" + tab-bar location="zellij:tab-bar" + welcome-screen location="zellij:session-manager" { + welcome_screen true + } +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__plugins_to_string_with_file_and_web.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__plugins_to_string_with_file_and_web.snap new file mode 100644 index 0000000000..4f99822558 --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__plugins_to_string_with_file_and_web.snap @@ -0,0 +1,11 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 3891 +expression: serialized.to_string() +--- +plugins { + filepicker location="file:/path/to/my/plugin.wasm" { + cwd "/" + } + tab-bar location="https://foo.com/plugin.wasm" +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string.snap new file mode 100644 index 0000000000..7bbc19cd15 --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string.snap @@ -0,0 +1,20 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 3697 +expression: serialized.to_string() +--- +themes { + dracula { + fg 248 248 242 + bg 40 42 54 + red 255 85 85 + green 80 250 123 + yellow 241 250 140 + blue 98 114 164 + magenta 255 121 198 + orange 255 184 108 + cyan 139 233 253 + black 0 0 0 + white 255 255 255 + } +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_combined_definitions.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_combined_definitions.snap new file mode 100644 index 0000000000..3498af78e3 --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_combined_definitions.snap @@ -0,0 +1,20 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 3775 +expression: serialized.to_string() +--- +themes { + default { + fg 1 + bg 10 + red 30 + green 40 + yellow 50 + blue 60 + magenta 70 + orange 208 135 112 + cyan 80 + black 20 + white 255 255 255 + } +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_eight_bit_definitions.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_eight_bit_definitions.snap new file mode 100644 index 0000000000..5406cbb8cc --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_eight_bit_definitions.snap @@ -0,0 +1,20 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 3749 +expression: serialized.to_string() +--- +themes { + default { + fg 1 + bg 10 + red 30 + green 40 + yellow 50 + blue 60 + magenta 70 + orange 254 + cyan 80 + black 20 + white 90 + } +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_hex_definitions.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_hex_definitions.snap new file mode 100644 index 0000000000..3cbf559a25 --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_hex_definitions.snap @@ -0,0 +1,20 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 3723 +expression: serialized.to_string() +--- +themes { + nord { + fg 216 222 233 + bg 46 52 64 + red 191 97 106 + green 163 190 140 + yellow 235 203 139 + blue 129 161 193 + magenta 180 142 173 + orange 208 135 112 + cyan 136 192 208 + black 59 66 82 + white 229 233 240 + } +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_multiple_theme_definitions.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_multiple_theme_definitions.snap new file mode 100644 index 0000000000..f8d7d38b4a --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__themes_to_string_with_multiple_theme_definitions.snap @@ -0,0 +1,33 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 4821 +expression: serialized.to_string() +--- +themes { + dracula { + fg 248 248 242 + bg 40 42 54 + red 255 85 85 + green 80 250 123 + yellow 241 250 140 + blue 98 114 164 + magenta 255 121 198 + orange 255 184 108 + cyan 139 233 253 + black 0 0 0 + white 255 255 255 + } + nord { + fg 216 222 233 + bg 46 52 64 + red 191 97 106 + green 163 190 140 + yellow 235 203 139 + blue 129 161 193 + magenta 180 142 173 + orange 208 135 112 + cyan 136 192 208 + black 59 66 82 + white 229 233 240 + } +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__ui_config_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__ui_config_to_string.snap new file mode 100644 index 0000000000..5956ba922b --- /dev/null +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__ui_config_to_string.snap @@ -0,0 +1,11 @@ +--- +source: zellij-utils/src/kdl/mod.rs +assertion_line: 3932 +expression: serialized.to_string() +--- +ui { + pane_frames { + rounded_corners true + hide_session_name true + } +} diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index 610a3ac40b..a985806f5e 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -48,6 +48,7 @@ enum EventType { EditPaneOpened = 22; EditPaneExited = 23; CommandPaneReRun = 24; + FailedToWriteConfigToDisk = 25; } message EventNameList { @@ -77,9 +78,14 @@ message Event { EditPaneOpenedPayload edit_pane_opened_payload = 19; EditPaneExitedPayload edit_pane_exited_payload = 20; CommandPaneReRunPayload command_pane_rerun_payload = 21; + FailedToWriteConfigToDiskPayload failed_to_write_config_to_disk_payload = 22; } } +message FailedToWriteConfigToDiskPayload { + optional string file_path = 1; +} + message CommandPaneReRunPayload { uint32 terminal_pane_id = 1; repeated ContextItem context = 3; diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index b669a6729c..faeb53d2d9 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -312,6 +312,14 @@ impl TryFrom for Event { }, _ => Err("Malformed payload for the CommandPaneReRun Event"), }, + Some(ProtobufEventType::FailedToWriteConfigToDisk) => match protobuf_event.payload { + Some(ProtobufEventPayload::FailedToWriteConfigToDiskPayload( + failed_to_write_configuration_payload, + )) => Ok(Event::FailedToWriteConfigToDisk( + failed_to_write_configuration_payload.file_path, + )), + _ => Err("Malformed payload for the FailedToWriteConfigToDisk Event"), + }, None => Err("Unknown Protobuf Event"), } } @@ -620,6 +628,12 @@ impl TryFrom for ProtobufEvent { )), }) }, + Event::FailedToWriteConfigToDisk(file_path) => Ok(ProtobufEvent { + name: ProtobufEventType::FailedToWriteConfigToDisk as i32, + payload: Some(event::Payload::FailedToWriteConfigToDiskPayload( + FailedToWriteConfigToDiskPayload { file_path }, + )), + }), } } } @@ -1129,6 +1143,7 @@ impl TryFrom for EventType { ProtobufEventType::EditPaneOpened => EventType::EditPaneOpened, ProtobufEventType::EditPaneExited => EventType::EditPaneExited, ProtobufEventType::CommandPaneReRun => EventType::CommandPaneReRun, + ProtobufEventType::FailedToWriteConfigToDisk => EventType::FailedToWriteConfigToDisk, }) } } @@ -1162,6 +1177,7 @@ impl TryFrom for ProtobufEventType { EventType::EditPaneOpened => ProtobufEventType::EditPaneOpened, EventType::EditPaneExited => ProtobufEventType::EditPaneExited, EventType::CommandPaneReRun => ProtobufEventType::CommandPaneReRun, + EventType::FailedToWriteConfigToDisk => ProtobufEventType::FailedToWriteConfigToDisk, }) } } diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index d91cc817a3..be47f56725 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -160,7 +160,7 @@ message PluginCommand { KillSessionsPayload kill_sessions_payload = 60; string scan_host_folder_payload = 61; NewTabsWithLayoutInfoPayload new_tabs_with_layout_info_payload = 62; - string reconfigure_payload = 63; + ReconfigurePayload reconfigure_payload = 63; HidePaneWithIdPayload hide_pane_with_id_payload = 64; ShowPaneWithIdPayload show_pane_with_id_payload = 65; OpenCommandPanePayload open_command_pane_background_payload = 66; @@ -168,6 +168,11 @@ message PluginCommand { } } +message ReconfigurePayload { + string config = 1; + bool write_to_disk = 2; +} + message RerunCommandPanePayload { uint32 terminal_pane_id = 1; } diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index 3df405315e..4ffabbe419 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -11,9 +11,9 @@ pub use super::generated_api::api::{ MovePayload, NewPluginArgs as ProtobufNewPluginArgs, NewTabsWithLayoutInfoPayload, OpenCommandPanePayload, OpenFilePayload, PaneId as ProtobufPaneId, PaneType as ProtobufPaneType, PluginCommand as ProtobufPluginCommand, PluginMessagePayload, - RequestPluginPermissionPayload, RerunCommandPanePayload, ResizePayload, RunCommandPayload, - SetTimeoutPayload, ShowPaneWithIdPayload, SubscribePayload, SwitchSessionPayload, - SwitchTabToPayload, UnsubscribePayload, WebRequestPayload, + ReconfigurePayload, RequestPluginPermissionPayload, RerunCommandPanePayload, ResizePayload, + RunCommandPayload, SetTimeoutPayload, ShowPaneWithIdPayload, SubscribePayload, + SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload, WebRequestPayload, }, plugin_permission::PermissionType as ProtobufPermissionType, resize::ResizeAction as ProtobufResizeAction, @@ -935,7 +935,10 @@ impl TryFrom for PluginCommand { }, Some(CommandName::Reconfigure) => match protobuf_plugin_command.payload { Some(Payload::ReconfigurePayload(reconfigure_payload)) => { - Ok(PluginCommand::Reconfigure(reconfigure_payload)) + Ok(PluginCommand::Reconfigure( + reconfigure_payload.config, + reconfigure_payload.write_to_disk, + )) }, _ => Err("Mismatched payload for Reconfigure"), }, @@ -1558,9 +1561,12 @@ impl TryFrom for ProtobufPluginCommand { )), }) }, - PluginCommand::Reconfigure(reconfigure_payload) => Ok(ProtobufPluginCommand { + PluginCommand::Reconfigure(config, write_to_disk) => Ok(ProtobufPluginCommand { name: CommandName::Reconfigure as i32, - payload: Some(Payload::ReconfigurePayload(reconfigure_payload)), + payload: Some(Payload::ReconfigurePayload(ReconfigurePayload { + config, + write_to_disk, + })), }), PluginCommand::HidePaneWithId(pane_id_to_hide) => Ok(ProtobufPluginCommand { name: CommandName::HidePaneWithId as i32, diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 941c351b5f..de9547afa0 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -74,7 +74,8 @@ fn get_default_themes() -> Themes { let mut themes = Themes::default(); for file in ZELLIJ_DEFAULT_THEMES.files() { if let Some(content) = file.contents_utf8() { - match Themes::from_string(&content.to_string()) { + let sourced_from_external_file = true; + match Themes::from_string(&content.to_string(), sourced_from_external_file) { Ok(theme) => themes = themes.merge(theme), Err(_) => {}, } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap index 56cea06ede..bfda7107b1 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/setup.rs -assertion_line: 839 +assertion_line: 840 expression: "format!(\"{:#?}\", config)" --- Config { @@ -5671,6 +5671,7 @@ Config { 0, ), }, + sourced_from_external_file: false, }, "theme-from-config": Theme { palette: Palette { @@ -5772,6 +5773,7 @@ Config { 0, ), }, + sourced_from_external_file: false, }, "theme-from-layout": Theme { palette: Palette { @@ -5873,6 +5875,7 @@ Config { 0, ), }, + sourced_from_external_file: false, }, }, plugins: PluginAliases {