From 79bf6ab868cbdab1f9a3827c9b70198f54548b44 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 5 Oct 2022 07:44:00 +0200 Subject: feat(config): switch to kdl (#1759) * chore(config): default kdl keybindings config * tests * work * refactor(config): move stuff around * work * tab merge layout * work * work * layouts working * work * layout tests * work * work * feat(parsing): kdl layouts without config * refactor(kdl): move stuff around * work * tests(layout): add cases and fix bugs * work * fix(kdl): various bugs * chore(layouts): move all layouts to kdl * feat(kdl): shared keybidns * fix(layout): do not count fixed panes toward percentile * fix(keybinds): missing keybinds and actions * fix(config): adjust default tips * refactor(config): move stuff around * fix(tests): make e2e tests pass * fix(kdl): add verbose parsing errors * fix(kdl): focused tab * fix(layout): corret default_tab_template behavior * style(code): fix compile warnings * feat(cli): send actions through the cli * fix(cli): exit only when action is done * fix(cli): open embedded pane from floating pane * fix(cli): send actions to other sessions * feat(cli): command alias * feat(converter): convert old config * feat(converter): convert old layout and theme files * feat(kdl): pretty errors * feat(client): convert old YAML files on startup * fix: various bugs and styling issues * fix: e2e tests * fix(screen): propagate errors after merge * style(clippy): lower clippy level * fix(tests): own session_name variable * style(fmt): rustfmt * fix(cli): various action fixes * style(fmt): rustfmt * fix(themes): loading of theme files * style(fmt): rustfmt * fix(tests): theme fixtures * fix(layouts): better errors on unknown nodes * fix(kdl): clarify valid node terminator error * fix(e2e): adjust close tab test * fix(e2e): adjust close tab test again * style(code): cleanup some comments --- zellij-client/src/cli_client.rs | 34 + zellij-client/src/fake_client.rs | 184 --- zellij-client/src/input_handler.rs | 92 +- zellij-client/src/lib.rs | 20 +- .../old_config_converter/convert_old_yaml_files.rs | 332 +++++ zellij-client/src/old_config_converter/mod.rs | 6 + .../src/old_config_converter/old_config.rs | 1292 ++++++++++++++++++++ .../src/old_config_converter/old_layout.rs | 557 +++++++++ .../unit/convert_config_tests.rs | 126 ++ .../unit/convert_layout_tests.rs | 142 +++ .../unit/fixtures/multiple_tabs_layout.yaml | 90 ++ .../multiple_tabs_layout_htop_command.yaml | 93 ++ .../unit/fixtures/old_default_yaml_config.yaml | 639 ++++++++++ .../unit/fixtures/old_default_yaml_layout.yaml | 22 + .../old_yaml_config_with_custom_options.yaml | 639 ++++++++++ .../old_yaml_config_with_env_variables.yaml | 644 ++++++++++ ...ld_yaml_config_with_global_keybind_unbinds.yaml | 639 ++++++++++ .../unit/fixtures/old_yaml_config_with_themes.yaml | 677 ++++++++++ .../unit/fixtures/old_yaml_config_with_ui.yaml | 643 ++++++++++ ..._yaml_config_with_unbind_all_keys_per_mode.yaml | 639 ++++++++++ .../old_yaml_config_with_unbinds_in_mode.yaml | 640 ++++++++++ .../unit/fixtures/old_yaml_layout_with_config.yaml | 34 + ...d_yaml_layout_with_config_and_session_name.yaml | 36 + .../old_yaml_layout_with_session_name.yaml | 24 + ..._layout_with_session_name_and_attach_false.yaml | 25 + .../unit/fixtures/run_htop_layout.yaml | 21 + .../fixtures/run_htop_layout_with_plugins.yaml | 35 + ...g_test__convert_config_with_custom_options.snap | 411 +++++++ ...ig_test__convert_config_with_env_variables.snap | 415 +++++++ ...convert_config_with_global_keybind_unbinds.snap | 411 +++++++ ...onvert_config_with_keybind_unbinds_in_mode.snap | 411 +++++++ ...ig_test__convert_config_with_themes_config.snap | 451 +++++++ ...config_test__convert_config_with_ui_config.snap | 416 +++++++ ...nvert_config_with_unbind_all_keys_per_mode.snap | 410 +++++++ ...nfig_test__properly_convert_default_config.snap | 410 +++++++ ...yout_test__properly_convert_default_layout.snap | 14 + ...ut_test__properly_convert_layout_example_1.snap | 61 + ...ut_test__properly_convert_layout_example_2.snap | 60 + ...ut_test__properly_convert_layout_example_3.snap | 19 + ...ut_test__properly_convert_layout_example_4.snap | 27 + ..._test__properly_convert_layout_with_config.snap | 24 + ...onvert_layout_with_config_and_session_name.snap | 26 + ..._properly_convert_layout_with_session_name.snap | 16 + ..._layout_with_session_name_and_attach_false.snap | 16 + zellij-client/src/sessions.rs | 18 - 45 files changed, 11639 insertions(+), 302 deletions(-) create mode 100644 zellij-client/src/cli_client.rs delete mode 100644 zellij-client/src/fake_client.rs create mode 100644 zellij-client/src/old_config_converter/convert_old_yaml_files.rs create mode 100644 zellij-client/src/old_config_converter/mod.rs create mode 100644 zellij-client/src/old_config_converter/old_config.rs create mode 100644 zellij-client/src/old_config_converter/old_layout.rs create mode 100644 zellij-client/src/old_config_converter/unit/convert_config_tests.rs create mode 100644 zellij-client/src/old_config_converter/unit/convert_layout_tests.rs create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/multiple_tabs_layout.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/multiple_tabs_layout_htop_command.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_default_yaml_config.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_default_yaml_layout.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_config_with_custom_options.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_config_with_env_variables.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_config_with_global_keybind_unbinds.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_config_with_themes.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_config_with_ui.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_config_with_unbind_all_keys_per_mode.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_config_with_unbinds_in_mode.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_layout_with_config.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_layout_with_config_and_session_name.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_layout_with_session_name.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/old_yaml_layout_with_session_name_and_attach_false.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/run_htop_layout.yaml create mode 100644 zellij-client/src/old_config_converter/unit/fixtures/run_htop_layout_with_plugins.yaml create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_config__convert_config_test__convert_config_with_custom_options.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_config__convert_config_test__convert_config_with_env_variables.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_config__convert_config_test__convert_config_with_global_keybind_unbinds.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_config__convert_config_test__convert_config_with_keybind_unbinds_in_mode.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_config__convert_config_test__convert_config_with_themes_config.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_config__convert_config_test__convert_config_with_ui_config.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_config__convert_config_test__convert_config_with_unbind_all_keys_per_mode.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_config__convert_config_test__properly_convert_default_config.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_layout__convert_layout_test__properly_convert_default_layout.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_layout__convert_layout_test__properly_convert_layout_example_1.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_layout__convert_layout_test__properly_convert_layout_example_2.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_layout__convert_layout_test__properly_convert_layout_example_3.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_layout__convert_layout_test__properly_convert_layout_example_4.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_layout__convert_layout_test__properly_convert_layout_with_config.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_layout__convert_layout_test__properly_convert_layout_with_config_and_session_name.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_layout__convert_layout_test__properly_convert_layout_with_session_name.snap create mode 100644 zellij-client/src/old_config_converter/unit/snapshots/zellij_client__old_config_converter__old_layout__convert_layout_test__properly_convert_layout_with_session_name_and_attach_false.snap delete mode 100644 zellij-client/src/sessions.rs (limited to 'zellij-client/src') diff --git a/zellij-client/src/cli_client.rs b/zellij-client/src/cli_client.rs new file mode 100644 index 000000000..95cd9d133 --- /dev/null +++ b/zellij-client/src/cli_client.rs @@ -0,0 +1,34 @@ +//! The `[cli_client]` is used to attach to a running server session +//! and dispatch actions, that are specified through the command line. +use std::process; +use std::{fs, path::PathBuf}; + +use crate::os_input_output::ClientOsApi; +use zellij_utils::{ + input::actions::Action, + ipc::{ClientToServerMsg, ServerToClientMsg}, +}; + +pub fn start_cli_client(os_input: Box, session_name: &str, actions: Vec) { + let zellij_ipc_pipe: PathBuf = { + let mut sock_dir = zellij_utils::consts::ZELLIJ_SOCK_DIR.clone(); + fs::create_dir_all(&sock_dir).unwrap(); + zellij_utils::shared::set_permissions(&sock_dir, 0o700).unwrap(); + sock_dir.push(session_name); + sock_dir + }; + os_input.connect_to_server(&*zellij_ipc_pipe); + for action in actions { + let msg = ClientToServerMsg::Action(action, None); + os_input.send_to_server(msg); + } + loop { + match os_input.recv_from_server() { + Some((ServerToClientMsg::UnblockInputThread, _)) => { + os_input.send_to_server(ClientToServerMsg::ClientExited); + process::exit(0); + }, + _ => {}, + } + } +} diff --git a/zellij-client/src/fake_client.rs b/zellij-client/src/fake_client.rs deleted file mode 100644 index 475298e95..000000000 --- a/zellij-client/src/fake_client.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! The `[fake_client]` is used to attach to a running server session -//! and dispatch actions, that are specified through the command line. -//! Multiple actions at the same time can be dispatched. -use log::debug; -use std::sync::{Arc, Mutex}; -use std::{fs, path::PathBuf, thread}; - -use crate::{ - command_is_executing::CommandIsExecuting, input_handler::input_actions, - os_input_output::ClientOsApi, stdin_ansi_parser::StdinAnsiParser, stdin_handler::stdin_loop, - ClientInfo, ClientInstruction, InputInstruction, -}; -use zellij_utils::{ - channels::{self, ChannelWithContext, SenderWithContext}, - cli::CliArgs, - data::{ClientId, Style}, - errors::ContextType, - input::{actions::Action, config::Config, layout::LayoutFromYaml, options::Options}, - ipc::{ClientAttributes, ClientToServerMsg, ServerToClientMsg}, -}; - -pub fn start_fake_client( - os_input: Box, - _opts: CliArgs, - config: Config, - config_options: Options, - info: ClientInfo, - _layout: Option, - actions: Vec, -) { - debug!("Starting fake Zellij client!"); - let session_name = info.get_session_name(); - - // TODO: Ideally the `fake_client` would not need to specify these options, - // but the `[NewTab:]` action depends on this state being - // even in this client. - let palette = config.themes.clone().map_or_else( - || os_input.load_palette(), - |t| { - t.theme_config(&config_options) - .unwrap_or_else(|| os_input.load_palette()) - }, - ); - - let full_screen_ws = os_input.get_terminal_size_using_fd(0); - let client_attributes = ClientAttributes { - size: full_screen_ws, - style: Style { - colors: palette, - rounded_corners: config.ui.unwrap_or_default().pane_frames.rounded_corners, - }, - keybinds: config.keybinds.clone(), - }; - - let first_msg = ClientToServerMsg::AttachClient(client_attributes, config_options.clone()); - - let zellij_ipc_pipe: PathBuf = { - let mut sock_dir = zellij_utils::consts::ZELLIJ_SOCK_DIR.clone(); - fs::create_dir_all(&sock_dir).unwrap(); - zellij_utils::shared::set_permissions(&sock_dir, 0o700).unwrap(); - sock_dir.push(session_name); - sock_dir - }; - os_input.connect_to_server(&*zellij_ipc_pipe); - os_input.send_to_server(first_msg); - - let mut command_is_executing = CommandIsExecuting::new(); - - let (send_client_instructions, receive_client_instructions): ChannelWithContext< - ClientInstruction, - > = channels::bounded(50); - let send_client_instructions = SenderWithContext::new(send_client_instructions); - - let (send_input_instructions, receive_input_instructions): ChannelWithContext< - InputInstruction, - > = channels::bounded(50); - let send_input_instructions = SenderWithContext::new(send_input_instructions); - - std::panic::set_hook({ - use zellij_utils::errors::handle_panic; - let send_client_instructions = send_client_instructions.clone(); - Box::new(move |info| { - handle_panic(info, &send_client_instructions); - }) - }); - - let stdin_ansi_parser = Arc::new(Mutex::new(StdinAnsiParser::new())); - let _stdin_thread = thread::Builder::new() - .name("stdin_handler".to_string()) - .spawn({ - let os_input = os_input.clone(); - let send_input_instructions = send_input_instructions.clone(); - move || stdin_loop(os_input, send_input_instructions, stdin_ansi_parser) - }); - - let clients: Vec; - os_input.send_to_server(ClientToServerMsg::ListClients); - #[allow(clippy::collapsible_match)] - loop { - if let Some((msg, _)) = os_input.recv_from_server() { - if let ServerToClientMsg::ActiveClients(active_clients) = msg { - clients = active_clients; - break; - } - } - } - debug!("The connected client id's are: {:?}.", clients); - - let _input_thread = thread::Builder::new() - .name("input_handler".to_string()) - .spawn({ - let send_client_instructions = send_client_instructions.clone(); - let command_is_executing = command_is_executing.clone(); - let os_input = os_input.clone(); - let default_mode = config_options.default_mode.unwrap_or_default(); - let session_name = session_name.to_string(); - move || { - input_actions( - os_input, - config, - config_options, - command_is_executing, - clients, - send_client_instructions, - default_mode, - receive_input_instructions, - actions, - session_name, - ) - } - }); - - let router_thread = thread::Builder::new() - .name("router".to_string()) - .spawn({ - let os_input = os_input.clone(); - let mut should_break = false; - move || loop { - if let Some((instruction, err_ctx)) = os_input.recv_from_server() { - err_ctx.update_thread_ctx(); - if let ServerToClientMsg::Exit(_) = instruction { - should_break = true; - } - send_client_instructions.send(instruction.into()).unwrap(); - if should_break { - break; - } - } - } - }) - .unwrap(); - - loop { - let (client_instruction, mut err_ctx) = receive_client_instructions - .recv() - .expect("failed to receive app instruction on channel"); - - err_ctx.add_call(ContextType::Client((&client_instruction).into())); - match client_instruction { - ClientInstruction::Exit(_) => { - os_input.send_to_server(ClientToServerMsg::ClientExited); - break; - }, - ClientInstruction::Error(_) => { - let _ = os_input.send_to_server(ClientToServerMsg::Action(Action::Quit, None)); - // handle_error(backtrace); - }, - ClientInstruction::Render(_) => { - // This is a fake client, that doesn't render, but - // dispatches actions. - }, - ClientInstruction::UnblockInputThread => { - command_is_executing.unblock_input_thread(); - }, - ClientInstruction::SwitchToMode(input_mode) => { - send_input_instructions - .send(InputInstruction::SwitchToMode(input_mode)) - .unwrap(); - }, - _ => {}, - } - } - router_thread.join().unwrap(); -} diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index bebf1ee77..076ab35d6 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -11,7 +11,6 @@ use zellij_utils::{ actions::Action, cast_termwiz_key, config::Config, - keybinds::Keybinds, mouse::{MouseButton, MouseEvent}, options::Options, }, @@ -133,7 +132,9 @@ impl InputHandler { } fn handle_key(&mut self, key: &Key, raw_bytes: Vec) { let keybinds = &self.config.keybinds; - for action in Keybinds::key_to_actions(key, raw_bytes, &self.mode, keybinds) { + for action in + keybinds.get_actions_for_key_in_mode_or_default_action(&self.mode, key, raw_bytes) + { let should_exit = self.dispatch_action(action, None); if should_exit { self.should_exit = true; @@ -230,65 +231,6 @@ impl InputHandler { }, } } - fn handle_actions(&mut self, actions: Vec, session_name: &str, clients: Vec) { - let mut detached = false; - for action in actions { - match action { - Action::Quit => { - crate::sessions::kill_session(session_name); - break; - }, - Action::Detach => { - let first = clients.first().unwrap(); - let last = clients.last().unwrap(); - self.os_input - .send_to_server(ClientToServerMsg::DetachSession(vec![*first, *last])); - detached = true; - break; - }, - // Actions, that are independent from the specific client - // and not session idempotent should be specified here - Action::NewTab(_) - | Action::Run(_) - | Action::NewPane(_) - | Action::WriteChars(_) - | Action::EditScrollback - | Action::DumpScreen(_) - | Action::ToggleActiveSyncTab - | Action::ToggleFloatingPanes - | Action::TogglePaneEmbedOrFloating - | Action::TogglePaneFrames - | Action::ToggleFocusFullscreen - | Action::Write(_) => { - let client_id = clients.first().unwrap(); - log::debug!("Sending action to client: {}", client_id); - self.dispatch_action(action, Some(*client_id)); - }, - Action::CloseFocus | Action::CloseTab => { - let client_id = clients.first().unwrap(); - log::debug!("Sending action to client: {}", client_id); - log::warn!("Running this action from the focused pane, can lead to unexpected behaviour."); - self.dispatch_action(action, Some(*client_id)); - }, - _ => { - // FIXME: If a specific `session_id` is specified, - // then only send the actions to that specific `client_id` - for client_id in &clients { - self.dispatch_action(action.clone(), Some(*client_id)); - } - }, - } - } - self.dispatch_action(Action::Detach, None); - self.should_exit = true; - log::error!("Quitting Now. Dispatched the actions"); - if detached { - self.exit(ExitReason::NormalDetached); - } else { - self.exit(ExitReason::Normal); - } - } - /// Dispatches an [`Action`]. /// /// This function's body dictates what each [`Action`] actually does when @@ -329,7 +271,7 @@ impl InputHandler { | Action::Run(_) | Action::ToggleFloatingPanes | Action::TogglePaneEmbedOrFloating - | Action::NewTab(_) + | Action::NewTab(..) | Action::GoToNextTab | Action::GoToPreviousTab | Action::CloseTab @@ -381,29 +323,3 @@ pub(crate) fn input_loop( ) .handle_input(); } -/// Entry point to the module. Instantiates an [`InputHandler`] and starts -/// its [`InputHandler::handle_input()`] loop. -#[allow(clippy::too_many_arguments)] -pub(crate) fn input_actions( - os_input: Box, - config: Config, - options: Options, - command_is_executing: CommandIsExecuting, - clients: Vec, - send_client_instructions: SenderWithContext, - default_mode: InputMode, - receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, - actions: Vec, - session_name: String, -) { - let _handler = InputHandler::new( - os_input, - command_is_executing, - config, - options, - send_client_instructions, - default_mode, - receive_input_instructions, - ) - .handle_actions(actions, &session_name, clients); -} diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 9c5169cc7..bac08715f 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -1,9 +1,9 @@ pub mod os_input_output; +pub mod cli_client; mod command_is_executing; -pub mod fake_client; mod input_handler; -mod sessions; +pub mod old_config_converter; mod stdin_ansi_parser; mod stdin_handler; @@ -30,7 +30,7 @@ use zellij_utils::{ ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg}, termwiz::input::InputEvent, }; -use zellij_utils::{cli::CliArgs, input::layout::LayoutFromYaml}; +use zellij_utils::{cli::CliArgs, input::layout::Layout}; /// Instructions related to the client-side application #[derive(Debug, Clone)] @@ -124,7 +124,7 @@ pub fn start_client( config: Config, config_options: Options, info: ClientInfo, - layout: Option, + layout: Option, ) { info!("Starting Zellij client!"); let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l"; @@ -143,20 +143,16 @@ pub fn start_client( envs::set_zellij("0".to_string()); config.env.set_vars(); - let palette = config.themes.clone().map_or_else( - || os_input.load_palette(), - |t| { - t.theme_config(&config_options) - .unwrap_or_else(|| os_input.load_palette()) - }, - ); + let palette = config + .theme_config(&config_options) + .unwrap_or_else(|| os_input.load_palette()); let full_screen_ws = os_input.get_terminal_size_using_fd(0); let client_attributes = ClientAttributes { size: full_screen_ws, style: Style { colors: palette, - rounded_corners: config.ui.unwrap_or_default().pane_frames.rounded_corners, + rounded_corners: config.ui.pane_frames.rounded_corners, }, keybinds: config.keybinds.clone(), }; diff --git a/zellij-client/src/old_config_converter/convert_old_yaml_files.rs b/zellij-client/src/old_config_converter/convert_old_yaml_files.rs new file mode 100644 index 000000000..0d5bf2e52 --- /dev/null +++ b/zellij-client/src/old_config_converter/convert_old_yaml_files.rs @@ -0,0 +1,332 @@ +use super::{config_yaml_to_config_kdl, layout_yaml_to_layout_kdl}; +use std::path::PathBuf; +use zellij_utils::{ + cli::CliArgs, + setup::{find_default_config_dir, get_layout_dir, get_theme_dir}, +}; + +const OLD_CONFIG_NAME: &str = "config.yaml"; + +pub fn convert_old_yaml_files(opts: &CliArgs) { + let config_dir = opts.config_dir.clone().or_else(find_default_config_dir); + let layout_dir = get_layout_dir(config_dir.clone()); + let theme_dir = get_theme_dir(find_default_config_dir()); + let specified_config_location = opts.config.as_ref(); + + let mut layout_files_to_convert = vec![]; + let mut theme_files_to_convert = vec![]; + if let Some(layout) = opts.layout.as_ref() { + if layout.extension().map(|s| s.to_string_lossy().to_string()) == Some("yaml".into()) { + if layout.exists() { + layout_files_to_convert.push((layout.clone(), true)); + } + } + } + layout_files_to_convert.dedup(); + if let Some(layout_dir) = layout_dir { + if let Ok(files) = std::fs::read_dir(layout_dir) { + for file in files { + if let Ok(file) = file { + if file + .path() + .extension() + .map(|s| s.to_string_lossy().to_string()) + == Some("yaml".into()) + { + let mut new_file_path = file.path().clone(); + new_file_path.set_extension("kdl"); + if !new_file_path.exists() { + layout_files_to_convert.push((file.path().clone(), false)); + } + } + } + } + } + } + + if let Some(theme_dir) = theme_dir { + if theme_dir.is_dir() { + if let Ok(files) = std::fs::read_dir(theme_dir) { + for entry in files.flatten() { + if let Some(extension) = entry.path().extension() { + if extension == "yaml" { + let mut new_file_path = entry.path().clone(); + new_file_path.set_extension("kdl"); + if !new_file_path.exists() { + theme_files_to_convert.push(entry.path()) + } + } + } + } + } + } + } + + if let Some(config_dir) = config_dir { + let yaml_config_location = specified_config_location.cloned().filter(|c| { + c.extension().map(|s| s.to_string_lossy().to_string()) == Some("yaml".into()) + }); + let specified_yaml_config_location = yaml_config_location.is_some(); + let config_location = + yaml_config_location.unwrap_or_else(|| config_dir.join(OLD_CONFIG_NAME)); + match convert_yaml( + config_location, + layout_files_to_convert, + theme_files_to_convert, + specified_yaml_config_location, + ) { + Ok(should_exit) => { + if should_exit { + std::process::exit(0); + } + }, + Err(e) => { + eprintln!(""); + eprintln!("\u{1b}[1;31mFailed to convert yaml config\u{1b}[m: {}", e); + eprintln!(""); + std::process::exit(1); + }, + } + } +} + +fn print_conversion_title_message() { + println!(""); + println!("\u{1b}[1mZellij has moved to a new configuration format (KDL - https://kdl.dev) and has now been run with an old YAML configuration/layout/theme file.\u{1b}[m"); +} + +fn print_converting_config_message(old_file_name: String, new_file_name: String) { + println!( + "- Converting configuration file: \u{1b}[1;36m{}\u{1b}[m to the new configuration format at the same location: \u{1b}[1;36m{}\u{1b}[m", + old_file_name, + new_file_name + ); +} + +fn print_conversion_layouts_message(layout_files_to_convert: Vec<(PathBuf, bool)>) { + println!("- Converting the following layout YAML files to KDL files in the same location:"); + for (layout_file, _was_explicitly_set) in layout_files_to_convert.iter() { + let mut new_layout_file_name = layout_file.clone(); + new_layout_file_name.set_extension("kdl"); + println!( + "\u{1b}[1;36m{}\u{1b}[m => \u{1b}[1;36m{}\u{1b}[m", + layout_file.as_path().as_os_str().to_string_lossy(), + new_layout_file_name.as_path().as_os_str().to_string_lossy() + ); + } +} + +fn print_conversion_themes_message(theme_files_to_convert: Vec) { + println!("- Converting the following theme YAML files to KDL files in the same location:"); + for theme_file in theme_files_to_convert.iter() { + let mut new_theme_file_name = theme_file.clone(); + new_theme_file_name.set_extension("kdl"); + println!( + "\u{1b}[1;36m{}\u{1b}[m => \u{1b}[1;36m{}\u{1b}[m", + theme_file.as_path().as_os_str().to_string_lossy(), + new_theme_file_name.as_path().as_os_str().to_string_lossy() + ); + } +} + +fn print_no_actions_and_wait_for_user_input() -> Result<(), String> { + println!("\u{1b}[1;32mNo actions are required of you. Press ENTER to continue.\u{1b}[m"); + std::io::stdin() + .read_line(&mut String::new()) + .map_err(|e| format!("Failed to read from STDIN: {:?}", e))?; + Ok(()) +} + +fn print_remain_unmodified_message(will_exit: bool) { + println!("The original file(s) will remain unmodified."); + if !will_exit { + println!("Will then use the new converted file(s) for this and the next runs."); + } + println!(""); +} + +fn print_flag_help_message( + layout_files_to_convert: Vec<(PathBuf, bool)>, + yaml_config_file: &PathBuf, + yaml_config_was_explicitly_set: bool, +) -> Result<(), String> { + println!("\u{1b}[1;32mWhat do you need to do?\u{1b}[m"); + match layout_files_to_convert + .iter() + .find(|(_f, explicit)| *explicit) + { + Some((explicitly_specified_layout, _)) => { + let mut kdl_config_file_path = yaml_config_file.clone(); + let mut kdl_explicitly_specified_layout = explicitly_specified_layout.clone(); + kdl_config_file_path.set_extension("kdl"); + kdl_explicitly_specified_layout.set_extension("kdl"); + if yaml_config_was_explicitly_set { + println!("Since both the YAML config and a YAML layout file were explicitly specified, you'll need to re-run Zellij and point it to the new files:"); + println!( + "\u{1b}[1;33mzellij --config {} --layout {}\u{1b}[m", + kdl_config_file_path + .as_path() + .as_os_str() + .to_string_lossy() + .to_string(), + kdl_explicitly_specified_layout + .as_path() + .as_os_str() + .to_string_lossy() + .to_string(), + ); + } else { + println!("Since a YAML layout was explicitly specified, you'll need to re-run Zellij and point it to the new layout:"); + println!( + "\u{1b}[1;33mzellij --layout {}\u{1b}[m", + kdl_explicitly_specified_layout + .as_path() + .as_os_str() + .to_string_lossy() + .to_string(), + ); + } + }, + None => { + if yaml_config_was_explicitly_set { + let mut kdl_config_file_path = yaml_config_file.clone(); + kdl_config_file_path.set_extension("kdl"); + println!("Since the YAML config was explicitly specified, you'll need to re-run Zellij and point it to the new config:"); + println!( + "\u{1b}[1;33mzellij --config {}\u{1b}[m", + kdl_config_file_path + .as_path() + .as_os_str() + .to_string_lossy() + .to_string(), + ); + } + }, + } + println!(""); + println!("\u{1b}[1;32mPress ENTER to continue.\u{1b}[m"); + std::io::stdin() + .read_line(&mut String::new()) + .map_err(|e| format!("Failed to read from STDIN: {:?}", e))?; + Ok(()) +} + +fn convert_layouts(layout_files_to_convert: Vec<(PathBuf, bool)>) -> Result<(), String> { + for (layout_file, _was_explicitly_set) in layout_files_to_convert { + let raw_layout_file = std::fs::read_to_string(&layout_file) + .map_err(|e| format!("Failed to read layout file {:?}: {:?}", layout_file, e))?; + let kdl_layout = layout_yaml_to_layout_kdl(&raw_layout_file)?; + let mut new_layout_file = layout_file.clone(); + new_layout_file.set_extension("kdl"); + std::fs::write(&new_layout_file, kdl_layout).map_err(|e| { + format!( + "Failed to write new layout file to {:?}: {:?}", + new_layout_file, e + ) + })?; + } + Ok(()) +} + +fn convert_themes(theme_files_to_convert: Vec) -> Result<(), String> { + for theme_file in theme_files_to_convert { + let raw_theme_file = std::fs::read_to_string(&theme_file) + .map_err(|e| format!("Failed to read theme file {:?}: {:?}", theme_file, e))?; + let kdl_theme = config_yaml_to_config_kdl(&raw_theme_file, true)?; + let mut new_theme_file = theme_file.clone(); + new_theme_file.set_extension("kdl"); + std::fs::write(&new_theme_file, kdl_theme).map_err(|e| { + format!( + "Failed to write new theme file to {:?}: {:?}", + new_theme_file, e + ) + })?; + } + Ok(()) +} + +fn convert_config(yaml_config_file: PathBuf, new_config_file: PathBuf) -> Result<(), String> { + if yaml_config_file.exists() && !new_config_file.exists() { + let raw_config_file = std::fs::read_to_string(&yaml_config_file) + .map_err(|e| format!("Failed to read config file {:?}: {:?}", yaml_config_file, e))?; + let kdl_config = config_yaml_to_config_kdl(&raw_config_file, false)?; + std::fs::write(&new_config_file, kdl_config).map_err(|e| { + format!( + "Failed to write new config file to {:?}: {:?}", + new_config_file, e + ) + })?; + } + Ok(()) +} + +fn convert_yaml( + yaml_config_file: PathBuf, + layout_files_to_convert: Vec<(PathBuf, bool)>, + theme_files_to_convert: Vec, + yaml_config_was_explicitly_set: bool, +) -> Result { + let mut should_exit = false; + let mut new_config_file = yaml_config_file.clone(); + new_config_file.set_extension("kdl"); + let yaml_config_file_exists = yaml_config_file.exists(); + let layout_was_explicitly_set = layout_files_to_convert + .iter() + .find(|(_l, was_explicitly_set)| *was_explicitly_set) + .is_some(); + let new_config_file_exists = new_config_file.exists(); + let no_need_to_convert_config = + (new_config_file_exists && !yaml_config_was_explicitly_set) || !yaml_config_file_exists; + if no_need_to_convert_config + && layout_files_to_convert.is_empty() + && theme_files_to_convert.is_empty() + && !layout_was_explicitly_set + { + // Nothing to do... + return Ok(should_exit); + } + print_conversion_title_message(); + if yaml_config_file_exists && !new_config_file_exists { + print_converting_config_message( + yaml_config_file + .as_path() + .as_os_str() + .to_string_lossy() + .to_string(), + new_config_file + .as_path() + .as_os_str() + .to_string_lossy() + .to_string(), + ); + } else if yaml_config_file_exists && new_config_file_exists && yaml_config_was_explicitly_set { + return Err( + format!( + "Specified old YAML format config (--config {}) but a new KDL file exists in that location. To fix, point to it the new file instead: zellij --config {}", + yaml_config_file.as_path().as_os_str().to_string_lossy().to_string(), + new_config_file.as_path().as_os_str().to_string_lossy().to_string() + ) + ); + } + if !layout_files_to_convert.is_empty() { + print_conversion_layouts_message(layout_files_to_convert.clone()); + } + if !theme_files_to_convert.is_empty() { + print_conversion_themes_message(theme_files_to_convert.clone()); + } + print_remain_unmodified_message(layout_was_explicitly_set || yaml_config_was_explicitly_set); + if layout_was_explicitly_set || yaml_config_was_explicitly_set { + print_flag_help_message( + layout_files_to_convert.clone(), + &yaml_config_file, + yaml_config_was_explicitly_set, + )?; + should_exit = true; + } else { + print_no_actions_and_wait_for_user_input()?; + } + convert_layouts(layout_files_to_convert)?; + convert_themes(theme_files_to_convert)?; + convert_config(yaml_config_file, new_config_file)?; + Ok(should_exit) +} diff --git a/zellij-client/src/old_config_converter/mod.rs b/zellij-client/src/old_config_converter/mod.rs new file mode 100644 index 000000000..ac6d76762 --- /dev/null +++ b/zellij-client/src/old_config_converter/mod.rs @@ -0,0 +1,6 @@ +mod convert_old_yaml_files; +mod old_config; +mod old_layout; +pub use convert_old_yaml_files::convert_old_yaml_files; +pub use old_config::config_yaml_to_config_kdl; +pub use old_layout::layout_yaml_to_layout_kdl; diff --git a/zellij-client/src/old_config_converter/old_config.rs b/zellij-client/src/old_config_converter/old_config.rs new file mode 100644 index 000000000..73e098b7d --- /dev/null +++ b/zellij-client/src/old_config_converter/old_config.rs @@ -0,0 +1,1292 @@ +// This is a converter from the old yaml config to the new KDL config. +// +// It is supposed to be mostly self containing - please refrain from adding to it, importing +// from it or changing it +use std::fmt; +use std::path::PathBuf; + +use serde::de::{Error, Visitor}; +use serde::{Deserialize, Deserializer, Serialize}; +use std::collections::{BTreeMap, HashMap}; +use url::Url; + +const ON_FORCE_CLOSE_DESCRIPTION: &'static str = " +// 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 +// +"; + +const SIMPLIFIED_UI_DESCRIPTION: &'static str = " +// Send a request for a simplified ui (without arrow fonts) to plugins +// Options: +// - true +// - false (Default) +// +"; + +const DEFAULT_SHELL_DESCRIPTION: &'static str = " +// Choose the path to the default shell that zellij will use for opening new panes +// Default: $SHELL +// +"; + +const PANE_FRAMES_DESCRIPTION: &'static str = " +// Toggle between having pane frames around the panes +// Options: +// - true (default) +// - false +// +"; + +const DEFAULT_THEME_DESCRIPTION: &'static str = " +// Choose the theme that is specified in the themes section. +// Default: default +// +"; + +const DEFAULT_MODE_DESCRIPTION: &'static str = " +// Choose the mode that zellij uses when starting up. +// Default: normal +// +"; + +const MOUSE_MODE_DESCRIPTION: &'static str = " +// Toggle enabling the mouse mode. +// On certain configurations, or terminals this could +// potentially interfere with copying text. +// Options: +// - true (default) +// - false +// +"; + +const SCROLL_BUFFER_SIZE_DESCRIPTION: &'static str = " +// 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 +// +"; + +const COPY_COMMAND_DESCRIPTION: &'static str = " +// 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 +"; + +const COPY_CLIPBOARD_DESCRIPTION: &'static str = " +// 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 +// +"; + +const COPY_ON_SELECT_DESCRIPTION: &'static str = " +// Enable or disable automatic copy (and clear) of selection when releasing mouse +// Default: true +// +"; + +const SCROLLBACK_EDITOR_DESCRIPTION: &'static str = " +// Path to the default editor to use to edit pane scrollbuffer +// Default: $EDITOR or $VISUAL +// +"; + +const MIRROR_SESSION_DESCRIPTION: &'static str = " +// 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 +// +"; + +const DEFAULT_LAYOUT_DESCRIPTION: &'static str = " +// The name of the default layout to load on startup +// Default: \"default\" +// +"; + +const LAYOUT_DIR_DESCRIPTION: &'static str = " +// The folder in which Zellij will look for layouts +// +"; + +const THEME_DIR_DESCRIPTION: &'static str = " +// The folder in which Zellij will look for themes +// +"; + +fn options_yaml_to_options_kdl(options_yaml: &OldOptions, no_comments: bool) -> String { + let mut options_kdl = String::new(); + + macro_rules! push_option { + ($attribute_name:ident, $description_text:ident, $present_pattern:expr) => { + if !no_comments { + options_kdl.push_str($description_text); + } + if let Some($attribute_name) = &options_yaml.$attribute_name { + options_kdl.push_str(&format!($present_pattern, $attribute_name)); + options_kdl.push('\n'); + }; + }; + ($attribute_name:ident, $description_text:ident, $present_pattern:expr, $absent_pattern:expr) => { + if !no_comments { + options_kdl.push_str($description_text); + } + match &options_yaml.$attribute_name { + Some($attribute_name) => { + options_kdl.push_str(&format!($present_pattern, $attribute_name)); + }, + None => { + if !no_comments { + options_kdl.push_str(&format!($absent_pattern)); + } + }, + }; + if !no_comments || options_yaml.$attribute_name.is_some() { + options_kdl.push('\n'); + } + }; + } + + push_option!( + on_force_close, + ON_FORCE_CLOSE_DESCRIPTION, + "on_force_close \"{}\"", + "// on_force_close \"quit\"" + ); + push_option!( + simplified_ui, + SIMPLIFIED_UI_DESCRIPTION, + "simplified_ui {}", + "// simplified_ui true" + ); + push_option!( + default_shell, + DEFAULT_SHELL_DESCRIPTION, + "default_shell {:?}", + "// default_shell \"fish\"" + ); + push_option!( + pane_frames, + PANE_FRAMES_DESCRIPTION, + "pane_frames {}", + "// pane_frames true" + ); + push_option!( + theme, + DEFAULT_THEME_DESCRIPTION, + "theme {:?} ", + "// theme \"default\"" + ); + push_option!( + default_layout, + DEFAULT_LAYOUT_DESCRIPTION, + "default_layout {:?}", + "// default_layout \"compact\"" + ); + push_option!( + default_mode, + DEFAULT_MODE_DESCRIPTION, + "default_mode \"{}\"", + "// default_mode \"locked\"" + ); + push_option!( + mouse_mode, + MOUSE_MODE_DESCRIPTION, + "mouse_mode {}", + "// mouse_mode false" + ); + push_option!( + scroll_buffer_size, + SCROLL_BUFFER_SIZE_DESCRIPTION, + "scroll_buffer_size {}", + "// scroll_buffer_size 10000" + ); + push_option!(copy_command, COPY_COMMAND_DESCRIPTION, "copy_command {:?}"); + push_option!( + copy_clipboard, + COPY_CLIPBOARD_DESCRIPTION, + "copy_clipboard \"{}\"", + "// copy_clipboard \"primary\"" + ); + push_option!( + copy_on_select, + COPY_ON_SELECT_DESCRIPTION, + "copy_on_select {}", + "// copy_on_select false" + ); + push_option!( + scrollback_editor, + SCROLLBACK_EDITOR_DESCRIPTION, + "scrollback_editor {:?}", + "// scrollback_editor \"/usr/bin/vim\"" + ); + push_option!( + mirror_session, + MIRROR_SESSION_DESCRIPTION, + "mirror_session {}", + "// mirror_session true" + ); + push_option!( + layout_dir, + LAYOUT_DIR_DESCRIPTION, + "layout_dir {:?}", + "// layout_dir /path/to/my/layout_dir" + ); + push_option!( + theme_dir, + THEME_DIR_DESCRIPTION, + "theme_dir {:?}", + "// theme_dir \"/path/to/my/theme_dir\"" + ); + + options_kdl +} + +fn env_yaml_to_env_kdl(env_yaml: &OldEnvironmentVariablesFromYaml) -> String { + let mut env_kdl = String::new(); + let mut env_vars: Vec<(String, String)> = env_yaml + .env + .iter() + .map(|(name, val)| (name.clone(), val.clone())) + .collect(); + env_vars.sort_unstable(); + env_kdl.push_str("env {\n"); + for (name, val) in env_vars { + env_kdl.push_str(&format!(" {} \"{}\"\n", name, val)); + } + env_kdl.push_str("}\n"); + env_kdl +} + +fn plugins_yaml_to_plugins_kdl(plugins_yaml_to_plugins_kdl: &OldPluginsConfigFromYaml) -> String { + let mut plugins_kdl = String::new(); + if !&plugins_yaml_to_plugins_kdl.0.is_empty() { + plugins_kdl.push_str("\n"); + plugins_kdl.push_str("plugins {\n") + } + for plugin_config in &plugins_yaml_to_plugins_kdl.0 { + if plugin_config._allow_exec_host_cmd { + plugins_kdl.push_str(&format!( + " {} {{ path {:?}; _allow_exec_host_cmd true; }}\n", + plugin_config.tag.0, plugin_config.path + )); + } else { + plugins_kdl.push_str(&format!( + " {} {{ path {:?}; }}\n", + plugin_config.tag.0, plugin_config.path + )); + } + } + if !&plugins_yaml_to_plugins_kdl.0.is_empty() { + plugins_kdl.push_str("}\n") + } + plugins_kdl +} + +fn ui_config_yaml_to_ui_config_kdl(ui_config_yaml: &OldUiConfigFromYaml) -> String { + let mut kdl_ui_config = String::new(); + if ui_config_yaml.pane_frames.rounded_corners { + kdl_ui_config.push_str("\n"); + kdl_ui_config.push_str("ui {\n"); + kdl_ui_config.push_str(" pane_frames {\n"); + kdl_ui_config.push_str(" rounded_corners true\n"); + kdl_ui_config.push_str(" }\n"); + kdl_ui_config.push_str("}\n"); + } else { + // I'm not sure this is a thing, but since it's possible, why not? + kdl_ui_config.push_str("\n"); + kdl_ui_config.push_str("ui {\n"); + kdl_ui_config.push_str(" pane_frames {\n"); + kdl_ui_config.push_str(" rounded_corners false\n"); + kdl_ui_config.push_str(" }\n"); + kdl_ui_config.push_str("}\n"); + } + kdl_ui_config +} + +fn theme_config_yaml_to_theme_config_kdl( + theme_config_yaml: &OldThemesFromYamlIntermediate, +) -> String { + macro_rules! theme_color { + ($theme:ident, $color:ident, $color_name:expr, $kdl_theme_config:expr) => { + match $theme.palette.$color { + OldPaletteColorFromYaml::Rgb((r, g, b)) => { + $kdl_theme_config + .push_str(&format!(" {} {} {} {}\n", $color_name, r, g, b)); + }, + OldPaletteColorFromYaml::EightBit(eight_bit_color) => { + $kdl_theme_config + .push_str(&format!(" {} {}\n", $color_name, eight_bit_color)); + }, + OldPaletteColorFromYaml::Hex(OldHexColor(r, g, b)) => { + $kdl_theme_config + .push_str(&format!(" {} {} {} {}\n", $color_name, r, g, b)); + }, + } + }; + } + + let mut kdl_theme_config = String::new(); + if !theme_config_yaml.0.is_empty() { + kdl_theme_config.push_str("themes {\n") + } + let mut themes: Vec<(String, OldTheme)> = theme_config_yaml + .0 + .iter() + .map(|(theme_name, theme)| (theme_name.clone(), theme.clone())) + .collect(); + themes.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + for (theme_name, theme) in themes { + kdl_theme_config.push_str(&format!(" {} {{\n", theme_name)); + theme_color!(theme, fg, "fg", kdl_theme_config); + theme_color!(theme, bg, "bg", kdl_theme_config); + theme_color!(theme, black, "black", kdl_theme_config); + theme_color!(theme, red, "red", kdl_theme_config); + theme_color!(theme, green, "green", kdl_theme_config); + theme_color!(theme, yellow, "yellow", kdl_theme_config); + theme_color!(theme, blue, "blue", kdl_theme_config); + theme_color!(theme, magenta, "magenta", kdl_theme_config); + theme_color!(theme, cyan, "cyan", kdl_theme_config); + theme_color!(theme, white, "white", kdl_theme_config); + theme_color!(theme, orange, "orange", kdl_theme_config); + kdl_theme_config.push_str(" }\n"); + } + if !theme_config_yaml.0.is_empty() { + kdl_theme_config.push_str("}\n") + } + kdl_theme_config +} + +fn keybinds_yaml_to_keybinds_kdl(keybinds_yaml: &OldKeybindsFromYaml) -> String { + let mut kdl_keybinds = String::new(); + let modes = vec![ + // mode sort order + OldInputMode::Normal, + OldInputMode::Locked, + OldInputMode::Pane, + OldInputMode::Tab, + OldInputMode::Resize, + OldInputMode::Move, + OldInputMode::Scroll, + OldInputMode::Session, + OldInputMode::Search, + OldInputMode::EnterSearch, + OldInputMode::RenameTab, + OldInputMode::RenamePane, + OldInputMode::Prompt, + OldInputMode::Tmux, + ]; + + // title and global unbinds / clear-defaults + match &keybinds_yaml.unbind { + OldUnbind::Keys(keys_to_unbind) => { + kdl_keybinds.push_str("keybinds {\n"); + let key_string: String = keys_to_unbind + .iter() + .map(|k| format!("\"{}\"", k)) + .collect::>() + .join(" "); + kdl_keybinds.push_str(&format!(" unbind {}\n", key_string)); + }, + OldUnbind::All(should_unbind_all_defaults) => { + if *should_unbind_all_defaults { + kdl_keybinds.push_str("keybinds clear-defaults=true {\n"); + } else { + kdl_keybinds.push_str("keybinds {\n"); + } + }, + } + + for mode in modes { + if let Some(mode_keybinds) = keybinds_yaml.keybinds.get(&mode) { + let mut should_clear_mode_defaults = false; + let mut kdl_mode_keybinds = String::new(); + for key_action_unbind in mode_keybinds { + match key_action_unbind { + OldKeyActionUnbind::KeyAction(key_action) => { + let keys = &key_action.key; + let actions = &key_action.action; + let key_string: String = keys + .iter() + .map(|k| format!("\"{}\"", k)) + .collect::>() + .join(" "); + let actions_string: String = actions + .iter() + .map(|a| format!("{};", a)) + .collect::>() + .join(" "); + kdl_mode_keybinds.push_str(&format!( + " bind {} {{ {} }}\n", + key_string, actions_string + )); + }, + OldKeyActionUnbind::Unbind(unbind) => match &unbind.unbind { + OldUnbind::Keys(keys_to_unbind) => { + let key_string: String = keys_to_unbind + .iter() + .map(|k| format!("\"{}\"", k)) + .collect::>() + .join(" "); + kdl_mode_keybinds.push_str(&format!(" unbind {}\n", key_string)); + }, + OldUnbind::All(unbind_all) => { + if *unbind_all { + should_clear_mode_defaults = true; + } + }, + }, + } + } + if should_clear_mode_defaults { + kdl_keybinds.push_str(&format!(" {} clear-defaults=true {{\n", mode)); + } else { + kdl_keybinds.push_str(&format!(" {} {{\n", mode)); + } + kdl_keybinds.push_str(&kdl_mode_keybinds); + kdl_keybinds.push_str(" }\n"); + } + } + kdl_keybinds.push_str("}\n"); + kdl_keybinds +} + +pub fn config_yaml_to_config_kdl( + raw_yaml_config: &str, + no_comments: bool, +) -> Result { + // returns the raw kdl config + let config_from_yaml: OldConfigFromYaml = serde_yaml::from_str(raw_yaml_config) + .map_err(|e| format!("Failed to parse yaml: {:?}", e))?; + let mut kdl_config = String::new(); + if let Some(old_config_keybinds) = config_from_yaml.keybinds.as_ref() { + kdl_config.push_str(&keybinds_yaml_to_keybinds_kdl(old_config_keybinds)); + } + if let Some(old_config_options) = config_from_yaml.options.as_ref() { + kdl_config.push_str(&options_yaml_to_options_kdl( + old_config_options, + no_comments, + )); + } + if let Some(old_config_env_variables) = config_from_yaml.env.as_ref() { + kdl_config.push_str(&env_yaml_to_env_kdl(old_config_env_variables)); + } + kdl_config.push_str(&plugins_yaml_to_plugins_kdl(&config_from_yaml.plugins)); + if let Some(old_ui_config) = config_from_yaml.ui.as_ref() { + kdl_config.push_str(&ui_config_yaml_to_ui_config_kdl(old_ui_config)); + } + if let Some(old_themes_config) = config_from_yaml.themes.as_ref() { + kdl_config.push_str(&theme_config_yaml_to_theme_config_kdl(old_themes_config)); + } + Ok(kdl_config) +} + +#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq)] +pub struct OldConfigFromYaml { + #[serde(flatten)] + pub options: Option, + pub keybinds: Option, + pub themes: Option, + #[serde(flatten)] + pub env: Option, + #[serde(default)] + pub plugins: OldPluginsConfigFromYaml, + pub ui: Option, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct OldKeybindsFromYaml { + #[serde(flatten)] + keybinds: HashMap>, + #[serde(default)] + unbind: OldUnbind, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(untagged)] +enum OldUnbind { + // This is the correct order, don't rearrange! + // Suspected Bug in the untagged macro. + // 1. Keys + Keys(Vec), + // 2. All + All(bool), +} + +impl Default for OldUnbind { + fn default() -> OldUnbind { + OldUnbind::All(false) + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +enum OldKeyActionUnbind { + KeyAction(OldKeyActionFromYaml), + Unbind(OldUnbindFromYaml), +} + +/// Intermediate struct used for deserialisation +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +struct OldKeyActionFromYaml { + action: Vec, + key: Vec, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +struct OldUnbindFromYaml { + unbind: OldUnbind, +} + +/// Main configuration. +#[derive(Debug, Clone, PartialEq, Deserialize)] +struct OldConfig { + pub keybinds: OldKeybinds, + pub options: OldOptions, + pub themes: Option, + pub plugins: OldPluginsConfig, + pub ui: Option, + pub env: OldEnvironmentVariablesFromYaml, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +struct OldKeybinds(HashMap); + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +struct OldModeKeybinds(BTreeMap>); + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct OldThemesFromYamlIntermediate(HashMap); + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Default)] +struct OldPaletteFromYaml { + pub fg: OldPaletteColorFromYaml, + pub bg: OldPaletteColorFromYaml, + pub black: OldPaletteColorFromYaml, + pub red: OldPaletteColorFromYaml, + pub green: OldPaletteColorFromYaml, + pub yellow: OldPaletteColorFromYaml, + pub blue: OldPaletteColorFromYaml, + pub magenta: OldPaletteColorFromYaml, + pub cyan: OldPaletteColorFromYaml, + pub white: OldPaletteColorFromYaml, + pub orange: OldPaletteColorFromYaml, +} + +/// Intermediate deserialization enum +// This is here in order to make the untagged enum work +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(untagged)] +enum OldPaletteColorFromYaml { + Rgb((u8, u8, u8)), + EightBit(u8), + Hex(OldHexColor), +} + +impl From for (u8, u8, u8) { + fn from(e: OldHexColor) -> (u8, u8, u8) { + let OldHexColor(r, g, b) = e; + (r, g, b) + } +} + +struct OldHexColorVisitor(); + +impl<'de> Visitor<'de> for OldHexColorVisitor { + type Value = OldHexColor; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a hex color in the format #RGB or #RRGGBB") + } + + fn visit_str(self, s: &str) -> Result + where + E: Error, + { + if let Some(stripped) = s.strip_prefix('#') { + return self.visit_str(stripped); + } + + if s.len() == 3 { + Ok(OldHexColor( + u8::from_str_radix(&s[0..1], 16).map_err(E::custom)? * 0x11, + u8::from_str_radix(&s[1..2], 16).map_err(E::custom)? * 0x11, + u8::from_str_radix(&s[2..3], 16).map_err(E::custom)? * 0x11, + )) + } else if s.len() == 6 { + Ok(OldHexColor( + u8::from_str_radix(&s[0..2], 16).map_err(E::custom)?, + u8::from_str_radix(&s[2..4], 16).map_err(E::custom)?, + u8::from_str_radix(&s[4..6], 16).map_err(E::custom)?, + )) + } else { + Err(Error::custom( + "Hex color must be of form \"#RGB\" or \"#RRGGBB\"", + )) + } + } +} + +impl<'de> Deserialize<'de> for OldHexColor { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(OldHexColorVisitor()) + } +} + +impl Default for OldPaletteColorFromYaml { + fn default() -> Self { + OldPaletteColorFromYaml::EightBit(0) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)] +struct OldHexColor(u8, u8, u8); + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +struct OldTheme { + #[serde(flatten)] + palette: OldPaletteFromYaml, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Deserialize, Serialize)] +pub struct OldUiConfigFromYaml { + pub pane_frames: OldFrameConfigFromYaml, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Deserialize, Serialize)] +pub struct OldFrameConfigFromYaml { + pub rounded_corners: bool, +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub struct OldEnvironmentVariablesFromYaml { + env: HashMap, +} + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct OldPluginsConfigFromYaml(Vec); + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +struct OldPluginConfigFromYaml { + pub path: PathBuf, + pub tag: OldPluginTag, + #[serde(default)] + pub run: OldPluginTypeFromYaml, + #[serde(default)] + pub config: serde_yaml::Value, + #[serde(default)] + pub _allow_exec_host_cmd: bool, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +enum OldPluginTypeFromYaml { + Headless, + Pane, +} + +impl Default for OldPluginTypeFromYaml { + fn default() -> Self { + Self::Pane + } +} + +/// Tag used to identify the plugin in layout and config yaml files +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +struct OldPluginTag(String); + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +struct OldPluginsConfig(HashMap); + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +struct OldPluginConfig { + /// P