diff --git a/CHANGELOG.md b/CHANGELOG.md index 828f43c57..d28657f08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * infra: migrate wasm runtime from wasmtime to wasmi (https://github.com/zellij-org/zellij/pull/4449) * feat: allow attaching to sessions as "read-only" (https://github.com/zellij-org/zellij/pull/4487) * feat: improve cli interface (add detach, switch-session and more) (https://github.com/zellij-org/zellij/pull/4492) +* feat: command sequences, conditionally blocking CLI commands, new plugin APIs (https://github.com/zellij-org/zellij/pull/4546) ## [0.43.1] - 2025-08-08 * fix: pane rename backspace regression (https://github.com/zellij-org/zellij/pull/4346) diff --git a/Cargo.lock b/Cargo.lock index e7fc87d2a..3c7b4b875 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1618,15 +1618,6 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.0" @@ -3379,6 +3370,20 @@ dependencies = [ "pest", ] +[[package]] +name = "sequence" +version = "0.1.0" +dependencies = [ + "fuzzy-matcher", + "regex", + "serde", + "serde_json", + "strum", + "unicode-width 0.1.10", + "uuid", + "zellij-tile", +] + [[package]] name = "serde" version = "1.0.217" @@ -3724,20 +3729,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.20.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" [[package]] name = "strum_macros" -version = "0.20.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck 0.3.3", + "heck 0.5.0", "proc-macro2", "quote", - "syn 1.0.96", + "syn 2.0.96", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 41f220889..3fb2b188b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ members = [ "default-plugins/about", "default-plugins/share", "default-plugins/multiple-select", + "default-plugins/sequence", "zellij-client", "zellij-server", "zellij-utils", @@ -90,8 +91,8 @@ regex = { version = "1.5.5", default-features = false, features = ["perf", "std" serde = { version = "1.0", default-features = false, features = ["derive", "std"] } serde_json = { version = "1.0", default-features = false, features = ["std"] } signal-hook = { version = "0.3", default-features = false, features = ["iterator"] } -strum = { version = "0.20.0", default-features = false } -strum_macros = { version = "0.20.0", default-features = false } +strum = { version = "0.27.2", default-features = false, features = ["std"] } +strum_macros = { version = "0.27.2", default-features = false } suggest = { version = "0.4.0", default-features = false } tempfile = { version = "3.2.0", default-features = false } termwiz = { version = "0.23.2", default-features = false } diff --git a/default-plugins/compact-bar/src/keybind_utils.rs b/default-plugins/compact-bar/src/keybind_utils.rs index aeb862d11..2f84497ce 100644 --- a/default-plugins/compact-bar/src/keybind_utils.rs +++ b/default-plugins/compact-bar/src/keybind_utils.rs @@ -440,7 +440,8 @@ impl KeybindProcessor { action, Action::NewStackedPane { command: None, - pane_name: None + pane_name: None, + near_current_pane: false, } ) }, @@ -481,7 +482,9 @@ impl KeybindProcessor { swap_floating_layouts: None, tab_name: None, should_change_focus_to_new_tab: true, - cwd: None + cwd: None, + initial_panes: _, + first_pane_unblock_condition: _, } ) }, diff --git a/default-plugins/configuration/src/presets.rs b/default-plugins/configuration/src/presets.rs index 0c972a1de..7e875073d 100644 --- a/default-plugins/configuration/src/presets.rs +++ b/default-plugins/configuration/src/presets.rs @@ -155,6 +155,12 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Locked" }} + bind "q" {{ + LaunchPlugin "zellij:sequence" {{ + floating true + }}; + SwitchToMode "Locked" + }} }} shared_except "locked" "renametab" "renamepane" {{ bind "{primary_modifier} g" {{ SwitchToMode "Locked"; }} @@ -371,6 +377,12 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Normal" }} + bind "q" {{ + LaunchPlugin "zellij:sequence" {{ + floating true + }}; + SwitchToMode "Normal" + }} }} tmux {{ bind "[" {{ SwitchToMode "Scroll"; }} @@ -587,6 +599,12 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Normal" }} + bind "q" {{ + LaunchPlugin "zellij:sequence" {{ + floating true + }}; + SwitchToMode "Normal" + }} }} tmux {{ bind "[" {{ SwitchToMode "Scroll"; }} @@ -784,6 +802,12 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Normal" }} + bind "q" {{ + LaunchPlugin "zellij:sequence" {{ + floating true + }}; + SwitchToMode "Normal" + }} }} tmux {{ bind "[" {{ SwitchToMode "Scroll"; }} @@ -981,6 +1005,12 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Normal" }} + bind "q" {{ + LaunchPlugin "zellij:sequence" {{ + floating true + }}; + SwitchToMode "Normal" + }} }} tmux {{ bind "[" {{ SwitchToMode "Scroll"; }} @@ -1173,6 +1203,12 @@ keybinds clear-defaults=true {{ }}; SwitchToMode "Normal" }} + bind "q" {{ + LaunchPlugin "zellij:sequence" {{ + floating true + }}; + SwitchToMode "Normal" + }} }} tmux {{ bind "[" {{ SwitchToMode "Scroll"; }} diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 014898606..9f5b7e93d 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; #[allow(unused_imports)] use std::io::prelude::*; +use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::*; // This is a fixture plugin used only for tests in Zellij @@ -59,6 +60,8 @@ impl ZellijPlugin for State { PermissionType::ReadCliPipes, PermissionType::MessageAndLaunchOtherPlugins, PermissionType::Reconfigure, + PermissionType::WriteToClipboard, + PermissionType::RunActionsAsUser, ]); self.configuration = configuration; subscribe(&[ @@ -258,11 +261,13 @@ impl ZellijPlugin for State { }, BareKey::Char('t') if key.has_modifiers(&[KeyModifier::Ctrl]) => { let should_float_if_hidden = false; - focus_terminal_pane(1, should_float_if_hidden); + let should_be_in_place_if_hidden = false; + focus_terminal_pane(1, should_float_if_hidden, should_be_in_place_if_hidden); }, BareKey::Char('u') if key.has_modifiers(&[KeyModifier::Ctrl]) => { let should_float_if_hidden = false; - focus_plugin_pane(1, should_float_if_hidden); + let should_be_in_place_if_hidden = false; + focus_plugin_pane(1, should_float_if_hidden, should_be_in_place_if_hidden); }, BareKey::Char('v') if key.has_modifiers(&[KeyModifier::Ctrl]) => { rename_terminal_pane(1, "new terminal_pane_name"); @@ -354,7 +359,7 @@ impl ZellijPlugin for State { hide_pane_with_id(PaneId::Terminal(1)); }, BareKey::Char('b') if key.has_modifiers(&[KeyModifier::Alt]) => { - show_pane_with_id(PaneId::Terminal(1), true); + show_pane_with_id(PaneId::Terminal(1), true, true); }, BareKey::Char('c') if key.has_modifiers(&[KeyModifier::Alt]) => { open_command_pane_background( @@ -549,6 +554,37 @@ impl ZellijPlugin for State { BareKey::Char('z') if key.has_modifiers(&[KeyModifier::Alt]) => { list_clients(); }, + BareKey::Char('a') if key.has_modifiers(&[KeyModifier::Super]) => { + // Test show_cursor with coordinates + show_cursor(Some((5, 10))); + }, + BareKey::Char('b') if key.has_modifiers(&[KeyModifier::Super]) => { + // Test hide_cursor + show_cursor(None); + }, + BareKey::Char('c') if key.has_modifiers(&[KeyModifier::Super]) => { + // Test copy_to_clipboard + copy_to_clipboard("test clipboard text"); + }, + BareKey::Char('d') if key.has_modifiers(&[KeyModifier::Super]) => { + // Test run_action with MoveFocus + let mut context = BTreeMap::new(); + context.insert("test_key".to_string(), "test_value".to_string()); + run_action( + Action::MoveFocus { + direction: Direction::Left, + }, + context, + ); + }, + BareKey::Char('e') if key.has_modifiers(&[KeyModifier::Super]) => { + // Test send_sigint_to_pane_id + send_sigint_to_pane_id(PaneId::Terminal(1)); + }, + BareKey::Char('f') if key.has_modifiers(&[KeyModifier::Super]) => { + // Test send_sigkill_to_pane_id + send_sigkill_to_pane_id(PaneId::Terminal(1)); + }, _ => {}, }, Event::CustomMessage(message, payload) => { diff --git a/default-plugins/plugin-manager/src/main.rs b/default-plugins/plugin-manager/src/main.rs index 8e1c91a88..5cd243f5a 100644 --- a/default-plugins/plugin-manager/src/main.rs +++ b/default-plugins/plugin-manager/src/main.rs @@ -910,7 +910,7 @@ impl State { } pub fn focus_selected(&self) { if let Some(selected_plugin_id) = self.selected_plugin_id() { - focus_pane_with_id(PaneId::Plugin(selected_plugin_id), true); + focus_pane_with_id(PaneId::Plugin(selected_plugin_id), true, false); } } pub fn reload_selected(&self) { diff --git a/default-plugins/sequence/.cargo/config.toml b/default-plugins/sequence/.cargo/config.toml new file mode 100644 index 000000000..6b509f5b7 --- /dev/null +++ b/default-plugins/sequence/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-wasip1" diff --git a/default-plugins/sequence/.gitignore b/default-plugins/sequence/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/default-plugins/sequence/.gitignore @@ -0,0 +1 @@ +/target diff --git a/default-plugins/sequence/Cargo.toml b/default-plugins/sequence/Cargo.toml new file mode 100644 index 000000000..4f97ae6c6 --- /dev/null +++ b/default-plugins/sequence/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "sequence" +version = "0.1.0" +authors = ["Aram Drevekenin "] +edition = "2018" + +[dependencies] +zellij-tile = { path = "../../zellij-tile" } +unicode-width = "0.1" +fuzzy-matcher = "0.3" +# TODO: get this from Zellij so that we don't have to match versions +strum = "0.27" +regex = "1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +uuid = { version = "1.0", features = ["v4"] } diff --git a/default-plugins/sequence/LICENSE.md b/default-plugins/sequence/LICENSE.md new file mode 120000 index 000000000..f0608a63a --- /dev/null +++ b/default-plugins/sequence/LICENSE.md @@ -0,0 +1 @@ +../../LICENSE.md \ No newline at end of file diff --git a/default-plugins/sequence/src/lib.rs b/default-plugins/sequence/src/lib.rs new file mode 100644 index 000000000..1e1249791 --- /dev/null +++ b/default-plugins/sequence/src/lib.rs @@ -0,0 +1,5 @@ +pub mod path_formatting; +pub mod state; +pub mod ui; + +pub use state::State; diff --git a/default-plugins/sequence/src/main.rs b/default-plugins/sequence/src/main.rs new file mode 100644 index 000000000..0ac286d99 --- /dev/null +++ b/default-plugins/sequence/src/main.rs @@ -0,0 +1,1131 @@ +mod path_formatting; +mod state; +mod ui; + +use crate::state::CommandStatus; +use crate::ui::components; +use crate::ui::fuzzy_complete; +use crate::ui::layout_calculations::calculate_viewport; +use crate::ui::text_input::InputAction; +use crate::ui::truncation::truncate_middle; +use state::State; +use zellij_tile::prelude::actions::Action; +use zellij_tile::prelude::*; + +use std::collections::BTreeMap; +use std::path::PathBuf; + +use unicode_width::UnicodeWidthStr; + +register_plugin!(State); + +impl ZellijPlugin for State { + fn load(&mut self, _configuration: BTreeMap) { + subscribe(&[ + EventType::ModeUpdate, + EventType::SessionUpdate, + EventType::Key, + EventType::PermissionRequestResult, + EventType::HostFolderChanged, + EventType::RunCommandResult, + EventType::ActionComplete, + EventType::PaneUpdate, + EventType::Timer, + EventType::PastedText, + EventType::TabUpdate, + EventType::CommandPaneOpened, + ]); + + // Store our own plugin ID and client ID + let plugin_ids = get_plugin_ids(); + self.plugin_id = Some(plugin_ids.plugin_id); + self.client_id = Some(plugin_ids.client_id); + self.cwd = Some(plugin_ids.initial_cwd); + update_title(self); + } + + fn update(&mut self, event: Event) -> bool { + handle_event(self, event) + } + + fn render(&mut self, rows: usize, cols: usize) { + // Store dimensions for use in cursor calculations + self.own_rows = Some(rows); + self.own_columns = Some(cols); + + let max_visible_rows = rows.saturating_sub(5); + + let base_x = 1; + let base_y = 0; + + let (offset, visible_count, hidden_above, hidden_below) = calculate_viewport( + self.execution.all_commands.len(), + max_visible_rows, + self.selection.current_selected_command_index, + self.execution.current_running_command_index, + ); + + let mut table = components::build_table_header(hidden_above > 0 || hidden_below > 0); + + for index in offset..offset + visible_count { + table = components::add_command_row( + table, + self, + index, + offset, + visible_count, + hidden_above, + hidden_below, + ); + } + + print_table_with_coordinates( + table, + base_x, + base_y, + self.own_columns.map(|o| o.saturating_sub(base_x)), + None, + ); + + let help_y = base_y + visible_count + 2; + let (first_help, _, second_help) = components::render_help_lines(self, Some(cols)); + print_text_with_coordinates(first_help, base_x, help_y, None, None); + + if let Some((second_help_text, _)) = second_help { + print_text_with_coordinates(second_help_text, base_x, help_y + 1, None, None); + } + } +} + +pub fn handle_event(state: &mut State, event: Event) -> bool { + use std::path::PathBuf; + + match event { + Event::PermissionRequestResult(_) => { + change_host_folder(PathBuf::from("/")); + update_title(state); + true + }, + Event::ModeUpdate(mode_info) => { + state.shell = mode_info.shell.clone(); + + false + }, + Event::Key(key) => { + let mut should_render = handle_key_event(state, key); + let repositioned = state.reposition_plugin(); + if repositioned { + // we only want to render once we have repositioned, we will do this in TabUpdate + should_render = false; + } + update_cursor(state); + should_render + }, + Event::SessionUpdate(session_infos, _resurrectable_sessions) => { + if state.is_first_run { + // Find the current session + let current_session = session_infos.iter().find(|s| s.is_current_session); + + if let Some(session) = current_session { + // Get the pane history for this client + if let Some(client_id) = state.client_id { + if let Some(pane_history) = session.pane_history.get(&client_id) { + let own_pane_id = state.plugin_id.map(|id| PaneId::Plugin(id)); + state.primary_pane_id = select_primary_pane_from_history( + pane_history, + &session.panes, + own_pane_id, + ); + state.original_pane_id = state.primary_pane_id; // pane id focused + // before plugin + // launched + state.is_first_run = false; + } + } + } + } + false + }, + Event::Timer(_elapsed) => { + // Timer events are used for lock backoff + + let should_render = update_spinner(state); + + should_render + }, + Event::PastedText(pasted_text) => { + // Split pasted text into lines + let mut should_render = true; + let lines: Vec<&str> = pasted_text + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .collect(); + + if lines.is_empty() { + return false; + } + state.pasted_lines(lines); + let repositioned = state.reposition_plugin(); + if repositioned { + // we only want to render once we have repositioned, we will do this in TabUpdate + should_render = false; + } + update_cursor(state); + + should_render + }, + Event::TabUpdate(tab_infos) => { + if let Some(tab_info) = tab_infos.iter().find(|t| t.active) { + let new_cols = Some(tab_info.viewport_columns); + let new_rows = Some(tab_info.viewport_rows); + + // Check if dimensions changed + let dimensions_changed = new_cols != state.total_viewport_columns + || new_rows != state.total_viewport_rows; + + state.total_viewport_columns = new_cols; + state.total_viewport_rows = new_rows; + + if dimensions_changed { + state.reposition_plugin(); + } + update_cursor(state); + } + + false + }, + Event::PaneUpdate(pane_manifest) => handle_pane_update(state, pane_manifest), + Event::ActionComplete(_action, pane_id, context) => { + let should_render = handle_action_complete(state, pane_id, context); + update_title(state); + should_render + }, + Event::CommandPaneOpened(terminal_pane_id, context) => { + // we get this event immediately as the pane opens, we use it to associate the + // pane's id with our state + + if !is_running_sequence(&context, state.execution.sequence_id) { + // action from previous sequence or unrelated + return false; + } + if let Some(command_text) = context.get("command_text") { + state.update_pane_id_for_command(PaneId::Terminal(terminal_pane_id), command_text); + if !state.layout.spinner_timer_scheduled { + set_timeout(0.1); + state.layout.spinner_timer_scheduled = true; + } + update_title(state); + } + + true + }, + _ => false, + } +} + +fn handle_key_event(state: &mut State, key: KeyWithModifier) -> bool { + // Ctrl+Enter - Insert command after current with AND chain type + if key.has_modifiers(&[KeyModifier::Ctrl]) && matches!(key.bare_key, BareKey::Enter) { + let mut is_cd = false; + if let Some(current_text) = state.editing_input_text() { + if let Some(path) = state::detect_cd_command(¤t_text) { + if let Some(new_cwd) = path_formatting::resolve_path(state.cwd.as_ref(), &path) { + state.current_selected_command_mut().map(|c| { + c.set_cwd(Some(new_cwd)); + c.set_text("".to_owned()); + }); + state.editing.editing_input.as_mut().map(|i| i.clear()); + is_cd = true + } + } + }; + + state.add_empty_command_after_current_selected(); + if is_cd { + state.start_editing_selected(); + } + return true; + } + + // Ctrl+X - Cycle chain type + if key.has_modifiers(&[KeyModifier::Ctrl]) && matches!(key.bare_key, BareKey::Char('x')) { + // return state.ui.command_sequence.cycle_current_chain_type(); + state.cycle_chain_type(); + return true; + } + + // Ctrl+Space, copy to clipboard + if key.has_modifiers(&[KeyModifier::Ctrl]) && matches!(key.bare_key, BareKey::Char(' ')) { + state.copy_to_clipboard(); + return false; + } + + // Ctrl+w, close all panes and clear sequence + if key.has_modifiers(&[KeyModifier::Ctrl]) && matches!(key.bare_key, BareKey::Char('w')) { + if !state.execution.is_running && !state.all_commands_are_pending() { + close_panes_and_return_to_shell(state); + } + return true; + } + + if matches!(key.bare_key, BareKey::Up) + && !key.has_modifiers(&[KeyModifier::Ctrl]) + && !key.has_modifiers(&[KeyModifier::Alt]) + { + state.move_selection_up(); + return true; + } + + if matches!(key.bare_key, BareKey::Down) + && !key.has_modifiers(&[KeyModifier::Ctrl]) + && !key.has_modifiers(&[KeyModifier::Alt]) + { + state.move_selection_down(); + return true; + } + + // Del - delete current command + if matches!(key.bare_key, BareKey::Delete) + && !key.has_modifiers(&[KeyModifier::Ctrl]) + && !key.has_modifiers(&[KeyModifier::Alt]) + { + state.remove_current_selected_command(); + return true; + } + + // Ctrl+C - Clear currently focused command, or remove it if already empty + if key.has_modifiers(&[KeyModifier::Ctrl]) && matches!(key.bare_key, BareKey::Char('c')) { + if state.execution.is_running { + interrupt_sequence(state); + return true; + } else if state.editing.editing_input.is_some() { + let is_empty = state.current_selected_command_is_empty(); + let has_more_than_one_command = state.execution.all_commands.len() > 1; + + if is_empty && has_more_than_one_command { + state.remove_current_selected_command(); + } else if is_empty { + // last command, return to shell + close_panes_and_return_to_shell(state); + } else { + // If not empty, clear it + state.clear_current_selected_command(); + } + return true; + } + } + + if state.editing.editing_input.is_none() + && key.has_no_modifiers() + && matches!(key.bare_key, BareKey::Char('e')) + { + state.start_editing_selected(); + return true; + } + + if state.editing.editing_input.is_none() + && key.has_no_modifiers() + && matches!(key.bare_key, BareKey::Enter) + { + rerun_sequence(state); + return true; + } + + // Handle input actions from TextInput + let is_backspace = matches!(key.bare_key, BareKey::Backspace); + + if let Some(action) = state + .editing + .editing_input + .as_mut() + .map(|i| i.handle_key(key)) + { + match action { + InputAction::Submit => { + return handle_submit(state); + }, + InputAction::Cancel => { + state.cancel_editing_selected(); + if state.all_commands_are_pending() { + // we always want to stay in editing mode in main screen + state.start_editing_selected(); + } + true + }, + InputAction::Complete => { + state + .editing_input_text() + .map(|current_text| { + if let Some(completed) = fuzzy_complete::fuzzy_complete( + ¤t_text, + &Default::default(), // TODO: get rid of this whoel thing? + state.cwd.as_ref(), + ) { + state.set_editing_input_text(completed); + true + } else { + false + } + }) + .unwrap_or(false) + }, + InputAction::Continue => { + if let Some(current_text) = state.editing_input_text() { + if let Some((cmd_text, chain_type)) = + state::detect_chain_operator_at_end(¤t_text) + { + let mut should_add_empty_line = true; + if let Some(path) = state::detect_cd_command(&cmd_text) { + if let Some(new_cwd) = + path_formatting::resolve_path(state.cwd.as_ref(), &path) + { + let remaining_text = + state::get_remaining_after_first_segment(¤t_text) + .unwrap_or_else(|| "".to_owned()); + if remaining_text.len() > 0 { + should_add_empty_line = false; + } + state.current_selected_command_mut().map(|c| { + c.set_cwd(Some(new_cwd)); + c.set_text(remaining_text); + }); + } + } else { + state.current_selected_command_mut().map(|c| { + c.set_text(cmd_text); + c.set_chain_type(chain_type); + }); + }; + state.cancel_editing_selected(); + if should_add_empty_line { + state.add_empty_command_after_current_selected(); + } + state.start_editing_selected(); + } + } + + // Handle backspace on empty command + if is_backspace { + let is_empty = state.current_selected_command_is_empty() + || state + .editing_input_text() + .map(|i| i.is_empty()) + .unwrap_or(false); + let has_more_than_one_command = state.execution.all_commands.len() > 1; + + if is_empty && has_more_than_one_command { + let current_index = + state.selection.current_selected_command_index.unwrap_or(0); + if current_index > 0 { + state.remove_current_selected_command(); + return true; + } + } else if is_empty { + // last command + close_panes_and_return_to_shell(state); + } + } + + true + }, + InputAction::NoAction => false, + } + } else { + false + } +} + +/// Handle Enter key - submit and execute the command sequence +fn handle_submit(state: &mut State) -> bool { + let cwd = state.cwd.clone(); + if state.handle_editing_submit(&cwd) { + // handled the command internally (eg. cd) no need to run sequence + return true; + } + + if state.execution.all_commands.len() == 1 && state.can_run_sequence() { + rerun_sequence(state); + } else if state.execution.all_commands.len() == 1 { + state.move_selection_down(); + state.start_editing_selected(); + } + true +} + +pub fn handle_pane_update(state: &mut State, pane_manifest: PaneManifest) -> bool { + // Store the manifest for later use + state.pane_manifest = Some(pane_manifest.clone()); + let mut needs_rerender = false; + if state.update_exited_command_statuses(&pane_manifest) { + needs_rerender = true; + } + if state.update_sequence_stopped_state() { + needs_rerender = true; + } + state.reposition_plugin(); + update_primary_and_original_pane_ids(state, &pane_manifest); + needs_rerender +} + +pub fn calculate_cursor_position(state: &mut State) -> Option<(usize, usize)> { + // Get pane dimensions from state (must be set by render) + let Some(cols) = state.own_columns else { + eprintln!("Warning: own_columns not set"); + return None; + }; + let Some(rows) = state.own_rows else { + eprintln!("Warning: own_rows not set"); + return None; + }; + + // Only show cursor if in edit mode + let (text, cursor_pos) = if let Some(text_input) = &state.editing.editing_input { + ( + text_input.get_text().to_string(), + text_input.cursor_position(), + ) + } else { + return None; + }; + + let Some(edit_index) = state.selection.current_selected_command_index else { + return None; + }; + + // Calculate folder column width using the longest cwd across all commands + let longest_cwd_display = state.execution.longest_cwd_display(&state.cwd); + let folder_display = format!("{} >", longest_cwd_display); + let folder_col_width = folder_display.width().max(1); + + let base_x = 1; + let base_y = 0; + + adjust_scroll_offset(state, rows); + + let max_visible_rows = state.own_rows.map(|r| r.saturating_sub(5)).unwrap_or(0); + + let (offset, visible_count, hidden_above, hidden_below) = calculate_viewport( + state.execution.all_commands.len(), + max_visible_rows, + state.selection.current_selected_command_index, + state.execution.current_running_command_index, + ); + + let (max_chain_width, max_status_width) = + components::calculate_max_widths(&state.execution.all_commands, state.layout.spinner_frame); + + let Some((_, available_cmd_width)) = components::calculate_row_layout_info( + edit_index, + offset, + visible_count, + hidden_above, + hidden_below, + cols, + folder_col_width, + max_chain_width, + max_status_width, + ) else { + return None; + }; + + // Get cursor position in truncated text + let (_, cursor_in_truncated) = truncate_middle(&text, available_cmd_width, Some(cursor_pos)); + let Some(cursor_in_truncated) = cursor_in_truncated else { + return None; + }; + + // Calculate relative coordinates within the visible table + let relative_y = edit_index.saturating_sub(offset) + 1; // +1 for header row + let relative_x = folder_col_width + 1 + cursor_in_truncated; + + let x = base_x + relative_x; + let y = base_y + relative_y; + + Some((x, y)) +} + +pub fn handle_action_complete( + state: &mut State, + pane_id: Option, + context: BTreeMap, +) -> bool { + // Update primary_pane_id to the pane that just completed the action + // This ensures we always have a valid pane to target for InPlace launches + // But only if it's not a floating pane + if let (Some(pane_id), Some(manifest)) = (pane_id, &state.pane_manifest) { + if !is_pane_floating(pane_id, manifest) { + state.primary_pane_id = Some(pane_id); + } else { + eprintln!("Not setting primary_pane_id to floating pane {:?}", pane_id); + } + } + + if !is_running_sequence(&context, state.execution.sequence_id) { + // action from previous sequence or unrelated + return false; + } + + // If the sequence has been stopped (e.g., via Ctrl+C), don't continue + if !state.execution.is_running { + return true; // Re-render to show the stopped state + } + + // Move to the next command + let next_index = state.execution.current_running_command_index + 1; + + if next_index < state.execution.all_commands.len() { + // Execute the next command + // let (next_command, next_chain_type) = &state.execution.all_commands[next_index]; + let Some(next_command) = state.execution.all_commands.get(next_index) else { + // invalid state + return true; + }; + let next_chain_type = next_command.get_chain_type(); + let next_command_cwd = next_command.get_cwd(); + let next_command_text = next_command.get_text(); + + let shell = state + .shell + .clone() + .unwrap_or_else(|| PathBuf::from("/bin/bash")); + + let command = zellij_tile::prelude::actions::RunCommandAction { + command: shell, + args: vec!["-ic".to_string(), next_command_text.trim().to_string()], + cwd: next_command_cwd, + hold_on_close: true, + ..Default::default() + }; + + // Determine placement based on layout mode + let placement = NewPanePlacement::Stacked(pane_id); + + // Update status: mark current command as completed + // Use the pane_id from ActionComplete if the status is still Pending + let pane_id_to_mark = state + .execution + .all_commands + .get(state.execution.current_running_command_index) + .and_then(|c| match c.get_status() { + CommandStatus::Running(pid) => pid, + CommandStatus::Pending => pane_id, // Use the pane from ActionComplete + CommandStatus::Exited(_, pid) => pid, // Already marked, keep it + CommandStatus::Interrupted(pid) => pid, // Already marked, keep it + }); + + state + .execution + .all_commands + .get_mut(state.execution.current_running_command_index) + .map(|c| c.set_status(CommandStatus::Exited(None, pane_id_to_mark))); + state.execution.current_running_command_index = next_index; + + // Determine unblock_condition based on whether this is the last command + let unblock_condition = if next_index < state.execution.all_commands.len() - 1 { + // Not the last command - use the chain type's unblock condition + next_chain_type.to_unblock_condition() + } else { + // Last command - use UnblockCondition::OnAnyExit + Some(UnblockCondition::OnAnyExit) + }; + + let action = Action::NewBlockingPane { + placement, + command: Some(command), + pane_name: Some(next_command_text.trim().to_string()), + unblock_condition, + near_current_pane: true, + }; + + // Pass the sequence ID in the context + let mut context = BTreeMap::new(); + context.insert( + "sequence_id".to_string(), + state.execution.sequence_id.to_string(), + ); + context.insert("command_text".to_string(), next_command_text.to_string()); + + // Put the sequence back with updated index + run_action(action, context); + } else { + // Sequence complete - mark the last command as Exited + + let pane_id_to_mark = state + .execution + .all_commands + .get(state.execution.current_running_command_index) + .and_then(|c| match c.get_status() { + CommandStatus::Running(pid) => pid, + CommandStatus::Pending => pane_id, // Use the pane from ActionComplete + CommandStatus::Exited(_, pid) => pid, // Already marked, keep it + CommandStatus::Interrupted(pid) => pid, // Already marked, keep it + }); + + state + .execution + .all_commands + .get_mut(state.execution.current_running_command_index) + .map(|c| c.set_status(CommandStatus::Exited(None, pane_id_to_mark))); + } + true +} + +/// Adjust scroll offset to keep selected or running command visible +pub fn adjust_scroll_offset(sequence: &mut crate::state::State, rows: usize) { + let max_visible = rows.saturating_sub(6); + let total_commands = sequence.execution.all_commands.len(); + + if total_commands <= max_visible { + sequence.selection.scroll_offset = 0; + return; + } + + let focus_index = sequence + .selection + .current_selected_command_index + .unwrap_or(sequence.execution.current_running_command_index); + let half_visible = max_visible / 2; + + if focus_index < half_visible { + sequence.selection.scroll_offset = 0; + } else if focus_index >= total_commands.saturating_sub(half_visible) { + sequence.selection.scroll_offset = total_commands.saturating_sub(max_visible); + } else { + sequence.selection.scroll_offset = focus_index.saturating_sub(half_visible); + } + + sequence.selection.scroll_offset = sequence + .selection + .scroll_offset + .min(total_commands.saturating_sub(max_visible)); +} + +pub fn is_pane_floating(pane_id: PaneId, pane_manifest: &PaneManifest) -> bool { + pane_manifest + .panes + .iter() + .flat_map(|(_, panes)| panes.iter()) + .find(|pane_info| match pane_id { + PaneId::Terminal(id) => !pane_info.is_plugin && pane_info.id == id, + PaneId::Plugin(id) => pane_info.is_plugin && pane_info.id == id, + }) + .map(|pane_info| pane_info.is_floating) + .unwrap_or(false) +} + +fn is_running_sequence(context: &BTreeMap, running_sequence_id: u64) -> bool { + if let Some(context_sequence_id) = context.get("sequence_id") { + if let Ok(context_id) = context_sequence_id.parse::() { + if context_id != running_sequence_id { + // This action is from a different sequence, ignore it + return false; + } else { + return true; + } + } else { + // Failed to parse sequence_id, ignore this action + return false; + } + } else { + return false; + } +} + +fn update_primary_and_original_pane_ids(state: &mut State, pane_manifest: &PaneManifest) { + if let Some(primary_pane_id) = state.primary_pane_id { + let mut primary_pane_found = false; + + // Check if the primary pane still exists + for (_tab_index, panes) in &pane_manifest.panes { + for pane_info in panes { + let pane_matches = match primary_pane_id { + PaneId::Terminal(id) => !pane_info.is_plugin && pane_info.id == id, + PaneId::Plugin(id) => pane_info.is_plugin && pane_info.id == id, + }; + + if pane_matches { + primary_pane_found = true; + break; + } + } + if primary_pane_found { + break; + } + } + + // If primary pane was not found, select a new one + if !primary_pane_found { + state.primary_pane_id = None; + } + } + if let Some(original_pane_id) = state.original_pane_id { + let mut original_pane_found = false; + + // Check if the original pane still exists + for (_tab_index, panes) in &pane_manifest.panes { + for pane_info in panes { + let pane_matches = match original_pane_id { + PaneId::Terminal(id) => !pane_info.is_plugin && pane_info.id == id, + PaneId::Plugin(id) => pane_info.is_plugin && pane_info.id == id, + }; + + if pane_matches { + original_pane_found = true; + break; + } + } + if original_pane_found { + break; + } + } + + // If original pane was not found, select a new one + if !original_pane_found { + state.original_pane_id = None; + } + } +} + +pub fn select_primary_pane_from_history( + pane_history: &[PaneId], + pane_manifest: &PaneManifest, + own_pane_id: Option, +) -> Option { + pane_history + .iter() + .rev() // Most recent first + .find(|&id| { + // Skip self + if Some(*id) == own_pane_id { + return false; + } + + // Skip floating panes + !is_pane_floating(*id, pane_manifest) + }) + .copied() +} + +fn close_panes_and_return_to_shell(state: &mut State) -> bool { + // Close all panes in the sequence before clearing it + // Handle the first pane: replace it with the primary_pane_id_before_sequence if available + if let Some(first_pane_id) = state.execution.all_commands.iter().find_map(|command| { + // we look for the first command in the sequence that has a pane that's + // actually open + match command.get_status() { + CommandStatus::Running(pane_id) => pane_id, + CommandStatus::Exited(_, pane_id) => pane_id, + CommandStatus::Interrupted(pane_id) => pane_id, + _ => None, + } + }) { + if let Some(original_pane_id) = state.original_pane_id { + replace_pane_with_existing_pane( + first_pane_id, + original_pane_id, + true, // suppress_replaced_pane (closes the first pane) + ); + } else { + // Fallback: if we don't have a saved primary pane, just close it + close_pane_with_id(first_pane_id); + } + } + + // Close all other panes (starting from index 1) + for (idx, command) in state.execution.all_commands.iter().skip(1).enumerate() { + let pane_id = match command.get_status() { + CommandStatus::Running(pane_id) => pane_id, + CommandStatus::Exited(_, pane_id) => pane_id, + CommandStatus::Interrupted(pane_id) => pane_id, + _ => None, + }; + + if let Some(pane_id) = pane_id { + close_pane_with_id(pane_id); + } else { + eprintln!( + "Warning: Cannot close pane at index {}: pane_id is None", + idx + 1 + ); + } + } + + state.clear_all_commands(); + // Transition back to shell screen + if let Some(plugin_id) = state.plugin_id { + change_floating_pane_coordinates( + plugin_id, + Some(25), + Some(25), + Some(50), + Some(50), + false, // pinned (this should unpin it) + ); + } + update_title(state); + true +} + +fn change_floating_pane_coordinates( + own_plugin_id: u32, + x: Option, + y: Option, + width: Option, + height: Option, + should_be_pinned: bool, +) { + let coordinates = FloatingPaneCoordinates::new( + x.map(|x| format!("{}%", x)), + y.map(|y| format!("{}%", y)), + width.map(|width| format!("{}%", width)), + height.map(|height| format!("{}%", height)), + Some(should_be_pinned), + ); + if let Some(coordinates) = coordinates { + // TODO: better + // show_self(true); + change_floating_panes_coordinates(vec![(PaneId::Plugin(own_plugin_id), coordinates)]); + } +} + +fn update_spinner(state: &mut State) -> bool { + // Advance spinner frame for RUNNING animation + state.layout.spinner_frame = (state.layout.spinner_frame + 1) % 8; + + // Reset timer scheduled flag since this timer just fired + state.layout.spinner_timer_scheduled = false; + + // If we have a running command, schedule next timer event + let has_running = state + .execution + .all_commands + .iter() + .any(|command| matches!(command.get_status(), CommandStatus::Running(_))); + if has_running && !state.layout.spinner_timer_scheduled { + set_timeout(0.1); + state.layout.spinner_timer_scheduled = true; + } + has_running +} + +fn rerun_sequence(state: &mut State) { + if !state.can_run_sequence() { + return; + } + + state.execution.all_commands.retain(|c| !c.is_empty()); + + if state.execution.all_commands.is_empty() { + return; + } + + let selected_index = state + .selection + .current_selected_command_index + .unwrap_or(0) + .min(state.execution.all_commands.len().saturating_sub(1)); + + let mut close_replaced_pane = true; + + // Extract the selected pane's ID BEFORE resetting statuses (needed for in-place replacement) + let selected_pane_id_for_replacement = &state + .execution + .all_commands + .get(selected_index) + .and_then(|c| match c.get_status() { + CommandStatus::Running(pane_id) => pane_id, + CommandStatus::Exited(_, pane_id) => pane_id, + CommandStatus::Interrupted(pane_id) => pane_id, + _ => None, + }) + .or_else(|| { + if state.all_commands_are_pending() { + close_replaced_pane = false; // we should never close the original pane_id + state.original_pane_id + } else { + None + } + }); + + // Close all panes AFTER the selected one (selected will be replaced in-place) + for i in (selected_index + 1)..state.execution.all_commands.len() { + // Extract pane_id from the command status + let pane_id = state + .execution + .all_commands + .get(i) + .and_then(|c| match c.get_status() { + CommandStatus::Running(pane_id) => pane_id, + CommandStatus::Exited(_, pane_id) => pane_id, + CommandStatus::Interrupted(pane_id) => pane_id, + _ => None, + }); + + // Close the pane if we have a pane_id + if let Some(pane_id) = pane_id { + close_pane_with_id(pane_id); + } else { + eprintln!( + "Warning: Cannot close pane for command at index {}: pane_id is None", + i + ); + } + + // Reset the command status to Pending + state + .execution + .all_commands + .get_mut(i) + .map(|c| c.set_status(CommandStatus::Pending)); + } + + // Reset the selected pane's status to Pending (it will be replaced in-place) + state + .execution + .all_commands + .get_mut(selected_index) + .map(|c| c.set_status(CommandStatus::Pending)); + + // Set the current command index to the selected command + state.execution.current_running_command_index = selected_index; + + // Execute the command at the selected index + if let Some((command_text, chain_type, command_cwd)) = state + .execution + .all_commands + .get(selected_index) + .map(|c| (c.get_text(), c.get_chain_type(), c.get_cwd())) + { + let shell = state + .shell + .clone() + .unwrap_or_else(|| PathBuf::from("/bin/bash")); + + let command = zellij_tile::prelude::actions::RunCommandAction { + command: shell, + args: vec!["-ic".to_string(), command_text.trim().to_string()], + cwd: command_cwd, + hold_on_close: true, + ..Default::default() + }; + + // Determine placement - always replace the selected pane in-place if we have its ID + let placement = if let Some(pane_id) = selected_pane_id_for_replacement { + NewPanePlacement::InPlace { + pane_id_to_replace: Some(*pane_id), + close_replaced_pane, + } + } else { + let pane_id_to_stack_under = state + .execution + .all_commands + .iter() + .find_map(|c| c.get_pane_id()); + NewPanePlacement::Stacked(pane_id_to_stack_under) + }; + + // Determine unblock_condition based on whether this is the last command + let unblock_condition = if selected_index < state.execution.all_commands.len() - 1 { + // Not the last command - use the chain type's unblock condition + chain_type.to_unblock_condition() + } else { + // Last command - use UnblockCondition::OnAnyExit + Some(UnblockCondition::OnAnyExit) + }; + + let action = Action::NewBlockingPane { + placement, + command: Some(command), + pane_name: Some(command_text.trim().to_string()), + unblock_condition, + near_current_pane: true, + }; + + // Generate a new sequence ID for the restart + state.execution.sequence_id += 1; + + // Reset the stopped flag so the sequence can run + state.execution.is_running = true; + state.layout.needs_reposition = true; + state.execution.primary_pane_id_before_sequence = state.primary_pane_id; + + // Pass the sequence ID in the context + let mut context = BTreeMap::new(); + context.insert( + "sequence_id".to_string(), + state.execution.sequence_id.to_string(), + ); + context.insert("command_text".to_string(), command_text.to_string()); + run_action(action, context); + } +} + +fn interrupt_sequence(state: &mut State) { + if state.execution.is_running { + // Ctrl-C press when running - interrupt the running command + for command in state.execution.all_commands.iter_mut() { + if let CommandStatus::Running(pane_id) = command.get_status() { + if let Some(pane_id) = pane_id { + send_sigkill_to_pane_id(pane_id); + } + command.set_status(CommandStatus::Interrupted(pane_id)); + } + } + state.execution.is_running = false; + } else { + eprintln!("Cannot interrupt a sequence that is not running."); + } +} + +pub fn update_cursor(state: &mut State) { + // Calculate new cursor position + let new_coords = calculate_cursor_position(state); + + // Only update if the position changed + if new_coords != state.layout.cached_cursor_position { + match new_coords { + Some(coords) => show_cursor(Some(coords)), + None => show_cursor(None), + } + state.layout.cached_cursor_position = new_coords; + } +} + +pub fn update_title(state: &mut State) { + let Some(plugin_id) = state.plugin_id else { + return; + }; + + let title = if state.all_commands_are_pending() { + "Run one or more commands in sequence" + } else { + let has_running_commands = state + .execution + .all_commands + .iter() + .any(|c| matches!(c.get_status(), CommandStatus::Running(..))); + let has_interrupted_commands = state + .execution + .all_commands + .iter() + .any(|c| matches!(c.get_status(), CommandStatus::Interrupted(..))); + let all_commands_complete = state + .execution + .all_commands + .iter() + .all(|c| matches!(c.get_status(), CommandStatus::Exited(..))); + if has_running_commands { + let current = state.execution.current_running_command_index + 1; + let total = state.execution.all_commands.len(); + return rename_plugin_pane(plugin_id, &format!("Running {}/{}", current, total)); + } else if has_interrupted_commands { + "Sequence Interrupted" + } else if all_commands_complete { + "Sequence Complete" + } else { + "Stopped" + } + }; + rename_plugin_pane(plugin_id, title); +} diff --git a/default-plugins/sequence/src/path_formatting.rs b/default-plugins/sequence/src/path_formatting.rs new file mode 100644 index 000000000..01bf50e69 --- /dev/null +++ b/default-plugins/sequence/src/path_formatting.rs @@ -0,0 +1,71 @@ +use std::path::PathBuf; + +/// Expand a path string to an absolute PathBuf +/// Handles ~, ./, .., relative and absolute paths +pub fn expand_path(path_str: &str, current_cwd: Option<&PathBuf>) -> Option { + let expanded = if path_str.starts_with("~/") || path_str == "~" { + let home_dir = std::env::var("HOME").ok()?; + if path_str == "~" { + PathBuf::from(home_dir) + } else { + PathBuf::from(home_dir).join(&path_str[2..]) + } + } else if path_str.starts_with('/') { + PathBuf::from(path_str) + } else { + let base = current_cwd?; + base.join(path_str) + }; + + let mut normalized = PathBuf::new(); + for component in expanded.components() { + match component { + std::path::Component::ParentDir => { + normalized.pop(); + }, + std::path::Component::CurDir => {}, + _ => { + normalized.push(component); + }, + } + } + + Some(normalized) +} + +/// Format a path for display in the prompt +/// Example: /home/user/some/long/path -> ~/s/l/path +pub fn format_cwd(cwd: &PathBuf) -> String { + let path_str = cwd.to_string_lossy().to_string(); + + let home_dir = std::env::var("HOME").unwrap_or_default(); + let path_str = if !home_dir.is_empty() && path_str.starts_with(&home_dir) { + path_str.replacen(&home_dir, "~", 1) + } else { + path_str + }; + + let parts: Vec<&str> = path_str.split('/').collect(); + + if parts.len() <= 1 { + return path_str; + } + + let mut formatted_parts = Vec::new(); + for (i, part) in parts.iter().enumerate() { + if i == parts.len() - 1 { + formatted_parts.push(part.to_string()); + } else if part.is_empty() { + formatted_parts.push(String::new()); + } else { + formatted_parts.push(part.chars().next().unwrap_or_default().to_string()); + } + } + + formatted_parts.join("/") +} + +/// Alias for expand_path for backwards compatibility +pub fn resolve_path(current_cwd: Option<&PathBuf>, path_str: &str) -> Option { + expand_path(path_str, current_cwd) +} diff --git a/default-plugins/sequence/src/state/chain_type.rs b/default-plugins/sequence/src/state/chain_type.rs new file mode 100644 index 000000000..3da3316cb --- /dev/null +++ b/default-plugins/sequence/src/state/chain_type.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; +use zellij_tile::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ChainType { + And, + Or, + Then, + None, +} + +impl Default for ChainType { + fn default() -> Self { + ChainType::None + } +} + +impl ChainType { + pub fn to_unblock_condition(&self) -> Option { + match self { + ChainType::And => Some(UnblockCondition::OnExitSuccess), + ChainType::Or => Some(UnblockCondition::OnExitFailure), + ChainType::Then => Some(UnblockCondition::OnAnyExit), + ChainType::None => None, + } + } + + pub fn as_str(&self) -> &str { + match self { + ChainType::And => "&&", + ChainType::Or => "||", + ChainType::Then => ";", + ChainType::None => "", + } + } + + pub fn cycle_next(&mut self) { + *self = match self { + ChainType::And => ChainType::Or, + ChainType::Or => ChainType::Then, + ChainType::Then => ChainType::And, + ChainType::None => ChainType::And, + }; + } +} diff --git a/default-plugins/sequence/src/state/command_entry.rs b/default-plugins/sequence/src/state/command_entry.rs new file mode 100644 index 000000000..96a0020b0 --- /dev/null +++ b/default-plugins/sequence/src/state/command_entry.rs @@ -0,0 +1,86 @@ +use super::{ChainType, CommandStatus}; +use std::path::PathBuf; +use std::time::Instant; +use zellij_tile::prelude::PaneId; + +#[derive(Debug, Clone)] +pub struct CommandEntry { + text: String, + cwd: Option, + pub(super) chain_type: ChainType, + pub(super) status: CommandStatus, + pub(super) start_time: std::time::Instant, +} + +impl Default for CommandEntry { + fn default() -> Self { + CommandEntry { + text: String::default(), + cwd: None, + chain_type: ChainType::default(), + status: CommandStatus::default(), + start_time: Instant::now(), + } + } +} + +impl CommandEntry { + pub fn new(text: &str, cwd: Option) -> Self { + CommandEntry { + text: text.to_owned(), + cwd, + ..Default::default() + } + } + pub fn with_and(mut self) -> Self { + self.chain_type = ChainType::And; + self + } + pub fn get_text(&self) -> String { + self.text.clone() + } + pub fn set_text(&mut self, text: String) { + self.text = text; + } + pub fn clear_text(&mut self) { + self.text.clear(); + } + pub fn get_chain_type(&self) -> ChainType { + self.chain_type + } + pub fn set_chain_type(&mut self, chain_type: ChainType) { + self.chain_type = chain_type; + } + pub fn get_status(&self) -> CommandStatus { + self.status.clone() + } + pub fn set_status(&mut self, status: CommandStatus) { + self.status = status; + } + pub fn get_pane_id(&self) -> Option { + self.status.get_pane_id() + } + pub fn is_empty(&self) -> bool { + self.text.trim().is_empty() + } + pub fn fill_chain_type_if_empty(&mut self) { + if let ChainType::None = self.chain_type { + self.chain_type = ChainType::And; + } + } + pub fn clear_chain_type(&mut self) { + self.chain_type = ChainType::None; + } + pub fn clear_status(&mut self) { + self.status = CommandStatus::Pending; + } + pub fn cycle_chain_type(&mut self) { + self.chain_type.cycle_next(); + } + pub fn get_cwd(&self) -> Option { + self.cwd.clone() + } + pub fn set_cwd(&mut self, cwd: Option) { + self.cwd = cwd; + } +} diff --git a/default-plugins/sequence/src/state/command_parser.rs b/default-plugins/sequence/src/state/command_parser.rs new file mode 100644 index 000000000..7f0898f53 --- /dev/null +++ b/default-plugins/sequence/src/state/command_parser.rs @@ -0,0 +1,327 @@ +use super::ChainType; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ParseState { + Normal, + InSingleQuote, + InDoubleQuote, + Escaped(EscapeContext), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum EscapeContext { + Normal, + DoubleQuote, +} + +pub fn split_by_chain_operators(text: &str) -> Vec<(String, Option)> { + let mut segments = Vec::new(); + let mut current_segment = String::new(); + let mut state = ParseState::Normal; + let chars: Vec = text.chars().collect(); + let mut i = 0; + + while i < chars.len() { + let ch = chars[i]; + + match state { + ParseState::Normal => { + if ch == '\\' { + state = ParseState::Escaped(EscapeContext::Normal); + current_segment.push(ch); + i += 1; + } else if ch == '\'' { + state = ParseState::InSingleQuote; + current_segment.push(ch); + i += 1; + } else if ch == '"' { + state = ParseState::InDoubleQuote; + current_segment.push(ch); + i += 1; + } else if ch == '&' && i + 1 < chars.len() && chars[i + 1] == '&' { + let segment_text = current_segment.trim().to_string(); + if !segment_text.is_empty() { + segments.push((segment_text, Some(ChainType::And))); + } + current_segment.clear(); + i += 2; + } else if ch == '|' && i + 1 < chars.len() && chars[i + 1] == '|' { + let segment_text = current_segment.trim().to_string(); + if !segment_text.is_empty() { + segments.push((segment_text, Some(ChainType::Or))); + } + current_segment.clear(); + i += 2; + } else if ch == ';' { + let segment_text = current_segment.trim().to_string(); + if !segment_text.is_empty() { + segments.push((segment_text, Some(ChainType::Then))); + } + current_segment.clear(); + i += 1; + } else { + current_segment.push(ch); + i += 1; + } + }, + ParseState::InSingleQuote => { + current_segment.push(ch); + if ch == '\'' { + state = ParseState::Normal; + } + i += 1; + }, + ParseState::InDoubleQuote => { + if ch == '\\' { + state = ParseState::Escaped(EscapeContext::DoubleQuote); + current_segment.push(ch); + i += 1; + } else if ch == '"' { + current_segment.push(ch); + state = ParseState::Normal; + i += 1; + } else { + current_segment.push(ch); + i += 1; + } + }, + ParseState::Escaped(context) => { + current_segment.push(ch); + state = match context { + EscapeContext::Normal => ParseState::Normal, + EscapeContext::DoubleQuote => ParseState::InDoubleQuote, + }; + i += 1; + }, + } + } + + let final_segment = current_segment.trim().to_string(); + if !final_segment.is_empty() { + segments.push((final_segment, None)); + } + + segments +} + +pub fn detect_chain_operator_at_end(text: &str) -> Option<(String, ChainType)> { + let segments = split_by_chain_operators(text); + + if segments.is_empty() { + return None; + } + + if segments.len() == 1 { + if let Some((text, Some(chain_type))) = segments.first() { + return Some((text.clone(), *chain_type)); + } + return None; + } + + if let Some((first_text, Some(chain_type))) = segments.first() { + return Some((first_text.clone(), *chain_type)); + } + + None +} + +pub fn get_remaining_after_first_segment(text: &str) -> Option { + let segments = split_by_chain_operators(text); + + if segments.len() <= 1 { + return None; + } + + let mut result = String::new(); + for (i, (segment_text, chain_type_opt)) in segments.iter().enumerate().skip(1) { + if i > 1 { + if let Some(chain_type) = chain_type_opt { + result.push_str(&format!(" {} ", chain_type.as_str())); + } + } + result.push_str(segment_text); + if let Some(chain_type) = chain_type_opt { + result.push_str(&format!(" {} ", chain_type.as_str())); + } + } + + Some(result.trim().to_string()) +} + +pub fn detect_cd_command(text: &str) -> Option { + let trimmed = text.trim(); + + if trimmed == "cd" { + return Some("~".to_string()); + } + + if trimmed.starts_with("cd ") { + let path = trimmed[3..].trim(); + return Some(path.to_string()); + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_single_command_no_operator() { + let result = split_by_chain_operators("ls"); + assert_eq!(result, vec![("ls".to_string(), None)]); + } + + #[test] + fn test_two_commands_with_and() { + let result = split_by_chain_operators("ls && pwd"); + assert_eq!( + result, + vec![ + ("ls".to_string(), Some(ChainType::And)), + ("pwd".to_string(), None) + ] + ); + } + + #[test] + fn test_two_commands_with_or() { + let result = split_by_chain_operators("cmd1 || cmd2"); + assert_eq!( + result, + vec![ + ("cmd1".to_string(), Some(ChainType::Or)), + ("cmd2".to_string(), None) + ] + ); + } + + #[test] + fn test_two_commands_with_semicolon() { + let result = split_by_chain_operators("echo hi ; ls"); + assert_eq!( + result, + vec![ + ("echo hi".to_string(), Some(ChainType::Then)), + ("ls".to_string(), None) + ] + ); + } + + #[test] + fn test_three_commands_mixed() { + let result = split_by_chain_operators("a && b || c"); + assert_eq!( + result, + vec![ + ("a".to_string(), Some(ChainType::And)), + ("b".to_string(), Some(ChainType::Or)), + ("c".to_string(), None) + ] + ); + } + + #[test] + fn test_operator_in_single_quotes() { + let result = split_by_chain_operators("echo '&&' && ls"); + assert_eq!( + result, + vec![ + ("echo '&&'".to_string(), Some(ChainType::And)), + ("ls".to_string(), None) + ] + ); + } + + #[test] + fn test_operator_in_double_quotes() { + let result = split_by_chain_operators("echo \"||\" || pwd"); + assert_eq!( + result, + vec![ + ("echo \"||\"".to_string(), Some(ChainType::Or)), + ("pwd".to_string(), None) + ] + ); + } + + #[test] + fn test_escaped_operator() { + let result = split_by_chain_operators("echo \\&& test"); + assert_eq!(result, vec![("echo \\&& test".to_string(), None)]); + } + + #[test] + fn test_complex_quoting() { + let result = split_by_chain_operators("cmd1 && echo \"a || b\" && cmd2"); + assert_eq!( + result, + vec![ + ("cmd1".to_string(), Some(ChainType::And)), + ("echo \"a || b\"".to_string(), Some(ChainType::And)), + ("cmd2".to_string(), None) + ] + ); + } + + #[test] + fn test_empty_segments_filtered() { + let result = split_by_chain_operators("cmd1 && && cmd2"); + assert_eq!( + result, + vec![ + ("cmd1".to_string(), Some(ChainType::And)), + ("cmd2".to_string(), None) + ] + ); + } + + #[test] + fn test_detect_operator_just_typed_and() { + let result = detect_chain_operator_at_end("ls &&"); + assert_eq!(result, Some(("ls".to_string(), ChainType::And))); + } + + #[test] + fn test_detect_operator_just_typed_or() { + let result = detect_chain_operator_at_end("cmd ||"); + assert_eq!(result, Some(("cmd".to_string(), ChainType::Or))); + } + + #[test] + fn test_detect_operator_just_typed_semicolon() { + let result = detect_chain_operator_at_end("echo ;"); + assert_eq!(result, Some(("echo".to_string(), ChainType::Then))); + } + + #[test] + fn test_detect_operator_no_operator() { + let result = detect_chain_operator_at_end("ls"); + assert_eq!(result, None); + } + + #[test] + fn test_detect_operator_in_quotes() { + let result = detect_chain_operator_at_end("echo '&&'"); + assert_eq!(result, None); + } + + #[test] + fn test_whitespace_trimming() { + let result = split_by_chain_operators(" cmd1 && cmd2 "); + assert_eq!( + result, + vec![ + ("cmd1".to_string(), Some(ChainType::And)), + ("cmd2".to_string(), None) + ] + ); + } + + #[test] + fn test_trailing_operator_with_space() { + let result = split_by_chain_operators("ls && "); + assert_eq!(result, vec![("ls".to_string(), Some(ChainType::And))]); + } +} diff --git a/default-plugins/sequence/src/state/command_status.rs b/default-plugins/sequence/src/state/command_status.rs new file mode 100644 index 000000000..39717e8bb --- /dev/null +++ b/default-plugins/sequence/src/state/command_status.rs @@ -0,0 +1,26 @@ +use zellij_tile::prelude::*; + +#[derive(Debug, Clone)] +pub enum CommandStatus { + Exited(Option, Option), + Running(Option), + Pending, + Interrupted(Option), +} + +impl CommandStatus { + pub fn get_pane_id(&self) -> Option { + match self { + CommandStatus::Exited(_, pane_id) => *pane_id, + CommandStatus::Running(pane_id) => *pane_id, + CommandStatus::Pending => None, + CommandStatus::Interrupted(pane_id) => *pane_id, + } + } +} + +impl Default for CommandStatus { + fn default() -> Self { + CommandStatus::Pending + } +} diff --git a/default-plugins/sequence/src/state/editing.rs b/default-plugins/sequence/src/state/editing.rs new file mode 100644 index 000000000..3deac619c --- /dev/null +++ b/default-plugins/sequence/src/state/editing.rs @@ -0,0 +1,80 @@ +use crate::path_formatting; +use crate::state::CommandEntry; +use crate::ui::text_input::TextInput; +use std::path::PathBuf; + +pub struct Editing { + pub editing_input: Option, +} + +impl Editing { + pub fn new() -> Self { + Self { + editing_input: Some(TextInput::new("".to_owned())), + } + } + + pub fn start_editing(&mut self, text: String) { + self.editing_input = Some(TextInput::new(text)); + } + + pub fn cancel_editing(&mut self) { + self.editing_input = None; + } + + pub fn input_text(&self) -> Option { + self.editing_input.as_ref().map(|e| e.get_text().to_owned()) + } + + pub fn set_input_text(&mut self, text: String) { + self.editing_input.as_mut().map(|e| e.set_text(text)); + } + + pub fn handle_submit( + &mut self, + selection_index: Option, + all_commands: &mut Vec, + current_cwd: &Option, + ) -> (bool, Option) { + let mut handled_internally = false; + let mut new_selection_index = None; + + if let Some(current_text) = self.editing_input.as_ref().map(|i| i.get_text()) { + if let Some(index) = selection_index { + if let Some(command) = all_commands.get_mut(index) { + if current_text.starts_with("cd ") || current_text == "cd" { + let path = if current_text == "cd" { + "~" + } else { + current_text[3..].trim() + }; + + if let Some(new_cwd) = + path_formatting::resolve_path(current_cwd.as_ref(), path) + { + command.set_cwd(Some(new_cwd)); + } + + command.set_text("".to_owned()); + self.editing_input.as_mut().map(|c| c.clear()); + new_selection_index = selection_index; // remain editing after a cd command + handled_internally = true; + return (handled_internally, new_selection_index); // avoid setting edit_input to None below, we + // still want to be in editing mode after + // changing directory with cd + } else { + command.set_text(current_text.to_owned()); + } + } + } + } + self.editing_input = None; + (handled_internally, new_selection_index) + } +} + +impl Default for Editing { + fn default() -> Self { + Self::new() + } +} diff --git a/default-plugins/sequence/src/state/execution.rs b/default-plugins/sequence/src/state/execution.rs new file mode 100644 index 000000000..ff630ec03 --- /dev/null +++ b/default-plugins/sequence/src/state/execution.rs @@ -0,0 +1,265 @@ +use crate::path_formatting::format_cwd; +use crate::state::{ChainType, CommandEntry, CommandStatus}; +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::time::Duration; +use zellij_tile::prelude::*; + +pub struct Execution { + pub all_commands: Vec, + pub current_running_command_index: usize, + pub is_running: bool, + pub sequence_id: u64, + pub primary_pane_id_before_sequence: Option, +} + +impl Execution { + pub fn new() -> Self { + Self { + all_commands: vec![CommandEntry::default()], + current_running_command_index: 0, + is_running: false, + sequence_id: 0, + primary_pane_id_before_sequence: None, + } + } + + pub fn longest_cwd_display(&self, global_cwd: &Option) -> String { + self.all_commands + .iter() + .map(|cmd| { + let cwd = cmd.get_cwd().or_else(|| global_cwd.clone()); + if let Some(cwd) = &cwd { + format_cwd(cwd) + } else { + "~".to_string() + } + }) + .max_by_key(|s| s.len()) + .unwrap_or_else(|| "~".to_string()) + } + + pub fn remove_empty_commands(&mut self) { + if self.all_commands.iter().len() > 1 { + self.all_commands.retain(|c| !c.get_text().is_empty()); + } + } + + pub fn get_first_command(&self) -> Option { + self.all_commands.iter().next().cloned() + } + + pub fn set_command_status(&mut self, command_index: usize, status: CommandStatus) { + self.all_commands + .get_mut(command_index) + .map(|c| c.set_status(status)); + } + + pub fn set_current_running_command_status(&mut self, status: CommandStatus) { + self.all_commands + .get_mut(self.current_running_command_index) + .map(|c| c.set_status(status)); + } + + pub fn get_current_running_command_status(&self) -> Option { + self.all_commands + .get(self.current_running_command_index) + .map(|c| c.get_status()) + } + + pub fn can_run_sequence(&self) -> bool { + self.all_commands.iter().any(|command| !command.is_empty()) + } + + pub fn copy_to_clipboard(&self) { + let text_to_copy = self + .all_commands + .iter() + .map(|c| format!("{}", c.get_text())) + .collect::>() + .join("\n"); + if !text_to_copy.is_empty() { + copy_to_clipboard(text_to_copy); + } + } + + pub fn update_pane_id_for_command(&mut self, pane_id: PaneId, command_text: &str) { + for command in self.all_commands.iter_mut() { + if let CommandStatus::Pending | CommandStatus::Running(None) = command.get_status() { + let cmd_text = command.get_text(); + if cmd_text == command_text { + command.set_status(CommandStatus::Running(Some(pane_id))); + break; + } + } + } + } + + pub fn update_exited_command_statuses(&mut self, pane_manifest: &PaneManifest) -> bool { + let mut updated = false; + for command in self.all_commands.iter_mut() { + let status = command.get_status(); + let pane_id_opt = match &status { + CommandStatus::Running(pid) => *pid, + CommandStatus::Exited(_, pid) => *pid, + CommandStatus::Interrupted(pid) => *pid, + _ => None, + }; + + if let Some(pane_id) = pane_id_opt { + let mut pane_found = false; + for (_tab_index, panes) in &pane_manifest.panes { + for pane_info in panes { + let pane_matches = match pane_id { + PaneId::Terminal(id) => !pane_info.is_plugin && pane_info.id == id, + PaneId::Plugin(id) => pane_info.is_plugin && pane_info.id == id, + }; + + if pane_matches { + pane_found = true; + if pane_info.exited { + command.set_status(CommandStatus::Exited( + pane_info.exit_status, + Some(pane_id), + )); + updated = true; + } + break; + } + } + if pane_found { + break; + } + } + + if !pane_found && command.start_time.elapsed() > Duration::from_millis(400) { + match command.get_status() { + CommandStatus::Running(_) => { + eprintln!( + "Pane {:?} was closed while running, setting pane_id to None", + pane_id + ); + command.set_status(CommandStatus::Running(None)); + updated = true; + }, + CommandStatus::Exited(exit_code, _) => { + eprintln!( + "Pane {:?} was closed after exiting, setting pane_id to None", + pane_id + ); + command.set_status(CommandStatus::Exited(exit_code, None)); + updated = true; + }, + CommandStatus::Interrupted(_) => { + eprintln!("Pane {:?} was closed after being interrupted, setting pane_id to None", pane_id); + command.set_status(CommandStatus::Interrupted(None)); + updated = true; + }, + _ => {}, + } + } + } + } + updated + } + + pub fn update_sequence_stopped_state(&mut self) -> bool { + let mut needs_rerender = false; + if self.is_running { + let current_idx = self.current_running_command_index; + if let Some(command) = self.all_commands.get_mut(current_idx) { + if let CommandStatus::Exited(exit_code, _) = command.get_status() { + let should_stop; + if current_idx >= self.all_commands.len().saturating_sub(1) { + should_stop = true; + } else { + if let Some(chain_type) = &self + .all_commands + .get(current_idx) + .map(|c| c.get_chain_type()) + { + match chain_type { + ChainType::And => { + should_stop = exit_code.unwrap_or(0) != 0; + }, + ChainType::Or => { + should_stop = exit_code.unwrap_or(0) == 0; + }, + ChainType::Then => { + should_stop = false; + }, + ChainType::None => { + should_stop = true; + }, + } + } else { + should_stop = true; + } + }; + if should_stop { + self.is_running = false; + } + } + } + needs_rerender = true; + } + needs_rerender + } + + pub fn execute_command_sequence( + &mut self, + shell: &Option, + global_cwd: &Option, + primary_pane_id: Option, + ) { + use zellij_tile::prelude::actions::{Action, RunCommandAction}; + + self.all_commands.retain(|c| !c.is_empty()); + + let Some(first_active_sequence_command) = self.get_first_command() else { + return; + }; + + let shell = shell.clone().unwrap_or_else(|| PathBuf::from("/bin/bash")); + + let first_command = first_active_sequence_command.get_text(); + let first_chain_type = first_active_sequence_command.get_chain_type(); + let command_cwd = first_active_sequence_command + .get_cwd() + .or_else(|| global_cwd.clone()); + + let command = RunCommandAction { + command: shell.clone(), + args: vec!["-ic".to_string(), first_command.trim().to_string()], + cwd: command_cwd, + hold_on_close: true, + ..Default::default() + }; + + let placement = NewPanePlacement::InPlace { + pane_id_to_replace: primary_pane_id, + close_replaced_pane: false, + }; + + let action = Action::NewBlockingPane { + placement, + command: Some(command), + pane_name: Some(first_command.trim().to_string()), + unblock_condition: first_chain_type.to_unblock_condition(), + near_current_pane: true, + }; + + self.sequence_id += 1; + + let mut context = BTreeMap::new(); + context.insert("sequence_id".to_string(), self.sequence_id.to_string()); + run_action(action, context); + self.set_command_status(0, CommandStatus::Running(None)); + } +} + +impl Default for Execution { + fn default() -> Self { + Self::new() + } +} diff --git a/default-plugins/sequence/src/state/layout.rs b/default-plugins/sequence/src/state/layout.rs new file mode 100644 index 000000000..65a267e02 --- /dev/null +++ b/default-plugins/sequence/src/state/layout.rs @@ -0,0 +1,73 @@ +use crate::state::positioning; +use std::path::PathBuf; +use zellij_tile::prelude::*; + +pub struct Layout { + pub needs_reposition: bool, + pub cached_cursor_position: Option<(usize, usize)>, + pub spinner_frame: usize, + pub spinner_timer_scheduled: bool, +} + +impl Layout { + pub fn new() -> Self { + Self { + needs_reposition: false, + cached_cursor_position: None, + spinner_frame: 0, + spinner_timer_scheduled: false, + } + } + + pub fn reposition_plugin_based_on_sequence_state( + &mut self, + is_running: bool, + first_command_pane_id: Option, + pane_manifest: &PaneManifest, + plugin_id: Option, + total_viewport_columns: Option, + total_viewport_rows: Option, + total_commands: usize, + _selected_index: Option, + _running_index: usize, + _cwd: &Option, + ) { + let Some(plugin_id) = plugin_id else { + return; + }; + let (Some(total_cols), Some(total_rows)) = (total_viewport_columns, total_viewport_rows) + else { + return; + }; + + if !self.needs_reposition || !is_running { + return; + } + + let Some(first_pane_id) = first_command_pane_id else { + return; + }; + + for (_tab_index, panes) in &pane_manifest.panes { + for pane in panes { + if !pane.is_plugin && PaneId::Terminal(pane.id) == first_pane_id { + positioning::reposition_plugin_for_sequence( + plugin_id, + total_cols, + total_rows, + total_commands, + ); + + self.needs_reposition = false; + return; + } + } + } + } +} + +impl Default for Layout { + fn default() -> Self { + Self::new() + } +} diff --git a/default-plugins/sequence/src/state/mod.rs b/default-plugins/sequence/src/state/mod.rs new file mode 100644 index 000000000..19738be76 --- /dev/null +++ b/default-plugins/sequence/src/state/mod.rs @@ -0,0 +1,446 @@ +mod chain_type; +mod command_entry; +mod command_parser; +mod command_status; +mod editing; +mod execution; +mod layout; +pub mod positioning; +mod selection; + +pub use chain_type::ChainType; +pub use command_entry::CommandEntry; +pub use command_parser::{ + detect_cd_command, detect_chain_operator_at_end, get_remaining_after_first_segment, + split_by_chain_operators, +}; +pub use command_status::CommandStatus; +pub use editing::Editing; +pub use execution::Execution; +pub use layout::Layout; +pub use selection::Selection; + +use crate::ui::text_input::TextInput; +use std::path::PathBuf; +use zellij_tile::prelude::*; + +pub struct State { + pub shell: Option, + pub plugin_id: Option, + pub client_id: Option, + pub own_rows: Option, + pub own_columns: Option, + pub cwd: Option, + pub original_pane_id: Option, + pub primary_pane_id: Option, + pub is_first_run: bool, + pub pane_manifest: Option, + pub total_viewport_columns: Option, + pub total_viewport_rows: Option, + pub selection: Selection, + pub editing: Editing, + pub execution: Execution, + pub layout: Layout, + pub current_position: Option, +} + +impl State { + pub fn new() -> Self { + Self { + shell: None, + plugin_id: None, + client_id: None, + own_rows: None, + own_columns: None, + cwd: None, + original_pane_id: None, + primary_pane_id: None, + is_first_run: true, + pane_manifest: None, + total_viewport_columns: None, + total_viewport_rows: None, + selection: Selection::new(), + editing: Editing::new(), + execution: Execution::new(), + layout: Layout::new(), + current_position: None, + } + } + + pub fn set_plugin_id(&mut self, plugin_id: u32) { + self.plugin_id = Some(plugin_id); + } + + /// Check if the sequence has finished executing (all commands are done) + pub fn has_finished(&self) -> bool { + self.execution + .all_commands + .iter() + .all(|command| matches!(command.status, CommandStatus::Exited(_, _))) + } + pub fn add_empty_command_after_current_selected(&mut self) { + self.selection.add_empty_command_after_current_selected( + &mut self.execution.all_commands, + &mut self.editing, + &self.cwd, + ); + } + + pub fn current_selected_command_is_empty(&self) -> bool { + self.selection + .current_selected_command_is_empty(&self.execution.all_commands, &self.editing) + } + + pub fn remove_current_selected_command(&mut self) { + let removed_pane_id = self + .selection + .remove_current_selected_command(&mut self.execution.all_commands, &mut self.editing); + if let Some(removed_pane_id) = removed_pane_id { + close_pane_with_id(removed_pane_id); + } + } + + pub fn clear_current_selected_command(&mut self) { + self.selection + .clear_current_selected_command(&mut self.execution.all_commands, &mut self.editing); + } + + pub fn move_selection_up(&mut self) { + self.selection + .move_up(&mut self.execution.all_commands, &mut self.editing); + if let Some(pane_id) = self + .current_selected_command() + .and_then(|c| c.get_pane_id()) + { + show_pane_with_id(pane_id, true, false); + } + } + + pub fn move_selection_down(&mut self) { + self.selection + .move_down(&mut self.execution.all_commands, &mut self.editing); + if let Some(pane_id) = self + .current_selected_command() + .and_then(|c| c.get_pane_id()) + { + show_pane_with_id(pane_id, true, false); + } + } + pub fn start_editing_selected(&mut self) { + if let Some(current_command_text) = self.current_selected_command().map(|c| c.get_text()) { + self.editing.start_editing(current_command_text); + } + } + + pub fn cancel_editing_selected(&mut self) { + self.editing.cancel_editing(); + } + + pub fn editing_input_text(&self) -> Option { + self.editing.input_text() + } + + pub fn set_editing_input_text(&mut self, text: String) { + self.editing.set_input_text(text); + } + fn handle_first_pasted_segment( + &mut self, + current_text: &str, + segment_text: &str, + chain_type_opt: &Option, + ) { + let new_text = if current_text.trim().is_empty() { + segment_text.to_string() + } else { + format!("{}{}", current_text, segment_text) + }; + + if let Some(path) = detect_cd_command(&new_text) { + use crate::path_formatting; + if let Some(new_cwd) = path_formatting::resolve_path(self.cwd.as_ref(), &path) { + self.current_selected_command_mut().map(|c| { + c.set_cwd(Some(new_cwd)); + c.set_text("".to_owned()); + if let Some(chain_type) = chain_type_opt { + c.set_chain_type(*chain_type); + } + }); + return; + } + } + + self.current_selected_command_mut().map(|c| { + c.set_text(new_text); + if let Some(chain_type) = chain_type_opt { + c.set_chain_type(*chain_type); + } + }); + } + + fn insert_new_pasted_segment( + &mut self, + segment_text: &str, + chain_type_opt: &Option, + is_last_line: bool, + ) { + use crate::path_formatting; + + let cd_path = detect_cd_command(segment_text); + + if let Some(path) = cd_path { + if let Some(new_cwd) = path_formatting::resolve_path(self.cwd.as_ref(), &path) { + self.current_selected_command_mut().map(|c| { + c.set_cwd(Some(new_cwd)); + if let Some(chain_type) = chain_type_opt { + c.set_chain_type(*chain_type); + } + }); + return; + } + } + + let Some(new_selected_index) = self.selection.current_selected_command_index.map(|i| i + 1) + else { + return; + }; + + let mut new_command = CommandEntry::new(segment_text, self.cwd.clone()); + if let Some(chain_type) = chain_type_opt { + new_command.set_chain_type(*chain_type); + } else if !is_last_line { + new_command.set_chain_type(ChainType::And); + } + + self.execution + .all_commands + .insert(new_selected_index, new_command); + self.selection.current_selected_command_index = Some(new_selected_index); + } + + fn ensure_line_end_chain_type(&mut self) { + if let Some(last_cmd_index) = self.selection.current_selected_command_index { + self.execution + .all_commands + .get_mut(last_cmd_index) + .map(|c| { + if matches!(c.get_chain_type(), ChainType::None) { + c.set_chain_type(ChainType::And); + } + }); + } + } + + pub fn pasted_lines(&mut self, lines: Vec<&str>) { + let Some(current_text) = self.editing_input_text() else { + return; + }; + + for (line_index, line) in lines.iter().enumerate() { + let segments = split_by_chain_operators(line); + let is_last_line = line_index == lines.len().saturating_sub(1); + + for (seg_index, (segment_text, chain_type_opt)) in segments.iter().enumerate() { + if line_index == 0 && seg_index == 0 { + self.handle_first_pasted_segment(¤t_text, segment_text, chain_type_opt); + } else { + self.insert_new_pasted_segment(segment_text, chain_type_opt, is_last_line); + } + } + + if !is_last_line { + self.ensure_line_end_chain_type(); + } + } + + self.start_editing_selected(); + } + pub fn remove_empty_commands(&mut self) { + self.execution.remove_empty_commands(); + } + + pub fn get_first_command(&self) -> Option { + self.execution.get_first_command() + } + + pub fn set_command_status(&mut self, command_index: usize, status: CommandStatus) { + self.execution.set_command_status(command_index, status); + } + + pub fn set_current_running_command_status(&mut self, status: CommandStatus) { + self.execution.set_current_running_command_status(status); + } + + pub fn get_current_running_command_status(&mut self) -> Option { + self.execution.get_current_running_command_status() + } + + pub fn handle_editing_submit(&mut self, current_cwd: &Option) -> bool { + let (handled_internally, new_selection_index) = self.editing.handle_submit( + self.selection.current_selected_command_index, + &mut self.execution.all_commands, + current_cwd, + ); + self.selection.current_selected_command_index = new_selection_index; + handled_internally + } + + pub fn update_pane_id_for_command(&mut self, pane_id: PaneId, command_text: &str) { + self.execution + .update_pane_id_for_command(pane_id, command_text); + } + pub fn update_exited_command_statuses(&mut self, pane_manifest: &PaneManifest) -> bool { + self.execution.update_exited_command_statuses(pane_manifest) + } + pub fn update_sequence_stopped_state(&mut self) -> bool { + self.execution.update_sequence_stopped_state() + } + + pub fn cycle_chain_type(&mut self) { + self.current_selected_command_mut() + .map(|c| c.cycle_chain_type()); + } + + pub fn set_primary_pane_id_before_sequence(&mut self, pane_id: Option) { + if pane_id.is_some() { + self.execution.primary_pane_id_before_sequence = pane_id; + } + } + pub fn clear_all_commands(&mut self) { + self.execution.all_commands = vec![CommandEntry::new("", self.cwd.clone())]; + self.editing.editing_input = Some(TextInput::new("".to_owned())); + self.selection.current_selected_command_index = Some(0); + self.execution.current_running_command_index = 0; + } + pub fn update_running_state(&mut self, primary_pane_id: Option) { + self.remove_empty_commands(); + self.set_primary_pane_id_before_sequence(primary_pane_id); + self.layout.needs_reposition = true; + self.execution.is_running = true; + } + + pub fn can_run_sequence(&self) -> bool { + self.execution.can_run_sequence() + } + + pub fn copy_to_clipboard(&mut self) { + self.execution.copy_to_clipboard(); + } + + pub fn current_selected_command_mut(&mut self) -> Option<&mut CommandEntry> { + let Some(i) = self.selection.current_selected_command_index else { + return None; + }; + self.execution.all_commands.get_mut(i) + } + pub fn reposition_plugin(&mut self) -> bool { + let mut repositioned = false; + let Some(plugin_id) = self.plugin_id else { + return repositioned; + }; + let Some(total_viewport_rows) = self.total_viewport_rows else { + return repositioned; + }; + let Some(total_viewport_columns) = self.total_viewport_columns else { + return repositioned; + }; + let total_commands = std::cmp::max(self.execution.all_commands.iter().len(), 1); + let height_padding = 7; + if self.all_commands_are_pending() { + let initial_height = total_commands + height_padding; + let y = total_viewport_rows.saturating_sub(initial_height) / 2; + let coordinates = FloatingPaneCoordinates::new( + Some(format!("25%")), + Some(format!("{}", y)), + Some(format!("50%")), + Some(format!("{}", initial_height)), + Some(false), // should not be pinned when running sequence + ); + coordinates.map(|coordinates| { + if Some(&coordinates) != self.current_position.as_ref() { + repositioned = true; + self.current_position = Some(coordinates.clone()); + change_floating_panes_coordinates(vec![( + PaneId::Plugin(plugin_id), + coordinates, + )]); + } + }); + } else { + // Calculate longest table row width + let longest_cwd_display = self.execution.longest_cwd_display(&self.cwd); + let longest_command = self + .execution + .all_commands + .iter() + .map(|cmd| cmd.get_text().chars().count()) + .max() + .unwrap_or(1); + + let (max_chain_width, max_status_width) = crate::ui::components::calculate_max_widths( + &self.execution.all_commands, + self.layout.spinner_frame, + ); + + let longest_line = crate::ui::components::calculate_longest_line( + &longest_cwd_display, + longest_command, + max_chain_width, + max_status_width, + ); + + let ui_width = std::cmp::max(longest_line, 50) + 4; // 2 for ui-padding, 2 for pane frame + let width = std::cmp::min(ui_width, total_viewport_columns / 2); + + let height = (height_padding + total_commands).min((total_viewport_rows * 25) / 100); + + // Position: top-right with 1-space margins + let x = total_viewport_columns.saturating_sub(width); + let y = 1; + + let coordinates = FloatingPaneCoordinates::new( + Some(x.to_string()), + Some(y.to_string()), + Some(width.to_string()), + Some(height.to_string()), + Some(true), // should be pinned for sequence + ); + coordinates.map(|coordinates| { + if Some(&coordinates) != self.current_position.as_ref() { + self.current_position = Some(coordinates.clone()); + repositioned = true; + change_floating_panes_coordinates(vec![( + PaneId::Plugin(plugin_id), + coordinates, + )]); + } + }); + } + repositioned + } + pub fn all_commands_are_pending(&self) -> bool { + self.execution + .all_commands + .iter() + .all(|command| matches!(command.get_status(), CommandStatus::Pending)) + } + + fn current_selected_command(&self) -> Option<&CommandEntry> { + let Some(i) = self.selection.current_selected_command_index else { + return None; + }; + self.execution.all_commands.get(i) + } + + pub fn execute_command_sequence(&mut self) { + self.execution + .execute_command_sequence(&self.shell, &self.cwd, self.primary_pane_id); + } +} + +impl Default for State { + fn default() -> Self { + let mut state = Self::new(); + state.is_first_run = true; + state + } +} diff --git a/default-plugins/sequence/src/state/positioning.rs b/default-plugins/sequence/src/state/positioning.rs new file mode 100644 index 000000000..6acadfff5 --- /dev/null +++ b/default-plugins/sequence/src/state/positioning.rs @@ -0,0 +1,48 @@ +use zellij_tile::prelude::*; + +pub fn reposition_plugin_for_sequence( + plugin_id: u32, + total_cols: usize, + total_rows: usize, + total_commands: usize, +) { + // Width: 30% of viewport + let width = std::cmp::max((total_cols * 30) / 100, 50); + + // Height: UI overhead + commands, capped at 25% of viewport + let overhead_rows = 6; + let height = (overhead_rows + total_commands).min((total_rows * 25) / 100); + + // Position: top-right with 1-space margins + let x = total_cols.saturating_sub(width); + let y = 1; + + change_floating_pane_coordinates_absolute( + plugin_id, + Some(x), + Some(y), + Some(width), + Some(height), + true, + ); +} + +pub fn change_floating_pane_coordinates_absolute( + own_plugin_id: u32, + x: Option, + y: Option, + width: Option, + height: Option, + should_be_pinned: bool, +) { + let coordinates = FloatingPaneCoordinates::new( + x.map(|x| x.to_string()), + y.map(|y| y.to_string()), + width.map(|width| width.to_string()), + height.map(|height| height.to_string()), + Some(should_be_pinned), + ); + if let Some(coordinates) = coordinates { + change_floating_panes_coordinates(vec![(PaneId::Plugin(own_plugin_id), coordinates)]); + } +} diff --git a/default-plugins/sequence/src/state/selection.rs b/default-plugins/sequence/src/state/selection.rs new file mode 100644 index 000000000..18f83bcd7 --- /dev/null +++ b/default-plugins/sequence/src/state/selection.rs @@ -0,0 +1,214 @@ +use crate::state::{CommandEntry, Editing}; +use std::path::PathBuf; +use zellij_tile::prelude::*; + +pub struct Selection { + pub current_selected_command_index: Option, + pub scroll_offset: usize, +} + +impl Selection { + pub fn new() -> Self { + Self { + current_selected_command_index: Some(0), + scroll_offset: 0, + } + } + + pub fn add_empty_command_after_current_selected( + &mut self, + all_commands: &mut Vec, + editing: &mut Editing, + cwd: &Option, + ) { + self.save_editing_buffer_to_current_selected(all_commands, editing); + editing.editing_input.as_mut().map(|i| i.clear()); + if !self.current_selected_command_is_empty(all_commands, editing) { + self.current_selected_command_mut(all_commands) + .map(|c| c.fill_chain_type_if_empty()); + + if let Some(current_selected_command_index) = self.current_selected_command_index.take() + { + let new_command = CommandEntry::new("", cwd.clone()); + if current_selected_command_index == all_commands.len().saturating_sub(1) { + all_commands.push(new_command); + } else { + all_commands.insert(current_selected_command_index + 1, new_command); + } + + self.current_selected_command_index = Some(current_selected_command_index + 1); + } + } + } + + pub fn current_selected_command_is_empty( + &self, + all_commands: &[CommandEntry], + editing: &Editing, + ) -> bool { + let text_input_is_empty = editing + .editing_input + .as_ref() + .map(|i| i.is_empty()) + .unwrap_or(true); + let current_selected_command_is_empty = self + .current_selected_command_index + .and_then(|i| all_commands.get(i).map(|c| c.is_empty())) + .unwrap_or(true); + text_input_is_empty && current_selected_command_is_empty + } + + pub fn remove_current_selected_command( + &mut self, + all_commands: &mut Vec, + editing: &mut Editing, + ) -> Option { + // returns the pane_id of the removed command, if any + let mut removed_pane_id = None; + let Some(mut current_selected_command_index) = self.current_selected_command_index else { + return removed_pane_id; + }; + if all_commands.len() > 1 && all_commands.len() > current_selected_command_index { + let command = all_commands.remove(current_selected_command_index); + removed_pane_id = command.get_pane_id(); + } else { + self.clear_current_selected_command(all_commands, editing); + } + if current_selected_command_index >= all_commands.len() + && current_selected_command_index > 0 + { + current_selected_command_index -= 1; + } + self.update_current_selected_command_index( + current_selected_command_index, + all_commands, + editing, + ); + if current_selected_command_index == all_commands.len().saturating_sub(1) { + self.current_selected_command_mut(all_commands) + .map(|c| c.clear_chain_type()); + } + removed_pane_id + } + + pub fn clear_current_selected_command( + &mut self, + all_commands: &mut [CommandEntry], + editing: &mut Editing, + ) { + let Some(current_selected_command_index) = self.current_selected_command_index else { + return; + }; + self.current_selected_command_mut(all_commands) + .map(|c| c.clear_text()); + self.clear_editing_buffer(editing); + if current_selected_command_index == all_commands.len().saturating_sub(1) { + self.current_selected_command_mut(all_commands) + .map(|c| c.clear_chain_type()); + } + } + + pub fn move_up(&mut self, all_commands: &mut [CommandEntry], editing: &mut Editing) { + self.save_editing_buffer_to_current_selected(all_commands, editing); + match self.current_selected_command_index.as_mut() { + Some(i) if *i > 0 => { + *i -= 1; + }, + None => { + self.current_selected_command_index = Some(all_commands.len().saturating_sub(1)); + }, + _ => { + self.current_selected_command_index = None; + }, + } + self.update_editing_buffer_to_current_selected(all_commands, editing); + } + + pub fn move_down(&mut self, all_commands: &mut [CommandEntry], editing: &mut Editing) { + self.save_editing_buffer_to_current_selected(all_commands, editing); + match self.current_selected_command_index.as_mut() { + Some(i) if *i < all_commands.len().saturating_sub(1) => { + *i += 1; + }, + None => { + self.current_selected_command_index = Some(0); + }, + _ => { + self.current_selected_command_index = None; + }, + } + self.update_editing_buffer_to_current_selected(all_commands, editing); + } + + fn current_selected_command_mut<'a>( + &self, + all_commands: &'a mut [CommandEntry], + ) -> Option<&'a mut CommandEntry> { + let Some(i) = self.current_selected_command_index else { + return None; + }; + all_commands.get_mut(i) + } + + fn current_selected_command<'a>( + &self, + all_commands: &'a [CommandEntry], + ) -> Option<&'a CommandEntry> { + let Some(i) = self.current_selected_command_index else { + return None; + }; + all_commands.get(i) + } + + fn clear_editing_buffer(&self, editing: &mut Editing) { + editing.editing_input.as_mut().map(|c| c.clear()); + } + + fn update_current_selected_command_index( + &mut self, + new_index: usize, + all_commands: &[CommandEntry], + editing: &mut Editing, + ) { + self.current_selected_command_index = Some(new_index); + self.update_editing_buffer_to_current_selected(all_commands, editing); + } + + fn get_text_of_current_selected_command( + &self, + all_commands: &[CommandEntry], + ) -> Option { + self.current_selected_command(all_commands) + .map(|c| c.get_text()) + } + + fn save_editing_buffer_to_current_selected( + &self, + all_commands: &mut [CommandEntry], + editing: &mut Editing, + ) { + if let Some(text_input) = editing.editing_input.as_ref().map(|i| i.get_text()) { + self.current_selected_command_mut(all_commands) + .map(|c| c.set_text(text_input.to_owned())); + } + } + + fn update_editing_buffer_to_current_selected( + &self, + all_commands: &[CommandEntry], + editing: &mut Editing, + ) { + let new_text_of_current_selected_command = self + .get_text_of_current_selected_command(all_commands) + .unwrap_or_else(|| String::new()); + if let Some(editing_input) = editing.editing_input.as_mut() { + editing_input.set_text(new_text_of_current_selected_command); + } + } +} + +impl Default for Selection { + fn default() -> Self { + Self::new() + } +} diff --git a/default-plugins/sequence/src/ui/components.rs b/default-plugins/sequence/src/ui/components.rs new file mode 100644 index 000000000..3fe2ae1a0 --- /dev/null +++ b/default-plugins/sequence/src/ui/components.rs @@ -0,0 +1,596 @@ +use crate::path_formatting::format_cwd; +use crate::state::{CommandStatus, State}; +use crate::ui::truncation::{calculate_available_cmd_width, truncate_middle}; +use std::path::PathBuf; +use zellij_tile::prelude::*; + +const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"]; + +const HELP_RUNNING_WITH_SELECTION: &str = " - interrupt, - run from selected"; +const HELP_RUNNING_NO_SELECTION: &str = " - interrupt, <↓↑> - navigate"; +const HELP_STOPPED_WITH_SELECTION: &str = + " - close all, - run from selected, - edit"; +const HELP_STOPPED_NO_SELECTION: &str = + " - close all, <↓↑> - navigate, - run from first"; + +const HELP_ONE_PENDING_COMMAND: &str = " - run, - add command"; + +const HELP_ALL_COMMANDS_PENDING: &str = + " - run, - add command, <↓↑> - navigate"; +const HELP_ALL_COMMANDS_PENDING_WITH_SELECTION: &str = + " - run, - add command, <↓↑> - navigate, - edit selected"; + +const HELP_EDITING_FIRST_LINE: &str = + " - accept, - add command, <↓↑> - navigate"; + +fn select_help_text(sequence: &State) -> &'static str { + let is_running = sequence + .execution + .all_commands + .iter() + .any(|command| matches!(command.get_status(), CommandStatus::Running(_))); + let all_pending = sequence + .execution + .all_commands + .iter() + .all(|command| matches!(command.get_status(), CommandStatus::Pending)); + let has_selection = sequence.selection.current_selected_command_index.is_some(); + let is_editing = sequence.editing.editing_input.is_some(); + + if all_pending && !sequence.execution.is_running { + if sequence.execution.all_commands.len() == 1 { + HELP_ONE_PENDING_COMMAND + } else if has_selection && !is_editing { + HELP_ALL_COMMANDS_PENDING_WITH_SELECTION + } else if is_editing { + HELP_EDITING_FIRST_LINE + } else { + HELP_ALL_COMMANDS_PENDING + } + } else if is_running { + if has_selection { + HELP_RUNNING_WITH_SELECTION + } else { + HELP_RUNNING_NO_SELECTION + } + } else { + if has_selection && !is_editing { + HELP_STOPPED_WITH_SELECTION + } else if is_editing { + HELP_EDITING_FIRST_LINE + } else { + HELP_STOPPED_NO_SELECTION + } + } +} + +fn style_help_text(text: &str) -> Text { + Text::new(text) + .color_substring(3, "") + .color_substring(3, "") + .color_substring(3, "<↓↑>") + .color_substring(3, "") + .color_substring(3, "") + .color_substring(3, "") + .color_substring(3, "") +} + +pub fn render_help_lines( + sequence: &State, + max_width: Option, +) -> (Text, usize, Option<(Text, usize)>) { + let is_editing = sequence.editing.editing_input.is_some(); + + if is_editing { + let help_text = select_help_text(sequence); + let (truncated_text, help_len) = if let Some(width) = max_width { + truncate_help_line(help_text, width) + } else { + (help_text.to_string(), help_text.chars().count()) + }; + let first_line = (style_help_text(&truncated_text).unbold_all(), help_len); + + let editing_help_text = "Navigate with cd, chain with ||, &&, ; "; + let editing_help = Text::new(editing_help_text) + .color_substring(3, "cd") + .color_substring(3, "||") + .color_substring(3, "&&") + .color_substring(3, ";") + .unbold_all(); + let editing_len = editing_help_text.len(); + + return ( + first_line.0, + first_line.1, + Some((editing_help, editing_len)), + ); + } + + let help_text = select_help_text(sequence); + + if let Some(width) = max_width { + if let Some((first_line, second_line)) = split_help_line(help_text, width) { + let first_len = first_line.chars().count(); + let first_styled = style_help_text(&first_line).unbold_all(); + + let second_line_width = second_line.chars().count(); + let (second_styled, second_len) = if second_line_width > width { + let (truncated, len) = truncate_help_line(&second_line, width); + (style_help_text(&truncated).unbold_all(), len) + } else { + ( + style_help_text(&second_line).unbold_all(), + second_line_width, + ) + }; + + return (first_styled, first_len, Some((second_styled, second_len))); + } + + let (truncated_text, help_len) = truncate_help_line(help_text, width); + ( + style_help_text(&truncated_text).unbold_all(), + help_len, + None, + ) + } else { + let help_len = help_text.chars().count(); + (style_help_text(help_text).unbold_all(), help_len, None) + } +} + +fn split_help_line(help_text: &str, max_width: usize) -> Option<(String, String)> { + let text_width = help_text.chars().count(); + + if text_width <= max_width { + return None; + } + + let keybindings = parse_help_keybindings(help_text); + + if keybindings.is_empty() { + return None; + } + + for split_point in 1..keybindings.len() { + let first_part: Vec = keybindings + .iter() + .take(split_point) + .map(|(key, _, action)| format!("{} - {}", key, action)) + .collect(); + let first_line = first_part.join(", "); + + let second_part: Vec = keybindings + .iter() + .skip(split_point) + .map(|(key, _, action)| format!("{} - {}", key, action)) + .collect(); + let second_line = second_part.join(", "); + + if first_line.chars().count() <= max_width && second_line.chars().count() <= max_width { + return Some((first_line, second_line)); + } + } + + None +} + +fn folder_cell(cwd: &Option) -> (Text, String) { + let cwd_display = if let Some(cwd) = cwd { + format_cwd(cwd) + } else { + "~".to_string() + }; + let folder_display = format!("{} >", cwd_display); + let text = Text::new(&folder_display).color_range(0, 0..cwd_display.len()); + (text, folder_display) +} + +fn command_cell(text: &str) -> Text { + let text = if text.is_empty() { " " } else { text }; + Text::new(text) +} + +fn chain_cell(chain_type: &crate::state::ChainType) -> Text { + let text = chain_type_to_str(chain_type); + if !text.is_empty() { + Text::new(text).color_range(1, 0..text.len()) + } else { + Text::new(text) + } +} + +fn get_spinner_frame(frame: usize) -> &'static str { + SPINNER_FRAMES[frame % SPINNER_FRAMES.len()] +} + +pub fn format_status_text(status: &CommandStatus, spinner_frame: usize) -> String { + match status { + CommandStatus::Running(_) => { + let spinner = get_spinner_frame(spinner_frame); + format!("[RUNNING] {}", spinner) + }, + CommandStatus::Interrupted(_) => "[INTERRUPTED]".to_string(), + CommandStatus::Exited(Some(code), _) => format!("[EXIT CODE: {}]", code), + CommandStatus::Exited(None, _) => "[EXITED]".to_string(), + CommandStatus::Pending => " ".to_string(), + } +} + +fn apply_status_color(text: Text, status: &CommandStatus, status_str: &str) -> Text { + match status { + CommandStatus::Running(_) => text.color_range(3, 0..status_str.len()), + CommandStatus::Interrupted(_) => text.error_color_range(0..status_str.len()), + CommandStatus::Exited(Some(0), _) => { + let number_start = 12; + let number_end = status_str.len() - 1; + text.success_color_range(number_start..number_end) + }, + CommandStatus::Exited(Some(_), _) => { + let number_start = 12; + let number_end = status_str.len() - 1; + text.error_color_range(number_start..number_end) + }, + _ => text, + } +} + +fn status_cell(status: &CommandStatus, spinner_frame: usize) -> Text { + let status_str = format_status_text(status, spinner_frame); + let text = Text::new(&status_str); + apply_status_color(text, status, &status_str) +} + +fn apply_row_styles(cells: Vec, is_selected: bool) -> Vec { + let mut styled_cells = cells; + + if is_selected { + styled_cells = styled_cells.into_iter().map(|c| c.selected()).collect(); + } + + styled_cells +} + +fn overflow_cell(indicator: &str, is_selected: bool) -> Text { + let mut text = Text::new(indicator).dim_all(); + if is_selected { + text = text.selected(); + } + text +} + +fn command_sequence_row( + state: &State, + cwd: &Option, + truncated_cmd_text: &str, + chain_type: &crate::state::ChainType, + status: &crate::state::CommandStatus, + is_selected: bool, + overflow_indicator: Option, +) -> (Vec, usize) { + let (folder_text, folder_display) = folder_cell(cwd); + + let mut cells = vec![ + folder_text, + command_cell(truncated_cmd_text), + chain_cell(chain_type), + status_cell(status, state.layout.spinner_frame), + ]; + + cells = apply_row_styles(cells, is_selected); + + let truncated_cmd_text = if truncated_cmd_text.is_empty() { + " " + } else { + truncated_cmd_text + }; + let mut row_length = folder_display.chars().count() + + truncated_cmd_text.chars().count() + + chain_type_to_str(chain_type).chars().count() + + format_status_text(status, state.layout.spinner_frame) + .chars() + .count() + + 3; + + if let Some(indicator) = overflow_indicator { + let indicator_len = indicator.chars().count(); + cells.push(overflow_cell(&indicator, is_selected)); + row_length += indicator_len + 1; + } + + (cells, row_length) +} + +pub fn build_table_header(has_overflow: bool) -> Table { + let header_cols = if has_overflow { + vec![" ", " ", " ", " ", " "] + } else { + vec![" ", " ", " ", " "] + }; + Table::new().add_row(header_cols) +} + +fn calculate_overflow_indicator( + visible_idx: usize, + visible_count: usize, + hidden_above: usize, + hidden_below: usize, +) -> Option { + if visible_idx == 0 && hidden_above > 0 { + Some(format!("[+{}]", hidden_above)) + } else if visible_idx == visible_count.saturating_sub(1) && hidden_below > 0 { + Some(format!("[+{}]", hidden_below)) + } else if hidden_above > 0 || hidden_below > 0 { + Some(format!(" ")) + } else { + None + } +} + +pub fn calculate_row_layout_info( + index: usize, + offset: usize, + visible_count: usize, + hidden_above: usize, + hidden_below: usize, + cols: usize, + folder_width: usize, + max_chain_width: usize, + max_status_width: usize, +) -> Option<(Option, usize)> { + if index < offset || index >= offset + visible_count { + return None; + } + + let visible_idx = index.saturating_sub(offset); + let overflow_indicator = + calculate_overflow_indicator(visible_idx, visible_count, hidden_above, hidden_below); + + let available_cmd_width = calculate_available_cmd_width( + cols, + folder_width, + overflow_indicator.as_ref(), + max_chain_width, + max_status_width, + ); + + Some((overflow_indicator, available_cmd_width)) +} + +pub fn add_command_row( + table: Table, + state: &State, + index: usize, + offset: usize, + visible_count: usize, + hidden_above: usize, + hidden_below: usize, +) -> Table { + let command = match state.execution.all_commands.get(index) { + Some(cmd) => cmd, + None => return table, + }; + + let cmd_text = &command.get_text(); + let chain_type = &command.get_chain_type(); + let status = &command.get_status(); + let command_cwd = command.get_cwd().or_else(|| state.cwd.clone()); + + let cols = state.own_columns.unwrap_or(80); + let longest_cwd_display = state.execution.longest_cwd_display(&state.cwd); + let folder_display = format!("{} >", longest_cwd_display); + let folder_width = folder_display.chars().count(); + + let (max_chain_width, max_status_width) = + calculate_max_widths(&state.execution.all_commands, state.layout.spinner_frame); + + let Some((overflow_indicator, available_cmd_width)) = calculate_row_layout_info( + index, + offset, + visible_count, + hidden_above, + hidden_below, + cols, + folder_width, + max_chain_width, + max_status_width, + ) else { + return table; + }; + + if let Some(text_input) = &state.editing.editing_input { + if state.selection.current_selected_command_index == Some(index) { + let (truncated_editing_text, _) = truncate_middle( + text_input.get_text(), + available_cmd_width, + Some(text_input.cursor_position()), + ); + let (row, _) = command_sequence_row( + state, + &command_cwd, + &truncated_editing_text, + chain_type, + &CommandStatus::Pending, + false, + overflow_indicator, + ); + return table.add_styled_row(row); + } + } + + let truncated_cmd_text = truncate_middle(cmd_text, available_cmd_width, None).0; + + let (row, _) = command_sequence_row( + state, + &command_cwd, + &truncated_cmd_text, + chain_type, + status, + state.selection.current_selected_command_index == Some(index), + overflow_indicator, + ); + + table.add_styled_row(row) +} + +pub fn calculate_max_widths( + commands: &[crate::state::CommandEntry], + spinner_frame: usize, +) -> (usize, usize) { + let max_chain_width = commands + .iter() + .map(|cmd| chain_type_to_str(&cmd.get_chain_type()).chars().count()) + .max() + .unwrap_or(0); + + let max_status_width = commands + .iter() + .map(|cmd| { + format_status_text(&cmd.get_status(), spinner_frame) + .chars() + .count() + }) + .max() + .unwrap_or(0); + + (max_chain_width, max_status_width) +} + +pub fn calculate_longest_line( + longest_cwd_display: &str, + longest_command: usize, + max_chain_width: usize, + max_status_width: usize, +) -> usize { + let folder_display = format!("{} >", longest_cwd_display); + let cell_padding = 3; + folder_display.chars().count() + + longest_command + + max_chain_width + + max_status_width + + cell_padding +} + +pub fn chain_type_to_str(chain_type: &crate::state::ChainType) -> &'static str { + match chain_type { + crate::state::ChainType::And => "AND", + crate::state::ChainType::Or => "OR", + crate::state::ChainType::Then => "THEN", + crate::state::ChainType::None => " ", + } +} + +pub fn truncate_help_line(help_text: &str, max_width: usize) -> (String, usize) { + let text_width = help_text.chars().count(); + + if text_width <= max_width { + return (help_text.to_string(), text_width); + } + + let keybindings = parse_help_keybindings(help_text); + + if let Some(result) = try_shortened_help(&keybindings, max_width) { + return result; + } + + if let Some(result) = try_keys_spaced(&keybindings, max_width) { + return result; + } + + if let Some(result) = try_keys_tight(&keybindings, max_width) { + return result; + } + + truncate_with_ellipsis( + &keybindings + .iter() + .map(|(k, _, _)| *k) + .collect::>() + .join("/"), + max_width, + ) +} + +fn parse_help_keybindings(help_text: &str) -> Vec<(&str, &str, &str)> { + help_text + .split(", ") + .filter_map(|part| { + part.find(" - ").map(|dash_pos| { + let key = &part[..dash_pos]; + let action = &part[dash_pos + 3..]; + let first_word = action.split_whitespace().next().unwrap_or(action); + (key, first_word, action) + }) + }) + .collect() +} + +fn try_shortened_help( + keybindings: &[(&str, &str, &str)], + max_width: usize, +) -> Option<(String, usize)> { + let shortened: Vec = keybindings + .iter() + .map(|(key, first_word, _)| format!("{} - {}", key, first_word)) + .collect(); + let shortened_text = shortened.join(", "); + let shortened_width = shortened_text.chars().count(); + + if shortened_width <= max_width { + Some((shortened_text, shortened_width)) + } else { + None + } +} + +fn try_keys_spaced( + keybindings: &[(&str, &str, &str)], + max_width: usize, +) -> Option<(String, usize)> { + let keys_spaced_text = keybindings + .iter() + .map(|(key, _, _)| *key) + .collect::>() + .join(" / "); + let keys_spaced_width = keys_spaced_text.chars().count(); + + if keys_spaced_width <= max_width { + Some((keys_spaced_text, keys_spaced_width)) + } else { + None + } +} + +fn try_keys_tight(keybindings: &[(&str, &str, &str)], max_width: usize) -> Option<(String, usize)> { + let keys_tight_text = keybindings + .iter() + .map(|(key, _, _)| *key) + .collect::>() + .join("/"); + let keys_tight_width = keys_tight_text.chars().count(); + + if keys_tight_width <= max_width { + Some((keys_tight_text, keys_tight_width)) + } else { + None + } +} + +fn truncate_with_ellipsis(text: &str, max_width: usize) -> (String, usize) { + let available = max_width.saturating_sub(3); + let mut truncated = String::new(); + let mut current_width = 0; + + for ch in text.chars() { + if current_width + 1 <= available { + truncated.push(ch); + current_width += 1; + } else { + break; + } + } + + truncated.push_str("..."); + let truncated_width = truncated.chars().count(); + (truncated, truncated_width) +} diff --git a/default-plugins/sequence/src/ui/fuzzy_complete.rs b/default-plugins/sequence/src/ui/fuzzy_complete.rs new file mode 100644 index 000000000..0feae7e43 --- /dev/null +++ b/default-plugins/sequence/src/ui/fuzzy_complete.rs @@ -0,0 +1,321 @@ +use fuzzy_matcher::skim::SkimMatcherV2; +use fuzzy_matcher::FuzzyMatcher; +use std::collections::BTreeMap; +use std::fs; +use std::path::PathBuf; + +/// Result of a fuzzy completion operation +#[derive(Debug, Clone)] +pub struct CompletionResult { + pub completed_text: String, + pub score: i64, + pub is_directory: bool, + pub is_prefix_completion: bool, // True if this is the shortest of multiple matches with same prefix +} + +/// Type of completion +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CompletionType { + Command, + Path, +} + +/// Fuzzy match executables (commands) from the PATH +pub fn fuzzy_complete_command( + query: &str, + executables: &BTreeMap, +) -> Option { + if query.is_empty() { + return None; + } + + let matcher = SkimMatcherV2::default().ignore_case(); + + // Collect all matches with their scores + let mut all_matches: Vec<(String, i64)> = Vec::new(); + + for executable_name in executables.keys() { + if let Some((score, _indices)) = matcher.fuzzy_indices(executable_name, query) { + all_matches.push((executable_name.clone(), score)); + } + } + + if all_matches.is_empty() { + return None; + } + + // Sort by score (highest first), then by length (shortest first) + all_matches.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.len().cmp(&b.0.len()))); + + // Get the best match + let (best_name, best_score) = &all_matches[0]; + + // Check if there are multiple matches that share the same prefix + let is_prefix_completion = check_for_prefix_matches(&all_matches, best_name); + + Some(CompletionResult { + completed_text: best_name.clone(), + score: *best_score, + is_directory: false, // Commands are not directories + is_prefix_completion, + }) +} + +/// Fuzzy match paths (directories and files) relative to the current working directory +pub fn fuzzy_complete_path(query: &str, cwd: Option<&PathBuf>) -> Option { + if query.is_empty() { + return None; + } + + let matcher = SkimMatcherV2::default().ignore_case(); + + // Determine the base directory to search in + let (base_dir, search_prefix) = if query.starts_with('/') { + // Absolute path + let parts: Vec<&str> = query.rsplitn(2, '/').collect(); + if parts.len() == 2 { + // Has a directory component + let dir = parts[1]; + let prefix = parts[0]; + (PathBuf::from(dir), prefix.to_string()) + } else { + // Just "/" + (PathBuf::from("/"), String::new()) + } + } else if query.starts_with("~/") { + // Home directory + let home_dir = std::env::var("HOME").ok()?; + let query_without_tilde = &query[2..]; + let parts: Vec<&str> = query_without_tilde.rsplitn(2, '/').collect(); + if parts.len() == 2 { + let dir = PathBuf::from(&home_dir).join(parts[1]); + let prefix = parts[0]; + (dir, prefix.to_string()) + } else { + // Just "~/" or "~/something" + (PathBuf::from(home_dir), query_without_tilde.to_string()) + } + } else { + // Relative path + let base = cwd?; + let parts: Vec<&str> = query.rsplitn(2, '/').collect(); + if parts.len() == 2 { + // Has a directory component + let dir = base.join(parts[1]); + let prefix = parts[0]; + (dir, prefix.to_string()) + } else { + // Just a name in the current directory + (base.clone(), query.to_string()) + } + }; + + // Convert to host path + let host_base_dir = + PathBuf::from("/host").join(base_dir.strip_prefix("/").unwrap_or(&base_dir)); + + // Read directory entries + let entries = fs::read_dir(&host_base_dir).ok()?; + + // Collect all matches with their scores and directory status + let mut all_matches: Vec<(String, i64, bool)> = Vec::new(); + + for entry in entries.flatten() { + if let Some(name) = entry.file_name().to_str() { + // Skip hidden files unless the query starts with a dot + if name.starts_with('.') && !search_prefix.starts_with('.') { + continue; + } + + if let Some((score, _indices)) = matcher.fuzzy_indices(name, &search_prefix) { + // Check if this entry is a directory + let is_dir = entry.metadata().ok().map(|m| m.is_dir()).unwrap_or(false); + all_matches.push((name.to_string(), score, is_dir)); + } + } + } + + if all_matches.is_empty() { + return None; + } + + // Sort by score (highest first), then by length (shortest first) + all_matches.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.len().cmp(&b.0.len()))); + + // Get the best match + let (name, score, is_directory) = all_matches[0].clone(); + + // Check if there are multiple matches that share the same prefix + let name_matches: Vec<(String, i64)> = all_matches + .iter() + .map(|(n, s, _)| (n.clone(), *s)) + .collect(); + let is_prefix_completion = check_for_prefix_matches(&name_matches, &name); + + // Reconstruct the full path + let completed_text = if query.starts_with('/') { + // Absolute path + let dir_part = query.rsplitn(2, '/').nth(1).unwrap_or(""); + if dir_part.is_empty() { + format!("/{}", name) + } else { + format!("{}/{}", dir_part, name) + } + } else if query.starts_with("~/") { + // Home directory + let query_without_tilde = &query[2..]; + let dir_part = query_without_tilde.rsplitn(2, '/').nth(1).unwrap_or(""); + if dir_part.is_empty() { + format!("~/{}", name) + } else { + format!("~/{}/{}", dir_part, name) + } + } else { + // Relative path + let dir_part = query.rsplitn(2, '/').nth(1).unwrap_or(""); + if dir_part.is_empty() { + name.clone() + } else { + format!("{}/{}", dir_part, name) + } + }; + + Some(CompletionResult { + completed_text, + score, + is_directory, + is_prefix_completion, + }) +} + +/// Check if there are multiple matches that start with the best match's name +/// Returns true if the best match is a prefix of other matches +fn check_for_prefix_matches(matches: &[(String, i64)], best_match: &str) -> bool { + if matches.len() <= 1 { + return false; + } + + // Count how many matches start with the best_match text + let prefix_count = matches + .iter() + .filter(|(name, _)| name.starts_with(best_match) && name != best_match) + .count(); + + prefix_count > 0 +} + +/// Perform fuzzy completion on the current input +/// Returns the best completion (command or path) with appropriate suffix (space or slash) +pub fn fuzzy_complete( + input: &str, + executables: &BTreeMap, + cwd: Option<&PathBuf>, +) -> Option { + // If input is empty, nothing to complete + if input.is_empty() { + return None; + } + + // Extract the last word/token to complete + // For simplicity, we'll complete the entire input if it's a single word, + // or the last space-separated token if there are multiple words + let tokens: Vec<&str> = input.split_whitespace().collect(); + let to_complete = if tokens.is_empty() { + input + } else { + tokens.last().unwrap() + }; + + // Get both command and path completions + let command_result = fuzzy_complete_command(to_complete, executables); + let path_result = fuzzy_complete_path(to_complete, cwd); + + // Choose the better match based on score + match (command_result, path_result) { + (Some(cmd), Some(path)) => { + // Both matched, use the higher score + let (result, completion_type, is_dir, is_prefix) = if cmd.score >= path.score { + ( + cmd.completed_text, + CompletionType::Command, + false, + cmd.is_prefix_completion, + ) + } else { + ( + path.completed_text, + CompletionType::Path, + path.is_directory, + path.is_prefix_completion, + ) + }; + + // Add appropriate suffix (skip slash if this is a prefix completion) + let suffix = get_completion_suffix(completion_type, is_dir, is_prefix); + let completed = format!("{}{}", result, suffix); + + // Replace the last token with the completion + Some(replace_last_token(input, &completed)) + }, + (Some(cmd), None) => { + // Command completion - add space (unless it's a prefix completion) + let suffix = if cmd.is_prefix_completion { "" } else { " " }; + let completed = format!("{}{}", cmd.completed_text, suffix); + Some(replace_last_token(input, &completed)) + }, + (None, Some(path)) => { + // Path completion - add slash if directory (unless it's a prefix completion) + let suffix = if path.is_prefix_completion { + "" + } else if path.is_directory { + "/" + } else { + "" + }; + let completed = format!("{}{}", path.completed_text, suffix); + Some(replace_last_token(input, &completed)) + }, + (None, None) => None, + } +} + +/// Get the appropriate suffix for a completion +fn get_completion_suffix( + completion_type: CompletionType, + is_directory: bool, + is_prefix_completion: bool, +) -> &'static str { + // No suffix for prefix completions + if is_prefix_completion { + return ""; + } + + match completion_type { + CompletionType::Command => " ", + CompletionType::Path => { + if is_directory { + "/" + } else { + "" + } + }, + } +} + +/// Replace the last whitespace-separated token in the input with the completion +fn replace_last_token(input: &str, completion: &str) -> String { + let tokens: Vec<&str> = input.split_whitespace().collect(); + if tokens.is_empty() { + return completion.to_string(); + } + + if tokens.len() == 1 { + completion.to_string() + } else { + // Join all tokens except the last one, then add the completion + let mut result = tokens[..tokens.len() - 1].join(" "); + result.push(' '); + result.push_str(completion); + result + } +} diff --git a/default-plugins/sequence/src/ui/layout_calculations.rs b/default-plugins/sequence/src/ui/layout_calculations.rs new file mode 100644 index 000000000..911dbc627 --- /dev/null +++ b/default-plugins/sequence/src/ui/layout_calculations.rs @@ -0,0 +1,28 @@ +pub fn calculate_viewport( + total_commands: usize, + max_visible: usize, + selected_index: Option, + running_index: usize, +) -> (usize, usize, usize, usize) { + if total_commands <= max_visible { + return (0, total_commands, 0, 0); + } + + let focus_index = selected_index.unwrap_or(running_index); + let half_visible = max_visible / 2; + + let offset = if focus_index < half_visible { + 0 + } else if focus_index >= total_commands.saturating_sub(half_visible) { + total_commands.saturating_sub(max_visible) + } else { + focus_index.saturating_sub(half_visible) + }; + + let offset = offset.min(total_commands.saturating_sub(max_visible)); + let visible_count = max_visible.min(total_commands.saturating_sub(offset)); + let hidden_above = offset; + let hidden_below = total_commands.saturating_sub(offset + visible_count); + + (offset, visible_count, hidden_above, hidden_below) +} diff --git a/default-plugins/sequence/src/ui/mod.rs b/default-plugins/sequence/src/ui/mod.rs new file mode 100644 index 000000000..6d1ec3035 --- /dev/null +++ b/default-plugins/sequence/src/ui/mod.rs @@ -0,0 +1,5 @@ +pub mod components; +pub mod fuzzy_complete; +pub mod layout_calculations; +pub mod text_input; +pub mod truncation; diff --git a/default-plugins/sequence/src/ui/text_input.rs b/default-plugins/sequence/src/ui/text_input.rs new file mode 100644 index 000000000..2694e17ae --- /dev/null +++ b/default-plugins/sequence/src/ui/text_input.rs @@ -0,0 +1,997 @@ +use zellij_tile::prelude::*; + +const MAX_UNDO_STACK_SIZE: usize = 100; + +/// Action returned by TextInput after handling a key event +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(test, derive(Eq))] +pub enum InputAction { + /// Continue editing + Continue, + /// User pressed Enter to submit + Submit, + /// User pressed Esc to cancel + Cancel, + /// User pressed Tab to request completion + Complete, + /// Key was not handled by the input + NoAction, +} + +/// A reusable text input component with cursor support and standard editing keybindings +#[derive(Debug, Clone)] +pub struct TextInput { + buffer: String, + cursor_position: usize, // Character position (0-based), NOT byte position + undo_stack: Vec<(String, usize)>, // (buffer, cursor) snapshots + redo_stack: Vec<(String, usize)>, + last_edit_was_insert: bool, // For coalescing consecutive inserts +} + +impl TextInput { + /// Create a new TextInput with the given initial text + /// Cursor is positioned at the end of the text + pub fn new(initial_text: String) -> Self { + let cursor_position = initial_text.chars().count(); + Self { + buffer: initial_text, + cursor_position, + undo_stack: Vec::new(), + redo_stack: Vec::new(), + last_edit_was_insert: false, + } + } + + /// Create an empty TextInput + pub fn empty() -> Self { + Self::new(String::new()) + } + + /// Get the current text + pub fn get_text(&self) -> &str { + &self.buffer + } + + /// Get the cursor position (in characters, not bytes) + pub fn get_cursor_position(&self) -> usize { + self.cursor_position + } + + /// Check if the input is empty + pub fn is_empty(&self) -> bool { + self.buffer.is_empty() + } + + /// Get a shorthand for cursor_position + pub fn cursor_position(&self) -> usize { + self.cursor_position + } + + /// Get mutable access to the underlying buffer for direct manipulation + pub fn get_text_mut(&mut self) -> &mut String { + &mut self.buffer + } + + /// Set the text and move cursor to the end + pub fn set_text(&mut self, text: String) { + self.break_coalescing(); + self.save_undo_state(); + self.cursor_position = text.chars().count(); + self.buffer = text; + } + + /// Set cursor position (clamped to text length) + pub fn set_cursor_position(&mut self, pos: usize) { + let text_len = self.buffer.chars().count(); + self.cursor_position = pos.min(text_len); + } + + /// Clear all text and reset cursor + pub fn clear(&mut self) { + self.break_coalescing(); + self.save_undo_state(); + self.buffer.clear(); + self.cursor_position = 0; + } + + /// Insert a character at the current cursor position + pub fn insert_char(&mut self, c: char) { + self.save_undo_state_unless_coalescing(); + // Convert cursor position (char index) to byte index + let byte_index = self.char_index_to_byte_index(self.cursor_position); + self.buffer.insert(byte_index, c); + self.cursor_position += 1; + } + + /// Delete the character before the cursor (backspace) + pub fn backspace(&mut self) { + if self.cursor_position > 0 { + self.break_coalescing(); + self.save_undo_state(); + self.cursor_position -= 1; + let byte_index = self.char_index_to_byte_index(self.cursor_position); + self.buffer.remove(byte_index); + } + } + + /// Delete the character at the cursor position (delete key) + pub fn delete(&mut self) { + let len = self.buffer.chars().count(); + if self.cursor_position < len { + self.break_coalescing(); + self.save_undo_state(); + let byte_index = self.char_index_to_byte_index(self.cursor_position); + self.buffer.remove(byte_index); + } + } + + /// Delete the word before the cursor (Ctrl/Alt + Backspace) + pub fn delete_word_backward(&mut self) { + if self.cursor_position == 0 { + return; + } + + self.break_coalescing(); + self.save_undo_state(); + + let old_position = self.cursor_position; + self.move_word_left(); + let new_position = self.cursor_position; + + // Delete from new position to old position + let start_byte = self.char_index_to_byte_index(new_position); + let end_byte = self.char_index_to_byte_index(old_position); + self.buffer.drain(start_byte..end_byte); + } + + /// Delete the word after the cursor (Ctrl/Alt + Delete) + pub fn delete_word_forward(&mut self) { + let chars: Vec = self.buffer.chars().collect(); + let len = chars.len(); + + if self.cursor_position >= len { + return; + } + + self.break_coalescing(); + self.save_undo_state(); + + let start_position = self.cursor_position; + let mut end_position = start_position; + + // Skip the current word + while end_position < len && !chars[end_position].is_whitespace() { + end_position += 1; + } + + // Skip any whitespace after the word + while end_position < len && chars[end_position].is_whitespace() { + end_position += 1; + } + + // Delete from start to end position + let start_byte = self.char_index_to_byte_index(start_position); + let end_byte = self.char_index_to_byte_index(end_position); + self.buffer.drain(start_byte..end_byte); + } + + /// Move cursor one position to the left + pub fn move_left(&mut self) { + if self.cursor_position > 0 { + self.break_coalescing(); + self.cursor_position -= 1; + } + } + + /// Move cursor one position to the right + pub fn move_right(&mut self) { + let len = self.buffer.chars().count(); + if self.cursor_position < len { + self.break_coalescing(); + self.cursor_position += 1; + } + } + + /// Move cursor to the start of the text (Ctrl-A / Home) + pub fn move_to_start(&mut self) { + self.break_coalescing(); + self.cursor_position = 0; + } + + /// Move cursor to the end of the text (Ctrl-E / End) + pub fn move_to_end(&mut self) { + self.break_coalescing(); + self.cursor_position = self.buffer.chars().count(); + } + + /// Move cursor to the start of the previous word (Ctrl/Alt + Left) + pub fn move_word_left(&mut self) { + if self.cursor_position == 0 { + return; + } + + self.break_coalescing(); + + let chars: Vec = self.buffer.chars().collect(); + let mut pos = self.cursor_position; + + // Skip any whitespace immediately to the left + while pos > 0 && chars[pos - 1].is_whitespace() { + pos -= 1; + } + + // Skip the word characters + while pos > 0 && !chars[pos - 1].is_whitespace() { + pos -= 1; + } + + self.cursor_position = pos; + } + + /// Move cursor to the start of the next word (Ctrl/Alt + Right) + pub fn move_word_right(&mut self) { + let chars: Vec = self.buffer.chars().collect(); + let len = chars.len(); + + if self.cursor_position >= len { + return; + } + + self.break_coalescing(); + + let mut pos = self.cursor_position; + + // Skip the current word + while pos < len && !chars[pos].is_whitespace() { + pos += 1; + } + + // Skip any whitespace + while pos < len && chars[pos].is_whitespace() { + pos += 1; + } + + self.cursor_position = pos; + } + + /// Handle a key event and return the appropriate action + /// This is the main entry point for key handling + pub fn handle_key(&mut self, key: KeyWithModifier) -> InputAction { + // Check for Ctrl modifiers + if key.has_modifiers(&[KeyModifier::Ctrl]) { + match key.bare_key { + BareKey::Char('a') => { + self.move_to_start(); + return InputAction::Continue; + }, + BareKey::Char('e') => { + self.move_to_end(); + return InputAction::Continue; + }, + BareKey::Char('c') => { + // Ctrl-C clears the prompt + return InputAction::Cancel; + }, + BareKey::Char('z') => { + // Ctrl-Z: Undo + self.undo(); + return InputAction::Continue; + }, + BareKey::Char('y') => { + // Ctrl-Y: Redo + self.redo(); + return InputAction::Continue; + }, + BareKey::Left => { + self.move_word_left(); + return InputAction::Continue; + }, + BareKey::Right => { + self.move_word_right(); + return InputAction::Continue; + }, + BareKey::Backspace => { + self.delete_word_backward(); + return InputAction::Continue; + }, + BareKey::Delete => { + self.delete_word_forward(); + return InputAction::Continue; + }, + _ => {}, + } + } + + // Check for Ctrl+Shift modifiers (alternative redo: Ctrl+Shift+Z) + if key.has_modifiers(&[KeyModifier::Ctrl, KeyModifier::Shift]) { + match key.bare_key { + BareKey::Char('Z') => { + // Ctrl-Shift-Z: Redo (alternative) + self.redo(); + return InputAction::Continue; + }, + _ => {}, + } + } + + // Check for Alt modifiers + if key.has_modifiers(&[KeyModifier::Alt]) { + match key.bare_key { + BareKey::Left => { + self.move_word_left(); + return InputAction::Continue; + }, + BareKey::Right => { + self.move_word_right(); + return InputAction::Continue; + }, + BareKey::Backspace => { + self.delete_word_backward(); + return InputAction::Continue; + }, + BareKey::Delete => { + self.delete_word_forward(); + return InputAction::Continue; + }, + _ => {}, + } + } + + // Handle bare keys (no modifiers) + match key.bare_key { + BareKey::Enter => InputAction::Submit, + BareKey::Esc => InputAction::Cancel, + BareKey::Tab => InputAction::Complete, + BareKey::Backspace => { + self.backspace(); + InputAction::Continue + }, + BareKey::Delete => { + self.delete(); + InputAction::Continue + }, + BareKey::Left => { + self.move_left(); + InputAction::Continue + }, + BareKey::Right => { + self.move_right(); + InputAction::Continue + }, + BareKey::Home => { + self.move_to_start(); + InputAction::Continue + }, + BareKey::End => { + self.move_to_end(); + InputAction::Continue + }, + BareKey::Char(c) => { + self.insert_char(c); + InputAction::Continue + }, + _ => InputAction::NoAction, + } + } + + /// Helper: Convert character index to byte index + fn char_index_to_byte_index(&self, char_index: usize) -> usize { + self.buffer + .char_indices() + .nth(char_index) + .map(|(byte_idx, _)| byte_idx) + .unwrap_or(self.buffer.len()) + } + + /// Save current state to undo stack before making changes + fn save_undo_state(&mut self) { + if self.undo_stack.len() >= MAX_UNDO_STACK_SIZE { + self.undo_stack.remove(0); + } + self.undo_stack + .push((self.buffer.clone(), self.cursor_position)); + self.redo_stack.clear(); + } + + /// Save state only if not coalescing with previous insert + fn save_undo_state_unless_coalescing(&mut self) { + if !self.last_edit_was_insert { + self.save_undo_state(); + } + self.last_edit_was_insert = true; + } + + /// Mark that a non-insert edit occurred (breaks coalescing) + fn break_coalescing(&mut self) { + self.last_edit_was_insert = false; + } + + /// Undo last change + pub fn undo(&mut self) -> bool { + if let Some((buffer, cursor)) = self.undo_stack.pop() { + self.redo_stack + .push((self.buffer.clone(), self.cursor_position)); + self.buffer = buffer; + self.cursor_position = cursor; + self.break_coalescing(); + true + } else { + false + } + } + + /// Redo last undone change + pub fn redo(&mut self) -> bool { + if let Some((buffer, cursor)) = self.redo_stack.pop() { + self.undo_stack + .push((self.buffer.clone(), self.cursor_position)); + self.buffer = buffer; + self.cursor_position = cursor; + self.break_coalescing(); + true + } else { + false + } + } + + /// Check if undo is available + pub fn can_undo(&self) -> bool { + !self.undo_stack.is_empty() + } + + /// Check if redo is available + pub fn can_redo(&self) -> bool { + !self.redo_stack.is_empty() + } + + pub fn drain_text(&mut self) -> String { + self.cursor_position = 0; + self.buffer.drain(..).collect() + } +} + +// run with: +// cargo test --lib --target x86_64-unknown-linux-gnu +// +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_and_empty() { + let input = TextInput::new("hello".to_string()); + assert_eq!(input.get_text(), "hello"); + assert_eq!(input.get_cursor_position(), 5); + + let empty = TextInput::empty(); + assert_eq!(empty.get_text(), ""); + assert_eq!(empty.get_cursor_position(), 0); + } + + #[test] + fn test_insert_char() { + let mut input = TextInput::new("helo".to_string()); + input.cursor_position = 3; // Position after "hel" + input.insert_char('l'); + assert_eq!(input.get_text(), "hello"); + assert_eq!(input.get_cursor_position(), 4); + } + + #[test] + fn test_backspace() { + let mut input = TextInput::new("hello".to_string()); + input.backspace(); + assert_eq!(input.get_text(), "hell"); + assert_eq!(input.get_cursor_position(), 4); + + // Backspace at start does nothing + input.cursor_position = 0; + input.backspace(); + assert_eq!(input.get_text(), "hell"); + assert_eq!(input.get_cursor_position(), 0); + } + + #[test] + fn test_delete() { + let mut input = TextInput::new("hello".to_string()); + input.cursor_position = 0; + input.delete(); + assert_eq!(input.get_text(), "ello"); + assert_eq!(input.get_cursor_position(), 0); + + // Delete at end does nothing + input.move_to_end(); + input.delete(); + assert_eq!(input.get_text(), "ello"); + } + + #[test] + fn test_cursor_movement() { + let mut input = TextInput::new("hello".to_string()); + assert_eq!(input.get_cursor_position(), 5); + + input.move_left(); + assert_eq!(input.get_cursor_position(), 4); + + input.move_right(); + assert_eq!(input.get_cursor_position(), 5); + + input.move_to_start(); + assert_eq!(input.get_cursor_position(), 0); + + input.move_to_end(); + assert_eq!(input.get_cursor_position(), 5); + } + + #[test] + fn test_unicode_support() { + let mut input = TextInput::new("hello 🦀 world".to_string()); + assert_eq!(input.get_cursor_position(), 13); // 13 characters + + input.cursor_position = 6; // After "hello " + input.insert_char('🐱'); + assert_eq!(input.get_text(), "hello 🐱🦀 world"); + } + + #[test] + fn test_word_jump_right() { + let mut input = TextInput::new("hello world foo bar".to_string()); + input.cursor_position = 0; + + // Jump from start to "world" + input.move_word_right(); + assert_eq!(input.get_cursor_position(), 6); // After "hello " + + // Jump to "foo" + input.move_word_right(); + assert_eq!(input.get_cursor_position(), 12); // After "world " + + // Jump to "bar" + input.move_word_right(); + assert_eq!(input.get_cursor_position(), 16); // After "foo " + + // Jump to end + input.move_word_right(); + assert_eq!(input.get_cursor_position(), 19); // At end + } + + #[test] + fn test_word_jump_left() { + let mut input = TextInput::new("hello world foo bar".to_string()); + input.move_to_end(); + assert_eq!(input.get_cursor_position(), 19); + + // Jump back to "bar" + input.move_word_left(); + assert_eq!(input.get_cursor_position(), 16); // Start of "bar" + + // Jump back to "foo" + input.move_word_left(); + assert_eq!(input.get_cursor_position(), 12); // Start of "foo" + + // Jump back to "world" + input.move_word_left(); + assert_eq!(input.get_cursor_position(), 6); // Start of "world" + + // Jump back to "hello" + input.move_word_left(); + assert_eq!(input.get_cursor_position(), 0); // Start of "hello" + } + + #[test] + fn test_word_jump_with_multiple_spaces() { + let mut input = TextInput::new("hello world".to_string()); + input.cursor_position = 0; + + // Jump over multiple spaces + input.move_word_right(); + assert_eq!(input.get_cursor_position(), 8); // After "hello ", at start of "world" + + // Jump back should skip spaces + input.move_word_left(); + assert_eq!(input.get_cursor_position(), 0); // Back to start of "hello" + } + + #[test] + fn test_word_jump_boundaries() { + let mut input = TextInput::new("test".to_string()); + + // At start - word left does nothing + input.cursor_position = 0; + input.move_word_left(); + assert_eq!(input.get_cursor_position(), 0); + + // At end - word right does nothing + input.move_to_end(); + let end_pos = input.get_cursor_position(); + input.move_word_right(); + assert_eq!(input.get_cursor_position(), end_pos); + } + + #[test] + fn test_up_down_arrows() { + let mut input = TextInput::new("hello world".to_string()); + + // Start in the middle + input.cursor_position = 5; + assert_eq!(input.get_cursor_position(), 5); + + // Up arrow should go to start + input.move_to_start(); + assert_eq!(input.get_cursor_position(), 0); + + // Move back to middle + input.cursor_position = 5; + + // Down arrow should go to end + input.move_to_end(); + assert_eq!(input.get_cursor_position(), 11); + } + + #[test] + fn test_delete_word_backward() { + let mut input = TextInput::new("hello world foo".to_string()); + + // Delete "foo" from end + input.move_to_end(); + input.delete_word_backward(); + assert_eq!(input.get_text(), "hello world "); + assert_eq!(input.get_cursor_position(), 12); + + // Delete "world " + input.delete_word_backward(); + assert_eq!(input.get_text(), "hello "); + assert_eq!(input.get_cursor_position(), 6); + + // Delete "hello " + input.delete_word_backward(); + assert_eq!(input.get_text(), ""); + assert_eq!(input.get_cursor_position(), 0); + + // Delete on empty buffer does nothing + input.delete_word_backward(); + assert_eq!(input.get_text(), ""); + assert_eq!(input.get_cursor_position(), 0); + } + + #[test] + fn test_delete_word_backward_middle() { + let mut input = TextInput::new("hello world foo".to_string()); + + // Position in middle of "world" + input.cursor_position = 8; // After "hello wo" + input.delete_word_backward(); + assert_eq!(input.get_text(), "hello rld foo"); + assert_eq!(input.get_cursor_position(), 6); // After "hello " + } + + #[test] + fn test_delete_word_forward() { + let mut input = TextInput::new("hello world foo".to_string()); + + // Delete "hello " from start + input.cursor_position = 0; + input.delete_word_forward(); + assert_eq!(input.get_text(), "world foo"); + assert_eq!(input.get_cursor_position(), 0); + + // Delete "world " + input.delete_word_forward(); + assert_eq!(input.get_text(), "foo"); + assert_eq!(input.get_cursor_position(), 0); + + // Delete "foo" + input.delete_word_forward(); + assert_eq!(input.get_text(), ""); + assert_eq!(input.get_cursor_position(), 0); + + // Delete on empty buffer does nothing + input.delete_word_forward(); + assert_eq!(input.get_text(), ""); + assert_eq!(input.get_cursor_position(), 0); + } + + #[test] + fn test_delete_word_forward_middle() { + let mut input = TextInput::new("hello world foo".to_string()); + + // Position in middle of "world" + input.cursor_position = 8; // After "hello wo" + input.delete_word_forward(); + assert_eq!(input.get_text(), "hello wofoo"); + assert_eq!(input.get_cursor_position(), 8); // Same position, text deleted forward + } + + #[test] + fn test_delete_word_with_multiple_spaces() { + let mut input = TextInput::new("hello world".to_string()); + + // Delete forward includes trailing spaces + input.cursor_position = 0; + input.delete_word_forward(); + assert_eq!(input.get_text(), "world"); + assert_eq!(input.get_cursor_position(), 0); + } + + #[test] + fn test_undo_redo_basic() { + let mut input = TextInput::empty(); + + // Type "hello" + input.insert_char('h'); + input.insert_char('e'); + input.insert_char('l'); + input.insert_char('l'); + input.insert_char('o'); + assert_eq!(input.get_text(), "hello"); + + // Undo should remove all characters (coalesced into one undo entry) + assert!(input.can_undo()); + assert!(input.undo()); + assert_eq!(input.get_text(), ""); + assert_eq!(input.get_cursor_position(), 0); + + // Redo should restore "hello" + assert!(input.can_redo()); + assert!(input.redo()); + assert_eq!(input.get_text(), "hello"); + assert_eq!(input.get_cursor_position(), 5); + } + + #[test] + fn test_undo_coalescing_breaks_on_cursor_move() { + let mut input = TextInput::empty(); + + // Type "he" + input.insert_char('h'); + input.insert_char('e'); + + // Move cursor to start (breaks coalescing) + input.move_to_start(); + + // Type "llo" + input.insert_char('l'); + input.insert_char('l'); + input.insert_char('o'); + + assert_eq!(input.get_text(), "llohe"); + + // First undo removes "llo" (second coalesced group) + input.undo(); + assert_eq!(input.get_text(), "he"); + + // Second undo removes "he" (first coalesced group) + input.undo(); + assert_eq!(input.get_text(), ""); + } + + #[test] + fn test_undo_backspace() { + let mut input = TextInput::new("hello".to_string()); + + // Backspace once + input.backspace(); + assert_eq!(input.get_text(), "hell"); + + // Undo should restore "hello" + input.undo(); + assert_eq!(input.get_text(), "hello"); + assert_eq!(input.get_cursor_position(), 5); + } + + #[test] + fn test_undo_delete() { + let mut input = TextInput::new("hello".to_string()); + input.cursor_position = 0; + + // Delete first character + input.delete(); + assert_eq!(input.get_text(), "ello"); + + // Undo should restore "hello" + input.undo(); + assert_eq!(input.get_text(), "hello"); + assert_eq!(input.get_cursor_position(), 0); + } + + #[test] + fn test_undo_word_delete() { + let mut input = TextInput::new("hello world".to_string()); + + // Delete "world" backward + input.delete_word_backward(); + assert_eq!(input.get_text(), "hello "); + + // Undo should restore "hello world" + input.undo(); + assert_eq!(input.get_text(), "hello world"); + assert_eq!(input.get_cursor_position(), 11); + } + + #[test] + fn test_undo_clear() { + let mut input = TextInput::new("hello world".to_string()); + + // Clear the buffer + input.clear(); + assert_eq!(input.get_text(), ""); + + // Undo should restore the text + input.undo(); + assert_eq!(input.get_text(), "hello world"); + } + + #[test] + fn test_undo_set_text() { + let mut input = TextInput::new("hello".to_string()); + + // Replace with new text + input.set_text("goodbye".to_string()); + assert_eq!(input.get_text(), "goodbye"); + + // Undo should restore "hello" + input.undo(); + assert_eq!(input.get_text(), "hello"); + } + + #[test] + fn test_redo_clears_on_new_edit() { + let mut input = TextInput::empty(); + + // Type "hello" + input.insert_char('h'); + input.insert_char('e'); + input.insert_char('l'); + input.insert_char('l'); + input.insert_char('o'); + + // Undo + input.undo(); + assert_eq!(input.get_text(), ""); + assert!(input.can_redo()); + + // Make a new edit (should clear redo stack) + input.insert_char('x'); + assert!(!input.can_redo()); + } + + #[test] + fn test_multiple_undo_redo() { + let mut input = TextInput::empty(); + + // First edit: type "hello" + for c in "hello".chars() { + input.insert_char(c); + } + + // Break coalescing + input.move_left(); + + // Second edit: type "world" + for c in "world".chars() { + input.insert_char(c); + } + + assert_eq!(input.get_text(), "hellworldo"); + + // Undo "world" + input.undo(); + assert_eq!(input.get_text(), "hello"); + + // Undo "hello" + input.undo(); + assert_eq!(input.get_text(), ""); + + // Redo "hello" + input.redo(); + assert_eq!(input.get_text(), "hello"); + + // Redo "world" + input.redo(); + assert_eq!(input.get_text(), "hellworldo"); + } + + #[test] + fn test_undo_stack_limit() { + let mut input = TextInput::empty(); + + // Perform 102 separate edits (breaking coalescing each time) + for _i in 0..102 { + input.backspace(); // Break coalescing + input.insert_char('x'); + } + + // Should have at most 100 undo entries + let mut undo_count = 0; + while input.undo() { + undo_count += 1; + } + + // We should have 100 undo entries (the stack limit) + // Plus the final state change from the last coalescing break + assert!( + undo_count <= 100, + "Undo count should be at most 100, got {}", + undo_count + ); + } + + #[test] + fn test_undo_redo_empty_stack() { + let mut input = TextInput::empty(); + + // Undo on empty stack should return false + assert!(!input.can_undo()); + assert!(!input.undo()); + + // Redo on empty stack should return false + assert!(!input.can_redo()); + assert!(!input.redo()); + } + + #[test] + fn test_undo_restores_cursor_position() { + let mut input = TextInput::new("hello world".to_string()); + + // Move cursor to position 5 (before "world") + input.cursor_position = 5; + + // Insert a space + input.insert_char(' '); + assert_eq!(input.get_text(), "hello world"); + assert_eq!(input.get_cursor_position(), 6); + + // Undo should restore both text and cursor position + input.undo(); + assert_eq!(input.get_text(), "hello world"); + assert_eq!(input.get_cursor_position(), 5); + } + + #[test] + fn test_coalescing_consecutive_inserts() { + let mut input = TextInput::empty(); + + // Type several characters + input.insert_char('a'); + input.insert_char('b'); + input.insert_char('c'); + + assert_eq!(input.get_text(), "abc"); + + // Single undo should remove all three (they were coalesced) + input.undo(); + assert_eq!(input.get_text(), ""); + + // No more undo available + assert!(!input.can_undo()); + } + + #[test] + fn test_backspace_breaks_coalescing() { + let mut input = TextInput::empty(); + + // Type "ab" + input.insert_char('a'); + input.insert_char('b'); + + // Backspace + input.backspace(); + assert_eq!(input.get_text(), "a"); + + // Type "c" + input.insert_char('c'); + assert_eq!(input.get_text(), "ac"); + + // Undo should remove just "c" + input.undo(); + assert_eq!(input.get_text(), "a"); + + // Undo should remove backspace operation + input.undo(); + assert_eq!(input.get_text(), "ab"); + + // Undo should remove "ab" + input.undo(); + assert_eq!(input.get_text(), ""); + } +} diff --git a/default-plugins/sequence/src/ui/truncation.rs b/default-plugins/sequence/src/ui/truncation.rs new file mode 100644 index 000000000..0c4073a35 --- /dev/null +++ b/default-plugins/sequence/src/ui/truncation.rs @@ -0,0 +1,373 @@ +use unicode_width::UnicodeWidthStr; + +const BASE_SEPARATOR_WIDTH: usize = 2; + +pub fn calculate_available_cmd_width( + cols: usize, + folder_width: usize, + overflow_indicator: Option<&String>, + chain_width: usize, + status_width: usize, +) -> usize { + let mut separator_width = BASE_SEPARATOR_WIDTH; + if overflow_indicator.is_some() { + separator_width += 1; + } + if status_width > 0 { + separator_width += 1; + } + + let overflow_width = overflow_indicator.map(|s| s.chars().count()).unwrap_or(0); + + cols.saturating_sub(folder_width) + .saturating_sub(chain_width) + .saturating_sub(status_width) + .saturating_sub(separator_width) + .saturating_sub(overflow_width) + .saturating_sub(2) + .max(1) +} + +pub fn truncate_middle( + text: &str, + max_width: usize, + cursor_position: Option, +) -> (String, Option) { + let text_width = text.width(); + + if text_width <= max_width { + return (text.to_string(), cursor_position); + } + + if max_width < 5 { + return truncate_minimal(text, max_width, cursor_position); + } + + if let Some(cursor_char_idx) = cursor_position { + return truncate_with_cursor(text, max_width, cursor_char_idx); + } + + truncate_no_cursor(text, max_width) +} + +fn truncate_minimal( + text: &str, + max_width: usize, + cursor_position: Option, +) -> (String, Option) { + let mut result = String::new(); + let mut current_width = 0; + let mut new_cursor_pos = None; + let mut char_pos = 0; + + for ch in text.chars() { + let ch_width = ch.to_string().width(); + if current_width + ch_width <= max_width { + result.push(ch); + if let Some(cursor_char) = cursor_position { + if char_pos == cursor_char { + new_cursor_pos = Some(char_pos); + } + } + char_pos += 1; + current_width += ch_width; + } else { + break; + } + } + (result, new_cursor_pos) +} + +fn truncate_no_cursor(text: &str, max_width: usize) -> (String, Option) { + let available_for_text = max_width.saturating_sub(3); + let left_width = available_for_text / 2; + let right_width = available_for_text - left_width; + + let mut left_part = String::new(); + let mut current_width = 0; + for ch in text.chars() { + let ch_width = ch.to_string().width(); + if current_width + ch_width <= left_width { + left_part.push(ch); + current_width += ch_width; + } else { + break; + } + } + + let chars: Vec = text.chars().collect(); + let mut right_part = String::new(); + let mut current_width = 0; + for ch in chars.iter().rev() { + let ch_width = ch.to_string().width(); + if current_width + ch_width <= right_width { + right_part.insert(0, *ch); + current_width += ch_width; + } else { + break; + } + } + + (format!("{}...{}", left_part, right_part), None) +} + +fn truncate_with_cursor( + text: &str, + max_width: usize, + cursor_char_idx: usize, +) -> (String, Option) { + let chars: Vec = text.chars().collect(); + let char_widths: Vec = chars.iter().map(|ch| ch.to_string().width()).collect(); + + let width_before_cursor: usize = char_widths[..cursor_char_idx].iter().sum(); + let width_after_cursor: usize = char_widths[cursor_char_idx..].iter().sum(); + let available_one_ellipsis = max_width.saturating_sub(3); + + let (start_idx, end_idx) = if width_before_cursor <= available_one_ellipsis { + calculate_end_truncation(&chars, &char_widths, available_one_ellipsis) + } else if width_after_cursor <= available_one_ellipsis { + calculate_start_truncation(&chars, &char_widths, available_one_ellipsis) + } else { + calculate_middle_truncation(&char_widths, max_width, cursor_char_idx) + }; + + let (start_idx, end_idx) = + adjust_small_truncations(start_idx, end_idx, &char_widths, cursor_char_idx, &chars); + let (start_idx, end_idx) = + trim_excess(&char_widths, start_idx, end_idx, max_width, cursor_char_idx); + + build_truncated_result(&chars, start_idx, end_idx, cursor_char_idx) +} + +fn calculate_end_truncation( + chars: &[char], + char_widths: &[usize], + available: usize, +) -> (usize, usize) { + let mut end_idx = 0; + let mut width = 0; + while end_idx < chars.len() && width + char_widths[end_idx] <= available { + width += char_widths[end_idx]; + end_idx += 1; + } + (0, end_idx) +} + +fn calculate_start_truncation( + chars: &[char], + char_widths: &[usize], + available: usize, +) -> (usize, usize) { + let mut start_idx = chars.len(); + let mut width = 0; + while start_idx > 0 && width + char_widths[start_idx - 1] <= available { + start_idx -= 1; + width += char_widths[start_idx]; + } + (start_idx, chars.len()) +} + +fn calculate_middle_truncation( + char_widths: &[usize], + max_width: usize, + cursor_char_idx: usize, +) -> (usize, usize) { + let available_both_ellipsis = max_width.saturating_sub(6); + let target_before = available_both_ellipsis / 2; + let target_after = available_both_ellipsis - target_before; + + let mut start_idx = cursor_char_idx; + let mut width_before = 0; + while start_idx > 0 && width_before + char_widths[start_idx - 1] <= target_before { + start_idx -= 1; + width_before += char_widths[start_idx]; + } + + let mut end_idx = cursor_char_idx; + let mut width_after = 0; + while end_idx < char_widths.len() && width_after + char_widths[end_idx] <= target_after { + width_after += char_widths[end_idx]; + end_idx += 1; + } + + let leftover_before = target_before.saturating_sub(width_before); + let leftover_after = target_after.saturating_sub(width_after); + + if leftover_before > 0 { + let mut extra = leftover_before; + while end_idx < char_widths.len() && char_widths[end_idx] <= extra { + extra -= char_widths[end_idx]; + end_idx += 1; + } + } + if leftover_after > 0 { + let mut extra = leftover_after; + while start_idx > 0 && char_widths[start_idx - 1] <= extra { + start_idx -= 1; + extra -= char_widths[start_idx]; + } + } + + (start_idx, end_idx) +} + +fn adjust_small_truncations( + start_idx: usize, + end_idx: usize, + char_widths: &[usize], + cursor_char_idx: usize, + chars: &[char], +) -> (usize, usize) { + let width_truncated_start: usize = char_widths[..start_idx].iter().sum(); + let width_truncated_end: usize = char_widths[end_idx..].iter().sum(); + + let mut start_idx = start_idx; + let mut end_idx = end_idx; + + if width_truncated_start > 0 && width_truncated_start < 3 && end_idx < chars.len() { + let gained = width_truncated_start; + start_idx = 0; + let mut removed = 0; + while end_idx > cursor_char_idx + 1 && removed < gained { + end_idx -= 1; + removed += char_widths[end_idx]; + } + } else if width_truncated_end > 0 && width_truncated_end < 3 && start_idx > 0 { + let gained = width_truncated_end; + end_idx = chars.len(); + let mut removed = 0; + while start_idx < cursor_char_idx && removed < gained { + removed += char_widths[start_idx]; + start_idx += 1; + } + } + + (start_idx, end_idx) +} + +fn trim_excess( + char_widths: &[usize], + start_idx: usize, + end_idx: usize, + max_width: usize, + cursor_char_idx: usize, +) -> (usize, usize) { + let truncate_start = start_idx > 0; + let truncate_end = end_idx < char_widths.len(); + let ellipsis_width = match (truncate_start, truncate_end) { + (true, true) => 6, + (true, false) | (false, true) => 3, + (false, false) => 0, + }; + + let visible_width: usize = char_widths[start_idx..end_idx].iter().sum(); + let mut start_idx = start_idx; + let mut end_idx = end_idx; + + if visible_width + ellipsis_width > max_width { + let mut excess = visible_width + ellipsis_width - max_width; + if truncate_end { + while excess > 0 && end_idx > cursor_char_idx + 1 { + end_idx -= 1; + excess = excess.saturating_sub(char_widths[end_idx]); + } + } + if excess > 0 && truncate_start { + while excess > 0 && start_idx < cursor_char_idx { + excess = excess.saturating_sub(char_widths[start_idx]); + start_idx += 1; + } + } + } + + (start_idx, end_idx) +} + +fn build_truncated_result( + chars: &[char], + start_idx: usize, + end_idx: usize, + cursor_char_idx: usize, +) -> (String, Option) { + let mut result = String::new(); + let mut new_cursor_char_pos = 0; + let mut current_char_pos = 0; + + if start_idx > 0 { + result.push_str("..."); + current_char_pos = 3; + } + + for i in start_idx..end_idx { + if i == cursor_char_idx { + new_cursor_char_pos = current_char_pos; + } + result.push(chars[i]); + current_char_pos += 1; + } + + if cursor_char_idx >= end_idx { + new_cursor_char_pos = current_char_pos; + } + + if end_idx < chars.len() { + result.push_str("..."); + } + + (result, Some(new_cursor_char_pos)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_truncate_middle_no_truncation_needed() { + let text = "hello world"; + let (result, cursor_pos) = truncate_middle(text, 20, Some(6)); + assert_eq!(result, "hello world"); + assert_eq!(cursor_pos, Some(6)); + } + + #[test] + fn test_truncate_middle_no_cursor() { + let text = "this is a very long string that needs truncation"; + let (result, cursor_pos) = truncate_middle(text, 20, None); + assert!(result.contains("...")); + assert_eq!(cursor_pos, None); + } + + #[test] + fn test_truncate_middle_cursor_at_start() { + let text = "this is a very long string that needs truncation"; + let (result, cursor_pos) = truncate_middle(text, 20, Some(0)); + assert!(result.starts_with("this")); + assert!(result.ends_with("...")); + assert_eq!(cursor_pos, Some(0)); + } + + #[test] + fn test_truncate_middle_cursor_at_end() { + let text = "this is a very long string that needs truncation"; + let cursor_char = text.chars().count(); + let (result, _cursor_pos) = truncate_middle(text, 20, Some(cursor_char)); + assert!(result.starts_with("...")); + assert!(result.ends_with("truncation")); + } + + #[test] + fn test_truncate_middle_cursor_in_middle() { + let text = "this is a very long string that needs truncation"; + let cursor_char = "this is a very ".chars().count(); + let (result, cursor_pos) = truncate_middle(text, 20, Some(cursor_char)); + assert!(result.contains("very")); + assert!(cursor_pos.is_some()); + } + + #[test] + fn test_truncate_middle_wide_chars() { + let text = "こんにちは世界"; + let (result, _) = truncate_middle(text, 10, None); + assert!(result.width() <= 10); + } +} diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs index 6be29dc50..5c014779f 100644 --- a/default-plugins/session-manager/src/main.rs +++ b/default-plugins/session-manager/src/main.rs @@ -508,9 +508,9 @@ impl State { if is_current_session { if let Some((pane_id, is_plugin)) = selected_pane { if is_plugin { - focus_plugin_pane(pane_id, true); + focus_plugin_pane(pane_id, true, false); } else { - focus_terminal_pane(pane_id, true); + focus_terminal_pane(pane_id, true, false); } } else if let Some(tab_position) = selected_tab { go_to_tab(tab_position as u32); diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs index a3fdec774..a73c7b80b 100644 --- a/default-plugins/status-bar/src/one_line_ui.rs +++ b/default-plugins/status-bar/src/one_line_ui.rs @@ -1386,7 +1386,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec Vec<(String, String, Vec Vec<(String, String, Vec Vec<(String, String, Vec Vec<(String, String, Vec &'static Vec { crate_name: "default-plugins/share", build: true, }, + WorkspaceMember { + crate_name: "default-plugins/sequence", + build: true, + }, WorkspaceMember { crate_name: "zellij-utils", build: false, diff --git a/zellij-client/src/old_config_converter/old_config.rs b/zellij-client/src/old_config_converter/old_config.rs index bfe345dfc..b5083c17f 100644 --- a/zellij-client/src/old_config_converter/old_config.rs +++ b/zellij-client/src/old_config_converter/old_config.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use serde::de::{Error, Visitor}; use serde::{Deserialize, Deserializer, Serialize}; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use url::Url; const ON_FORCE_CLOSE_DESCRIPTION: &'static str = " @@ -560,23 +560,6 @@ 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); @@ -720,36 +703,6 @@ impl Default for OldPluginTypeFromYaml { #[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 { - /// Path of the plugin, see resolve_wasm_bytes for resolution semantics - pub path: PathBuf, - /// Plugin type - pub run: OldPluginType, - /// Allow command execution from plugin - pub _allow_exec_host_cmd: bool, - /// Original location of the - pub location: OldRunPluginLocation, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -enum OldRunPluginLocation { - File(PathBuf), - Zellij(OldPluginTag), -} - -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -enum OldPluginType { - /// Starts immediately when Zellij is started and runs without a visible pane - Headless, - /// Runs once per pane declared inside a layout file - Pane(Option), // tab_index -} - #[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize)] pub enum OldOnForceClose { #[serde(alias = "quit")] diff --git a/zellij-client/src/web_client/types.rs b/zellij-client/src/web_client/types.rs index 201a0cf4d..32676fd35 100644 --- a/zellij-client/src/web_client/types.rs +++ b/zellij-client/src/web_client/types.rs @@ -168,22 +168,6 @@ pub struct AppState { pub is_https: bool, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StdinMessage { - pub web_client_id: String, - pub stdin: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ShutdownSignal { - Shutdown, -} - -#[derive(Serialize, Debug)] -pub struct SendShutdownSignalResponse { - pub status: String, -} - #[derive(Serialize)] pub struct CreateClientIdResponse { pub web_client_id: String, diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs index 9bc8a9384..629070488 100644 --- a/zellij-server/src/background_jobs.rs +++ b/zellij-server/src/background_jobs.rs @@ -24,6 +24,7 @@ use std::time::{Duration, Instant}; use crate::panes::PaneId; use crate::plugins::{PluginId, PluginInstruction}; +use crate::pty::PtyInstruction; use crate::screen::ScreenInstruction; use crate::thread_bus::Bus; use crate::ClientId; @@ -228,6 +229,7 @@ pub(crate) fn background_jobs_main( session_infos_on_machine, resurrectable_sessions, )); + let _ = senders.send_to_pty(PtyInstruction::UpdateAndReportCwds); if last_serialization_time .lock() .unwrap() diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 1861fb68a..dd0939196 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -784,6 +784,8 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { floating_panes_layout, tab_name, swap_layouts, + None, // initial_panes + false, // block_on_first_terminal should_focus_tab, (client_id, is_web_client), None, diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index adcca9740..46010493e 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -500,6 +500,8 @@ pub trait ServerOsApi: Send + Sync { fn kill(&self, pid: Pid) -> Result<()>; /// Terminate the process with process ID `pid`. (SIGKILL) fn force_kill(&self, pid: Pid) -> Result<()>; + /// Send SIGINT to the process with process ID `pid` + fn send_sigint(&self, pid: Pid) -> Result<()>; /// Returns a [`Box`] pointer to this [`ServerOsApi`] struct. fn box_clone(&self) -> Box; fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) -> Result<()>; @@ -697,6 +699,10 @@ impl ServerOsApi for ServerOsInputOutput { let _ = kill(pid, Some(Signal::SIGKILL)); Ok(()) } + fn send_sigint(&self, pid: Pid) -> Result<()> { + let _ = kill(pid, Some(Signal::SIGINT)); + Ok(()) + } fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) -> Result<()> { let err_context = || format!("failed to send message to client {client_id}"); diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 6ae71b438..d347dcd04 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -18,6 +18,7 @@ use crate::{ ClientId, }; use std::cell::RefCell; +use std::cmp::Ordering; use std::collections::{BTreeMap, HashMap, HashSet}; use std::rc::Rc; use std::time::Instant; @@ -129,6 +130,7 @@ impl FloatingPanes { .insert(pane_id, pane.position_and_size()); self.panes.insert(pane_id, pane); self.z_indices.push(pane_id); + self.make_sure_pinned_panes_are_on_top(); } pub fn replace_active_pane( &mut self, @@ -169,6 +171,7 @@ impl FloatingPanes { .with_context(err_context)?; self.z_indices.remove(z_index); self.z_indices.insert(z_index, with_pane_id); + self.make_sure_pinned_panes_are_on_top(); Ok(removed_pane) }); @@ -415,21 +418,10 @@ impl FloatingPanes { vec![] }; floating_panes.sort_by(|(a_id, _a_pane), (b_id, _b_pane)| { - self.z_indices - .iter() - .position(|id| id == *a_id) - .with_context(err_context) - .fatal() - .cmp( - &self - .z_indices - .iter() - .position(|id| id == *b_id) - .with_context(err_context) - .fatal(), - ) + let a_pos = self.z_indices.iter().position(|id| id == *a_id); + let b_pos = self.z_indices.iter().position(|id| id == *b_id); + a_pos.cmp(&b_pos) }); - for (z_index, (kind, pane)) in floating_panes.iter_mut().enumerate() { let mut active_panes = active_panes.clone(); let multiple_users_exist_in_session = @@ -806,6 +798,16 @@ impl FloatingPanes { pane_geom.adjust_coordinates(new_coordinates, viewport); pane.set_geom(pane_geom); pane.set_should_render(true); + + // we do this in case this moves the pane under another pane so that the pane user's + // are focused on will always be on top + let is_focused = self.active_panes.pane_id_is_focused(&pane_id); + if is_focused { + self.z_indices.retain(|p_id| *p_id != pane_id); + self.z_indices.push(pane_id); + self.make_sure_pinned_panes_are_on_top(); + } + self.desired_pane_positions.insert(pane_id, pane_geom); } let _ = self.set_pane_frames(); @@ -840,7 +842,7 @@ impl FloatingPanes { self.focus_pane(next_active_pane_id, client_id); }, None => { - self.defocus_pane(pane_id, client_id); + self.defocus_pane(client_id); }, } } @@ -855,6 +857,7 @@ impl FloatingPanes { } self.z_indices.retain(|p_id| *p_id != pane_id); self.z_indices.push(pane_id); + self.make_sure_pinned_panes_are_on_top(); self.set_pane_active_at(pane_id); self.set_force_render(); } @@ -888,8 +891,7 @@ impl FloatingPanes { }, } } - pub fn defocus_pane(&mut self, pane_id: PaneId, client_id: ClientId) { - self.z_indices.retain(|p_id| *p_id != pane_id); + pub fn defocus_pane(&mut self, client_id: ClientId) { self.active_panes.remove(&client_id, &mut self.panes); self.set_force_render(); } @@ -1244,4 +1246,27 @@ impl FloatingPanes { ); floating_pane_grid.next_selectable_pane_id_to_the_right(&pane_id) } + fn make_sure_pinned_panes_are_on_top(&mut self) { + let pinned_status: std::collections::HashMap<_, _> = self + .z_indices + .iter() + .map(|id| (id.clone(), self.pane_id_is_pinned(id))) + .collect(); + + self.z_indices.sort_by(|a_id, b_id| { + let a_is_pinned = pinned_status.get(a_id).copied().unwrap_or(false); + let b_is_pinned = pinned_status.get(b_id).copied().unwrap_or(false); + match (a_is_pinned, b_is_pinned) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + _ => Ordering::Equal, + } + }); + } + fn pane_id_is_pinned(&self, pane_id: &PaneId) -> bool { + self.panes + .get(pane_id) + .map(|p| p.position_and_size().is_pinned) + .unwrap_or(false) + } } diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 0e74a9def..ffad3d84d 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -90,6 +90,7 @@ pub(crate) struct PluginPane { character_cell_size: Rc>>, vte_parsers: HashMap, grids: HashMap, + cursor_visibility: HashMap>, prev_pane_name: String, frame: HashMap, borderless: bool, @@ -149,6 +150,7 @@ impl PluginPane { sixel_image_store, vte_parsers: HashMap::new(), grids: HashMap::new(), + cursor_visibility: HashMap::new(), style, pane_frame_color_override: None, invoked_with, @@ -248,8 +250,25 @@ impl Pane for PluginPane { self.should_render.insert(client_id, true); } - fn cursor_coordinates(&self) -> Option<(usize, usize)> { - None + fn cursor_coordinates(&self, client_id: Option) -> Option<(usize, usize)> { + let own_content_columns = self.get_content_columns(); + let own_content_rows = self.get_content_rows(); + let Offset { top, left, .. } = self.content_offset; + if let Some(coordinates) = + client_id.and_then(|client_id| self.cursor_visibility.get(&client_id)) + { + coordinates + .map(|(x, y)| (x + left, y + top)) + .and_then(|(x, y)| { + if x >= own_content_columns || y >= own_content_rows { + None + } else { + Some((x, y)) // these are 0 indexed + } + }) + } else { + None + } } fn adjust_input_to_terminal( &mut self, @@ -360,6 +379,10 @@ impl Pane for PluginPane { fn set_selectable(&mut self, selectable: bool) { self.selectable = selectable; } + fn show_cursor(&mut self, client_id: ClientId, cursor_position: Option<(usize, usize)>) { + self.cursor_visibility.insert(client_id, cursor_position); + self.should_render.insert(client_id, true); + } fn request_permissions_from_user(&mut self, permissions: Option) { self.requesting_permissions = permissions; self.handle_plugin_bytes_for_all_clients(Default::default()); // to trigger the render of @@ -440,7 +463,7 @@ impl Pane for PluginPane { let res = match self.frame.get(&client_id) { // TODO: use and_then or something? Some(last_frame) => { - if &frame != last_frame { + if &frame != last_frame || is_pinned { if !self.borderless { let frame_output = frame.render().with_context(err_context)?; self.frame.insert(client_id, frame); diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 1c5667a50..97bf5dcce 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -197,7 +197,7 @@ impl Pane for TerminalPane { self.vte_parser.advance(&mut self.grid, byte); } } - fn cursor_coordinates(&self) -> Option<(usize, usize)> { + fn cursor_coordinates(&self, _client_id: Option) -> Option<(usize, usize)> { // (x, y) if self.get_content_rows() < 1 || self.get_content_columns() < 1 { // do not render cursor if there's no room for it @@ -740,6 +740,14 @@ impl Pane for TerminalPane { if let Some(notification_end) = self.notification_end.as_mut() { if let Some(exit_status) = exit_status { notification_end.set_exit_status(exit_status); + + // Check if unblock condition is met + if let Some(condition) = notification_end.unblock_condition() { + if condition.is_met(exit_status) { + // Condition met - drop the NotificationEnd now to unblock + drop(self.notification_end.take()); + } + } } } if is_first_run { @@ -899,6 +907,13 @@ impl Pane for TerminalPane { fn update_exit_status(&mut self, exit_status: i32) { if let Some(notification_end) = self.notification_end.as_mut() { notification_end.set_exit_status(exit_status); + // Check if unblock condition is met + if let Some(condition) = notification_end.unblock_condition() { + if condition.is_met(exit_status) { + // Condition met - drop the NotificationEnd now to unblock + drop(self.notification_end.take()); + } + } } } } @@ -922,7 +937,7 @@ impl TerminalPane { arrow_fonts: bool, styled_underlines: bool, explicitly_disable_keyboard_protocol: bool, - notification_end: Option, + mut notification_end: Option, ) -> TerminalPane { let initial_pane_title = initial_pane_title.unwrap_or_else(|| format!("Pane #{}", pane_index)); @@ -940,6 +955,9 @@ impl TerminalPane { styled_underlines, explicitly_disable_keyboard_protocol, ); + if let Some(notification_end) = notification_end.as_mut() { + notification_end.set_affected_pane_id(PaneId::Terminal(pid)); + } TerminalPane { frame: HashMap::new(), content_offset: Offset::default(), diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index fb73a90d2..4df68db46 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -313,6 +313,7 @@ impl TiledPanes { Ok(new_pane_geom) => { pane.set_geom(new_pane_geom); self.panes.insert(pane_id, pane); // TODO: is set_geom the right one? + self.expand_pane_in_stack(pane_id); }, Err(e) => { log::error!("Failed to add pane to stack: {:?}", e); @@ -349,7 +350,8 @@ impl TiledPanes { Ok(new_pane_geom) => { pane.set_geom(new_pane_geom); self.panes.insert(pane_id, pane); - self.set_force_render(); // TODO: why do we need this? + + self.expand_pane_in_stack(pane_id); return; }, Err(_e) => { @@ -447,7 +449,9 @@ impl TiledPanes { .map(|p| p.is_stacked()) .unwrap_or(false); if !pane_id_is_stacked { - let _ = pane_grid.make_pane_stacked(&root_pane_id); + if let Err(e) = pane_grid.make_pane_stacked(&root_pane_id) { + log::error!("Failed to make pane stacked: {:?}", e); + } } match pane_grid.make_room_in_stack_of_pane_id_for_pane(&root_pane_id) { Ok(new_pane_geom) => { diff --git a/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs b/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs index ddb225a48..0c989a9e7 100644 --- a/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs +++ b/zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs @@ -1344,6 +1344,14 @@ impl<'a> TiledPaneGrid<'a> { let _ = pane_resizer.layout(direction, side_length); return true; } + } else { + // best effort resize - we just remove the pane and relayout everything + // this might happen if we are closing a fixed pane + self.panes.borrow_mut().remove(&id); + let mut pane_resizer = PaneResizer::new(self.panes.clone()); + let _ = pane_resizer.layout(SplitDirection::Horizontal, self.display_area.cols); + let _ = pane_resizer.layout(SplitDirection::Vertical, self.display_area.rows); + return true; } false } diff --git a/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs b/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs index 6b4edf40e..e4adfbe44 100644 --- a/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs +++ b/zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs @@ -1085,7 +1085,7 @@ impl Pane for MockPane { fn handle_plugin_bytes(&mut self, _client_id: ClientId, _bytes: VteBytes) { unimplemented!() } - fn cursor_coordinates(&self) -> Option<(usize, usize)> { + fn cursor_coordinates(&self, _client_id: Option) -> Option<(usize, usize)> { unimplemented!() } diff --git a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__table_ui_component.snap b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__table_ui_component.snap index 7ab7a56c5..530592404 100644 --- a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__table_ui_component.snap +++ b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__table_ui_component.snap @@ -2,13 +2,13 @@ source: zellij-server/src/panes/./unit/grid_tests.rs expression: "format!(\"{:?}\", grid)" --- -00 (C): title1 title2 title3 title4 title5 -01 (C): 1lksdjflksdjf lkjdsflksjdf lkjsdlkfjsfd lkjsdlkjfd lkjdslkjsdf -02 (C): 2lksdjflksdjf lkjdsflksjdf lkj😳😳sdlkfjsfd lkjsdlkjfd lkjdslkjsdf -03 (C): 3lksdjflksdjf lkjdsflksjdf lkjsd😳lkfjsfd lkjsdlkjfd lkjdslkjsdf -04 (C): 4lksdjflks222djf lk😳222jdsflksjdf lk222jsdlkfjsfd lkwwwjsdlkjfd lkjdslkjsdf -05 (C): 5lksdjflksdjf lkjdsflksjdf lkjsdlkfjsfd lksdjflksjfdlkjsdlkjfd lkjdrrrrslkjsdf -06 (C): 6lksdjflk😳sdjf l😳kjdsflksjdf lkjsdlkfjsfd lkjsdlkjfd lwwkwkjdslkjsdf +00 (C): title1 title2 title3 title4 title5 +01 (C): 1lksdjflksdjf lkjdsflksjdf lkjsdlkfjsfd lkjsdlkjfd lkjdslkjsdf +02 (C): 2lksdjflksdjf lkjdsflksjdf lkj😳😳sdlkfjsfd lkjsdlkjfd lkjdslkjsdf +03 (C): 3lksdjflksdjf lkjdsflksjdf lkjsd😳lkfjsfd lkjsdlkjfd lkjdslkjsdf +04 (C): 4lksdjflks222djf lk😳222jdsflksjdf lk222jsdlkfjsfd lkwwwjsdlkjfd lkjdslkjsdf +05 (C): 5lksdjflksdjf lkjdsflksjdf lkjsdlkfjsfd lksdjflksjfdlkjsdlkjfd lkjdrrrrslkjsdf +06 (C): 6lksdjflk😳sdjf l😳kjdsflksjdf lkjsdlkfjsfd lkjsdlkjfd lwwkwkjdslkjsdf 07 (C): 08 (C): 09 (C): diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index cede7901f..24a1e8111 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -30,12 +30,13 @@ use wasm_bridge::WasmBridge; use async_std::{channel, future::timeout, task}; use zellij_utils::{ data::{ - ClientInfo, Event, EventType, FloatingPaneCoordinates, InputMode, MessageToPlugin, - PermissionStatus, PermissionType, PipeMessage, PipeSource, PluginCapabilities, - WebServerStatus, + ClientInfo, CommandOrPlugin, Event, EventType, FloatingPaneCoordinates, InputMode, + MessageToPlugin, PermissionStatus, PermissionType, PipeMessage, PipeSource, + PluginCapabilities, WebServerStatus, }, errors::{prelude::*, ContextType, PluginContext}, input::{ + actions::Action, command::TerminalAction, keybinds::Keybinds, layout::{FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, TiledPaneLayout}, @@ -86,10 +87,12 @@ pub enum PluginInstruction { Option, Option, Vec, - usize, // tab_index - bool, // should change focus to new tab - (ClientId, bool), // bool -> is_web_client - Option, // completion signal + usize, // tab_index + Option>, // initial_panes + bool, // block_on_first_terminal + bool, // should change focus to new tab + (ClientId, bool), // bool -> is_web_client + Option, // completion signal ), ApplyCachedEvents { plugin_ids: Vec, @@ -175,6 +178,12 @@ pub enum PluginInstruction { WebServerStarted(String), // String -> the base url of the web server FailedToStartWebServer(String), PaneRenderReport(PaneRenderReport), + UserInput { + client_id: ClientId, + action: Action, + terminal_id: Option, + cli_client_id: Option, + }, Exit, } @@ -225,6 +234,7 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::WebServerStarted(..) => PluginContext::WebServerStarted, PluginInstruction::FailedToStartWebServer(..) => PluginContext::FailedToStartWebServer, PluginInstruction::PaneRenderReport(..) => PluginContext::PaneRenderReport, + PluginInstruction::UserInput { .. } => PluginContext::UserInput, } } } @@ -453,6 +463,8 @@ pub(crate) fn plugin_thread_main( mut tab_layout, mut floating_panes_layout, tab_index, + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, @@ -469,6 +481,26 @@ pub(crate) fn plugin_thread_main( let mut plugin_ids: HashMap> = HashMap::new(); tab_layout = tab_layout.or_else(|| Some(layout.new_tab().0)); + + // Match initial_panes plugins to empty slots in the layout + if let Some(ref initial_panes_vec) = initial_panes { + if let Some(ref mut tiled_layout) = tab_layout { + for initial_pane in initial_panes_vec.iter() { + if let CommandOrPlugin::Plugin(run_plugin_or_alias) = initial_pane { + if !tiled_layout.replace_next_empty_slot_with_run(Run::Plugin( + run_plugin_or_alias.clone(), + )) { + log::warn!( + "More initial_panes provided than empty slots available" + ); + break; + } + } + // Skip CommandOrPlugin::Command entries (handled by pty thread) + } + } + } + tab_layout.as_mut().map(|t| { t.populate_plugin_aliases_in_layout(&plugin_aliases); if let Some(cwd) = cwd.as_ref() { @@ -481,7 +513,7 @@ pub(crate) fn plugin_thread_main( .as_mut() .map(|f| f.populate_run_plugin_if_needed(&plugin_aliases)); }); - let mut extracted_run_instructions = tab_layout + let extracted_run_instructions = tab_layout .clone() .unwrap_or_else(|| layout.new_tab().0) .extract_run_instructions(); @@ -496,8 +528,10 @@ pub(crate) fn plugin_thread_main( .filter(|f| !f.already_running) .map(|f| f.run.clone()) .collect(); - extracted_run_instructions.append(&mut extracted_floating_plugins); - for run_instruction in extracted_run_instructions { + let mut all_run_instructions = extracted_run_instructions; + all_run_instructions.append(&mut extracted_floating_plugins); + + for run_instruction in all_run_instructions { if let Some(Run::Plugin(run_plugin_or_alias)) = run_instruction { let run_plugin = run_plugin_or_alias.get_run_plugin(); let cwd = run_plugin_or_alias @@ -525,6 +559,8 @@ pub(crate) fn plugin_thread_main( floating_panes_layout, tab_index, plugin_ids, + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, @@ -989,6 +1025,20 @@ pub(crate) fn plugin_thread_main( .handle_pane_render_report(pane_render_report, shutdown_send.clone()) .non_fatal(); }, + PluginInstruction::UserInput { + client_id, + action, + terminal_id, + cli_client_id, + } => { + // Fire Event::UserAction to all subscribed plugins with InterceptInput permission + let updates = vec![( + None, + None, + Event::UserAction(action, client_id, terminal_id, cli_client_id), + )]; + wasm_bridge.update_plugins(updates, shutdown_send.clone())?; + }, PluginInstruction::Exit => { break; }, diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 5f90ebbef..c0368fbcd 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -9137,3 +9137,670 @@ pub fn before_close_plugin_event() { .unwrap(); assert_snapshot!(format!("{:#?}", sent_instruction)); } + +#[test] +#[ignore] +pub fn show_cursor_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = grant_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::ShowPluginCursor, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('a')).with_super_modifier()), + )])); + + screen_thread.join().unwrap(); + teardown(); + + let show_cursor_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::ShowPluginCursor(plugin_id, client_id, cursor_position) = i { + Some((*plugin_id, *client_id, *cursor_position)) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", show_cursor_instruction)); +} + +#[test] +#[ignore] +pub fn hide_cursor_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = grant_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::ShowPluginCursor, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('b')).with_super_modifier()), + )])); + + screen_thread.join().unwrap(); + teardown(); + + let hide_cursor_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::ShowPluginCursor(plugin_id, client_id, cursor_position) = i { + Some((*plugin_id, *client_id, *cursor_position)) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", hide_cursor_instruction)); +} + +#[test] +#[ignore] +pub fn copy_to_clipboard_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = grant_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::CopyTextToClipboard, + screen_receiver, + 1, + &PermissionType::WriteToClipboard, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('c')).with_super_modifier()), + )])); + + screen_thread.join().unwrap(); + teardown(); + + let copy_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::CopyTextToClipboard(text, plugin_id) = i { + Some((text.clone(), *plugin_id)) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", copy_instruction)); +} + +#[test] +#[ignore] +pub fn run_action_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = grant_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::MoveFocusLeft, + screen_receiver, + 1, + &PermissionType::RunActionsAsUser, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('d')).with_super_modifier()), + )])); + + screen_thread.join().unwrap(); + teardown(); + + let move_focus_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::MoveFocusLeft(client_id, notification_end) = i { + Some((*client_id, notification_end.clone())) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", move_focus_instruction)); +} + +#[test] +#[ignore] +pub fn send_sigint_to_pane_id_plugin_command() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) = + create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); + + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let client_id = 1; + + let pty_thread = log_actions_in_thread!( + received_pty_instructions, + PtyInstruction::SendSigintToPaneId, + pty_receiver, + 1 + ); + + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let size = Size { + cols: 121, + rows: 20, + }; + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('e')).with_super_modifier()), + )])); + + pty_thread.join().unwrap(); + teardown(); + + let sigint_instruction = received_pty_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let PtyInstruction::SendSigintToPaneId(pane_id) = i { + Some(*pane_id) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", sigint_instruction)); +} + +#[test] +#[ignore] +pub fn send_sigkill_to_pane_id_plugin_command() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, pty_receiver, screen_receiver, teardown) = + create_plugin_thread_with_pty_receiver(Some(plugin_host_folder)); + + let received_pty_instructions = Arc::new(Mutex::new(vec![])); + let client_id = 1; + + let pty_thread = log_actions_in_thread!( + received_pty_instructions, + PtyInstruction::SendSigkillToPaneId, + pty_receiver, + 1 + ); + + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let size = Size { + cols: 121, + rows: 20, + }; + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('f')).with_super_modifier()), + )])); + + pty_thread.join().unwrap(); + teardown(); + + let sigkill_instruction = received_pty_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let PtyInstruction::SendSigkillToPaneId(pane_id) = i { + Some(*pane_id) + } else { + None + } + }); + + assert_snapshot!(format!("{:#?}", sigkill_instruction)); +} + +#[test] +#[ignore] +pub fn copy_to_clipboard_without_permission() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let client_id = 1; + + // This test denies the permission and expects no CopyTextToClipboard instruction + let screen_thread = deny_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::CopyTextToClipboard, + screen_receiver, + 1, + &PermissionType::WriteToClipboard, + cache_path, + plugin_thread_sender, + client_id + ); + + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let size = Size { + cols: 121, + rows: 20, + }; + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('c')).with_super_modifier()), + )])); + + // Give it time to potentially (incorrectly) send the instruction + std::thread::sleep(std::time::Duration::from_millis(1000)); + + let _ = plugin_thread_sender.send(PluginInstruction::Exit); + screen_thread.join().unwrap(); + teardown(); + + let copy_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::CopyTextToClipboard(text, plugin_id) = i { + Some((text.clone(), *plugin_id)) + } else { + None + } + }); + + // Should be None because permission was denied + assert_snapshot!(format!("{:#?}", copy_instruction)); +} + +#[test] +#[ignore] +pub fn run_action_without_permission() { + let temp_folder = tempdir().unwrap(); + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder)); + + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let client_id = 1; + + let screen_thread = deny_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::MoveFocusLeft, + screen_receiver, + 1, + &PermissionType::RunActionsAsUser, + cache_path, + plugin_thread_sender, + client_id + ); + + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let size = Size { + cols: 121, + rows: 20, + }; + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(KeyWithModifier::new(BareKey::Char('d')).with_super_modifier()), + )])); + + std::thread::sleep(std::time::Duration::from_millis(1000)); + + let _ = plugin_thread_sender.send(PluginInstruction::Exit); + screen_thread.join().unwrap(); + teardown(); + + let move_focus_instruction = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::MoveFocusLeft(client_id, notification_end) = i { + Some((*client_id, notification_end.clone())) + } else { + None + } + }); + + // Should be None because permission was denied + assert_snapshot!(format!("{:#?}", move_focus_instruction)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__clear_screen_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__clear_screen_plugin_command.snap index 975e20a3b..88023c4b1 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__clear_screen_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__clear_screen_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focus_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focus_plugin_command.snap index 133b23fff..38179b854 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focus_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focus_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focused_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focused_tab_plugin_command.snap index cdaa2362f..d03122184 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focused_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focused_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap index cbf3c8c22..b795da466 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap @@ -12,6 +12,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), None, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap index 1041e20fa..97e61a45f 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap @@ -12,6 +12,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), None, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_plugin_command.snap new file mode 100644 index 000000000..cc4717e37 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_plugin_command.snap @@ -0,0 +1,10 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", copy_instruction)" +--- +Some( + ( + "test clipboard text", + 0, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_without_permission.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_without_permission.snap new file mode 100644 index 000000000..333c0389c --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_without_permission.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", copy_instruction)" +--- +None diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__detach_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__detach_plugin_command.snap index ea71beac4..1de9477d4 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__detach_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__detach_plugin_command.snap @@ -11,6 +11,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_scrollback_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_scrollback_plugin_command.snap index cc572e0cf..370a7bbff 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_scrollback_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_scrollback_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_next_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_next_pane_plugin_command.snap index f8cf46237..3ef17f9d9 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_next_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_next_pane_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_or_create_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_or_create_tab_plugin_command.snap index 305eb5a57..fbd45fd08 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_or_create_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_or_create_tab_plugin_command.snap @@ -18,6 +18,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap index 6b98856a7..31a63f734 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap @@ -8,11 +8,14 @@ Some( 1, ), false, + false, 1, Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_previous_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_previous_pane_plugin_command.snap index 0b37d3188..8073ceb87 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_previous_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_previous_pane_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap index 345f04794..0a3a1d236 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap @@ -8,11 +8,14 @@ Some( 1, ), false, + false, 1, Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_next_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_next_tab_plugin_command.snap index ca2edc5fe..c76a5eccf 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_next_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_next_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_previous_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_previous_tab_plugin_command.snap index 1451fa506..f4653b620 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_previous_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_previous_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab.snap index 13bbdb4af..48cf0870d 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab.snap @@ -12,6 +12,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap index 3fd2e40f2..7e45c1fc6 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap @@ -18,6 +18,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap index 28c7dedc5..91ec8a9bc 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 5505 expression: "format!(\"{:#?}\", permissions)" --- Some( @@ -15,5 +14,7 @@ Some( ReadCliPipes, MessageAndLaunchOtherPlugins, Reconfigure, + RunActionsAsUser, + WriteToClipboard, ], ) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__hide_cursor_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__hide_cursor_plugin_command.snap new file mode 100644 index 000000000..31880c737 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__hide_cursor_plugin_command.snap @@ -0,0 +1,11 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", hide_cursor_instruction)" +--- +Some( + ( + 0, + 1, + None, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_or_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_or_tab_plugin_command.snap index c3b1d5f22..944191992 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_or_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_or_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_plugin_command.snap index 72b7dc153..f901ba75f 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_plugin_command.snap index b9306571f..1c2a8fc6a 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_with_direction_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_with_direction_plugin_command.snap index 9da828559..51f2cc158 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_with_direction_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_with_direction_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tab_plugin_command.snap index 1213308e9..128c7abe4 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tab_plugin_command.snap @@ -17,6 +17,8 @@ Some( [], [], ), + None, + false, true, ( 1, @@ -26,6 +28,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap index bc4faa999..1c74a94b7 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap @@ -63,6 +63,8 @@ Some( [], [], ), + None, + false, false, ( 1, @@ -72,6 +74,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap index 2ba4de2af..889a6e9d9 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap @@ -63,6 +63,8 @@ Some( [], [], ), + None, + false, true, ( 1, @@ -72,6 +74,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__next_swap_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__next_swap_layout_plugin_command.snap index 21c4257cd..87c3264c3 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__next_swap_layout_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__next_swap_layout_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_floating_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_floating_plugin_command.snap index ac0838cc7..3afcead66 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_floating_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_floating_plugin_command.snap @@ -38,6 +38,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_plugin_command.snap index ca751a220..596700c59 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_plugin_command.snap @@ -38,6 +38,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_floating_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_floating_plugin_command.snap index eb598cf0a..be8503def 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_floating_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_floating_plugin_command.snap @@ -36,6 +36,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_plugin_command.snap index d235c4167..b899e596f 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_plugin_command.snap @@ -36,6 +36,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_floating_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_floating_plugin_command.snap index cde191a5f..fc2087d67 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_floating_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_floating_plugin_command.snap @@ -38,6 +38,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_plugin_command.snap index 89986fd25..4d2689db2 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_plugin_command.snap @@ -38,6 +38,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_floating_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_floating_plugin_command.snap index b2f0f9ae9..b42943fcd 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_floating_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_floating_plugin_command.snap @@ -31,6 +31,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_plugin_command.snap index 5cc2ce7cc..dd197b763 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_plugin_command.snap @@ -31,6 +31,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), false, diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_down_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_down_plugin_command.snap index d85ffc61a..5292281bf 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_down_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_down_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_up_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_up_plugin_command.snap index 014ac3d3c..5709731cb 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_up_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_up_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__previous_swap_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__previous_swap_layout_plugin_command.snap index b35b3b0b7..523e76fd8 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__previous_swap_layout_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__previous_swap_layout_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__quit_zellij_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__quit_zellij_plugin_command.snap index d00a76630..5057701a7 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__quit_zellij_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__quit_zellij_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap index 5816c2830..119444e2f 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap @@ -33,6 +33,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap index 9450f7c0b..4846cd65d 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap @@ -23,6 +23,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap index f23a792c9..53b81db86 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap @@ -35,6 +35,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap index 75f47da7b..1400f0813 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 5414 expression: "format!(\"{:#?}\", new_tab_event)" --- Some( @@ -17,5 +16,7 @@ Some( ReadCliPipes, MessageAndLaunchOtherPlugins, Reconfigure, + WriteToClipboard, + RunActionsAsUser, ], ) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_plugin_command.snap index 37275f434..0184c9a66 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_plugin_command.snap @@ -14,6 +14,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_with_direction_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_with_direction_plugin_command.snap index d9fa542d1..71766b1ed 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_with_direction_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_with_direction_plugin_command.snap @@ -16,6 +16,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_plugin_command.snap new file mode 100644 index 000000000..dcbf70044 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_plugin_command.snap @@ -0,0 +1,17 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", move_focus_instruction)" +--- +Some( + ( + 1, + Some( + NotificationEnd { + channel: None, + exit_status: None, + unblock_condition: None, + affected_pane_id: None, + }, + ), + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_without_permission.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_without_permission.snap new file mode 100644 index 000000000..102abab0e --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_without_permission.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", move_focus_instruction)" +--- +None diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_down_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_down_plugin_command.snap index c8605a1c5..738a76255 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_down_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_down_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_bottom_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_bottom_plugin_command.snap index 410902d59..339ea8c4f 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_bottom_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_bottom_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_top_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_top_plugin_command.snap index 9f2effacd..5c0b90889 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_top_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_top_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_up_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_up_plugin_command.snap index acc0d6a64..d0250b34a 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_up_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_up_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap index aa9592c28..14438cf8a 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap @@ -18,6 +18,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigint_to_pane_id_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigint_to_pane_id_plugin_command.snap new file mode 100644 index 000000000..a7558487e --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigint_to_pane_id_plugin_command.snap @@ -0,0 +1,9 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", sigint_instruction)" +--- +Some( + Terminal( + 1, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigkill_to_pane_id_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigkill_to_pane_id_plugin_command.snap new file mode 100644 index 000000000..fb7dc0a11 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigkill_to_pane_id_plugin_command.snap @@ -0,0 +1,9 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", sigkill_instruction)" +--- +Some( + Terminal( + 1, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_cursor_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_cursor_plugin_command.snap new file mode 100644 index 000000000..5b53f360b --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_cursor_plugin_command.snap @@ -0,0 +1,16 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", show_cursor_instruction)" +--- +Some( + ( + 0, + 1, + Some( + ( + 5, + 10, + ), + ), + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_pane_with_id_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_pane_with_id_plugin_command.snap index 24f6c12f3..457bebdc1 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_pane_with_id_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_pane_with_id_plugin_command.snap @@ -8,6 +8,7 @@ Some( 1, ), true, + false, 1, None, ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap index a99b02303..41396ddb6 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap @@ -8,11 +8,14 @@ Some( 0, ), false, + false, 1, Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap index 290247c05..845f37ba0 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap @@ -21,6 +21,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap index 6df0b5df5..b4ff3c006 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap @@ -330,6 +330,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_active_tab_sync_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_active_tab_sync_plugin_command.snap index f8959104d..06cde2560 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_active_tab_sync_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_active_tab_sync_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_focus_fullscreen_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_focus_fullscreen_plugin_command.snap index 34e96dae9..44747b668 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_focus_fullscreen_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_focus_fullscreen_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_embed_or_eject_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_embed_or_eject_plugin_command.snap index 8f375b9c4..8e24bff8d 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_embed_or_eject_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_embed_or_eject_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_frames_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_frames_plugin_command.snap index e1bded4d9..c91f66aa0 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_frames_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_frames_plugin_command.snap @@ -8,6 +8,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_tab_plugin_command.snap index e088770bf..32965d58f 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_pane_plugin_command.snap index 6c500286b..99df84750 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_pane_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_pane_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_tab_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_tab_plugin_command.snap index 54566d681..7836e7d29 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_tab_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_tab_plugin_command.snap @@ -9,6 +9,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap index 8bb05ce1c..abf3d1a2a 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap @@ -16,6 +16,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_plugin_command.snap index 8bb05ce1c..abf3d1a2a 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_plugin_command.snap @@ -16,6 +16,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index 5dd78d13e..2453a0a15 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -113,7 +113,7 @@ impl LoadingContext { let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR .join(Url::from(&plugin_config.location).to_string()) .join(format!("{}-{}", plugin_id, client_id)); - let plugin_own_cache_dir = ZELLIJ_SESSION_CACHE_DIR + let plugin_own_cache_dir = ZELLIJ_CACHE_DIR .join(Url::from(&plugin_config.location).to_string()) .join(format!("plugin_cache")); let default_mode = wasm_bridge @@ -1901,9 +1901,11 @@ fn check_event_permission( | Event::EditPaneExited(..) | Event::FailedToWriteConfigToDisk(..) | Event::CommandPaneReRun(..) + | Event::CwdChanged(..) | Event::InputReceived => PermissionType::ReadApplicationState, Event::WebServerStatus(..) => PermissionType::StartWebServer, Event::PaneRenderReport(..) => PermissionType::ReadPaneContents, + Event::UserAction(..) => PermissionType::InterceptInput, _ => return (PermissionStatus::Granted, None), }; @@ -1943,54 +1945,63 @@ pub fn apply_event_to_plugin( mode_info.keybinds = running_plugin.store.data().keybinds.to_keybinds_vec(); mode_info.base_mode = Some(running_plugin.store.data().default_mode); } - let protobuf_event: ProtobufEvent = event - .clone() - .try_into() - .map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?; - let update = instance - .get_typed_func::<(), i32>(&mut running_plugin.store, "update") - .with_context(err_context)?; - wasi_write_object(running_plugin.store.data(), &protobuf_event.encode_to_vec()) - .with_context(err_context)?; - let should_render = update - .call(&mut running_plugin.store, ()) - .with_context(err_context)?; - let mut should_render = should_render == 1; - if let Event::PermissionRequestResult(..) = event { - // we always render in this case, otherwise the request permission screen stays on - // screen - should_render = true; - } - if rows > 0 && columns > 0 && should_render { - let rendered_bytes = instance - .get_typed_func::<(i32, i32), ()>(&mut running_plugin.store, "render") - .and_then(|render| { - render.call(&mut running_plugin.store, (rows as i32, columns as i32)) - }) - .map_err(|e| anyhow!(e)) - .and_then(|_| { - wasi_read_string(running_plugin.store.data()).map_err(|e| anyhow!(e)) - }) - .with_context(err_context)?; - let pipes_to_block_or_unblock = pipes_to_block_or_unblock(running_plugin, None); - let plugin_render_asset = PluginRenderAsset::new( - plugin_id, - client_id, - rendered_bytes.as_bytes().to_vec(), - ) - .with_pipes(pipes_to_block_or_unblock); - plugin_render_assets.push(plugin_render_asset); - } else { - // This is a bit of a hack to get around the fact that plugins are allowed not to - // render and still unblock CLI pipes - let pipes_to_block_or_unblock = pipes_to_block_or_unblock(running_plugin, None); - let plugin_render_asset = PluginRenderAsset::new(plugin_id, client_id, vec![]) - .with_pipes(pipes_to_block_or_unblock); - let _ = senders - .send_to_plugin(PluginInstruction::UnblockCliPipes(vec![ - plugin_render_asset, - ])) - .context("failed to unblock input pipe"); + let protobuf_event: Result = event.clone().try_into(); + match protobuf_event { + Ok(protobuf_event) => { + let update = instance + .get_typed_func::<(), i32>(&mut running_plugin.store, "update") + .with_context(err_context)?; + wasi_write_object(running_plugin.store.data(), &protobuf_event.encode_to_vec()) + .with_context(err_context)?; + let should_render = update + .call(&mut running_plugin.store, ()) + .with_context(err_context)?; + let mut should_render = should_render == 1; + if let Event::PermissionRequestResult(..) = event { + // we always render in this case, otherwise the request permission screen stays on + // screen + should_render = true; + } + if rows > 0 && columns > 0 && should_render { + let rendered_bytes = instance + .get_typed_func::<(i32, i32), ()>(&mut running_plugin.store, "render") + .and_then(|render| { + render + .call(&mut running_plugin.store, (rows as i32, columns as i32)) + }) + .map_err(|e| anyhow!(e)) + .and_then(|_| { + wasi_read_string(running_plugin.store.data()) + .map_err(|e| anyhow!(e)) + }) + .with_context(err_context)?; + let pipes_to_block_or_unblock = + pipes_to_block_or_unblock(running_plugin, None); + let plugin_render_asset = PluginRenderAsset::new( + plugin_id, + client_id, + rendered_bytes.as_bytes().to_vec(), + ) + .with_pipes(pipes_to_block_or_unblock); + plugin_render_assets.push(plugin_render_asset); + } else { + // This is a bit of a hack to get around the fact that plugins are allowed not to + // render and still unblock CLI pipes + let pipes_to_block_or_unblock = + pipes_to_block_or_unblock(running_plugin, None); + let plugin_render_asset = + PluginRenderAsset::new(plugin_id, client_id, vec![]) + .with_pipes(pipes_to_block_or_unblock); + let _ = senders + .send_to_plugin(PluginInstruction::UnblockCliPipes(vec![ + plugin_render_asset, + ])) + .context("failed to unblock input pipe"); + } + }, + Err(e) => { + log::error!("Failed to convert to protobuf: {:?}", e); + }, } }, (PermissionStatus::Denied, permission) => { diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 526b4bcb1..f64a9e18a 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -20,9 +20,9 @@ use std::{ }; use wasmi::{Caller, Linker}; use zellij_utils::data::{ - CommandType, ConnectToSession, FloatingPaneCoordinates, HttpVerb, KeyWithModifier, LayoutInfo, - MessageToPlugin, NewPanePlacement, OriginatingPlugin, PaneScrollbackResponse, PermissionStatus, - PermissionType, PluginPermission, + CommandType, ConnectToSession, Event, FloatingPaneCoordinates, GetPanePidResponse, HttpVerb, + KeyWithModifier, LayoutInfo, MessageToPlugin, NewPanePlacement, OriginatingPlugin, + PaneScrollbackResponse, PermissionStatus, PermissionType, PluginPermission, }; use zellij_utils::input::permission::PermissionCache; use zellij_utils::ipc::{ClientToServerMsg, IpcSenderWithContext}; @@ -39,7 +39,7 @@ use prost::Message; use zellij_utils::{ consts::{VERSION, ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR}, data::{ - CommandToRun, Direction, Event, EventType, FileToOpen, InputMode, PluginCommand, PluginIds, + CommandToRun, Direction, EventType, FileToOpen, InputMode, PluginCommand, PluginIds, PluginMessage, Resize, ResizeStrategy, }, errors::prelude::*, @@ -50,7 +50,7 @@ use zellij_utils::{ }, plugin_api::{ event::ProtobufPaneScrollbackResponse, - plugin_command::ProtobufPluginCommand, + plugin_command::{ProtobufGetPanePidResponse, ProtobufPluginCommand}, plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion}, }, }; @@ -104,6 +104,7 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?, PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?, PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable), + PluginCommand::ShowCursor(cursor_position) => show_cursor(env, cursor_position), PluginCommand::GetPluginIds => get_plugin_ids(env), PluginCommand::GetZellijVersion => get_zellij_version(env), PluginCommand::OpenFile(file_to_open, context) => { @@ -198,6 +199,7 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::EditScrollback => edit_scrollback(env), PluginCommand::Write(bytes) => write(env, bytes), PluginCommand::WriteChars(chars) => write_chars(env, chars), + PluginCommand::CopyToClipboard(text) => copy_to_clipboard(env, text)?, PluginCommand::ToggleTab => toggle_tab(env), PluginCommand::MovePane => move_pane(env), PluginCommand::MovePaneWithDirection(direction) => { @@ -233,12 +235,26 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::ClosePluginPane(plugin_pane_id) => { close_plugin_pane(env, plugin_pane_id) }, - PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden) => { - focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden) - }, - PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => { - focus_plugin_pane(env, plugin_pane_id, should_float_if_hidden) - }, + PluginCommand::FocusTerminalPane( + terminal_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ) => focus_terminal_pane( + env, + terminal_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ), + PluginCommand::FocusPluginPane( + plugin_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ) => focus_plugin_pane( + env, + plugin_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ), PluginCommand::RenameTerminalPane(terminal_pane_id, new_name) => { rename_terminal_pane(env, terminal_pane_id, &new_name) }, @@ -317,9 +333,16 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::HidePaneWithId(pane_id) => { hide_pane_with_id(env, pane_id.into())? }, - PluginCommand::ShowPaneWithId(pane_id, should_float_if_hidden) => { - show_pane_with_id(env, pane_id.into(), should_float_if_hidden) - }, + PluginCommand::ShowPaneWithId( + pane_id, + should_float_if_hidden, + should_focus_pane, + ) => show_pane_with_id( + env, + pane_id.into(), + should_float_if_hidden, + should_focus_pane, + ), PluginCommand::OpenCommandPaneBackground(command_to_run, context) => { open_command_pane_background(env, command_to_run, context) }, @@ -342,6 +365,13 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::WriteCharsToPaneId(chars, pane_id) => { write_chars_to_pane_id(env, chars, pane_id.into()) }, + PluginCommand::SendSigintToPaneId(pane_id) => { + send_sigint_to_pane_id(env, pane_id.into()) + }, + PluginCommand::SendSigkillToPaneId(pane_id) => { + send_sigkill_to_pane_id(env, pane_id.into()) + }, + PluginCommand::GetPanePid { pane_id } => get_pane_pid(env, pane_id.into()), PluginCommand::MovePaneWithPaneId(pane_id) => { move_pane_with_pane_id(env, pane_id.into()) }, @@ -509,11 +539,14 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::ReplacePaneWithExistingPane( pane_id_to_replace, existing_pane_id, + suppress_replaced_pane, ) => replace_pane_with_existing_pane( &mut env, pane_id_to_replace.into(), existing_pane_id.into(), + suppress_replaced_pane, ), + PluginCommand::RunAction(action, context) => run_action(&env, action, context), }, (PermissionStatus::Denied, permission) => { log::error!( @@ -595,6 +628,27 @@ fn set_selectable(env: &PluginEnv, selectable: bool) { .non_fatal(); } +fn show_cursor(env: &PluginEnv, cursor_position: Option<(usize, usize)>) { + env.senders + .send_to_screen(ScreenInstruction::ShowPluginCursor( + env.plugin_id, + env.client_id, + cursor_position, + )) + .with_context(|| { + format!( + "failed to {} plugin cursor from plugin {}", + if cursor_position.is_some() { + "show" + } else { + "hide" + }, + env.name() + ) + }) + .non_fatal(); +} + fn request_permission(env: &PluginEnv, permissions: Vec) -> Result<()> { if PermissionCache::from_path_or_default(None) .check_permissions(env.plugin.location.to_string(), &permissions) @@ -680,10 +734,74 @@ fn open_file(env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap) { + // Clone the necessary data to move into the thread + action.populate_originating_plugin(OriginatingPlugin::new( + env.plugin_id, + env.client_id, + context.clone(), + )); + let action_clone = action.clone(); + let client_id = env.client_id; + let plugin_id = env.plugin_id; + let senders = env.senders.clone(); + let capabilities = env.capabilities.clone(); + let client_attributes = env.client_attributes.clone(); + let default_shell = env.default_shell.clone(); + let default_layout = env.default_layout.clone(); + let keybinds = env.keybinds.clone(); + let default_mode = env.default_mode.clone(); + let plugin_name = env.name().to_string(); + + // Spawn a new thread to execute the action + thread::spawn(move || { + // Execute the action and capture the result + let pane_id = match route_action( + action, + client_id, + None, + Some(PaneId::Plugin(plugin_id)), + senders.clone(), + capabilities, + client_attributes, + default_shell, + default_layout, + None, + keybinds, + default_mode, + None, + ) { + Ok((_should_break, result)) => { + // Extract pane_id from ActionCompletionResult + result.and_then(|r| r.affected_pane_id) + }, + Err(e) => { + log::error!("failed to run action in plugin {}: {:?}", plugin_name, e); + None + }, + }; + + // After action completes, send ActionComplete event with context + // Convert server PaneId to zellij_utils PaneId + let pane_id_for_event = pane_id.map(|p| p.into()); + let updates = vec![( + Some(plugin_id), + Some(client_id), + Event::ActionComplete(action_clone, pane_id_for_event, context), + )]; + + // Send the ActionComplete event back to the plugin + if let Err(e) = senders.send_to_plugin(PluginInstruction::Update(updates)) { + log::error!("Failed to send ActionComplete event: {:?}", e); + } + }); +} + fn open_file_floating( env: &PluginEnv, file_to_open: FileToOpen, @@ -708,6 +826,7 @@ fn open_file_floating( in_place, start_suppressed, coordinates: floating_pane_coordinates, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -736,6 +855,7 @@ fn open_file_in_place( in_place, start_suppressed, coordinates: None, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -845,6 +965,7 @@ fn open_terminal(env: &PluginEnv, cwd: PathBuf) { direction: None, command: run_command_action, pane_name: None, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -894,6 +1015,7 @@ fn open_terminal_floating( command: run_command_action, pane_name: None, coordinates: floating_pane_coordinates, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -942,6 +1064,9 @@ fn open_terminal_in_place(env: &PluginEnv, cwd: PathBuf) { let action = Action::NewInPlacePane { command: run_command_action, pane_name: None, + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, }; apply_action!(action, error_msg, env); } @@ -1044,6 +1169,7 @@ fn open_command_pane( direction, command: Some(run_command_action), pane_name: name, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -1120,6 +1246,7 @@ fn open_command_pane_floating( command: Some(run_command_action), pane_name: name, coordinates: floating_pane_coordinates, + near_current_pane: false, }; apply_action!(action, error_msg, env); } @@ -1195,6 +1322,9 @@ fn open_command_pane_in_place( let action = Action::NewInPlacePane { command: Some(run_command_action), pane_name: name, + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, }; apply_action!(action, error_msg, env); } @@ -1418,20 +1548,36 @@ fn show_self(env: &PluginEnv, should_float_if_hidden: bool) { let action = Action::FocusPluginPaneWithId { pane_id: env.plugin_id, should_float_if_hidden, + should_be_in_place_if_hidden: false, }; let error_msg = || format!("Failed to show self for plugin"); apply_action!(action, error_msg, env); } -fn show_pane_with_id(env: &PluginEnv, pane_id: PaneId, should_float_if_hidden: bool) { - let _ = env - .senders - .send_to_screen(ScreenInstruction::FocusPaneWithId( - pane_id, - should_float_if_hidden, - env.client_id, - None, - )); +fn show_pane_with_id( + env: &PluginEnv, + pane_id: PaneId, + should_float_if_hidden: bool, + should_focus_pane: bool, +) { + if should_focus_pane { + let _ = env + .senders + .send_to_screen(ScreenInstruction::FocusPaneWithId( + pane_id, + should_float_if_hidden, + false, + env.client_id, + None, + )); + } else { + let _ = env + .senders + .send_to_screen(ScreenInstruction::UnsuppressOrExpandPane( + pane_id, + should_float_if_hidden, + )); + } } fn close_self(env: &PluginEnv) { @@ -1524,6 +1670,8 @@ fn apply_layout(env: &PluginEnv, layout: Layout) { tab_name: None, should_change_focus_to_new_tab: true, cwd, + initial_panes: None, + first_pane_unblock_condition: None, }; tabs_to_open.push(action); } else { @@ -1542,6 +1690,8 @@ fn apply_layout(env: &PluginEnv, layout: Layout) { tab_name, should_change_focus_to_new_tab: should_focus_tab, cwd: cwd.clone(), + initial_panes: None, + first_pane_unblock_condition: None, }; tabs_to_open.push(action); } @@ -1562,6 +1712,8 @@ fn new_tab(env: &PluginEnv, name: Option, cwd: Option) { tab_name: name, should_change_focus_to_new_tab: true, cwd, + initial_panes: None, + first_pane_unblock_condition: None, }; let error_msg = || format!("Failed to open new tab"); apply_action!(action, error_msg, env); @@ -1741,6 +1893,12 @@ fn write_chars(env: &PluginEnv, chars_to_write: String) { apply_action!(action, error_msg, env); } +fn copy_to_clipboard(env: &PluginEnv, text: String) -> Result<()> { + env.senders + .send_to_screen(ScreenInstruction::CopyTextToClipboard(text, env.plugin_id)) + .with_context(|| format!("failed to copy to clipboard in plugin {}", env.name())) +} + fn toggle_tab(env: &PluginEnv) { let error_msg = || format!("Failed to toggle tab"); let action = Action::ToggleTab; @@ -1938,19 +2096,31 @@ fn close_plugin_pane(env: &PluginEnv, plugin_pane_id: u32) { .non_fatal(); } -fn focus_terminal_pane(env: &PluginEnv, terminal_pane_id: u32, should_float_if_hidden: bool) { +fn focus_terminal_pane( + env: &PluginEnv, + terminal_pane_id: u32, + should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, +) { let action = Action::FocusTerminalPaneWithId { pane_id: terminal_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }; let error_msg = || format!("Failed to focus terminal pane"); apply_action!(action, error_msg, env); } -fn focus_plugin_pane(env: &PluginEnv, plugin_pane_id: u32, should_float_if_hidden: bool) { +fn focus_plugin_pane( + env: &PluginEnv, + plugin_pane_id: u32, + should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, +) { let action = Action::FocusPluginPaneWithId { pane_id: plugin_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }; let error_msg = || format!("Failed to focus plugin pane"); apply_action!(action, error_msg, env); @@ -2237,6 +2407,92 @@ fn write_chars_to_pane_id(env: &PluginEnv, chars: String, pane_id: PaneId) { .send_to_screen(ScreenInstruction::WriteToPaneId(bytes, pane_id)); } +fn send_sigint_to_pane_id(env: &PluginEnv, pane_id: PaneId) { + let err_context = || { + format!( + "failed to send SIGINT to pane {:?} from plugin {}", + pane_id, + env.name() + ) + }; + + env.senders + .send_to_pty(PtyInstruction::SendSigintToPaneId(pane_id)) + .with_context(err_context) + .non_fatal(); +} + +fn send_sigkill_to_pane_id(env: &PluginEnv, pane_id: PaneId) { + let err_context = || { + format!( + "failed to send SIGKILL to pane {:?} from plugin {}", + pane_id, + env.name() + ) + }; + + env.senders + .send_to_pty(PtyInstruction::SendSigkillToPaneId(pane_id)) + .with_context(err_context) + .non_fatal(); +} + +fn get_pane_pid(env: &PluginEnv, pane_id: PaneId) { + use crossbeam::channel::RecvTimeoutError; + use std::time::Duration; + + let err_context = || { + format!( + "failed to get PID for pane {:?} from plugin {}", + pane_id, + env.name() + ) + }; + + // Create oneshot channel for response + let (response_sender, response_receiver) = crossbeam::channel::bounded(1); + + // Send request directly to PTY thread + env.senders + .send_to_pty(PtyInstruction::GetPanePid { + pane_id, + response_channel: response_sender, + }) + .with_context(err_context) + .non_fatal(); + + // Block waiting for response with 100ms timeout + let response = match response_receiver.recv_timeout(Duration::from_millis(100)) { + Ok(response) => response, + Err(RecvTimeoutError::Timeout) => { + log::error!( + "GetPanePid timed out after 5s for plugin {} requesting pane {:?}", + env.plugin_id, + pane_id + ); + GetPanePidResponse::Err(format!("Timeout retrieving PID for pane {:?}", pane_id)) + }, + Err(RecvTimeoutError::Disconnected) => { + log::error!( + "GetPanePid channel disconnected for plugin {} requesting pane {:?}", + env.plugin_id, + pane_id + ); + GetPanePidResponse::Err(format!( + "Channel disconnected while retrieving PID for pane {:?}", + pane_id + )) + }, + }; + + // Encode response and write to plugin's stdin + let protobuf_response = ProtobufGetPanePidResponse::from(response); + let serialized = protobuf_response.encode_to_vec(); + wasi_write_object(env, &serialized) + .with_context(err_context) + .non_fatal(); +} + fn move_pane_with_pane_id(env: &PluginEnv, pane_id: PaneId) { let _ = env .senders @@ -2666,12 +2922,14 @@ fn replace_pane_with_existing_pane( env: &mut PluginEnv, pane_to_replace: PaneId, existing_pane: PaneId, + suppress_replaced_pane: bool, ) { let _ = env .senders .send_to_screen(ScreenInstruction::ReplacePaneWithExistingPane( pane_to_replace, existing_pane, + suppress_replaced_pane, )); } @@ -2762,6 +3020,7 @@ fn check_command_permission( | PluginCommand::WriteChars(..) | PluginCommand::WriteToPaneId(..) | PluginCommand::WriteCharsToPaneId(..) => PermissionType::WriteToStdin, + PluginCommand::CopyToClipboard(..) => PermissionType::WriteToClipboard, PluginCommand::SwitchTabTo(..) | PluginCommand::SwitchToMode(..) | PluginCommand::NewTabsWithLayout(..) @@ -2842,14 +3101,16 @@ fn check_command_permission( | PluginCommand::FloatMultiplePanes(..) | PluginCommand::EmbedMultiplePanes(..) | PluginCommand::ReplacePaneWithExistingPane(..) - | PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState, + | PluginCommand::KillSessions(..) + | PluginCommand::SendSigintToPaneId(..) + | PluginCommand::SendSigkillToPaneId(..) => PermissionType::ChangeApplicationState, PluginCommand::UnblockCliPipeInput(..) | PluginCommand::BlockCliPipeInput(..) | PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes, PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins, - PluginCommand::ListClients | PluginCommand::DumpSessionLayout => { - PermissionType::ReadApplicationState - }, + PluginCommand::ListClients + | PluginCommand::DumpSessionLayout + | PluginCommand::GetPanePid { .. } => PermissionType::ReadApplicationState, PluginCommand::RebindKeys { .. } | PluginCommand::Reconfigure(..) => { PermissionType::Reconfigure }, @@ -2868,6 +3129,7 @@ fn check_command_permission( PermissionType::InterceptInput }, PluginCommand::GetPaneScrollback { .. } => PermissionType::ReadPaneContents, + PluginCommand::RunAction(..) => PermissionType::RunActionsAsUser, _ => return (PermissionStatus::Granted, None), }; diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index 39bf4668c..fee6054e5 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -17,7 +17,10 @@ use nix::unistd::Pid; use std::sync::Arc; use std::{collections::HashMap, os::unix::io::RawFd, path::PathBuf}; use zellij_utils::{ - data::{Direction, Event, FloatingPaneCoordinates, NewPanePlacement, OriginatingPlugin}, + data::{ + CommandOrPlugin, Event, FloatingPaneCoordinates, GetPanePidResponse, NewPanePlacement, + OriginatingPlugin, + }, errors::prelude::*, errors::{ContextType, PtyContext}, input::{ @@ -66,6 +69,8 @@ pub enum PtyInstruction { Vec, usize, // tab_index HashMap>, // plugin_ids + Option>, // initial_panes + bool, // block_on_first_terminal bool, // should change focus to new tab (ClientId, bool), // bool -> is_web_client Option, // completion signal @@ -112,6 +117,13 @@ pub enum PtyInstruction { }, ListClientsToPlugin(SessionLayoutMetadata, PluginId, ClientId), ReportPluginCwd(PluginId, PathBuf), + SendSigintToPaneId(PaneId), + SendSigkillToPaneId(PaneId), + GetPanePid { + pane_id: PaneId, + response_channel: crossbeam::channel::Sender, + }, + UpdateAndReportCwds, Exit, } @@ -136,6 +148,10 @@ impl From<&PtyInstruction> for PtyContext { PtyInstruction::Reconfigure { .. } => PtyContext::Reconfigure, PtyInstruction::ListClientsToPlugin(..) => PtyContext::ListClientsToPlugin, PtyInstruction::ReportPluginCwd(..) => PtyContext::ReportPluginCwd, + PtyInstruction::SendSigintToPaneId(..) => PtyContext::SendSigintToPaneId, + PtyInstruction::SendSigkillToPaneId(..) => PtyContext::SendSigkillToPaneId, + PtyInstruction::GetPanePid { .. } => PtyContext::GetPanePid, + PtyInstruction::UpdateAndReportCwds => PtyContext::UpdateAndReportCwds, PtyInstruction::Exit => PtyContext::Exit, } } @@ -150,7 +166,8 @@ pub(crate) struct Pty { task_handles: HashMap>, // terminal_id to join-handle default_editor: Option, post_command_discovery_hook: Option, - plugin_cwds: HashMap, // plugin_id -> cwd + plugin_cwds: HashMap, // plugin_id -> cwd + terminal_cwds: HashMap, // terminal_id -> cwd } pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { @@ -430,6 +447,8 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { floating_panes_layout, tab_index, plugin_ids, + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, client_id_and_is_web_client, completion_tx, @@ -447,7 +466,9 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { floating_panes_layout, terminal_action.clone(), plugin_ids, + initial_panes, tab_index, + block_on_first_terminal, should_change_focus_to_new_tab, client_id_and_is_web_client, completion_tx, @@ -702,6 +723,22 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { } => { pty.reconfigure(default_editor, post_command_discovery_hook); }, + PtyInstruction::SendSigintToPaneId(pane_id) => { + pty.send_sigint_to_pane(pane_id); + }, + PtyInstruction::SendSigkillToPaneId(pane_id) => { + pty.send_sigkill_to_pane(pane_id); + }, + PtyInstruction::GetPanePid { + pane_id, + response_channel, + } => { + let response = pty.get_pane_pid(pane_id); + let _ = response_channel.send(response); + }, + PtyInstruction::UpdateAndReportCwds => { + pty.update_and_report_cwds(); + }, PtyInstruction::Exit => break, } } @@ -725,6 +762,7 @@ impl Pty { originating_plugins: HashMap::new(), post_command_discovery_hook, plugin_cwds: HashMap::new(), + terminal_cwds: HashMap::new(), } } pub fn get_default_terminal( @@ -774,12 +812,18 @@ impl Pty { .get(&client_id) .and_then(|pane| match pane { PaneId::Plugin(plugin_id) => self.plugin_cwds.get(plugin_id).cloned(), - PaneId::Terminal(id) => self.id_to_child_pid.get(id).and_then(|&id| { - self.bus - .os_input - .as_ref() - .and_then(|input| input.get_cwd(Pid::from_raw(id))) - }), + PaneId::Terminal(id) => { + // Try to get CWD from OS, fall back to cached value + self.id_to_child_pid + .get(id) + .and_then(|&pid| { + self.bus + .os_input + .as_ref() + .and_then(|input| input.get_cwd(Pid::from_raw(pid))) + }) + .or_else(|| self.terminal_cwds.get(id).cloned()) + }, }) }; }; @@ -789,12 +833,16 @@ impl Pty { if run_command.cwd.is_none() { run_command.cwd = match pane_id { PaneId::Terminal(terminal_pane_id) => { - self.id_to_child_pid.get(terminal_pane_id).and_then(|&id| { - self.bus - .os_input - .as_ref() - .and_then(|input| input.get_cwd(Pid::from_raw(id))) - }) + // Try to get CWD from OS, fall back to cached value + self.id_to_child_pid + .get(terminal_pane_id) + .and_then(|&pid| { + self.bus + .os_input + .as_ref() + .and_then(|input| input.get_cwd(Pid::from_raw(pid))) + }) + .or_else(|| self.terminal_cwds.get(terminal_pane_id).cloned()) }, PaneId::Plugin(plugin_id) => self.plugin_cwds.get(plugin_id).cloned(), }; @@ -938,6 +986,8 @@ impl Pty { self.task_handles.insert(terminal_id, terminal_bytes); self.id_to_child_pid.insert(terminal_id, child_fd); + self.capture_initial_cwd(terminal_id, child_fd); + let starts_held = false; Ok((terminal_id, starts_held)) } @@ -948,7 +998,9 @@ impl Pty { floating_panes_layout: Vec, default_shell: Option, plugin_ids: HashMap>, + initial_panes: Option>, tab_index: usize, + block_on_first_terminal: bool, should_change_focus_to_new_tab: bool, client_id_and_is_web_client: (ClientId, bool), completion_tx: Option, @@ -959,6 +1011,22 @@ impl Pty { default_shell.unwrap_or_else(|| self.get_default_terminal(cwd, None)); let (client_id, is_web_client) = client_id_and_is_web_client; self.fill_cwd(&mut default_shell, client_id); + + // Match initial_panes commands to empty slots in the layout + let mut layout = layout; + if let Some(ref initial_panes_vec) = initial_panes { + for initial_pane in initial_panes_vec.iter() { + if let CommandOrPlugin::Command(run_command_action) = initial_pane { + let run_command: RunCommand = run_command_action.clone().into(); + if !layout.replace_next_empty_slot_with_run(Run::Command(run_command)) { + log::warn!("More initial_panes provided than empty slots available"); + break; + } + } + // Skip CommandOrPlugin::Plugin entries (already handled by plugin thread) + } + } + let extracted_run_instructions = layout.extract_run_instructions(); let extracted_floating_run_instructions = floating_panes_layout .iter() @@ -968,24 +1036,55 @@ impl Pty { // starts_held, // run_command, // file_descriptor) + let mut new_floating_panes_pids: Vec<(u32, bool, Option, Result)> = vec![]; // same // as // new_pane_pids + + let mut originating_plugins_to_inform = vec![]; + for run_instruction in extracted_run_instructions { + let originating_plugin = run_instruction.as_ref().and_then(|r| { + if let Run::Command(run_command) = r { + run_command.originating_plugin.clone() + } else { + None + } + }); + let mut terminal_id = None; if let Some(new_pane_data) = self.apply_run_instruction(run_instruction, default_shell.clone())? { + terminal_id = Some(new_pane_data.0); new_pane_pids.push(new_pane_data); } + if let (Some(originating_plugin), Some(terminal_id)) = (originating_plugin, terminal_id) + { + originating_plugins_to_inform.push((terminal_id, originating_plugin)); + } } for run_instruction in extracted_floating_run_instructions { + let originating_plugin = run_instruction.as_ref().and_then(|r| { + if let Run::Command(run_command) = r { + run_command.originating_plugin.clone() + } else { + None + } + }); + let mut terminal_id = None; if let Some(new_pane_data) = self.apply_run_instruction(run_instruction, default_shell.clone())? { + terminal_id = Some(new_pane_data.0); new_floating_panes_pids.push(new_pane_data); } + if let (Some(originating_plugin), Some(terminal_id)) = (originating_plugin, terminal_id) + { + originating_plugins_to_inform.push((terminal_id, originating_plugin)); + } } + // Option should only be Some if the pane starts held let new_tab_pane_ids: Vec<(u32, Option)> = new_pane_pids .iter() @@ -1007,21 +1106,40 @@ impl Pty { } }) .collect(); + + // Track the first terminal_id if blocking is requested + let first_initial_pane_terminal_id = if block_on_first_terminal && !new_pane_pids.is_empty() + { + Some(new_pane_pids[0].0) + } else { + None + }; + + // Prepare blocking_terminal for ApplyLayout + let (direct_completion_tx, blocking_terminal) = + if let Some(terminal_id) = first_initial_pane_terminal_id { + (None, completion_tx.map(|tx| (terminal_id, tx))) + } else { + (completion_tx, None) + }; + self.bus .senders .send_to_screen(ScreenInstruction::ApplyLayout( layout, floating_panes_layout, - new_tab_pane_ids, - new_tab_floating_pane_ids, + new_tab_pane_ids.clone(), + new_tab_floating_pane_ids.clone(), plugin_ids, tab_index, should_change_focus_to_new_tab, (client_id, is_web_client), - completion_tx, + direct_completion_tx, + blocking_terminal, )) .with_context(err_context)?; let mut terminals_to_start = vec![]; + terminals_to_start.append(&mut new_pane_pids); terminals_to_start.append(&mut new_floating_panes_pids); for (terminal_id, starts_held, run_command, pid_primary) in terminals_to_start { @@ -1077,8 +1195,29 @@ impl Pty { }, } } + for (terminal_id, originating_plugin) in originating_plugins_to_inform { + self.inform_originating_plugin_of_open(terminal_id, originating_plugin); + } Ok(()) } + fn inform_originating_plugin_of_open( + &mut self, + terminal_id: u32, + originating_plugin: OriginatingPlugin, + ) { + self.originating_plugins + .insert(terminal_id, originating_plugin.clone()); + let update_event = Event::CommandPaneOpened(terminal_id, originating_plugin.context); + let _ = self + .bus + .senders + .send_to_plugin(PluginInstruction::Update(vec![( + Some(originating_plugin.plugin_id), + Some(originating_plugin.client_id), + update_event, + )])); + } + fn apply_run_instruction( &mut self, run_instruction: Option, @@ -1162,6 +1301,7 @@ impl Pty { { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); + self.capture_initial_cwd(terminal_id, child_fd); Ok(Some(( terminal_id, starts_held, @@ -1194,6 +1334,7 @@ impl Pty { { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); + self.capture_initial_cwd(terminal_id, child_fd); Ok(Some((terminal_id, starts_held, None, Ok(pid_primary)))) }, Err(err) => match err.downcast_ref::() { @@ -1225,6 +1366,7 @@ impl Pty { { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); + self.capture_initial_cwd(terminal_id, child_fd); Ok(Some((terminal_id, starts_held, None, Ok(pid_primary)))) }, Err(err) => match err.downcast_ref::() { @@ -1248,6 +1390,7 @@ impl Pty { { Ok((terminal_id, pid_primary, child_fd)) => { self.id_to_child_pid.insert(terminal_id, child_fd); + self.capture_initial_cwd(terminal_id, child_fd); Ok(Some((terminal_id, starts_held, None, Ok(pid_primary)))) }, Err(err) => match err.downcast_ref::() { @@ -1389,6 +1532,7 @@ impl Pty { self.task_handles.insert(id, terminal_bytes); self.id_to_child_pid.insert(id, child_fd); + self.capture_initial_cwd(id, child_fd); if let Some(originating_plugin) = self.originating_plugins.get(&id) { self.bus .senders @@ -1405,9 +1549,10 @@ impl Pty { } } pub fn populate_session_layout_metadata( - &self, + &mut self, session_layout_metadata: &mut SessionLayoutMetadata, ) { + log::info!("populate_session_layout_metadata"); let terminal_ids = session_layout_metadata.all_terminal_ids(); let mut terminal_ids_to_commands: HashMap> = HashMap::new(); let mut terminal_ids_to_cwds: HashMap = HashMap::new(); @@ -1476,12 +1621,18 @@ impl Pty { .get(&client_id) .and_then(|pane| match pane { PaneId::Plugin(plugin_id) => self.plugin_cwds.get(plugin_id).cloned(), - PaneId::Terminal(id) => self.id_to_child_pid.get(id).and_then(|&id| { - self.bus - .os_input - .as_ref() - .and_then(|input| input.get_cwd(Pid::from_raw(id))) - }), + PaneId::Terminal(id) => { + // Try to get CWD from OS, fall back to cached value + self.id_to_child_pid + .get(id) + .and_then(|&pid| { + self.bus + .os_input + .as_ref() + .and_then(|input| input.get_cwd(Pid::from_raw(pid))) + }) + .or_else(|| self.terminal_cwds.get(id).cloned()) + }, }) }; @@ -1516,6 +1667,59 @@ impl Pty { ))?; Ok(()) } + fn capture_initial_cwd(&mut self, terminal_id: u32, child_fd: RawFd) { + if let Some(os_input) = self.bus.os_input.as_ref() { + if let Some(cwd) = os_input.get_cwd(Pid::from_raw(child_fd)) { + self.terminal_cwds.insert(terminal_id, cwd); + } + } + } + + pub fn update_and_report_cwds(&mut self) { + let terminal_ids: Vec = self.id_to_child_pid.keys().copied().collect(); + + let pids: Vec<_> = terminal_ids + .iter() + .filter_map(|id| self.id_to_child_pid.get(&id)) + .map(|pid| Pid::from_raw(*pid)) + .collect(); + + let (pids_to_cwds, _) = self + .bus + .os_input + .as_ref() + .map(|os_input| os_input.get_cwds(pids)) + .unwrap_or_default(); + + for terminal_id in terminal_ids { + let process_id = self.id_to_child_pid.get(&terminal_id); + let cwd = process_id + .as_ref() + .and_then(|pid| pids_to_cwds.get(&Pid::from_raw(**pid))); + + if let Some(cwd) = cwd { + if self.terminal_cwds.get(&terminal_id) != Some(cwd) { + let pane_id = PaneId::Terminal(terminal_id); + let focused_client_ids: Vec = self + .active_panes + .iter() + .filter(|(_, active_pane)| *active_pane == &pane_id) + .map(|(client_id, _)| *client_id) + .collect(); + let _ = self + .bus + .senders + .send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::CwdChanged(pane_id.into(), cwd.clone(), focused_client_ids), + )])); + } + self.terminal_cwds.insert(terminal_id, cwd.clone()); + } + } + } + pub fn reconfigure( &mut self, default_editor: Option, @@ -1524,6 +1728,72 @@ impl Pty { self.default_editor = default_editor; self.post_command_discovery_hook = post_command_discovery_hook; } + + pub fn send_sigint_to_pane(&self, pane_id: PaneId) { + let err_context = || format!("failed to send SIGINT to pane {:?}", pane_id); + + match pane_id { + PaneId::Terminal(terminal_id) => { + if let Some(&child_fd) = self.id_to_child_pid.get(&terminal_id) { + let pid = Pid::from_raw(child_fd); + self.bus + .os_input + .as_ref() + .context("no OS I/O interface found") + .and_then(|os_input| os_input.send_sigint(pid)) + .with_context(err_context) + .non_fatal(); + } else { + log::warn!("Terminal pane {} not found or not running", terminal_id); + } + }, + PaneId::Plugin(plugin_id) => { + log::warn!("Cannot send SIGINT to plugin pane {}", plugin_id); + }, + } + } + + pub fn send_sigkill_to_pane(&self, pane_id: PaneId) { + let err_context = || format!("failed to send SIGKILL to pane {:?}", pane_id); + + match pane_id { + PaneId::Terminal(terminal_id) => { + if let Some(&child_fd) = self.id_to_child_pid.get(&terminal_id) { + let pid = Pid::from_raw(child_fd); + self.bus + .os_input + .as_ref() + .context("no OS I/O interface found") + .and_then(|os_input| os_input.force_kill(pid)) + .with_context(err_context) + .non_fatal(); + } else { + log::warn!("Terminal pane {} not found or not running", terminal_id); + } + }, + PaneId::Plugin(plugin_id) => { + log::warn!("Cannot send SIGKILL to plugin pane {}", plugin_id); + }, + } + } + + pub fn get_pane_pid(&self, pane_id: PaneId) -> GetPanePidResponse { + match pane_id { + PaneId::Terminal(terminal_id) => { + if let Some(&child_fd) = self.id_to_child_pid.get(&terminal_id) { + GetPanePidResponse::Ok(child_fd) + } else { + GetPanePidResponse::Err(format!( + "Terminal pane {} not found or not running", + terminal_id + )) + } + }, + PaneId::Plugin(plugin_id) => { + GetPanePidResponse::Err(format!("Cannot get PID for plugin pane {}", plugin_id)) + }, + } + } } impl Drop for Pty { diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 7d11ba658..ead4282d4 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -19,7 +19,7 @@ use zellij_utils::{ channels::SenderWithContext, data::{ BareKey, ConnectToSession, Direction, Event, InputMode, KeyModifier, NewPanePlacement, - PluginCapabilities, ResizeStrategy, + PluginCapabilities, ResizeStrategy, UnblockCondition, }, envs, errors::prelude::*, @@ -39,21 +39,28 @@ use crate::ClientId; const ACTION_COMPLETION_TIMEOUT: Duration = Duration::from_secs(1); +#[derive(Debug, Clone)] +pub struct ActionCompletionResult { + pub exit_status: Option, + pub affected_pane_id: Option, +} + fn wait_for_action_completion( - receiver: oneshot::Receiver>, // currently i32 signals an exit status, can be expanded as - // needed + receiver: oneshot::Receiver, action_name: &str, wait_forever: bool, -) -> Option { +) -> ActionCompletionResult { let runtime = get_tokio_runtime(); if wait_forever { runtime.block_on(async { match receiver.await { - Ok(Some(payload)) => Some(payload), - Ok(None) => None, + Ok(result) => result, Err(e) => { log::error!("Failed to wait for action {}: {}", action_name, e); - None + ActionCompletionResult { + exit_status: None, + affected_pane_id: None, + } }, } }) @@ -61,15 +68,17 @@ fn wait_for_action_completion( match runtime .block_on(async { tokio::time::timeout(ACTION_COMPLETION_TIMEOUT, receiver).await }) { - Ok(Ok(Some(payload))) => Some(payload), - Ok(Ok(None)) => None, + Ok(Ok(result)) => result, Err(_) | Ok(Err(_)) => { log::error!( "Action {} did not complete within {:?} timeout", action_name, ACTION_COMPLETION_TIMEOUT ); - None + ActionCompletionResult { + exit_status: None, + affected_pane_id: None, + } }, } } @@ -83,8 +92,10 @@ fn wait_for_action_completion( // that it can be included in various other larger structs - DO NOT RELY ON CLONING IT! #[derive(Debug)] pub struct NotificationEnd { - channel: Option>>, + channel: Option>, exit_status: Option, + unblock_condition: Option, + affected_pane_id: Option, // optional payload of the pane id affected by this action } impl Clone for NotificationEnd { @@ -93,26 +104,55 @@ impl Clone for NotificationEnd { NotificationEnd { channel: None, exit_status: self.exit_status, + unblock_condition: self.unblock_condition, + affected_pane_id: self.affected_pane_id, } } } impl NotificationEnd { - pub fn new(sender: oneshot::Sender>) -> Self { + pub fn new(sender: oneshot::Sender) -> Self { NotificationEnd { channel: Some(sender), exit_status: None, + unblock_condition: None, + affected_pane_id: None, } } + + pub fn new_with_condition( + sender: oneshot::Sender, + unblock_condition: UnblockCondition, + ) -> Self { + NotificationEnd { + channel: Some(sender), + exit_status: None, + unblock_condition: Some(unblock_condition), + affected_pane_id: None, + } + } + pub fn set_exit_status(&mut self, exit_status: i32) { self.exit_status = Some(exit_status); } + + pub fn set_affected_pane_id(&mut self, pane_id: PaneId) { + self.affected_pane_id = Some(pane_id); + } + + pub fn unblock_condition(&self) -> Option { + self.unblock_condition + } } impl Drop for NotificationEnd { fn drop(&mut self) { if let Some(tx) = self.channel.take() { - let _ = tx.send(self.exit_status); + let result = ActionCompletionResult { + exit_status: self.exit_status, + affected_pane_id: self.affected_pane_id, + }; + let _ = tx.send(result); } } } @@ -131,7 +171,7 @@ pub(crate) fn route_action( client_keybinds: Keybinds, default_mode: InputMode, os_input: Option>, -) -> Result { +) -> Result<(bool, Option)> { let mut should_break = false; let err_context = || format!("failed to route action for client {client_id}"); let action_name = action.to_string(); @@ -480,19 +520,51 @@ pub(crate) fn route_action( placement, pane_name, command, + unblock_condition, + near_current_pane, } => { let command = command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); let set_pane_blocking = true; + + let notification_end = if let Some(condition) = unblock_condition { + Some(NotificationEnd::new_with_condition( + completion_tx, + condition, + )) + } else { + Some(NotificationEnd::new(completion_tx)) + }; + + // we prefer the pane id provided by the action explicitly over the one that originated + // it (this might be a bit misleading with "near_current_pane", but it's still the + // right behavior - in the latter case, if the originator does not wish for this + // behavior, they should not provide pane + // inside the placement, but rather have the current pane id be picked up instead) + let pane_id = match placement { + NewPanePlacement::Stacked(pane_id_to_stack_under) => { + pane_id_to_stack_under.map(|p| p.into()).or(pane_id) + }, + NewPanePlacement::InPlace { + pane_id_to_replace, .. + } => pane_id_to_replace.map(|p| p.into()).or(pane_id), + _ => pane_id, + }; + + let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; senders .send_to_pty(PtyInstruction::SpawnTerminal( command, pane_name, placement, false, - ClientTabIndexOrPaneId::ClientId(client_id), - Some(NotificationEnd::new(completion_tx)), + client_tab_index_or_paneid, + notification_end, set_pane_blocking, )) .with_context(err_context)?; @@ -505,19 +577,20 @@ pub(crate) fn route_action( in_place: should_open_in_place, start_suppressed, coordinates: floating_pane_coordinates, + near_current_pane, } => { let title = format!("Editing: {}", open_file_payload.path.display()); let open_file = TerminalAction::OpenFile(open_file_payload); let pty_instr = if should_open_in_place { match pane_id { - Some(pane_id) => PtyInstruction::SpawnInPlaceTerminal( + Some(pane_id) if near_current_pane => PtyInstruction::SpawnInPlaceTerminal( Some(open_file), Some(title), false, ClientTabIndexOrPaneId::PaneId(pane_id), Some(NotificationEnd::new(completion_tx)), ), - None => PtyInstruction::SpawnInPlaceTerminal( + _ => PtyInstruction::SpawnInPlaceTerminal( Some(open_file), Some(title), false, @@ -579,17 +652,23 @@ pub(crate) fn route_action( command: run_command, pane_name: name, coordinates: floating_pane_coordinates, + near_current_pane, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); + let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, NewPanePlacement::Floating(floating_pane_coordinates), false, - ClientTabIndexOrPaneId::ClientId(client_id), + client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) @@ -598,28 +677,35 @@ pub(crate) fn route_action( Action::NewInPlacePane { command: run_command, pane_name: name, + near_current_pane, + pane_id_to_replace, + close_replace_pane, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); + let pane_id = match pane_id_to_replace { + Some(pane_id_to_replace) => pane_id_to_replace.try_into().ok(), + None => pane_id, + }; match pane_id { - Some(pane_id) => { + Some(pane_id) if near_current_pane => { senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( run_cmd, name, - false, + close_replace_pane, ClientTabIndexOrPaneId::PaneId(pane_id), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, - None => { + _ => { senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( run_cmd, name, - false, + close_replace_pane, ClientTabIndexOrPaneId::ClientId(client_id), Some(NotificationEnd::new(completion_tx)), )) @@ -630,12 +716,14 @@ pub(crate) fn route_action( Action::NewStackedPane { command: run_command, pane_name: name, + near_current_pane, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); + match pane_id { - Some(pane_id) => { + Some(pane_id) if near_current_pane => { senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, @@ -648,7 +736,7 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, - None => { + _ => { senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, @@ -667,17 +755,23 @@ pub(crate) fn route_action( direction, command: run_command, pane_name: name, + near_current_pane, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); + let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, NewPanePlacement::Tiled(direction), false, - ClientTabIndexOrPaneId::ClientId(client_id), + client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) @@ -717,15 +811,23 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, - Action::Run { command } => { + Action::Run { + command, + near_current_pane, + } => { let run_cmd = Some(TerminalAction::RunCommand(command.clone().into())); + let client_tab_index_or_paneid = if near_current_pane && pane_id.is_some() { + ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) + } else { + ClientTabIndexOrPaneId::ClientId(client_id) + }; senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, None, NewPanePlacement::Tiled(command.direction), false, - ClientTabIndexOrPaneId::ClientId(client_id), + client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) @@ -747,6 +849,8 @@ pub(crate) fn route_action( tab_name, should_change_focus_to_new_tab, cwd, + initial_panes, + first_pane_unblock_condition, } => { let shell = default_shell.clone(); let swap_tiled_layouts = @@ -755,6 +859,17 @@ pub(crate) fn route_action( .unwrap_or_else(|| default_layout.swap_floating_layouts.clone()); let is_web_client = false; // actions cannot be initiated directly from the web + // Construct completion_tx conditionally + let (completion_tx, block_on_first_terminal) = if let Some(condition) = + first_pane_unblock_condition + { + let notification = NotificationEnd::new_with_condition(completion_tx, condition); + wait_forever = true; + (notification, true) + } else { + (NotificationEnd::new(completion_tx), false) + }; + senders .send_to_screen(ScreenInstruction::NewTab( cwd, @@ -763,9 +878,11 @@ pub(crate) fn route_action( floating_panes_layout, tab_name, (swap_tiled_layouts, swap_floating_layouts), + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), - Some(NotificationEnd::new(completion_tx)), + Some(completion_tx), )) .with_context(err_context)?; }, @@ -921,20 +1038,12 @@ pub(crate) fn route_action( .with_context(err_context)?; }, Action::Confirm => { - senders - .send_to_screen(ScreenInstruction::ConfirmPrompt( - client_id, - Some(NotificationEnd::new(completion_tx)), - )) - .with_context(err_context)?; + // no-op, these are deprecated and should be removed when we upgrade the server/client + // contract }, Action::Deny => { - senders - .send_to_screen(ScreenInstruction::DenyPrompt( - client_id, - Some(NotificationEnd::new(completion_tx)), - )) - .with_context(err_context)?; + // no-op, these are deprecated and should be removed when we upgrade the server/client + // contract }, #[allow(clippy::single_match)] Action::SkipConfirm { action } => match *action { @@ -1147,11 +1256,13 @@ pub(crate) fn route_action( Action::FocusTerminalPaneWithId { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => { senders .send_to_screen(ScreenInstruction::FocusPaneWithId( PaneId::Terminal(pane_id), should_float_if_hidden, + should_be_in_place_if_hidden, client_id, Some(NotificationEnd::new(completion_tx)), )) @@ -1160,11 +1271,13 @@ pub(crate) fn route_action( Action::FocusPluginPaneWithId { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => { senders .send_to_screen(ScreenInstruction::FocusPaneWithId( PaneId::Plugin(pane_id), should_float_if_hidden, + should_be_in_place_if_hidden, client_id, Some(NotificationEnd::new(completion_tx)), )) @@ -1400,8 +1513,8 @@ pub(crate) fn route_action( .with_context(err_context)?; }, } - let exit_status = wait_for_action_completion(completion_rx, &action_name, wait_forever); - if let Some(exit_status) = exit_status { + let result = wait_for_action_completion(completion_rx, &action_name, wait_forever); + if let Some(exit_status) = result.exit_status { if let Some(cli_client_id) = cli_client_id { if let Some(os_input) = os_input { let _ = os_input.send_to_client( @@ -1413,14 +1526,14 @@ pub(crate) fn route_action( } } } - Ok(should_break) + Ok((should_break, Some(result))) } // this should only be used for one-off startup instructions macro_rules! send_to_screen_or_retry_queue { - ($rlocked_sessions:expr, $message:expr, $instruction: expr, $retry_queue:expr) => {{ - match $rlocked_sessions.as_ref() { - Some(session_metadata) => session_metadata.senders.send_to_screen($message), + ($senders:expr, $message:expr, $instruction: expr, $retry_queue:expr) => {{ + match $senders.as_ref() { + Some(senders) => senders.send_to_screen($message), None => { log::warn!("Server not ready, trying to place instruction in retry queue..."); if let Some(retry_queue) = $retry_queue.as_mut() { @@ -1455,8 +1568,11 @@ pub(crate) fn route_thread_main( >| -> Result { let mut should_break = false; - let rlocked_sessions = - session_data.read().to_anyhow().with_context(err_context)?; + let senders = session_data + .read() + .to_anyhow() + .ok() + .and_then(|r| r.as_ref().map(|r| r.senders.clone())); // Check if this is a watcher client and ignore input messages let is_watcher = session_state.read().unwrap().is_watcher(&client_id); @@ -1475,10 +1591,10 @@ pub(crate) fn route_thread_main( exit_reason: ExitReason::Normal, }, ); - let _ = rlocked_sessions.as_ref().map(|r| { - r.senders.send_to_screen( - ScreenInstruction::RemoveWatcherClient(client_id), - ) + let _ = senders.as_ref().map(|s| { + s.send_to_screen(ScreenInstruction::RemoveWatcherClient( + client_id, + )) }); should_break = true; } @@ -1487,7 +1603,7 @@ pub(crate) fn route_thread_main( // For watchers: send size to Screen for rendering adjustments, but // this does not affect the screen size send_to_screen_or_retry_queue!( - rlocked_sessions, + senders, ScreenInstruction::WatcherTerminalResize(client_id, *new_size), instruction.clone(), retry_queue @@ -1514,46 +1630,75 @@ pub(crate) fn route_thread_main( .unwrap() .set_last_active_client(client_id); - if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { - match rlocked_sessions.get_client_keybinds_and_mode(&client_id) { - Some((keybinds, input_mode, default_input_mode)) => { - for action in keybinds - .get_actions_for_key_in_mode_or_default_action( - &input_mode, - &key, - raw_bytes, - default_input_mode, - is_kitty_keyboard_protocol, - ) - { - if route_action( - action, + let session_data_assets = + session_data.read().as_ref().unwrap().as_ref().map(|s| { + ( + s.senders.clone(), + s.capabilities.clone(), + s.client_attributes.clone(), + s.default_shell.clone(), + s.layout.clone(), + s.session_configuration + .get_client_configuration(&client_id) + .options + .default_mode + .unwrap_or(InputMode::Normal) + .clone(), + ) + }); + if let Some((keybinds, input_mode, default_input_mode)) = session_data + .read() + .unwrap() + .as_ref() + .and_then(|s| s.get_client_keybinds_and_mode(&client_id)) + { + if let Some(( + senders, + capabilities, + client_attributes, + default_shell, + layout, + client_input_mode, + )) = session_data_assets + { + for action in keybinds + .get_actions_for_key_in_mode_or_default_action( + &input_mode, + &key, + raw_bytes, + default_input_mode, + is_kitty_keyboard_protocol, + ) + { + // Send user input to plugin thread for logging + let _ = + senders.send_to_plugin(PluginInstruction::UserInput { client_id, - None, - None, - rlocked_sessions.senders.clone(), - rlocked_sessions.capabilities.clone(), - rlocked_sessions.client_attributes.clone(), - rlocked_sessions.default_shell.clone(), - rlocked_sessions.layout.clone(), - Some(&mut seen_cli_pipes), - keybinds.clone(), - rlocked_sessions - .session_configuration - .get_client_configuration(&client_id) - .options - .default_mode - .unwrap_or(InputMode::Normal) - .clone(), - Some(os_input.clone()), - )? { - should_break = true; - } + action: action.clone(), + terminal_id: None, + cli_client_id: None, + }); + + if route_action( + action, + client_id, + None, + None, + senders.clone(), + capabilities, + client_attributes.clone(), + default_shell.clone(), + layout.clone(), + Some(&mut seen_cli_pipes), + keybinds.clone(), + client_input_mode, + Some(os_input.clone()), + )? + .0 + { + should_break = true; } - }, - None => { - log::error!("Failed to get keybindings for client"); - }, + } } } }, @@ -1580,31 +1725,67 @@ pub(crate) fn route_thread_main( } else { maybe_client_id.unwrap_or(client_id) }; - if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { + + // Send user input to plugin thread for logging + if let Some(ref senders) = senders { + let _ = senders.send_to_plugin(PluginInstruction::UserInput { + client_id, + action: action.clone(), + terminal_id: maybe_pane_id, + cli_client_id: if is_cli_client { + Some(cli_client_id) + } else { + None + }, + }); + } + + let session_data_assets = + session_data.read().unwrap().as_ref().map(|s| { + ( + s.senders.clone(), + s.capabilities.clone(), + s.client_attributes.clone(), + s.default_shell.clone(), + s.layout.clone(), + s.session_configuration + .get_client_configuration(&client_id) + .options + .default_mode + .unwrap_or(InputMode::Normal) + .clone(), + s.session_configuration + .get_client_keybinds(&client_id) + .clone(), + ) + }); + if let Some(( + senders, + capabilities, + client_attributes, + default_shell, + layout, + client_input_mode, + client_keybinds, + )) = session_data_assets + { if route_action( action, client_id, Some(cli_client_id), maybe_pane_id.map(|p| PaneId::Terminal(p)), - rlocked_sessions.senders.clone(), - rlocked_sessions.capabilities.clone(), - rlocked_sessions.client_attributes.clone(), - rlocked_sessions.default_shell.clone(), - rlocked_sessions.layout.clone(), + senders, + capabilities, + client_attributes, + default_shell, + layout, Some(&mut seen_cli_pipes), - rlocked_sessions - .session_configuration - .get_client_keybinds(&client_id) - .clone(), - rlocked_sessions - .session_configuration - .get_client_configuration(&client_id) - .options - .default_mode - .unwrap_or(InputMode::Normal) - .clone(), + client_keybinds, + client_input_mode, Some(os_input.clone()), - )? { + )? + .0 + { should_break = true; } } @@ -1614,7 +1795,7 @@ pub(crate) fn route_thread_main( if is_watcher { // For watchers: send size to Screen for tracking, don't affect screen size send_to_screen_or_retry_queue!( - rlocked_sessions, + senders.clone(), ScreenInstruction::WatcherTerminalResize(client_id, new_size), instruction, retry_queue @@ -1635,22 +1816,19 @@ pub(crate) fn route_thread_main( )) }) .and_then(|min_size| { - rlocked_sessions - .as_ref() - .context( - "couldn't get reference to read-locked session", - )? - .senders - .send_to_screen(ScreenInstruction::TerminalResize( + let _ = senders.as_ref().map(|s| { + s.send_to_screen(ScreenInstruction::TerminalResize( min_size, )) + }); + Ok(()) }) .with_context(err_context)?; } }, ClientToServerMsg::TerminalPixelDimensions { pixel_dimensions } => { send_to_screen_or_retry_queue!( - rlocked_sessions, + senders, ScreenInstruction::TerminalPixelDimensions(pixel_dimensions), instruction, retry_queue @@ -1661,7 +1839,7 @@ pub(crate) fn route_thread_main( color: ref background_color_instruction, } => { send_to_screen_or_retry_queue!( - rlocked_sessions, + senders, ScreenInstruction::TerminalBackgroundColor( background_color_instruction.clone() ), @@ -1674,7 +1852,7 @@ pub(crate) fn route_thread_main( color: ref foreground_color_instruction, } => { send_to_screen_or_retry_queue!( - rlocked_sessions, + senders, ScreenInstruction::TerminalForegroundColor( foreground_color_instruction.clone() ), @@ -1687,7 +1865,7 @@ pub(crate) fn route_thread_main( ref color_registers, } => { send_to_screen_or_retry_queue!( - rlocked_sessions, + senders, ScreenInstruction::TerminalColorRegisters( color_registers .iter() @@ -1718,10 +1896,11 @@ pub(crate) fn route_thread_main( pane_to_focus: pane_id_to_focus, is_web_client, } => { - let allow_web_connections = rlocked_sessions - .as_ref() - .map(|rlocked_sessions| { - rlocked_sessions.web_sharing.web_clients_allowed() + let allow_web_connections = session_data + .read() + .ok() + .and_then(|s| { + s.as_ref().map(|s| s.web_sharing.web_clients_allowed()) }) .unwrap_or(false); let should_allow_connection = !is_web_client || allow_web_connections; @@ -1801,7 +1980,9 @@ pub(crate) fn route_thread_main( }, None => { consecutive_unknown_messages_received += 1; - log::error!("Received unknown message from client."); + if consecutive_unknown_messages_received == 1 { + log::error!("Received unknown message from client."); + } if consecutive_unknown_messages_received >= 1000 { log::error!("Client sent over 1000 consecutive unknown messages, this is probably an infinite loop, logging client out"); let _ = os_input.send_to_client( diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 3c83d6b03..c89988315 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -12,9 +12,9 @@ use crate::route::NotificationEnd; use log::{debug, warn}; use zellij_utils::data::{ - Direction, FloatingPaneCoordinates, KeyWithModifier, NewPanePlacement, PaneContents, - PaneManifest, PaneScrollbackResponse, PluginPermission, Resize, ResizeStrategy, SessionInfo, - Styling, WebSharing, + CommandOrPlugin, Direction, FloatingPaneCoordinates, KeyWithModifier, NewPanePlacement, + PaneContents, PaneManifest, PaneScrollbackResponse, PluginPermission, Resize, ResizeStrategy, + SessionInfo, Styling, WebSharing, }; use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; @@ -51,10 +51,7 @@ use crate::{ pty::{get_default_shell, ClientTabIndexOrPaneId, PtyInstruction, VteBytes}, tab::{SuppressedPanes, Tab}, thread_bus::Bus, - ui::{ - loading_indication::LoadingIndication, - overlay::{Overlay, OverlayWindow}, - }, + ui::loading_indication::LoadingIndication, ClientId, ServerInstruction, }; use zellij_utils::{ @@ -218,6 +215,7 @@ pub enum ScreenInstruction { ToggleActiveTerminalFullscreen(ClientId, Option), TogglePaneFrames(Option), SetSelectable(PaneId, bool), + ShowPluginCursor(u32, ClientId, Option<(usize, usize)>), ClosePane( PaneId, Option, @@ -235,6 +233,8 @@ pub enum ScreenInstruction { Vec, Option, (Vec, Vec), // swap layouts + Option>, // initial_panes + bool, // block_on_first_terminal bool, // should_change_focus_to_new_tab (ClientId, bool), // bool -> is_web_client Option, // completion signal @@ -245,10 +245,11 @@ pub enum ScreenInstruction { Vec<(u32, HoldForCommand)>, // new pane pids Vec<(u32, HoldForCommand)>, // new floating pane pids HashMap>, - usize, // tab_index - bool, // should change focus to new tab - (ClientId, bool), // bool -> is_web_client - Option, // completion signal + usize, // tab_index + bool, // should change focus to new tab + (ClientId, bool), // bool -> is_web_client + Option, // regular completion signal + Option<(u32, NotificationEnd)>, // blocking_terminal (terminal_id, completion_tx) ), SwitchTabNext(ClientId, Option), SwitchTabPrev(ClientId, Option), @@ -284,10 +285,6 @@ pub enum ScreenInstruction { Option<(u32, bool)>, // (pane_id, is_plugin) => pane_id to focus ), RemoveClient(ClientId), - AddOverlay(Overlay, ClientId), - RemoveOverlay(ClientId), - ConfirmPrompt(ClientId, Option), - DenyPrompt(ClientId, Option), UpdateSearch(Vec, ClientId, Option), SearchDown(ClientId, Option), SearchUp(ClientId, Option), @@ -368,8 +365,12 @@ pub enum ScreenInstruction { ClientId, Option, ), // bools are: should_float, should_open_in_place Option is the pane id to replace, Option is an optional cwd, bool after is skip_cache - SuppressPane(PaneId, ClientId), // bool is should_float - FocusPaneWithId(PaneId, bool, ClientId, Option), // bool is should_float + SuppressPane(PaneId, ClientId), + UnsuppressPane(PaneId, bool), // bool -> should float if hidden + UnsuppressOrExpandPane(PaneId, bool), // bool -> should float if hidden + FocusPaneWithId(PaneId, bool, bool, ClientId, Option), // bools: + // should_float_if_hidden, + // should_be_in_place_if_hidden RenamePane(PaneId, Vec, Option), RenameTab(usize, Vec, Option), RequestPluginPermissions( @@ -422,6 +423,7 @@ pub enum ScreenInstruction { ResizePaneWithId(ResizeStrategy, PaneId), EditScrollbackForPaneWithId(PaneId, Option), WriteToPaneId(Vec, PaneId), + CopyTextToClipboard(String, u32), // String - text to copy, u32 - plugin_id MovePaneWithPaneId(PaneId), MovePaneWithPaneIdInDirection(PaneId, Direction), ClearScreenForPaneId(PaneId), @@ -467,7 +469,7 @@ pub enum ScreenInstruction { SetMouseSelectionSupport(PaneId, bool), InterceptKeyPresses(PluginId, ClientId), ClearKeyPressesIntercepts(ClientId), - ReplacePaneWithExistingPane(PaneId, PaneId), + ReplacePaneWithExistingPane(PaneId, PaneId, bool), // bool -> suppress_replaced_pane AddWatcherClient(ClientId, Size), RemoveWatcherClient(ClientId), SetFollowedClient(ClientId), @@ -553,6 +555,7 @@ impl From<&ScreenInstruction> for ScreenContext { }, ScreenInstruction::TogglePaneFrames(..) => ScreenContext::TogglePaneFrames, ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable, + ScreenInstruction::ShowPluginCursor(..) => ScreenContext::ShowPluginCursor, ScreenInstruction::ClosePane(..) => ScreenContext::ClosePane, ScreenInstruction::HoldPane(..) => ScreenContext::HoldPane, ScreenInstruction::UpdatePaneName(..) => ScreenContext::UpdatePaneName, @@ -591,10 +594,6 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ToggleTab(..) => ScreenContext::ToggleTab, ScreenInstruction::AddClient(..) => ScreenContext::AddClient, ScreenInstruction::RemoveClient(..) => ScreenContext::RemoveClient, - ScreenInstruction::AddOverlay(..) => ScreenContext::AddOverlay, - ScreenInstruction::RemoveOverlay(..) => ScreenContext::RemoveOverlay, - ScreenInstruction::ConfirmPrompt(..) => ScreenContext::ConfirmPrompt, - ScreenInstruction::DenyPrompt(..) => ScreenContext::DenyPrompt, ScreenInstruction::UpdateSearch(..) => ScreenContext::UpdateSearch, ScreenInstruction::SearchDown(..) => ScreenContext::SearchDown, ScreenInstruction::SearchUp(..) => ScreenContext::SearchUp, @@ -633,6 +632,8 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::LaunchOrFocusPlugin(..) => ScreenContext::LaunchOrFocusPlugin, ScreenInstruction::LaunchPlugin(..) => ScreenContext::LaunchPlugin, ScreenInstruction::SuppressPane(..) => ScreenContext::SuppressPane, + ScreenInstruction::UnsuppressPane(..) => ScreenContext::UnsuppressPane, + ScreenInstruction::UnsuppressOrExpandPane(..) => ScreenContext::UnsuppressOrExpandPane, ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId, ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane, ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab, @@ -657,6 +658,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenContext::EditScrollbackForPaneWithId }, ScreenInstruction::WriteToPaneId(..) => ScreenContext::WriteToPaneId, + ScreenInstruction::CopyTextToClipboard(..) => ScreenContext::CopyTextToClipboard, ScreenInstruction::MovePaneWithPaneId(..) => ScreenContext::MovePaneWithPaneId, ScreenInstruction::MovePaneWithPaneIdInDirection(..) => { ScreenContext::MovePaneWithPaneIdInDirection @@ -848,14 +850,13 @@ pub(crate) struct Screen { character_cell_size: Rc>>, stacked_resize: Rc>, sixel_image_store: Rc>, - /// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`] - overlay: OverlayWindow, terminal_emulator_colors: Rc>, terminal_emulator_color_codes: Rc>>, connected_clients: Rc>>, // bool -> is_web_client /// The indices of this [`Screen`]'s active [`Tab`]s. active_tab_indices: BTreeMap, tab_history: BTreeMap>, + pane_history: BTreeMap>, mode_info: BTreeMap, default_mode_info: ModeInfo, // TODO: restructure ModeInfo to prevent this duplication style: Style, @@ -942,10 +943,10 @@ impl Screen { connected_clients: Rc::new(RefCell::new(HashMap::new())), active_tab_indices: BTreeMap::new(), tabs: BTreeMap::new(), - overlay: OverlayWindow::default(), terminal_emulator_colors: Rc::new(RefCell::new(Palette::default())), terminal_emulator_color_codes: Rc::new(RefCell::new(HashMap::new())), tab_history: BTreeMap::new(), + pane_history: BTreeMap::new(), mode_info: BTreeMap::new(), default_mode_info: mode_info, draw_pane_frames, @@ -1644,11 +1645,6 @@ impl Screen { } } - /// Returns a mutable reference to this [`Screen`]'s active [`Overlays`]. - pub fn get_active_overlays_mut(&mut self) -> &mut Vec { - &mut self.overlay.overlay_stack - } - /// Returns a mutable reference to this [`Screen`]'s indexed [`Tab`]. pub fn get_indexed_tab_mut(&mut self, tab_index: usize) -> Option<&mut Tab> { self.get_tabs_mut().get_mut(&tab_index) @@ -1733,6 +1729,7 @@ impl Screen { tab_index: usize, should_change_client_focus: bool, client_id_and_is_web_client: (ClientId, bool), + blocking_terminal: Option<(u32, NotificationEnd)>, ) -> Result<()> { if self.tabs.get(&tab_index).is_none() { // TODO: we should prevent this situation with a UI - eg. cannot close tabs with a @@ -1817,6 +1814,7 @@ impl Screen { new_floating_terminal_ids, new_plugin_ids, client_id, + blocking_terminal, )?; tab.update_input_modes()?; @@ -1949,18 +1947,6 @@ impl Screen { } } - // Optional: getter for debugging/monitoring - pub fn get_watcher_size(&self, client_id: &ClientId) -> Option { - self.watcher_clients - .get(client_id) - .map(|state| state.size()) - } - - // Optional: get all watcher sizes - pub fn get_all_watcher_sizes(&self) -> &HashMap { - &self.watcher_clients - } - pub fn generate_and_report_tab_state(&mut self) -> Result> { let mut plugin_updates = vec![]; let mut tab_infos_for_screen_state = BTreeMap::new(); @@ -2063,6 +2049,8 @@ impl Screen { } fn log_and_report_session_state(&mut self) -> Result<()> { let err_context = || format!("Failed to log and report session state"); + + self.update_active_pane_ids(); // generate own session info let pane_manifest = self.generate_and_report_pane_state()?; let tab_infos = self.generate_and_report_tab_state()?; @@ -2090,6 +2078,11 @@ impl Screen { .count(), plugins: Default::default(), // these are filled in by the wasm thread tab_history: self.tab_history.clone(), + pane_history: self + .pane_history + .iter() + .map(|(k, v)| (*k, v.iter().map(|v| (*v).into()).collect())) + .collect(), }; self.bus .senders @@ -2496,6 +2489,7 @@ impl Screen { run_plugin: &RunPluginOrAlias, should_float: bool, move_to_focused_tab: bool, + should_be_in_place: bool, client_id: ClientId, ) -> Result { // true => found and focused, false => not @@ -2526,6 +2520,7 @@ impl Screen { None, true, )?; + // TODO: also should_be_in_place } else { new_active_tab.hide_floating_panes(); new_active_tab.add_tiled_pane( @@ -2542,7 +2537,7 @@ impl Screen { self.tabs .get_mut(&tab_index) .with_context(err_context)? - .focus_pane_with_id(plugin_pane_id, should_float, client_id) + .focus_pane_with_id(plugin_pane_id, should_float, should_be_in_place, client_id) .context("failed to focus plugin pane")?; self.log_and_report_session_state() .with_context(err_context)?; @@ -2556,6 +2551,7 @@ impl Screen { &mut self, pane_id: PaneId, should_float_if_hidden: bool, + should_open_in_place: bool, client_id: ClientId, ) -> Result<()> { let err_context = || format!("failed to focus_plugin_pane"); @@ -2570,7 +2566,14 @@ impl Screen { self.tabs .iter_mut() .find(|(_, t)| t.position == tab_index) - .map(|(_, t)| t.focus_pane_with_id(pane_id, should_float_if_hidden, client_id)) + .map(|(_, t)| { + t.focus_pane_with_id( + pane_id, + should_float_if_hidden, + should_open_in_place, + client_id, + ) + }) .with_context(err_context) .non_fatal(); }, @@ -2666,6 +2669,8 @@ impl Screen { Some(tiled_panes_layout), floating_panes_layout, tab_index, + None, // initial_panes + false, // block_on_first_terminal should_change_focus_to_new_tab, (client_id, is_web_client), None, @@ -2742,6 +2747,8 @@ impl Screen { Some(tiled_panes_layout), floating_panes_layout, tab_index, + None, // initial_panes + false, // block_on_first_terminal should_change_focus_to_new_tab, (client_id, is_web_client), None, @@ -2942,6 +2949,7 @@ impl Screen { &mut self, pane_id_to_replace: PaneId, pane_id_of_existing_pane: PaneId, + suppress_replaced_pane: bool, ) { let Some(tab_index_of_pane_id_to_replace) = self .tabs @@ -2965,7 +2973,7 @@ impl Screen { .tabs .iter_mut() .find(|(_, t)| t.position == tab_index_of_existing_pane) - .and_then(|(_, t)| t.extract_pane(pane_id_of_existing_pane, false)) + .and_then(|(_, t)| t.extract_pane(pane_id_of_existing_pane, true)) else { log::error!("Failed to find pane"); return; @@ -2975,11 +2983,19 @@ impl Screen { .iter_mut() .find(|(_, t)| t.position == tab_index_of_pane_id_to_replace) { - tab.1.close_pane_and_replace_with_other_pane( - pane_id_to_replace, - extracted_pane_from_other_tab, - None, - ); + if suppress_replaced_pane { + tab.1.suppress_pane_and_replace_with_other_pane( + pane_id_to_replace, + extracted_pane_from_other_tab, + None, + ); + } else { + tab.1.close_pane_and_replace_with_other_pane( + pane_id_to_replace, + extracted_pane_from_other_tab, + None, + ); + } } let _ = self.log_and_report_session_state(); } @@ -3505,6 +3521,18 @@ impl Screen { } } } + fn update_active_pane_ids(&mut self) { + let connected_clients: Vec = + self.connected_clients.borrow().keys().copied().collect(); + for client_id in connected_clients { + if let Some(active_pane_id) = self.get_active_pane_id(&client_id) { + let active_pane_id: PaneId = active_pane_id.into(); + let history = self.pane_history.entry(client_id).or_insert_with(|| vec![]); + history.retain(|e| e != &active_pane_id); + history.push(active_pane_id.into()); + } + } + } } #[cfg(not(test))] @@ -3692,9 +3720,11 @@ pub(crate) fn screen_thread_main( new_pane_placement, start_suppressed, client_or_tab_index, - completion_tx, + mut completion_tx, set_blocking, ) => { + completion_tx.as_mut().map(|c| c.set_affected_pane_id(pid)); + let blocking_notification = if set_blocking { completion_tx } else { None }; match client_or_tab_index { @@ -3747,6 +3777,7 @@ pub(crate) fn screen_thread_main( ClientTabIndexOrPaneId::PaneId(pane_id) => { let mut found = false; let all_tabs = screen.get_tabs_mut(); + let should_focus_pane = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.new_pane( @@ -3754,7 +3785,7 @@ pub(crate) fn screen_thread_main( initial_pane_title, invoked_with, start_suppressed, - true, + should_focus_pane, new_pane_placement, None, blocking_notification, // TODO: is this correct? @@ -4426,6 +4457,26 @@ pub(crate) fn screen_thread_main( screen.render(None)?; screen.log_and_report_session_state()?; }, + ScreenInstruction::ShowPluginCursor(pid, client_id, cursor_position) => { + let all_tabs = screen.get_tabs_mut(); + let mut found_plugin = false; + for tab in all_tabs.values_mut() { + if tab.has_plugin(pid) { + tab.show_plugin_cursor(pid, client_id, cursor_position); + found_plugin = true; + break; + } + } + if !found_plugin { + pending_events_waiting_for_tab.push(ScreenInstruction::ShowPluginCursor( + pid, + client_id, + cursor_position, + )); + } + screen.render(None)?; + screen.log_and_report_session_state()?; + }, ScreenInstruction::SetMouseSelectionSupport(pid, selection_support) => { let all_tabs = screen.get_tabs_mut(); let mut found_plugin = false; @@ -4564,6 +4615,8 @@ pub(crate) fn screen_thread_main( floating_panes_layout, tab_name, swap_layouts, + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, @@ -4590,6 +4643,8 @@ pub(crate) fn screen_thread_main( layout, floating_panes_layout, tab_index, + initial_panes, + block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, @@ -4604,9 +4659,14 @@ pub(crate) fn screen_thread_main( tab_index, should_change_focus_to_new_tab, (client_id, is_web_client), - _completion_tx, // the action ends here, dropping this will release anything - // waiting for it + mut completion_tx, + blocking_terminal, ) => { + if let Some(first_terminal_pane) = new_pane_pids.iter().next() { + completion_tx + .as_mut() + .map(|c| c.set_affected_pane_id(PaneId::Terminal(first_terminal_pane.0))); + } screen.apply_layout( layout, floating_panes_layout, @@ -4616,6 +4676,7 @@ pub(crate) fn screen_thread_main( tab_index, should_change_focus_to_new_tab, (client_id, is_web_client), + blocking_terminal, )?; pending_tab_ids.remove(&tab_index); if pending_tab_ids.is_empty() { @@ -4749,6 +4810,8 @@ pub(crate) fn screen_thread_main( None, vec![], tab_index, + None, // initial_panes + false, // block_on_first_terminal should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, @@ -4885,7 +4948,7 @@ pub(crate) fn screen_thread_main( } }); if let Some(pane_id) = pane_id { - screen.focus_pane_with_id(pane_id, true, client_id)?; + screen.focus_pane_with_id(pane_id, true, false, client_id)?; } else if let Some(tab_position_to_focus) = tab_position_to_focus { screen.go_to_tab(tab_position_to_focus, client_id)?; } @@ -4915,37 +4978,6 @@ pub(crate) fn screen_thread_main( screen.log_and_report_session_state()?; screen.render(None)?; }, - ScreenInstruction::AddOverlay(overlay, _client_id) => { - screen.get_active_overlays_mut().pop(); - screen.get_active_overlays_mut().push(overlay); - }, - ScreenInstruction::RemoveOverlay(_client_id) => { - screen.get_active_overlays_mut().pop(); - screen.render(None)?; - }, - ScreenInstruction::ConfirmPrompt( - _client_id, - _completion_tx, // the action ends here, dropping this will release anything - // waiting for it - ) => { - let overlay = screen.get_active_overlays_mut().pop(); - let instruction = overlay.and_then(|o| o.prompt_confirm()); - if let Some(instruction) = instruction { - screen - .bus - .senders - .send_to_server(*instruction) - .context("failed to confirm prompt")?; - } - }, - ScreenInstruction::DenyPrompt( - _client_id, - _completion_tx, // the action ends here, dropping this will release anything - // waiting for it - ) => { - screen.get_active_overlays_mut().pop(); - screen.render(None)?; - }, ScreenInstruction::UpdateSearch( c, client_id, @@ -5441,6 +5473,7 @@ pub(crate) fn screen_thread_main( &run_plugin, should_float, move_to_focused_tab, + should_open_in_place, client_id, )? { screen.render(None)?; @@ -5560,14 +5593,42 @@ pub(crate) fn screen_thread_main( } screen.log_and_report_session_state()?; }, + ScreenInstruction::UnsuppressPane(pane_id, should_float_if_hidden) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&pane_id) { + tab.unsuppress_pane(pane_id, should_float_if_hidden); + drop(screen.render(None)); + break; + } + } + screen.log_and_report_session_state()?; + }, + ScreenInstruction::UnsuppressOrExpandPane(pane_id, should_float_if_hidden) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&pane_id) { + tab.unsuppress_or_expand_pane(pane_id, should_float_if_hidden); + drop(screen.render(None)); + break; + } + } + screen.log_and_report_session_state()?; + }, ScreenInstruction::FocusPaneWithId( pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { - screen.focus_pane_with_id(pane_id, should_float_if_hidden, client_id)?; + screen.focus_pane_with_id( + pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + client_id, + )?; screen.log_and_report_session_state()?; }, ScreenInstruction::RenamePane( @@ -5655,9 +5716,11 @@ pub(crate) fn screen_thread_main( invoked_with, close_replaced_pane, client_id_tab_index_or_pane_id, - _completion_tx, // the action ends here, dropping this will release anything - // waiting for it + mut completion_tx, ) => { + completion_tx + .as_mut() + .map(|c| c.set_affected_pane_id(new_pane_id)); screen.replace_pane( new_pane_id, hold_for_command, @@ -5822,6 +5885,24 @@ pub(crate) fn screen_thread_main( } screen.render(None)?; }, + ScreenInstruction::CopyTextToClipboard(text, plugin_id) => { + let plugin_pane_id = PaneId::Plugin(plugin_id); + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&plugin_pane_id) { + tab.copy_text_to_clipboard(&text) + .with_context(|| { + format!( + "failed to copy text to clipboard from plugin {}", + plugin_id + ) + }) + .non_fatal(); + break; + } + } + screen.render(None)?; + }, ScreenInstruction::MovePaneWithPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { @@ -6053,7 +6134,7 @@ pub(crate) fn screen_thread_main( // waiting for it ) => { if let Some(last_pane_id) = screen.stack_panes(pane_ids_to_stack) { - let _ = screen.focus_pane_with_id(last_pane_id, false, client_id); + let _ = screen.focus_pane_with_id(last_pane_id, false, false, client_id); let _ = screen.render(None); let pane_group = screen.get_client_pane_group(&client_id); if !pane_group.is_empty() { @@ -6213,9 +6294,15 @@ pub(crate) fn screen_thread_main( ScreenInstruction::ClearKeyPressesIntercepts(client_id) => { keybind_intercepts.remove(&client_id); }, - ScreenInstruction::ReplacePaneWithExistingPane(old_pane_id, new_pane_id) => { - screen.replace_pane_with_existing_pane(old_pane_id, new_pane_id) - }, + ScreenInstruction::ReplacePaneWithExistingPane( + old_pane_id, + new_pane_id, + suppress_replaced_pane, + ) => screen.replace_pane_with_existing_pane( + old_pane_id, + new_pane_id, + suppress_replaced_pane, + ), ScreenInstruction::AddWatcherClient(client_id, size) => { screen .add_watcher_client(client_id) diff --git a/zellij-server/src/tab/layout_applier.rs b/zellij-server/src/tab/layout_applier.rs index 5e52888ac..559dc5dbf 100644 --- a/zellij-server/src/tab/layout_applier.rs +++ b/zellij-server/src/tab/layout_applier.rs @@ -11,7 +11,7 @@ use crate::{ plugins::PluginInstruction, pty::PtyInstruction, thread_bus::ThreadSenders, - ClientId, + ClientId, NotificationEnd, }; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; @@ -42,6 +42,7 @@ pub struct LayoutApplier<'a> { arrow_fonts: bool, styled_underlines: bool, explicitly_disable_kitty_keyboard_protocol: bool, + blocking_terminal: Option<(u32, NotificationEnd)>, } impl<'a> LayoutApplier<'a> { @@ -65,6 +66,7 @@ impl<'a> LayoutApplier<'a> { arrow_fonts: bool, styled_underlines: bool, explicitly_disable_kitty_keyboard_protocol: bool, + blocking_terminal: Option<(u32, NotificationEnd)>, ) -> Self { let viewport = viewport.clone(); let senders = senders.clone(); @@ -97,6 +99,7 @@ impl<'a> LayoutApplier<'a> { arrow_fonts, styled_underlines, explicitly_disable_kitty_keyboard_protocol, + blocking_terminal, } } pub fn apply_layout( @@ -496,6 +499,18 @@ impl<'a> LayoutApplier<'a> { Some(Run::Command(run_command)) => Some(run_command.to_string()), _ => None, }; + + // Check if this terminal should receive the blocking completion_tx + let notification_end = if let Some((blocking_pid, _)) = &self.blocking_terminal { + if *blocking_pid == pid { + self.blocking_terminal.take().map(|(_, tx)| tx) + } else { + None + } + } else { + None + }; + let mut new_pane = TerminalPane::new( pid, *position_and_size, @@ -513,7 +528,7 @@ impl<'a> LayoutApplier<'a> { self.arrow_fonts, self.styled_underlines, self.explicitly_disable_kitty_keyboard_protocol, - None, + notification_end, ); if let Some(pane_initial_contents) = &layout.pane_initial_contents { new_pane.handle_pty_bytes(pane_initial_contents.as_bytes().into()); diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index cd87b817e..7e441a0e3 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -7,7 +7,6 @@ mod layout_applier; mod swap_layouts; use copy_command::CopyCommand; -use serde; use std::env::temp_dir; use std::net::IpAddr; use std::path::PathBuf; @@ -46,7 +45,6 @@ use crate::{ thread_bus::ThreadSenders, ClientId, ServerInstruction, }; -use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::rc::Rc; use std::time::Instant; @@ -279,16 +277,6 @@ pub(crate) struct Tab { web_server_port: u16, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(crate = "self::serde")] -pub(crate) struct TabData { - pub position: usize, - pub name: String, - pub active: bool, - pub mode_info: ModeInfo, - pub colors: Styling, -} - // FIXME: Use a struct that has a pane_type enum, to reduce all of the duplication pub trait Pane { fn x(&self) -> usize; @@ -304,7 +292,8 @@ pub trait Pane { fn set_geom_override(&mut self, pane_geom: PaneGeom); fn handle_pty_bytes(&mut self, _bytes: VteBytes) {} fn handle_plugin_bytes(&mut self, _client_id: ClientId, _bytes: VteBytes) {} - fn cursor_coordinates(&self) -> Option<(usize, usize)>; + fn show_cursor(&mut self, _client_id: ClientId, _cursor_position: Option<(usize, usize)>) {} + fn cursor_coordinates(&self, _client_id: Option) -> Option<(usize, usize)>; fn is_mid_frame(&self) -> bool { false } @@ -818,6 +807,7 @@ impl Tab { new_floating_terminal_ids: Vec<(u32, HoldForCommand)>, new_plugin_ids: HashMap>, client_id: ClientId, + blocking_terminal: Option<(u32, NotificationEnd)>, ) -> Result<()> { self.swap_layouts .set_base_layout((layout.clone(), floating_panes_layout.clone())); @@ -841,6 +831,7 @@ impl Tab { self.arrow_fonts, self.styled_underlines, self.explicitly_disable_kitty_keyboard_protocol, + blocking_terminal, ) .apply_layout( layout, @@ -911,6 +902,7 @@ impl Tab { self.arrow_fonts, self.styled_underlines, self.explicitly_disable_kitty_keyboard_protocol, + None, ) .apply_floating_panes_layout_to_existing_panes(&layout_candidate) .non_fatal(); @@ -949,6 +941,7 @@ impl Tab { self.arrow_fonts, self.styled_underlines, self.explicitly_disable_kitty_keyboard_protocol, + None, ) .apply_tiled_panes_layout_to_existing_panes(&layout_candidate); if application_res.is_err() { @@ -2080,6 +2073,26 @@ impl Tab { drop(replaced_pane); } } + pub fn suppress_pane_and_replace_with_other_pane( + &mut self, + pane_id_to_replace: PaneId, + pane_to_replace_with: Box, + _completion_tx: Option, + ) { + let mut replaced_pane = if self.floating_panes.panes_contain(&pane_id_to_replace) { + self.floating_panes + .replace_pane(pane_id_to_replace, pane_to_replace_with) + .ok() + } else { + self.tiled_panes + .replace_pane(pane_id_to_replace, pane_to_replace_with) + }; + + if let Some(replaced_pane) = replaced_pane.take() { + let is_scrollback_editor = false; + self.insert_suppressed_pane(replaced_pane.pid(), (is_scrollback_editor, replaced_pane)); + } + } pub fn horizontal_split( &mut self, pid: PaneId, @@ -2683,7 +2696,7 @@ impl Tab { .get(&active_pane_id) .or_else(|| self.tiled_panes.get_pane(active_pane_id))?; active_terminal - .cursor_coordinates() + .cursor_coordinates(Some(client_id)) .map(|(x_in_terminal, y_in_terminal)| { let x = active_terminal.x() + x_in_terminal; let y = active_terminal.y() + y_in_terminal; @@ -3381,21 +3394,16 @@ impl Tab { if let Some(pane) = self.tiled_panes.get_pane_mut(id) { pane.set_selectable(selectable); if !selectable { - // there are some edge cases in which this causes a hard crash when there are no - // other selectable panes - ideally this should never happen unless it's a - // configuration error - but this *does* sometimes happen with the default - // configuration as well since we set this at run time. I left this here because - // this should very rarely happen and I hope in my heart that we will stop setting - // this at runtime in the default configuration at some point - // - // If however this is not the case and we find this does cause crashes, we can - // solve it by adding a "dangling_clients" struct to Tab which we would fill with - // the relevant client ids in this case and drain as soon as a new selectable pane - // is opened self.tiled_panes.move_clients_out_of_pane(id); } } else if let Some(pane) = self.floating_panes.get_pane_mut(id) { pane.set_selectable(selectable); + if !selectable { + self.floating_panes.move_clients_out_of_pane(id); + if !self.floating_panes.has_selectable_panes() { + self.hide_floating_panes(); + } + } } // we do this here because if there is a non-selectable pane on the edge, we consider it // outside the viewport (a ui-pane, eg. the status-bar and tab-bar) and need to adjust for it @@ -3539,8 +3547,15 @@ impl Tab { closed_pane.reset_logical_position(); } closed_pane - } else if self.suppressed_panes.contains_key(&id) { - self.suppressed_panes.remove(&id).map(|s_p| s_p.1) + } else if let Some(suppressed_key_of_pane) = self + .suppressed_panes + .iter() + .find_map(|(key, (_, pane))| if &pane.pid() == &id { Some(*key) } else { None }) + { + // TODO: test this (from the path in screen.rs focus_plugin_pane ~line 2519 + self.suppressed_panes + .remove(&suppressed_key_of_pane) + .map(|s_p| s_p.1) } else { None } @@ -4674,6 +4689,19 @@ impl Tab { } Ok(()) } + pub fn copy_text_to_clipboard(&self, text: &str) -> Result<()> { + self.write_selection_to_clipboard(text) + .with_context(|| format!("failed to write text to clipboard"))?; + self.senders + .send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::CopyToClipboard(self.clipboard_provider.as_copy_destination()), + )])) + .with_context(|| "failed to inform plugins about clipboard copy") + .non_fatal(); + Ok(()) + } fn write_selection_to_clipboard(&self, selection: &str) -> Result<()> { let err_context = || format!("failed to write selection to clipboard: '{}'", selection); @@ -4950,6 +4978,26 @@ impl Tab { plugin_pane.update_loading_indication(loading_indication); } } + pub fn show_plugin_cursor( + &mut self, + pid: u32, + client_id: ClientId, + cursor_position: Option<(usize, usize)>, + ) { + if let Some(plugin_pane) = self + .tiled_panes + .get_pane_mut(PaneId::Plugin(pid)) + .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) + .or_else(|| { + self.suppressed_panes + .values_mut() + .find(|s_p| s_p.1.pid() == PaneId::Plugin(pid)) + .map(|s_p| &mut s_p.1) + }) + { + plugin_pane.show_cursor(client_id, cursor_position); + } + } pub fn start_plugin_loading_indication( &mut self, pid: u32, @@ -5009,7 +5057,7 @@ impl Tab { .find(|(_id, (_, pane))| { run_plugin_or_alias.is_equivalent_to_run(pane.invoked_with()) }) - .map(|(id, _)| *id) + .map(|(_, (_, pane))| pane.pid()) // TODO: does this break things???? }) } @@ -5017,6 +5065,7 @@ impl Tab { &mut self, pane_id: PaneId, should_float: bool, + should_be_in_place: bool, client_id: ClientId, ) -> Result<()> { // TODO: should error if pane is not selectable @@ -5031,18 +5080,45 @@ impl Tab { }; focused_floating_pane }) - .or_else(|_| match self.suppressed_panes.remove(&pane_id) { - Some(mut pane) => { - pane.1.set_selectable(true); - if should_float { - self.show_floating_panes(); - self.add_floating_pane(pane.1, pane_id, None, true) - } else { - self.hide_floating_panes(); - self.add_tiled_pane(pane.1, pane_id, Some(client_id)) - } - }, - None => Ok(()), + // TODO: change suppressed_panes to be a proper struct with methods that make sense rather + // than doing this dance every time + .or_else(|_| { + match self + .suppressed_panes + .extract_if(|_key, (_, pane)| pane.pid() == pane_id) + .next() + .map(|(_key, (_, pane))| pane) + { + Some(mut pane) => { + pane.set_selectable(true); + if should_float { + self.show_floating_panes(); + self.add_floating_pane(pane, pane_id, None, true) + } else if should_be_in_place { + let replaced_pane = if self.are_floating_panes_visible() { + self.floating_panes + .replace_active_pane(pane, client_id) + .ok() + } else { + self.tiled_panes.replace_active_pane(pane, client_id) + }; + if let Some(replaced_pane) = replaced_pane { + let is_scrollback_editor = false; + self.insert_suppressed_pane( + pane_id, + (is_scrollback_editor, replaced_pane), + ); + } else { + log::error!("Could not find pane to replace, aborting."); + } + Ok(()) + } else { + self.hide_floating_panes(); + self.add_tiled_pane(pane, pane_id, Some(client_id)) + } + }, + None => Ok(()), + } }) } pub fn focus_suppressed_pane_for_all_clients(&mut self, pane_id: PaneId) { @@ -5064,11 +5140,101 @@ impl Tab { // scrollback editor), but it has to take itself out on its own (eg. a plugin using the // show_self() method) if let Some(pane) = self.extract_pane(pane_id, true) { - let is_scrollback_editor = false; - self.suppressed_panes - .insert(pane_id, (is_scrollback_editor, pane)); + self.insert_suppressed_pane(pane_id, (false, pane)); } } + pub fn unsuppress_pane(&mut self, pane_id: PaneId, should_float_if_hidden: bool) { + // removes a pane from being suppressed (hidden) but does not focus it + match self + .suppressed_panes + .extract_if(|_key, (_, pane)| pane.pid() == pane_id) + .next() + .map(|(_key, (_, pane))| pane) + { + Some(pane) => { + if should_float_if_hidden { + self.add_floating_pane(pane, pane_id, None, true) + .non_fatal(); + } else { + self.add_tiled_pane(pane, pane_id, None).non_fatal(); + } + }, + None => { + log::error!("Could not find suppressed pane with id: {:?}", pane_id); + }, + } + } + pub fn unsuppress_or_expand_pane(&mut self, pane_id: PaneId, should_float_if_hidden: bool) { + // removes a pane from being suppressed (hidden) but does not focus it + match self + .suppressed_panes + .extract_if(|_key, (_, pane)| pane.pid() == pane_id) + .next() + .map(|(_key, (_, pane))| pane) + { + Some(pane) => { + if should_float_if_hidden { + self.add_floating_pane(pane, pane_id, None, true) + .non_fatal(); + } else { + self.add_tiled_pane(pane, pane_id, None).non_fatal(); + } + }, + None => { + let expand_panes_success = self.tiled_panes.expand_pane_in_stack(pane_id).len() > 0; + if !expand_panes_success { + log::error!( + "Could not find suppressed or stacked pane with id: {:?}", + pane_id + ); + } + }, + } + } + fn insert_suppressed_pane( + &mut self, + suppressing_pane_id: PaneId, + pane_to_suppress: (bool, Box), + ) { + // bool -> is_scrollback_editor + // this method is intended to insert an existing provided pane into suppressed_panes, while + // making sure any existing panes already in suppressed_panes still remain there + // if there's a key collision (eg. the suppressing_pane_id already suppresses another + // pane), we fix the colliding pane so that it now becomes suppressed by its own id (i.e. + // needs to explicitly be removed from suppressed_panes by eg. a plugin action rather than + // being conditionally removed when its suppressing pane is closed or itself suppressed) + // + // - suppressing_pane_id: the id of the pane suppressing this pane - the pane that if + // closed, should trigger this pane being unsuppressed + // - pane_to_suppress: the pane itself that we want to suppress + + // this closure removes and returns an existing pane from the map if it exists under this key + // and inserts the given pane into the map under this key + let mut insert_and_return_existing_value = + |key: PaneId, value: (bool, Box)| -> Option<(bool, Box)> { + let existing = self.suppressed_panes.remove(&key); + self.suppressed_panes.insert(key, value); + existing + }; + + // here we try to insert a pane into the suppressed panes map while making sure not to drop + // (close) any existing panes in the map. We repeat the action of remapping existing panes + // under their own keys until all keys either reference the suppressing_pane_id (only one + // can do this) or themselves + let mut key_to_insert = suppressing_pane_id; + let mut pane_to_insert = Some(pane_to_suppress); + loop { + pane_to_insert = pane_to_insert + .take() + .and_then(|p| insert_and_return_existing_value(key_to_insert, p)); + if let Some(pane_to_insert) = pane_to_insert.as_ref() { + key_to_insert = pane_to_insert.1.pid(); + } else { + break; + } + } + } + pub fn pane_infos(&self) -> Vec { let mut pane_info = vec![]; let current_pane_group = { self.current_pane_group.borrow().clone_inner() }; @@ -5076,9 +5242,11 @@ impl Tab { let mut floating_pane_info = self.floating_panes.pane_info(¤t_pane_group); pane_info.append(&mut tiled_pane_info); pane_info.append(&mut floating_pane_info); - for (pane_id, (_is_scrollback_editor, pane)) in self.suppressed_panes.iter() { + for (_pane_id_of_suppressing_pane, (_is_scrollback_editor, pane)) in + self.suppressed_panes.iter() + { let mut pane_info_for_suppressed_pane = - pane_info_for_pane(pane_id, pane, ¤t_pane_group); + pane_info_for_pane(&pane.pid(), pane, ¤t_pane_group); pane_info_for_suppressed_pane.is_floating = false; pane_info_for_suppressed_pane.is_suppressed = true; pane_info_for_suppressed_pane.is_focused = false; @@ -5364,7 +5532,7 @@ impl Tab { } pub fn add_suppressed_panes(&mut self, mut suppressed_panes: SuppressedPanes) { for (pane_id, suppressed_pane_entry) in suppressed_panes.drain() { - self.suppressed_panes.insert(pane_id, suppressed_pane_entry); + self.insert_suppressed_pane(pane_id, suppressed_pane_entry); } } pub fn toggle_pane_pinned(&mut self, client_id: ClientId) { @@ -5531,7 +5699,7 @@ pub fn pane_info_for_pane( pane_info.pane_content_rows = pane.get_content_rows(); pane_info.pane_columns = pane.cols(); pane_info.pane_content_columns = pane.get_content_columns(); - pane_info.cursor_coordinates_in_pane = pane.cursor_coordinates(); + pane_info.cursor_coordinates_in_pane = pane.cursor_coordinates(None); pane_info.is_selectable = pane.selectable(); pane_info.title = pane.current_title(); pane_info.exited = pane.exited(); diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 029df49df..6eb39eae2 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -138,6 +138,9 @@ impl ServerOsApi for FakeInputOutput { fn clear_terminal_id(&self, _terminal_id: u32) -> Result<()> { unimplemented!() } + fn send_sigint(&self, pid: Pid) -> Result<()> { + unimplemented!() + } } struct MockPtyInstructionBus { @@ -280,6 +283,7 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab { vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -361,6 +365,7 @@ fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -469,6 +474,7 @@ fn create_new_tab_with_swap_layouts( new_floating_terminal_ids, new_plugin_ids, client_id, + None, ) .unwrap(); tab @@ -554,6 +560,7 @@ fn create_new_tab_with_os_api( vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -648,6 +655,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str) floating_pane_ids, HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -734,6 +742,7 @@ fn create_new_tab_with_mock_pty_writer( vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -822,6 +831,7 @@ fn create_new_tab_with_sixel_support( vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -1191,7 +1201,7 @@ fn render_stacks_without_pane_frames() { ) .unwrap(); } - tab.focus_pane_with_id(PaneId::Terminal(1), false, client_id); + tab.focus_pane_with_id(PaneId::Terminal(1), false, false, client_id); for i in 7..9 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( @@ -1206,7 +1216,7 @@ fn render_stacks_without_pane_frames() { ) .unwrap(); } - let _ = tab.focus_pane_with_id(PaneId::Terminal(1), false, client_id); + let _ = tab.focus_pane_with_id(PaneId::Terminal(1), false, false, client_id); for i in 9..11 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( @@ -1226,8 +1236,8 @@ fn render_stacks_without_pane_frames() { ResizeStrategy::new(Resize::Increase, Some(Direction::Right)), ) .unwrap(); - let _ = tab.focus_pane_with_id(PaneId::Terminal(7), false, client_id); - let _ = tab.focus_pane_with_id(PaneId::Terminal(5), false, client_id); + let _ = tab.focus_pane_with_id(PaneId::Terminal(7), false, false, client_id); + let _ = tab.focus_pane_with_id(PaneId::Terminal(5), false, false, client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index f5284b884..085f82114 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -106,6 +106,9 @@ impl ServerOsApi for FakeInputOutput { fn clear_terminal_id(&self, _terminal_id: u32) -> Result<()> { unimplemented!() } + fn send_sigint(&self, pid: Pid) -> Result<()> { + unimplemented!() + } } fn tab_resize_increase(tab: &mut Tab, id: ClientId) { @@ -220,6 +223,7 @@ fn create_new_tab(size: Size, stacked_resize: bool) -> Tab { vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -304,6 +308,7 @@ fn create_new_tab_with_layout(size: Size, layout: TiledPaneLayout) -> Tab { vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab @@ -386,6 +391,7 @@ fn create_new_tab_with_cell_size( vec![], HashMap::new(), client_id, + None, ) .unwrap(); tab diff --git a/zellij-server/src/ui/components/nested_list.rs b/zellij-server/src/ui/components/nested_list.rs index c2aa28603..b4b76b9b6 100644 --- a/zellij-server/src/ui/components/nested_list.rs +++ b/zellij-server/src/ui/components/nested_list.rs @@ -49,6 +49,7 @@ pub fn nested_list( Some(padding + bulletin.len()), &coordinates, &style_declaration, + &style.colors, text_style.bold(Some(AnsiCode::On)), ); text = pad_line(text, max_width, padding, text_width); diff --git a/zellij-server/src/ui/components/ribbon.rs b/zellij-server/src/ui/components/ribbon.rs index 98b681a56..adfa3a799 100644 --- a/zellij-server/src/ui/components/ribbon.rs +++ b/zellij-server/src/ui/components/ribbon.rs @@ -34,6 +34,7 @@ pub fn ribbon( padding, &component_coordinates, &declaration, + &colors, text_style, ); let mut stringified = component_coordinates diff --git a/zellij-server/src/ui/components/table.rs b/zellij-server/src/ui/components/table.rs index 978b741e2..80ab2a371 100644 --- a/zellij-server/src/ui/components/table.rs +++ b/zellij-server/src/ui/components/table.rs @@ -22,7 +22,8 @@ pub fn table( if is_too_high(row_index + 1, &coordinates) { break; } - for cell in row { + let cell_count = row.iter().len(); + for (cell_index, cell) in row.into_iter().enumerate() { let declaration = if is_title_row { style.colors.table_title } else { @@ -38,16 +39,21 @@ pub fn table( } else { CharacterStyles::from(declaration) }; + + // Default: bold + let text_style = text_style.bold(Some(AnsiCode::On)); + // here we intentionally don't pass our coordinates even if we have them, because // these cells have already been padded and truncated - let (text, _text_width) = stringify_text( - &cell, - None, - &None, - &declaration, - text_style.bold(Some(AnsiCode::On)), - ); - stringified.push_str(&format!("{}{} {}", text_style, text, RESET_STYLES)); + let (text, _text_width) = + stringify_text(&cell, None, &None, &declaration, &style.colors, text_style); + if cell_index == cell_count.saturating_sub(1) { + // do not add padding between columns for the last cell + stringified.push_str(&format!("{}{}{}", text_style, text, RESET_STYLES)); + } else { + // add padding between columns + stringified.push_str(&format!("{}{} {}", text_style, text, RESET_STYLES)); + } } let next_row_instruction = coordinates .as_ref() diff --git a/zellij-server/src/ui/components/text.rs b/zellij-server/src/ui/components/text.rs index c0088e605..eb8667f8a 100644 --- a/zellij-server/src/ui/components/text.rs +++ b/zellij-server/src/ui/components/text.rs @@ -15,6 +15,7 @@ pub fn text(content: Text, style: &Style, component_coordinates: Option, coordinates: &Option, style: &StyleDeclaration, + styling: &zellij_utils::data::Styling, component_text_style: CharacterStyles, ) -> (String, usize) { let mut text_width = 0; @@ -48,6 +51,7 @@ pub fn stringify_text( } else { component_text_style }; + stringified.push_str(&format!("{}", base_text_style)); for (i, character) in text.text.chars().enumerate() { let character_width = character.width().unwrap_or(0); if is_too_wide( @@ -67,7 +71,7 @@ pub fn stringify_text( if !text.indices.is_empty() || text.selected || text.opaque { let character_with_styling = - color_index_character(character, i, &text, style, base_text_style); + color_index_character(character, i, &text, style, styling, base_text_style); stringified.push_str(&character_with_styling); } else { stringified.push(character) @@ -100,12 +104,32 @@ pub fn color_index_character( index: usize, text: &Text, declaration: &StyleDeclaration, + styling: &zellij_utils::data::Styling, base_text_style: CharacterStyles, ) -> String { - let character_style = text - .style_of_index(index, declaration) + let mut character_style = text + .style_of_index(index, declaration, styling) .map(|foreground_style| base_text_style.foreground(Some(foreground_style.into()))) .unwrap_or(base_text_style); + + // Apply dim and unbold per-character based on index levels 4 and 5 + if text.is_unbold_at(index) { + // Remove bold for this character + character_style = character_style.bold(Some(AnsiCode::Reset)); + } else if text.is_dimmed_at(index) { + // Apply dim for this character + character_style = character_style + .foreground(Some(AnsiCode::Reset)) // some terminals (eg. alacritty) do not support dimming non 16 + // colors, so we have to defer to the terminal's default here + .dim(Some(AnsiCode::On)); + } else { + character_style = character_style + .bold(Some(AnsiCode::On)) + .dim(Some(AnsiCode::Reset)); // default, to reset any + // possible dim/bold values + // from previous indices + } + format!("{}{}{}", character_style, character, base_text_style) } @@ -141,7 +165,46 @@ impl Text { } } - pub fn style_of_index(&self, index: usize, style: &StyleDeclaration) -> Option { + pub fn is_dimmed_at(&self, index: usize) -> bool { + const DIM_LEVEL: usize = 4; + self.indices + .get(DIM_LEVEL) + .map(|indices| indices.contains(&index)) + .unwrap_or(false) + } + + pub fn is_unbold_at(&self, index: usize) -> bool { + const UNBOLD_LEVEL: usize = 5; + self.indices + .get(UNBOLD_LEVEL) + .map(|indices| indices.contains(&index)) + .unwrap_or(false) + } + + pub fn style_of_index( + &self, + index: usize, + style: &StyleDeclaration, + styling: &zellij_utils::data::Styling, + ) -> Option { + const ERROR_COLOR_LEVEL: usize = 6; + const SUCCESS_COLOR_LEVEL: usize = 7; + + // Check error color first (highest precedence) + if let Some(indices) = self.indices.get(ERROR_COLOR_LEVEL) { + if indices.contains(&index) { + return Some(styling.exit_code_error.base); + } + } + + // Check success color (second highest precedence) + if let Some(indices) = self.indices.get(SUCCESS_COLOR_LEVEL) { + if indices.contains(&index) { + return Some(styling.exit_code_success.base); + } + } + + // Check regular emphasis levels (existing code) let index_variant_styles = [ style.emphasis_0, style.emphasis_1, diff --git a/zellij-server/src/ui/mod.rs b/zellij-server/src/ui/mod.rs index d9d5e3a04..1a17e7436 100644 --- a/zellij-server/src/ui/mod.rs +++ b/zellij-server/src/ui/mod.rs @@ -1,6 +1,5 @@ pub mod boundaries; pub mod components; pub mod loading_indication; -pub mod overlay; pub mod pane_boundaries_frame; pub mod pane_contents_and_ui; diff --git a/zellij-server/src/ui/overlay/mod.rs b/zellij-server/src/ui/overlay/mod.rs deleted file mode 100644 index 7e3f821b3..000000000 --- a/zellij-server/src/ui/overlay/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! This module handles the overlay's over the [`Screen`] -//! -//! They consist of: -//! -//! prompt's: -//! -//! notification's: - -pub mod prompt; - -use crate::ServerInstruction; -use zellij_utils::errors::prelude::*; -use zellij_utils::pane_size::Size; - -#[derive(Clone, Debug)] -pub struct Overlay { - pub overlay_type: OverlayType, -} - -pub trait Overlayable { - /// Generates vte_output that can be passed into - /// the `render()` function - fn generate_overlay(&self, size: Size) -> Result; -} - -#[derive(Clone, Debug)] -pub enum OverlayType { - Prompt(prompt::Prompt), -} - -impl Overlayable for OverlayType { - fn generate_overlay(&self, size: Size) -> Result { - match &self { - OverlayType::Prompt(prompt) => prompt - .generate_overlay(size) - .context("failed to generate VTE output from overlay type"), - } - } -} - -/// Entrypoint from [`Screen`], which holds the context in which -/// the overlays are being rendered. -/// The most recent overlays draw over the previous overlays. -#[derive(Clone, Debug, Default)] -pub struct OverlayWindow { - pub overlay_stack: Vec, -} - -impl Overlayable for OverlayWindow { - fn generate_overlay(&self, size: Size) -> Result { - let mut output = String::new(); - //let clear_display = "\u{1b}[2J"; - //output.push_str(&clear_display); - for overlay in &self.overlay_stack { - let vte_output = overlay - .generate_overlay(size) - .context("failed to generate VTE output from overlay window")?; - output.push_str(&vte_output); - } - Ok(output) - } -} - -impl Overlay { - pub fn prompt_confirm(self) -> Option> { - match self.overlay_type { - OverlayType::Prompt(p) => p.confirm(), - } - } - pub fn prompt_deny(self) -> Option> { - match self.overlay_type { - OverlayType::Prompt(p) => p.deny(), - } - } -} - -impl Overlayable for Overlay { - fn generate_overlay(&self, size: Size) -> Result { - self.overlay_type - .generate_overlay(size) - .context("failed to generate VTE output from overlay") - } -} - -impl Overlay { - pub fn new(overlay_type: OverlayType) -> Self { - Self { overlay_type } - } - - fn pad_cols(output: &mut String, cols: usize) { - if let Some(padding) = cols.checked_sub(output.len()) { - for _ in 0..padding { - output.push(' '); - } - } - } -} diff --git a/zellij-server/src/ui/overlay/prompt.rs b/zellij-server/src/ui/overlay/prompt.rs deleted file mode 100644 index 0cbfe3516..000000000 --- a/zellij-server/src/ui/overlay/prompt.rs +++ /dev/null @@ -1,65 +0,0 @@ -use zellij_utils::pane_size::Size; - -use super::{Overlay, OverlayType, Overlayable}; -use crate::{ClientId, ServerInstruction}; -use zellij_utils::errors::prelude::*; - -use std::fmt::Write; - -#[derive(Clone, Debug)] -pub struct Prompt { - pub message: String, - on_confirm: Option>, - on_deny: Option>, -} - -impl Prompt { - pub fn new( - message: String, - on_confirm: Option>, - on_deny: Option>, - ) -> Self { - Self { - message, - on_confirm, - on_deny, - } - } - pub fn confirm(self) -> Option> { - self.on_confirm - } - pub fn deny(self) -> Option> { - self.on_deny - } -} - -impl Overlayable for Prompt { - fn generate_overlay(&self, size: Size) -> Result { - let mut output = String::new(); - let rows = size.rows; - let mut vte_output = self.message.clone(); - Overlay::pad_cols(&mut vte_output, size.cols); - for (x, h) in vte_output.chars().enumerate() { - write!( - &mut output, - "\u{1b}[{};{}H\u{1b}[48;5;238m{}", - rows, - x + 1, - h, - ) - .context("failed to generate VTE output from prompt")?; - } - Ok(output) - } -} - -pub fn _generate_quit_prompt(client_id: ClientId) -> Overlay { - let prompt = Prompt::new( - (" Do you want to quit zellij? [Y]es / [N]o").to_string(), - Some(Box::new(ServerInstruction::ClientExit(client_id, None))), - None, - ); - Overlay { - overlay_type: OverlayType::Prompt(prompt), - } -} diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs index c17a9b668..326dd96f1 100644 --- a/zellij-server/src/ui/pane_contents_and_ui.rs +++ b/zellij-server/src/ui/pane_contents_and_ui.rs @@ -159,7 +159,7 @@ impl<'a> PaneContentsAndUi<'a> { ) { let cursor_is_visible = self .pane - .cursor_coordinates() + .cursor_coordinates(Some(*fake_cursor_client_id)) .map(|(x, y)| { self.output .cursor_is_visible(self.pane.x() + x, self.pane.y() + y) @@ -279,6 +279,7 @@ impl<'a> PaneContentsAndUi<'a> { .add_post_vte_instruction_to_client(client_id, &vte_output); } } + Ok(()) } pub fn render_pane_boundaries( diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 171096f77..6345d6b84 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -249,6 +249,9 @@ impl ServerOsApi for FakeInputOutput { fn clear_terminal_id(&self, _terminal_id: u32) -> Result<()> { unimplemented!() } + fn send_sigint(&self, pid: Pid) -> Result<()> { + unimplemented!() + } } fn create_new_screen(size: Size, advanced_mouse_actions: bool) -> Screen { @@ -402,6 +405,8 @@ impl MockScreen { initial_floating_panes_layout.clone(), tab_name, (vec![], vec![]), // swap layouts + None, // initial_panes + false, should_change_focus_to_new_tab, (self.main_client_id, false), None, @@ -416,6 +421,7 @@ impl MockScreen { true, (self.main_client_id, false), None, + None, )); self.last_opened_tab_index = Some(tab_index); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for the async render @@ -492,6 +498,8 @@ impl MockScreen { initial_floating_panes_layout.clone(), tab_name, (vec![], vec![]), // swap layouts + None, // initial_panes + false, should_change_focus_to_new_tab, (self.main_client_id, false), None, @@ -506,6 +514,7 @@ impl MockScreen { true, (self.main_client_id, false), None, + None, )); self.last_opened_tab_index = Some(tab_index); screen_thread @@ -528,6 +537,8 @@ impl MockScreen { vec![], // floating_panes_layout tab_name, (vec![], vec![]), // swap layouts + None, // initial_panes + false, should_change_focus_to_new_tab, (self.main_client_id, false), None, @@ -542,6 +553,7 @@ impl MockScreen { true, (self.main_client_id, false), None, + None, )); self.last_opened_tab_index = Some(tab_index); } @@ -756,6 +768,7 @@ fn new_tab(screen: &mut Screen, pid: u32, tab_index: usize) { tab_index, true, (client_id, false), + None, ) .expect("TEST"); } @@ -2638,6 +2651,8 @@ pub fn send_cli_new_pane_action_with_default_parameters() { pinned: None, stacked: false, blocking: false, + unblock_condition: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2684,6 +2699,8 @@ pub fn send_cli_new_pane_action_with_split_direction() { pinned: None, stacked: false, blocking: false, + unblock_condition: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2730,6 +2747,8 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() { pinned: None, stacked: false, blocking: false, + unblock_condition: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2787,6 +2806,8 @@ pub fn send_cli_new_pane_action_with_floating_pane_and_coordinates() { pinned: None, stacked: false, blocking: false, + unblock_condition: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2826,6 +2847,7 @@ pub fn send_cli_edit_action_with_default_parameters() { width: None, height: None, pinned: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_edit_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2865,6 +2887,7 @@ pub fn send_cli_edit_action_with_line_number() { width: None, height: None, pinned: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_edit_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2904,6 +2927,7 @@ pub fn send_cli_edit_action_with_split_direction() { width: None, height: None, pinned: None, + near_current_pane: false, }; send_cli_action_to_server(&session_metadata, cli_edit_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -3089,6 +3113,13 @@ pub fn send_cli_new_tab_action_default_params() { layout: None, layout_dir: None, cwd: None, + initial_command: vec![], + initial_plugin: None, + close_on_exit: Default::default(), + start_suspended: Default::default(), + block_until_exit: false, + block_until_exit_success: false, + block_until_exit_failure: false, }; send_cli_action_to_server(&session_metadata, new_tab_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -3129,6 +3160,13 @@ pub fn send_cli_new_tab_action_with_name_and_layout() { ))), layout_dir: None, cwd: None, + initial_command: vec![], + initial_plugin: None, + close_on_exit: Default::default(), + start_suspended: Default::default(), + block_until_exit: false, + block_until_exit_success: false, + block_until_exit_failure: false, }; send_cli_action_to_server(&session_metadata, new_tab_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); @@ -3699,6 +3737,7 @@ pub fn screen_can_break_pane_to_a_new_tab() { true, (1, false), None, + None, )); std::thread::sleep(std::time::Duration::from_millis(100)); // move back to make sure the other pane is in the previous tab @@ -3806,6 +3845,7 @@ pub fn screen_can_break_floating_pane_to_a_new_tab() { true, (1, false), None, + None, )); std::thread::sleep(std::time::Duration::from_millis(200)); // move back to make sure the other pane is in the previous tab @@ -3880,6 +3920,7 @@ pub fn screen_can_break_plugin_pane_to_a_new_tab() { true, (1, false), None, + None, )); std::thread::sleep(std::time::Duration::from_millis(100)); // move back to make sure the other pane is in the previous tab @@ -3958,6 +3999,7 @@ pub fn screen_can_break_floating_plugin_pane_to_a_new_tab() { true, (1, false), None, + None, )); std::thread::sleep(std::time::Duration::from_millis(100)); // move back to make sure the other pane is in the previous tab diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_default_parameters.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_default_parameters.snap index e36f78b86..d322d6c19 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_default_parameters.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_default_parameters.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap index 73fec3930..b21357e74 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: Some(100), cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: Some(100), cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_split_direction.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_split_direction.snap index c09ce65b5..fc5e1af5b 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_split_direction.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_split_direction.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(Some(Down)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled(Some(Down)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap index ca824efed..2428bce00 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap @@ -36,6 +36,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap index de7e1ed5e..fcb5c1354 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", new_pane_instruction)" --- -Some(SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Tiled(Some(Right)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false)) +Some(SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Tiled(Some(Right)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false)) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_default_parameters.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_default_parameters.snap index 75f460d5f..57aaa6500 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_default_parameters.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_default_parameters.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled(None), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_floating_pane_and_coordinates.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_floating_pane_and_coordinates.snap index 4d5b11c81..3c3d85e75 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_floating_pane_and_coordinates.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_floating_pane_and_coordinates.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Floating(Some(FloatingPaneCoordinates { x: Some(Fixed(10)), y: None, width: Some(Percent(20)), height: None, pinned: None })), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Floating(Some(FloatingPaneCoordinates { x: Some(Fixed(10)), y: None, width: Some(Percent(20)), height: None, pinned: None })), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_split_direction.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_split_direction.snap index 7ac623b8d..2bdd6975d 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_split_direction.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_split_direction.snap @@ -2,4 +2,4 @@ source: zellij-server/src/./unit/screen_tests.rs expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" --- -[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled(Some(Right)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None }), false), Exit] +[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled(Some(Right)), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None }), false), Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap index 30640244c..0bdf3e1d8 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap @@ -59,6 +59,8 @@ Some( ), [], 0, + None, + false, true, ( 1, diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap index 1a0677e64..d61ae4d78 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap @@ -86,6 +86,8 @@ NewTab( ), [], 1, + None, + false, true, ( 10, @@ -95,6 +97,8 @@ NewTab( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_query_tab_names_action.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_query_tab_names_action.snap index ed7034530..5c1577625 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_query_tab_names_action.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_query_tab_names_action.snap @@ -13,6 +13,8 @@ Some( NotificationEnd { channel: None, exit_status: None, + unblock_condition: None, + affected_pane_id: None, }, ), ), diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 6030dcc7f..189f1f8f5 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -10,8 +10,8 @@ use zellij_utils::input::actions::Action; pub use zellij_utils::plugin_api; use zellij_utils::plugin_api::event::ProtobufPaneScrollbackResponse; use zellij_utils::plugin_api::plugin_command::{ - CreateTokenResponse, ListTokensResponse, ProtobufPluginCommand, RenameWebTokenResponse, - RevokeAllWebTokensResponse, RevokeTokenResponse, + CreateTokenResponse, ListTokensResponse, ProtobufGetPanePidResponse, ProtobufPluginCommand, + RenameWebTokenResponse, RevokeAllWebTokensResponse, RevokeTokenResponse, }; use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion}; @@ -48,6 +48,17 @@ pub fn set_selectable(selectable: bool) { unsafe { host_run_plugin_command() }; } +/// Shows the cursor at specific coordinates or hides it +/// +/// # Arguments +/// * `cursor_position` - None to hide cursor, Some((x, y)) to show at coordinates +pub fn show_cursor(cursor_position: Option<(usize, usize)>) { + let plugin_command = PluginCommand::ShowCursor(cursor_position); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + pub fn request_permission(permissions: &[PermissionType]) { let plugin_command = PluginCommand::RequestPluginPermissions(permissions.into()); let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); @@ -395,8 +406,9 @@ pub fn show_self(should_float_if_hidden: bool) { } /// Show the pane (unsuppress it if it is suppressed) with the specified [PaneId], focus it and switch to its tab -pub fn show_pane_with_id(pane_id: PaneId, should_float_if_hidden: bool) { - let plugin_command = PluginCommand::ShowPaneWithId(pane_id, should_float_if_hidden); +pub fn show_pane_with_id(pane_id: PaneId, should_float_if_hidden: bool, should_focus_pane: bool) { + let plugin_command = + PluginCommand::ShowPaneWithId(pane_id, should_float_if_hidden, should_focus_pane); let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); object_to_stdout(&protobuf_plugin_command.encode_to_vec()); unsafe { host_run_plugin_command() }; @@ -561,6 +573,17 @@ pub fn write_chars(chars: &str) { unsafe { host_run_plugin_command() }; } +/// Copy arbitrary text to the user's clipboard +/// +/// Respects the user's configured clipboard destination (system clipboard or primary selection). +/// Requires the WriteToClipboard permission. +pub fn copy_to_clipboard(text: impl Into) { + let plugin_command = PluginCommand::CopyToClipboard(text.into()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + /// Focused the previously focused tab (regardless of the tab position) pub fn toggle_tab() { let plugin_command = PluginCommand::ToggleTab; @@ -774,16 +797,32 @@ pub fn close_plugin_pane(plugin_pane_id: u32) { } /// Changes the focus to the terminal pane with the specified id, unsuppressing it if it was suppressed and switching to its tab and layer (eg. floating/tiled). -pub fn focus_terminal_pane(terminal_pane_id: u32, should_float_if_hidden: bool) { - let plugin_command = PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden); +pub fn focus_terminal_pane( + terminal_pane_id: u32, + should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, +) { + let plugin_command = PluginCommand::FocusTerminalPane( + terminal_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ); let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); object_to_stdout(&protobuf_plugin_command.encode_to_vec()); unsafe { host_run_plugin_command() }; } /// Changes the focus to the plugin pane with the specified id, unsuppressing it if it was suppressed and switching to its tab and layer (eg. floating/tiled). -pub fn focus_plugin_pane(plugin_pane_id: u32, should_float_if_hidden: bool) { - let plugin_command = PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden); +pub fn focus_plugin_pane( + plugin_pane_id: u32, + should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, +) { + let plugin_command = PluginCommand::FocusPluginPane( + plugin_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ); let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); object_to_stdout(&protobuf_plugin_command.encode_to_vec()); unsafe { host_run_plugin_command() }; @@ -1023,14 +1062,22 @@ pub fn resize_pane_with_id(resize_strategy: ResizeStrategy, pane_id: PaneId) { } /// Changes the focus to the pane with the specified id, unsuppressing it if it was suppressed and switching to its tab and layer (eg. floating/tiled). -pub fn focus_pane_with_id(pane_id: PaneId, should_float_if_hidden: bool) { +pub fn focus_pane_with_id( + pane_id: PaneId, + should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, +) { let plugin_command = match pane_id { - PaneId::Terminal(terminal_pane_id) => { - PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden) - }, - PaneId::Plugin(plugin_pane_id) => { - PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) - }, + PaneId::Terminal(terminal_pane_id) => PluginCommand::FocusTerminalPane( + terminal_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ), + PaneId::Plugin(plugin_pane_id) => PluginCommand::FocusPluginPane( + plugin_pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ), }; let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); object_to_stdout(&protobuf_plugin_command.encode_to_vec()); @@ -1102,6 +1149,48 @@ pub fn write_chars_to_pane_id(chars: &str, pane_id: PaneId) { unsafe { host_run_plugin_command() }; } +/// Send SIGINT to the process running inside a terminal pane identified by this PaneId +pub fn send_sigint_to_pane_id(pane_id: PaneId) { + let plugin_command = PluginCommand::SendSigintToPaneId(pane_id); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + +/// Send SIGKILL to the process running inside a terminal pane identified by this PaneId +pub fn send_sigkill_to_pane_id(pane_id: PaneId) { + let plugin_command = PluginCommand::SendSigkillToPaneId(pane_id); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + +/// Get the PID of the process running inside a terminal pane +pub fn get_pane_pid(pane_id: PaneId) -> Result { + let plugin_command = PluginCommand::GetPanePid { pane_id }; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; + + // Read response from stdin + let response_bytes = + bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?; + + // Decode protobuf response + let protobuf_response = ProtobufGetPanePidResponse::decode(response_bytes.as_slice()) + .map_err(|e| format!("Failed to decode protobuf response: {}", e))?; + + // Convert to Rust type + let response = GetPanePidResponse::try_from(protobuf_response) + .map_err(|e| format!("Failed to convert protobuf response: {}", e))?; + + // Convert Result enum to actual Result type + match response { + GetPanePidResponse::Ok(pid) => Ok(pid), + GetPanePidResponse::Err(error_msg) => Err(error_msg), + } +} + /// Switch the position of the pane with this id with a different pane pub fn move_pane_with_pane_id(pane_id: PaneId) { let plugin_command = PluginCommand::MovePaneWithPaneId(pane_id); @@ -1504,9 +1593,16 @@ pub fn clear_key_presses_intercepts() { unsafe { host_run_plugin_command() }; } -pub fn replace_pane_with_existing_pane(pane_id_to_replace: PaneId, existing_pane_id: PaneId) { - let plugin_command = - PluginCommand::ReplacePaneWithExistingPane(pane_id_to_replace, existing_pane_id); +pub fn replace_pane_with_existing_pane( + pane_id_to_replace: PaneId, + existing_pane_id: PaneId, + suppress_replaced_pane: bool, +) { + let plugin_command = PluginCommand::ReplacePaneWithExistingPane( + pane_id_to_replace, + existing_pane_id, + suppress_replaced_pane, + ); let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); object_to_stdout(&protobuf_plugin_command.encode_to_vec()); unsafe { host_run_plugin_command() }; @@ -1580,6 +1676,14 @@ pub fn post_message_to_plugin(plugin_message: PluginMessage) { unsafe { host_run_plugin_command() }; } +pub fn run_action(action: Action, context: BTreeMap) { + // TODO: also accept reference + let plugin_command = PluginCommand::RunAction(action, context); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + #[link(wasm_import_module = "zellij")] extern "C" { fn host_run_plugin_command(); diff --git a/zellij-tile/src/ui_components/nested_list.rs b/zellij-tile/src/ui_components/nested_list.rs index f9a31632b..0c0eb9a16 100644 --- a/zellij-tile/src/ui_components/nested_list.rs +++ b/zellij-tile/src/ui_components/nested_list.rs @@ -38,6 +38,58 @@ impl NestedListItem { self.content = self.content.color_range(index_level, indices); self } + pub fn error_color_indices(mut self, indices: Vec) -> Self { + self.content = self.content.error_color_indices(indices); + self + } + pub fn error_color_range>(mut self, indices: R) -> Self { + self.content = self.content.error_color_range(indices); + self + } + pub fn error_color_substring>(mut self, substr: S) -> Self { + self.content = self.content.error_color_substring(substr); + self + } + pub fn error_color_nth_substring>( + mut self, + substr: S, + occurrence_index: usize, + ) -> Self { + self.content = self + .content + .error_color_nth_substring(substr, occurrence_index); + self + } + pub fn error_color_all(mut self) -> Self { + self.content = self.content.error_color_all(); + self + } + pub fn success_color_indices(mut self, indices: Vec) -> Self { + self.content = self.content.success_color_indices(indices); + self + } + pub fn success_color_range>(mut self, indices: R) -> Self { + self.content = self.content.success_color_range(indices); + self + } + pub fn success_color_substring>(mut self, substr: S) -> Self { + self.content = self.content.success_color_substring(substr); + self + } + pub fn success_color_nth_substring>( + mut self, + substr: S, + occurrence_index: usize, + ) -> Self { + self.content = self + .content + .success_color_nth_substring(substr, occurrence_index); + self + } + pub fn success_color_all(mut self) -> Self { + self.content = self.content.success_color_all(); + self + } pub fn serialize(&self) -> String { let mut serialized = String::new(); for _ in 0..self.indentation_level { diff --git a/zellij-tile/src/ui_components/text.rs b/zellij-tile/src/ui_components/text.rs index 68e8c0a14..492e23526 100644 --- a/zellij-tile/src/ui_components/text.rs +++ b/zellij-tile/src/ui_components/text.rs @@ -29,6 +29,220 @@ impl Text { self.opaque = true; self } + pub fn dim_indices(mut self, mut indices: Vec) -> Self { + const DIM_LEVEL: usize = 4; + self.pad_indices(DIM_LEVEL); + self.indices + .get_mut(DIM_LEVEL) + .map(|i| i.append(&mut indices)); + self + } + pub fn dim_range>(mut self, indices: R) -> Self { + const DIM_LEVEL: usize = 4; + self.pad_indices(DIM_LEVEL); + let start = match indices.start_bound() { + Bound::Unbounded => 0, + Bound::Included(s) => *s, + Bound::Excluded(s) => *s, + }; + let end = match indices.end_bound() { + Bound::Unbounded => self.text.chars().count(), + Bound::Included(s) => *s + 1, + Bound::Excluded(s) => *s, + }; + let indices = (start..end).into_iter(); + self.indices + .get_mut(DIM_LEVEL) + .map(|i| i.append(&mut indices.into_iter().collect())); + self + } + pub fn dim_substring>(mut self, substr: S) -> Self { + let substr = substr.as_ref(); + let mut start = 0; + + while let Some(pos) = self.text[start..].find(substr) { + let abs_pos = start + pos; + self = self.dim_range(abs_pos..abs_pos + substr.chars().count()); + start = abs_pos + substr.len(); + } + + self + } + pub fn dim_all(self) -> Self { + const DIM_LEVEL: usize = 4; + self.color_range(DIM_LEVEL, ..) + } + pub fn unbold_indices(mut self, mut indices: Vec) -> Self { + const UNBOLD_LEVEL: usize = 5; + self.pad_indices(UNBOLD_LEVEL); + self.indices + .get_mut(UNBOLD_LEVEL) + .map(|i| i.append(&mut indices)); + self + } + pub fn unbold_range>(mut self, indices: R) -> Self { + const UNBOLD_LEVEL: usize = 5; + self.pad_indices(UNBOLD_LEVEL); + let start = match indices.start_bound() { + Bound::Unbounded => 0, + Bound::Included(s) => *s, + Bound::Excluded(s) => *s, + }; + let end = match indices.end_bound() { + Bound::Unbounded => self.text.chars().count(), + Bound::Included(s) => *s + 1, + Bound::Excluded(s) => *s, + }; + let indices = (start..end).into_iter(); + self.indices + .get_mut(UNBOLD_LEVEL) + .map(|i| i.append(&mut indices.into_iter().collect())); + self + } + pub fn unbold_substring>(mut self, substr: S) -> Self { + let substr = substr.as_ref(); + let mut start = 0; + + while let Some(pos) = self.text[start..].find(substr) { + let abs_pos = start + pos; + self = self.unbold_range(abs_pos..abs_pos + substr.chars().count()); + start = abs_pos + substr.len(); + } + + self + } + pub fn unbold_all(self) -> Self { + const UNBOLD_LEVEL: usize = 5; + self.color_range(UNBOLD_LEVEL, ..) + } + pub fn error_color_indices(mut self, mut indices: Vec) -> Self { + const ERROR_COLOR_LEVEL: usize = 6; + self.pad_indices(ERROR_COLOR_LEVEL); + self.indices + .get_mut(ERROR_COLOR_LEVEL) + .map(|i| i.append(&mut indices)); + self + } + pub fn error_color_range>(mut self, indices: R) -> Self { + const ERROR_COLOR_LEVEL: usize = 6; + self.pad_indices(ERROR_COLOR_LEVEL); + let start = match indices.start_bound() { + Bound::Unbounded => 0, + Bound::Included(s) => *s, + Bound::Excluded(s) => *s, + }; + let end = match indices.end_bound() { + Bound::Unbounded => self.text.chars().count(), + Bound::Included(s) => *s + 1, + Bound::Excluded(s) => *s, + }; + let indices = (start..end).into_iter(); + self.indices + .get_mut(ERROR_COLOR_LEVEL) + .map(|i| i.append(&mut indices.into_iter().collect())); + self + } + pub fn error_color_substring>(mut self, substr: S) -> Self { + let substr = substr.as_ref(); + let mut start = 0; + + while let Some(pos) = self.text[start..].find(substr) { + let abs_pos = start + pos; + self = self.error_color_range(abs_pos..abs_pos + substr.chars().count()); + start = abs_pos + substr.len(); + } + + self + } + pub fn error_color_nth_substring>( + self, + substr: S, + occurrence_index: usize, + ) -> Self { + const ERROR_COLOR_LEVEL: usize = 6; + let substr = substr.as_ref(); + let mut start = 0; + let mut count = 0; + + while let Some(pos) = self.text[start..].find(substr) { + if count == occurrence_index { + let abs_pos = start + pos; + return self.color_range(ERROR_COLOR_LEVEL, abs_pos..abs_pos + substr.len()); + } + count += 1; + start = start + pos + substr.len(); + } + + self + } + pub fn error_color_all(self) -> Self { + const ERROR_COLOR_LEVEL: usize = 6; + self.color_range(ERROR_COLOR_LEVEL, ..) + } + pub fn success_color_indices(mut self, mut indices: Vec) -> Self { + const SUCCESS_COLOR_LEVEL: usize = 7; + self.pad_indices(SUCCESS_COLOR_LEVEL); + self.indices + .get_mut(SUCCESS_COLOR_LEVEL) + .map(|i| i.append(&mut indices)); + self + } + pub fn success_color_range>(mut self, indices: R) -> Self { + const SUCCESS_COLOR_LEVEL: usize = 7; + self.pad_indices(SUCCESS_COLOR_LEVEL); + let start = match indices.start_bound() { + Bound::Unbounded => 0, + Bound::Included(s) => *s, + Bound::Excluded(s) => *s, + }; + let end = match indices.end_bound() { + Bound::Unbounded => self.text.chars().count(), + Bound::Included(s) => *s + 1, + Bound::Excluded(s) => *s, + }; + let indices = (start..end).into_iter(); + self.indices + .get_mut(SUCCESS_COLOR_LEVEL) + .map(|i| i.append(&mut indices.into_iter().collect())); + self + } + pub fn success_color_substring>(mut self, substr: S) -> Self { + let substr = substr.as_ref(); + let mut start = 0; + + while let Some(pos) = self.text[start..].find(substr) { + let abs_pos = start + pos; + self = self.success_color_range(abs_pos..abs_pos + substr.chars().count()); + start = abs_pos + substr.len(); + } + + self + } + pub fn success_color_nth_substring>( + self, + substr: S, + occurrence_index: usize, + ) -> Self { + const SUCCESS_COLOR_LEVEL: usize = 7; + let substr = substr.as_ref(); + let mut start = 0; + let mut count = 0; + + while let Some(pos) = self.text[start..].find(substr) { + if count == occurrence_index { + let abs_pos = start + pos; + return self.color_range(SUCCESS_COLOR_LEVEL, abs_pos..abs_pos + substr.len()); + } + count += 1; + start = start + pos + substr.len(); + } + + self + } + pub fn success_color_all(self) -> Self { + const SUCCESS_COLOR_LEVEL: usize = 7; + self.color_range(SUCCESS_COLOR_LEVEL, ..) + } pub fn color_indices(mut self, index_level: usize, mut indices: Vec) -> Self { self.pad_indices(index_level); self.indices @@ -54,16 +268,17 @@ impl Text { .map(|i| i.append(&mut indices.into_iter().collect())); self } + pub fn color_substring>(mut self, index_level: usize, substr: S) -> Self { let substr = substr.as_ref(); let mut start = 0; - while let Some(pos) = self.text[start..].find(substr) { let abs_pos = start + pos; - self = self.color_range(index_level, abs_pos..abs_pos + substr.chars().count()); + let char_start = self.text[..abs_pos].chars().count(); + let char_end = char_start + substr.chars().count(); + self = self.color_range(index_level, char_start..char_end); start = abs_pos + substr.len(); } - self } diff --git a/zellij-utils/assets/config/default.kdl b/zellij-utils/assets/config/default.kdl index 017e3407d..e06dbe5c9 100644 --- a/zellij-utils/assets/config/default.kdl +++ b/zellij-utils/assets/config/default.kdl @@ -150,6 +150,12 @@ keybinds { }; SwitchToMode "Normal" } + bind "q" { + LaunchPlugin "zellij:sequence" { + floating true + }; + SwitchToMode "Normal" + } } tmux { bind "[" { SwitchToMode "Scroll"; } @@ -235,6 +241,7 @@ plugins { configuration location="zellij:configuration" plugin-manager location="zellij:plugin-manager" about location="zellij:about" + sequence location="zellij:sequence" } // Plugins to load in the background when a new session starts diff --git a/zellij-utils/assets/plugins/about.wasm b/zellij-utils/assets/plugins/about.wasm index d5e4ee66a..ae99b4a11 100755 Binary files a/zellij-utils/assets/plugins/about.wasm and b/zellij-utils/assets/plugins/about.wasm differ diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm index 7e854b275..0d6ab290f 100755 Binary files a/zellij-utils/assets/plugins/compact-bar.wasm and b/zellij-utils/assets/plugins/compact-bar.wasm differ diff --git a/zellij-utils/assets/plugins/configuration.wasm b/zellij-utils/assets/plugins/configuration.wasm index 9510473b3..b448d343f 100755 Binary files a/zellij-utils/assets/plugins/configuration.wasm and b/zellij-utils/assets/plugins/configuration.wasm differ diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm index dfd22a4d5..407f41f19 100755 Binary files a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm and b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm differ diff --git a/zellij-utils/assets/plugins/multiple-select.wasm b/zellij-utils/assets/plugins/multiple-select.wasm index 92c10d9e4..77f0310ba 100755 Binary files a/zellij-utils/assets/plugins/multiple-select.wasm and b/zellij-utils/assets/plugins/multiple-select.wasm differ diff --git a/zellij-utils/assets/plugins/plugin-manager.wasm b/zellij-utils/assets/plugins/plugin-manager.wasm index 71a7526ce..5c9fb4617 100755 Binary files a/zellij-utils/assets/plugins/plugin-manager.wasm and b/zellij-utils/assets/plugins/plugin-manager.wasm differ diff --git a/zellij-utils/assets/plugins/sequence.wasm b/zellij-utils/assets/plugins/sequence.wasm new file mode 100755 index 000000000..0a982532c Binary files /dev/null and b/zellij-utils/assets/plugins/sequence.wasm differ diff --git a/zellij-utils/assets/plugins/session-manager.wasm b/zellij-utils/assets/plugins/session-manager.wasm index 17d95bbd6..0a8460d09 100755 Binary files a/zellij-utils/assets/plugins/session-manager.wasm and b/zellij-utils/assets/plugins/session-manager.wasm differ diff --git a/zellij-utils/assets/plugins/share.wasm b/zellij-utils/assets/plugins/share.wasm index 6918fb7fb..360e55a2a 100755 Binary files a/zellij-utils/assets/plugins/share.wasm and b/zellij-utils/assets/plugins/share.wasm differ diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm index 1ee3c21f7..ac1b076ae 100755 Binary files a/zellij-utils/assets/plugins/status-bar.wasm and b/zellij-utils/assets/plugins/status-bar.wasm differ diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm index 8f581fee3..ec6de8854 100755 Binary files a/zellij-utils/assets/plugins/strider.wasm and b/zellij-utils/assets/plugins/strider.wasm differ diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm index 73bebb9a9..4a31b561c 100755 Binary files a/zellij-utils/assets/plugins/tab-bar.wasm and b/zellij-utils/assets/plugins/tab-bar.wasm differ diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs index 8f381ee03..464350210 100644 --- a/zellij-utils/assets/prost/api.action.rs +++ b/zellij-utils/assets/prost/api.action.rs @@ -1,9 +1,266 @@ #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct PercentOrFixed { + #[prost(oneof="percent_or_fixed::SizeType", tags="1, 2")] + pub size_type: ::core::option::Option, +} +/// Nested message and enum types in `PercentOrFixed`. +pub mod percent_or_fixed { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum SizeType { + #[prost(uint32, tag="1")] + Percent(u32), + #[prost(uint32, tag="2")] + Fixed(u32), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LayoutConstraintWithValue { + #[prost(enumeration="LayoutConstraint", tag="1")] + pub constraint_type: i32, + #[prost(uint32, optional, tag="2")] + pub value: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PluginUserConfiguration { + #[prost(map="string, string", tag="1")] + pub configuration: ::std::collections::HashMap<::prost::alloc::string::String, ::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RunPluginLocationData { + #[prost(enumeration="RunPluginLocation", tag="1")] + pub location_type: i32, + #[prost(oneof="run_plugin_location_data::LocationData", tags="2, 3, 4")] + pub location_data: ::core::option::Option, +} +/// Nested message and enum types in `RunPluginLocationData`. +pub mod run_plugin_location_data { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum LocationData { + #[prost(string, tag="2")] + FilePath(::prost::alloc::string::String), + #[prost(message, tag="3")] + ZellijTag(super::PluginTag), + #[prost(string, tag="4")] + RemoteUrl(::prost::alloc::string::String), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PluginTag { + #[prost(string, tag="1")] + pub tag: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RunPlugin { + #[prost(bool, tag="1")] + pub allow_exec_host_cmd: bool, + #[prost(message, optional, tag="2")] + pub location: ::core::option::Option, + #[prost(message, optional, tag="3")] + pub configuration: ::core::option::Option, + #[prost(string, optional, tag="4")] + pub initial_cwd: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PluginAlias { + #[prost(string, tag="1")] + pub name: ::prost::alloc::string::String, + #[prost(message, optional, tag="2")] + pub configuration: ::core::option::Option, + #[prost(string, optional, tag="3")] + pub initial_cwd: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, optional, tag="4")] + pub run_plugin: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RunPluginOrAlias { + #[prost(oneof="run_plugin_or_alias::PluginType", tags="1, 2")] + pub plugin_type: ::core::option::Option, +} +/// Nested message and enum types in `RunPluginOrAlias`. +pub mod run_plugin_or_alias { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum PluginType { + #[prost(message, tag="1")] + Plugin(super::RunPlugin), + #[prost(message, tag="2")] + Alias(super::PluginAlias), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RunEditFileAction { + #[prost(string, tag="1")] + pub file_path: ::prost::alloc::string::String, + #[prost(uint32, optional, tag="2")] + pub line_number: ::core::option::Option, + #[prost(string, optional, tag="3")] + pub cwd: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PaneRun { + #[prost(oneof="pane_run::RunType", tags="1, 2, 3, 4")] + pub run_type: ::core::option::Option, +} +/// Nested message and enum types in `PaneRun`. +pub mod pane_run { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum RunType { + #[prost(message, tag="1")] + Command(super::RunCommandAction), + #[prost(message, tag="2")] + Plugin(super::RunPluginOrAlias), + #[prost(message, tag="3")] + EditFile(super::RunEditFileAction), + #[prost(string, tag="4")] + Cwd(::prost::alloc::string::String), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TiledPaneLayout { + #[prost(enumeration="SplitDirection", tag="1")] + pub children_split_direction: i32, + #[prost(string, optional, tag="2")] + pub name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, repeated, tag="3")] + pub children: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag="4")] + pub split_size: ::core::option::Option, + #[prost(message, optional, tag="5")] + pub run: ::core::option::Option, + #[prost(bool, tag="6")] + pub borderless: bool, + #[prost(string, optional, tag="7")] + pub focus: ::core::option::Option<::prost::alloc::string::String>, + #[prost(bool, optional, tag="8")] + pub exclude_from_sync: ::core::option::Option, + #[prost(bool, tag="9")] + pub children_are_stacked: bool, + #[prost(uint32, optional, tag="10")] + pub external_children_index: ::core::option::Option, + #[prost(bool, tag="11")] + pub is_expanded_in_stack: bool, + #[prost(bool, tag="12")] + pub hide_floating_panes: bool, + #[prost(string, optional, tag="13")] + pub pane_initial_contents: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FloatingPaneLayout { + #[prost(string, optional, tag="1")] + pub name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, optional, tag="2")] + pub height: ::core::option::Option, + #[prost(message, optional, tag="3")] + pub width: ::core::option::Option, + #[prost(message, optional, tag="4")] + pub x: ::core::option::Option, + #[prost(message, optional, tag="5")] + pub y: ::core::option::Option, + #[prost(bool, optional, tag="6")] + pub pinned: ::core::option::Option, + #[prost(message, optional, tag="7")] + pub run: ::core::option::Option, + #[prost(bool, optional, tag="8")] + pub focus: ::core::option::Option, + #[prost(bool, tag="9")] + pub already_running: bool, + #[prost(string, optional, tag="10")] + pub pane_initial_contents: ::core::option::Option<::prost::alloc::string::String>, + #[prost(uint32, optional, tag="11")] + pub logical_position: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SwapTiledLayout { + #[prost(message, repeated, tag="1")] + pub constraint_map: ::prost::alloc::vec::Vec, + #[prost(string, optional, tag="2")] + pub name: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SwapFloatingLayout { + #[prost(message, repeated, tag="1")] + pub constraint_map: ::prost::alloc::vec::Vec, + #[prost(string, optional, tag="2")] + pub name: ::core::option::Option<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LayoutConstraintTiledPair { + #[prost(message, optional, tag="1")] + pub constraint: ::core::option::Option, + #[prost(message, optional, tag="2")] + pub layout: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LayoutConstraintFloatingPair { + #[prost(message, optional, tag="1")] + pub constraint: ::core::option::Option, + #[prost(message, repeated, tag="2")] + pub layouts: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CommandOrPlugin { + #[prost(oneof="command_or_plugin::CommandOrPluginType", tags="1, 2")] + pub command_or_plugin_type: ::core::option::Option, +} +/// Nested message and enum types in `CommandOrPlugin`. +pub mod command_or_plugin { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum CommandOrPluginType { + #[prost(message, tag="1")] + Command(super::RunCommandAction), + #[prost(message, tag="2")] + Plugin(super::RunPluginOrAlias), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NewTabPayload { + #[prost(message, optional, tag="1")] + pub tiled_layout: ::core::option::Option, + #[prost(message, repeated, tag="2")] + pub floating_layouts: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="3")] + pub swap_tiled_layouts: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="4")] + pub swap_floating_layouts: ::prost::alloc::vec::Vec, + #[prost(string, optional, tag="5")] + pub tab_name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(bool, tag="6")] + pub should_change_focus_to_new_tab: bool, + #[prost(string, optional, tag="7")] + pub cwd: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, repeated, tag="8")] + pub initial_panes: ::prost::alloc::vec::Vec, + #[prost(enumeration="UnblockCondition", optional, tag="9")] + pub first_pane_unblock_condition: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct Action { #[prost(enumeration="ActionName", tag="1")] pub name: i32, - #[prost(oneof="action::OptionalPayload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49")] + #[prost(oneof="action::OptionalPayload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52")] pub optional_payload: ::core::option::Option, } /// Nested message and enum types in `Action`. @@ -101,6 +358,12 @@ pub mod action { MoveTabPayload(i32), #[prost(message, tag="49")] MouseEventPayload(super::MouseEventPayload), + #[prost(message, tag="50")] + NewBlockingPanePayload(super::NewBlockingPanePayload), + #[prost(message, tag="51")] + NewTabPayload(super::NewTabPayload), + #[prost(message, tag="52")] + NewInPlacePanePayload(super::NewInPlacePanePayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -130,6 +393,8 @@ pub struct PaneIdAndShouldFloat { pub pane_id: u32, #[prost(bool, tag="2")] pub should_float: bool, + #[prost(bool, tag="3")] + pub should_be_in_place: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -170,6 +435,8 @@ pub struct GoToTabNamePayload { pub struct NewFloatingPanePayload { #[prost(message, optional, tag="1")] pub command: ::core::option::Option, + #[prost(bool, tag="2")] + pub near_current_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -178,6 +445,8 @@ pub struct NewTiledPanePayload { pub command: ::core::option::Option, #[prost(enumeration="super::resize::ResizeDirection", optional, tag="2")] pub direction: ::core::option::Option, + #[prost(bool, tag="3")] + pub near_current_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -198,6 +467,8 @@ pub struct EditFilePayload { pub direction: ::core::option::Option, #[prost(bool, tag="5")] pub should_float: bool, + #[prost(bool, tag="6")] + pub near_current_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -215,6 +486,34 @@ pub struct NewPanePayload { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct NewBlockingPanePayload { + #[prost(message, optional, tag="1")] + pub placement: ::core::option::Option, + #[prost(string, optional, tag="2")] + pub pane_name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, optional, tag="3")] + pub command: ::core::option::Option, + #[prost(enumeration="UnblockCondition", optional, tag="4")] + pub unblock_condition: ::core::option::Option, + #[prost(bool, tag="5")] + pub near_current_pane: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NewInPlacePanePayload { + #[prost(message, optional, tag="1")] + pub command: ::core::option::Option, + #[prost(string, optional, tag="2")] + pub pane_name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(bool, tag="3")] + pub near_current_pane: bool, + #[prost(message, optional, tag="4")] + pub pane_id_to_replace: ::core::option::Option, + #[prost(bool, tag="5")] + pub close_replace_pane: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct SwitchToModePayload { #[prost(enumeration="super::input_mode::InputMode", tag="1")] pub input_mode: i32, @@ -224,6 +523,10 @@ pub struct SwitchToModePayload { pub struct WritePayload { #[prost(bytes="vec", tag="1")] pub bytes_to_write: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag="2")] + pub key_with_modifier: ::core::option::Option, + #[prost(bool, tag="3")] + pub is_kitty_keyboard_protocol: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -247,6 +550,109 @@ pub struct Position { #[prost(int64, tag="2")] pub column: i64, } +/// SplitSize represents a dimension that can be either a percentage or fixed size +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SplitSize { + #[prost(oneof="split_size::SplitSizeVariant", tags="1, 2")] + pub split_size_variant: ::core::option::Option, +} +/// Nested message and enum types in `SplitSize`. +pub mod split_size { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum SplitSizeVariant { + /// 1 to 100 + #[prost(uint32, tag="1")] + Percent(u32), + /// absolute number of columns or rows + #[prost(uint32, tag="2")] + Fixed(u32), + } +} +/// PaneId identifies either a terminal or plugin pane +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct PaneId { + #[prost(oneof="pane_id::PaneIdVariant", tags="1, 2")] + pub pane_id_variant: ::core::option::Option, +} +/// Nested message and enum types in `PaneId`. +pub mod pane_id { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum PaneIdVariant { + #[prost(uint32, tag="1")] + Terminal(u32), + #[prost(uint32, tag="2")] + Plugin(u32), + } +} +/// FloatingPaneCoordinates specifies the position and size of a floating pane +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FloatingPaneCoordinates { + #[prost(message, optional, tag="1")] + pub x: ::core::option::Option, + #[prost(message, optional, tag="2")] + pub y: ::core::option::Option, + #[prost(message, optional, tag="3")] + pub width: ::core::option::Option, + #[prost(message, optional, tag="4")] + pub height: ::core::option::Option, + #[prost(bool, optional, tag="5")] + pub pinned: ::core::option::Option, +} +/// NewPanePlacement specifies where a new pane should be placed +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NewPanePlacement { + #[prost(oneof="new_pane_placement::PlacementVariant", tags="1, 2, 3, 4, 5")] + pub placement_variant: ::core::option::Option, +} +/// Nested message and enum types in `NewPanePlacement`. +pub mod new_pane_placement { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum PlacementVariant { + #[prost(bool, tag="1")] + NoPreference(bool), + #[prost(message, tag="2")] + Tiled(super::TiledPlacement), + #[prost(message, tag="3")] + Floating(super::FloatingPlacement), + #[prost(message, tag="4")] + InPlace(super::InPlaceConfig), + #[prost(message, tag="5")] + Stacked(super::StackedPlacement), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TiledPlacement { + #[prost(enumeration="super::resize::ResizeDirection", optional, tag="1")] + pub direction: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FloatingPlacement { + #[prost(message, optional, tag="1")] + pub coordinates: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct InPlaceConfig { + #[prost(message, optional, tag="1")] + pub pane_id_to_replace: ::core::option::Option, + #[prost(bool, tag="2")] + pub close_replaced_pane: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct StackedPlacement { + #[prost(message, optional, tag="1")] + pub pane_id: ::core::option::Option, +} #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct MouseEventPayload { @@ -305,6 +711,115 @@ pub struct NameAndValue { #[prost(string, tag="2")] pub value: ::prost::alloc::string::String, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KeyWithModifier { + #[prost(enumeration="BareKey", tag="1")] + pub bare_key: i32, + #[prost(enumeration="KeyModifier", repeated, tag="2")] + pub key_modifiers: ::prost::alloc::vec::Vec, + /// Only set when bare_key is CHAR + #[prost(string, optional, tag="3")] + pub character: ::core::option::Option<::prost::alloc::string::String>, +} +// Layout and related types + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum SplitDirection { + Unspecified = 0, + Horizontal = 1, + Vertical = 2, +} +impl SplitDirection { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + SplitDirection::Unspecified => "SPLIT_DIRECTION_UNSPECIFIED", + SplitDirection::Horizontal => "SPLIT_DIRECTION_HORIZONTAL", + SplitDirection::Vertical => "SPLIT_DIRECTION_VERTICAL", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "SPLIT_DIRECTION_UNSPECIFIED" => Some(Self::Unspecified), + "SPLIT_DIRECTION_HORIZONTAL" => Some(Self::Horizontal), + "SPLIT_DIRECTION_VERTICAL" => Some(Self::Vertical), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum LayoutConstraint { + Unspecified = 0, + MaxPanes = 1, + MinPanes = 2, + ExactPanes = 3, + NoConstraint = 4, +} +impl LayoutConstraint { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + LayoutConstraint::Unspecified => "LAYOUT_CONSTRAINT_UNSPECIFIED", + LayoutConstraint::MaxPanes => "LAYOUT_CONSTRAINT_MAX_PANES", + LayoutConstraint::MinPanes => "LAYOUT_CONSTRAINT_MIN_PANES", + LayoutConstraint::ExactPanes => "LAYOUT_CONSTRAINT_EXACT_PANES", + LayoutConstraint::NoConstraint => "LAYOUT_CONSTRAINT_NO_CONSTRAINT", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "LAYOUT_CONSTRAINT_UNSPECIFIED" => Some(Self::Unspecified), + "LAYOUT_CONSTRAINT_MAX_PANES" => Some(Self::MaxPanes), + "LAYOUT_CONSTRAINT_MIN_PANES" => Some(Self::MinPanes), + "LAYOUT_CONSTRAINT_EXACT_PANES" => Some(Self::ExactPanes), + "LAYOUT_CONSTRAINT_NO_CONSTRAINT" => Some(Self::NoConstraint), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum RunPluginLocation { + Unspecified = 0, + File = 1, + Zellij = 2, + Remote = 3, +} +impl RunPluginLocation { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + RunPluginLocation::Unspecified => "RUN_PLUGIN_LOCATION_UNSPECIFIED", + RunPluginLocation::File => "RUN_PLUGIN_LOCATION_FILE", + RunPluginLocation::Zellij => "RUN_PLUGIN_LOCATION_ZELLIJ", + RunPluginLocation::Remote => "RUN_PLUGIN_LOCATION_REMOTE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "RUN_PLUGIN_LOCATION_UNSPECIFIED" => Some(Self::Unspecified), + "RUN_PLUGIN_LOCATION_FILE" => Some(Self::File), + "RUN_PLUGIN_LOCATION_ZELLIJ" => Some(Self::Zellij), + "RUN_PLUGIN_LOCATION_REMOTE" => Some(Self::Remote), + _ => None, + } + } +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum SearchDirection { @@ -477,6 +992,8 @@ pub enum ActionName { ToggleGroupMarking = 88, NewStackedPane = 89, SwitchSession = 90, + NewBlockingPane = 91, + NewInPlacePane = 92, } impl ActionName { /// String value of the enum field names used in the ProtoBuf definition. @@ -573,6 +1090,8 @@ impl ActionName { ActionName::ToggleGroupMarking => "ToggleGroupMarking", ActionName::NewStackedPane => "NewStackedPane", ActionName::SwitchSession => "SwitchSession", + ActionName::NewBlockingPane => "NewBlockingPane", + ActionName::NewInPlacePane => "NewInPlacePane", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -666,6 +1185,195 @@ impl ActionName { "ToggleGroupMarking" => Some(Self::ToggleGroupMarking), "NewStackedPane" => Some(Self::NewStackedPane), "SwitchSession" => Some(Self::SwitchSession), + "NewBlockingPane" => Some(Self::NewBlockingPane), + "NewInPlacePane" => Some(Self::NewInPlacePane), + _ => None, + } + } +} +/// UnblockCondition specifies when a blocking pane should unblock +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum UnblockCondition { + UnblockOnExitSuccess = 0, + UnblockOnExitFailure = 1, + UnblockOnAnyExit = 2, +} +impl UnblockCondition { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + UnblockCondition::UnblockOnExitSuccess => "UNBLOCK_ON_EXIT_SUCCESS", + UnblockCondition::UnblockOnExitFailure => "UNBLOCK_ON_EXIT_FAILURE", + UnblockCondition::UnblockOnAnyExit => "UNBLOCK_ON_ANY_EXIT", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNBLOCK_ON_EXIT_SUCCESS" => Some(Self::UnblockOnExitSuccess), + "UNBLOCK_ON_EXIT_FAILURE" => Some(Self::UnblockOnExitFailure), + "UNBLOCK_ON_ANY_EXIT" => Some(Self::UnblockOnAnyExit), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum BareKey { + Unspecified = 0, + PageDown = 1, + PageUp = 2, + Left = 3, + Down = 4, + Up = 5, + Right = 6, + Home = 7, + End = 8, + Backspace = 9, + Delete = 10, + Insert = 11, + F1 = 12, + F2 = 13, + F3 = 14, + F4 = 15, + F5 = 16, + F6 = 17, + F7 = 18, + F8 = 19, + F9 = 20, + F10 = 21, + F11 = 22, + F12 = 23, + Char = 24, + Tab = 25, + Esc = 26, + Enter = 27, + CapsLock = 28, + ScrollLock = 29, + NumLock = 30, + PrintScreen = 31, + Pause = 32, + Menu = 33, +} +impl BareKey { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + BareKey::Unspecified => "BARE_KEY_UNSPECIFIED", + BareKey::PageDown => "BARE_KEY_PAGE_DOWN", + BareKey::PageUp => "BARE_KEY_PAGE_UP", + BareKey::Left => "BARE_KEY_LEFT", + BareKey::Down => "BARE_KEY_DOWN", + BareKey::Up => "BARE_KEY_UP", + BareKey::Right => "BARE_KEY_RIGHT", + BareKey::Home => "BARE_KEY_HOME", + BareKey::End => "BARE_KEY_END", + BareKey::Backspace => "BARE_KEY_BACKSPACE", + BareKey::Delete => "BARE_KEY_DELETE", + BareKey::Insert => "BARE_KEY_INSERT", + BareKey::F1 => "BARE_KEY_F1", + BareKey::F2 => "BARE_KEY_F2", + BareKey::F3 => "BARE_KEY_F3", + BareKey::F4 => "BARE_KEY_F4", + BareKey::F5 => "BARE_KEY_F5", + BareKey::F6 => "BARE_KEY_F6", + BareKey::F7 => "BARE_KEY_F7", + BareKey::F8 => "BARE_KEY_F8", + BareKey::F9 => "BARE_KEY_F9", + BareKey::F10 => "BARE_KEY_F10", + BareKey::F11 => "BARE_KEY_F11", + BareKey::F12 => "BARE_KEY_F12", + BareKey::Char => "BARE_KEY_CHAR", + BareKey::Tab => "BARE_KEY_TAB", + BareKey::Esc => "BARE_KEY_ESC", + BareKey::Enter => "BARE_KEY_ENTER", + BareKey::CapsLock => "BARE_KEY_CAPS_LOCK", + BareKey::ScrollLock => "BARE_KEY_SCROLL_LOCK", + BareKey::NumLock => "BARE_KEY_NUM_LOCK", + BareKey::PrintScreen => "BARE_KEY_PRINT_SCREEN", + BareKey::Pause => "BARE_KEY_PAUSE", + BareKey::Menu => "BARE_KEY_MENU", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "BARE_KEY_UNSPECIFIED" => Some(Self::Unspecified), + "BARE_KEY_PAGE_DOWN" => Some(Self::PageDown), + "BARE_KEY_PAGE_UP" => Some(Self::PageUp), + "BARE_KEY_LEFT" => Some(Self::Left), + "BARE_KEY_DOWN" => Some(Self::Down), + "BARE_KEY_UP" => Some(Self::Up), + "BARE_KEY_RIGHT" => Some(Self::Right), + "BARE_KEY_HOME" => Some(Self::Home), + "BARE_KEY_END" => Some(Self::End), + "BARE_KEY_BACKSPACE" => Some(Self::Backspace), + "BARE_KEY_DELETE" => Some(Self::Delete), + "BARE_KEY_INSERT" => Some(Self::Insert), + "BARE_KEY_F1" => Some(Self::F1), + "BARE_KEY_F2" => Some(Self::F2), + "BARE_KEY_F3" => Some(Self::F3), + "BARE_KEY_F4" => Some(Self::F4), + "BARE_KEY_F5" => Some(Self::F5), + "BARE_KEY_F6" => Some(Self::F6), + "BARE_KEY_F7" => Some(Self::F7), + "BARE_KEY_F8" => Some(Self::F8), + "BARE_KEY_F9" => Some(Self::F9), + "BARE_KEY_F10" => Some(Self::F10), + "BARE_KEY_F11" => Some(Self::F11), + "BARE_KEY_F12" => Some(Self::F12), + "BARE_KEY_CHAR" => Some(Self::Char), + "BARE_KEY_TAB" => Some(Self::Tab), + "BARE_KEY_ESC" => Some(Self::Esc), + "BARE_KEY_ENTER" => Some(Self::Enter), + "BARE_KEY_CAPS_LOCK" => Some(Self::CapsLock), + "BARE_KEY_SCROLL_LOCK" => Some(Self::ScrollLock), + "BARE_KEY_NUM_LOCK" => Some(Self::NumLock), + "BARE_KEY_PRINT_SCREEN" => Some(Self::PrintScreen), + "BARE_KEY_PAUSE" => Some(Self::Pause), + "BARE_KEY_MENU" => Some(Self::Menu), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum KeyModifier { + Unspecified = 0, + Ctrl = 1, + Alt = 2, + Shift = 3, + Super = 4, +} +impl KeyModifier { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + KeyModifier::Unspecified => "KEY_MODIFIER_UNSPECIFIED", + KeyModifier::Ctrl => "KEY_MODIFIER_CTRL", + KeyModifier::Alt => "KEY_MODIFIER_ALT", + KeyModifier::Shift => "KEY_MODIFIER_SHIFT", + KeyModifier::Super => "KEY_MODIFIER_SUPER", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "KEY_MODIFIER_UNSPECIFIED" => Some(Self::Unspecified), + "KEY_MODIFIER_CTRL" => Some(Self::Ctrl), + "KEY_MODIFIER_ALT" => Some(Self::Alt), + "KEY_MODIFIER_SHIFT" => Some(Self::Shift), + "KEY_MODIFIER_SUPER" => Some(Self::Super), _ => None, } } diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs index 7ce917712..5e1d7d1ce 100644 --- a/zellij-utils/assets/prost/api.event.rs +++ b/zellij-utils/assets/prost/api.event.rs @@ -9,7 +9,7 @@ pub struct EventNameList { pub struct Event { #[prost(enumeration="EventType", tag="1")] 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, 22, 23, 24, 25, 26, 27, 28, 29, 30")] + #[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, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33")] pub payload: ::core::option::Option, } /// Nested message and enum types in `Event`. @@ -75,10 +75,26 @@ pub mod event { InterceptedKeyPayload(super::super::key::Key), #[prost(message, tag="30")] PaneRenderReportPayload(super::PaneRenderReportPayload), + #[prost(message, tag="31")] + UserActionPayload(super::UserActionPayload), + #[prost(message, tag="32")] + ActionCompletePayload(super::ActionCompletePayload), + #[prost(message, tag="33")] + CwdChangedPayload(super::CwdChangedPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct CwdChangedPayload { + #[prost(message, optional, tag="1")] + pub pane_id: ::core::option::Option, + #[prost(string, tag="2")] + pub new_cwd: ::prost::alloc::string::String, + #[prost(uint32, repeated, tag="3")] + pub focused_client_ids: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct FailedToStartWebServerPayload { #[prost(string, tag="1")] pub error: ::prost::alloc::string::String, @@ -339,6 +355,8 @@ pub struct SessionManifest { pub web_client_count: u32, #[prost(message, repeated, tag="10")] pub tab_history: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag="11")] + pub pane_history: ::prost::alloc::vec::Vec, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -350,6 +368,14 @@ pub struct ClientTabHistory { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct ClientPaneHistory { + #[prost(uint32, tag="1")] + pub client_id: u32, + #[prost(message, repeated, tag="2")] + pub pane_history: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct PluginInfo { #[prost(uint32, tag="1")] pub plugin_id: u32, @@ -569,6 +595,28 @@ pub struct SelectedText { #[prost(message, optional, tag="2")] pub end: ::core::option::Option, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct UserActionPayload { + #[prost(message, optional, tag="1")] + pub action: ::core::option::Option, + #[prost(uint32, tag="2")] + pub client_id: u32, + #[prost(uint32, optional, tag="3")] + pub terminal_id: ::core::option::Option, + #[prost(uint32, optional, tag="4")] + pub cli_client_id: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ActionCompletePayload { + #[prost(message, optional, tag="1")] + pub action: ::core::option::Option, + #[prost(message, optional, tag="2")] + pub pane_id: ::core::option::Option, + #[prost(message, repeated, tag="3")] + pub context: ::prost::alloc::vec::Vec, +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum EventType { @@ -623,6 +671,9 @@ pub enum EventType { FailedToStartWebServer = 34, InterceptedKeyPress = 35, PaneRenderReport = 36, + UserAction = 37, + ActionComplete = 38, + CwdChanged = 39, } impl EventType { /// String value of the enum field names used in the ProtoBuf definition. @@ -667,6 +718,9 @@ impl EventType { EventType::FailedToStartWebServer => "FailedToStartWebServer", EventType::InterceptedKeyPress => "InterceptedKeyPress", EventType::PaneRenderReport => "PaneRenderReport", + EventType::UserAction => "UserAction", + EventType::ActionComplete => "ActionComplete", + EventType::CwdChanged => "CwdChanged", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -708,6 +762,9 @@ impl EventType { "FailedToStartWebServer" => Some(Self::FailedToStartWebServer), "InterceptedKeyPress" => Some(Self::InterceptedKeyPress), "PaneRenderReport" => Some(Self::PaneRenderReport), + "UserAction" => Some(Self::UserAction), + "ActionComplete" => Some(Self::ActionComplete), + "CwdChanged" => Some(Self::CwdChanged), _ => None, } } diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index b33d0b8e2..931a3a730 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -3,7 +3,7 @@ pub struct PluginCommand { #[prost(enumeration="CommandName", tag="1")] pub name: i32, - #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113")] + #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 119, 120, 121, 122, 123")] pub payload: ::core::option::Option, } /// Nested message and enum types in `PluginCommand`. @@ -217,6 +217,18 @@ pub mod plugin_command { NewTabPayload(super::NewTabPayload), #[prost(message, tag="113")] GetPaneScrollbackPayload(super::GetPaneScrollbackPayload), + #[prost(message, tag="114")] + RunActionPayload(super::RunActionPayload), + #[prost(message, tag="119")] + ShowCursorPayload(super::ShowCursorPayload), + #[prost(message, tag="120")] + CopyToClipboardPayload(super::CopyToClipboardPayload), + #[prost(message, tag="121")] + SendSigintToPaneIdPayload(super::PaneId), + #[prost(message, tag="122")] + SendSigkillToPaneIdPayload(super::PaneId), + #[prost(message, tag="123")] + GetPanePidPayload(super::GetPanePidPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -229,11 +241,21 @@ pub struct NewTabPayload { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct RunActionPayload { + #[prost(message, optional, tag="1")] + pub action: ::core::option::Option, + #[prost(message, repeated, tag="2")] + pub context: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct ReplacePaneWithExistingPanePayload { #[prost(message, optional, tag="1")] pub pane_id_to_replace: ::core::option::Option, #[prost(message, optional, tag="2")] pub existing_pane_id: ::core::option::Option, + #[prost(bool, tag="3")] + pub suppress_replaced_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -610,6 +632,8 @@ pub struct ShowPaneWithIdPayload { pub pane_id: ::core::option::Option, #[prost(bool, tag="2")] pub should_float_if_hidden: bool, + #[prost(bool, tag="3")] + pub should_focus_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -899,6 +923,49 @@ pub struct GetPaneScrollbackPayload { #[prost(bool, tag="2")] pub get_full_scrollback: bool, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ShowCursorPayload { + #[prost(message, optional, tag="1")] + pub position: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CursorPosition { + #[prost(uint32, tag="1")] + pub x: u32, + #[prost(uint32, tag="2")] + pub y: u32, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct CopyToClipboardPayload { + #[prost(string, tag="1")] + pub text: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPanePidPayload { + #[prost(message, optional, tag="1")] + pub pane_id: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetPanePidResponse { + #[prost(oneof="get_pane_pid_response::Result", tags="1, 2")] + pub result: ::core::option::Option, +} +/// Nested message and enum types in `GetPanePidResponse`. +pub mod get_pane_pid_response { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Result { + #[prost(int32, tag="1")] + Pid(i32), + #[prost(string, tag="2")] + Error(::prost::alloc::string::String), + } +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum CommandName { @@ -1049,6 +1116,12 @@ pub enum CommandName { ClearKeyPressesIntercepts = 144, ReplacePaneWithExistingPane = 155, GetPaneScrollback = 163, + RunAction = 164, + ShowCursor = 169, + CopyToClipboard = 170, + SendSigintToPaneId = 171, + SendSigkillToPaneId = 172, + GetPanePid = 173, } impl CommandName { /// String value of the enum field names used in the ProtoBuf definition. @@ -1204,6 +1277,12 @@ impl CommandName { CommandName::ClearKeyPressesIntercepts => "ClearKeyPressesIntercepts", CommandName::ReplacePaneWithExistingPane => "ReplacePaneWithExistingPane", CommandName::GetPaneScrollback => "GetPaneScrollback", + CommandName::RunAction => "RunAction", + CommandName::ShowCursor => "ShowCursor", + CommandName::CopyToClipboard => "CopyToClipboard", + CommandName::SendSigintToPaneId => "SendSigintToPaneId", + CommandName::SendSigkillToPaneId => "SendSigkillToPaneId", + CommandName::GetPanePid => "GetPanePid", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -1356,6 +1435,12 @@ impl CommandName { "ClearKeyPressesIntercepts" => Some(Self::ClearKeyPressesIntercepts), "ReplacePaneWithExistingPane" => Some(Self::ReplacePaneWithExistingPane), "GetPaneScrollback" => Some(Self::GetPaneScrollback), + "RunAction" => Some(Self::RunAction), + "ShowCursor" => Some(Self::ShowCursor), + "CopyToClipboard" => Some(Self::CopyToClipboard), + "SendSigintToPaneId" => Some(Self::SendSigintToPaneId), + "SendSigkillToPaneId" => Some(Self::SendSigkillToPaneId), + "GetPanePid" => Some(Self::GetPanePid), _ => None, } } diff --git a/zellij-utils/assets/prost/api.plugin_permission.rs b/zellij-utils/assets/prost/api.plugin_permission.rs index 643f5b99f..e113273a9 100644 --- a/zellij-utils/assets/prost/api.plugin_permission.rs +++ b/zellij-utils/assets/prost/api.plugin_permission.rs @@ -15,6 +15,8 @@ pub enum PermissionType { StartWebServer = 11, InterceptInput = 12, ReadPaneContents = 13, + RunActionsAsUser = 14, + WriteToClipboard = 15, } impl PermissionType { /// String value of the enum field names used in the ProtoBuf definition. @@ -37,6 +39,8 @@ impl PermissionType { PermissionType::StartWebServer => "StartWebServer", PermissionType::InterceptInput => "InterceptInput", PermissionType::ReadPaneContents => "ReadPaneContents", + PermissionType::RunActionsAsUser => "RunActionsAsUser", + PermissionType::WriteToClipboard => "WriteToClipboard", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -56,6 +60,8 @@ impl PermissionType { "StartWebServer" => Some(Self::StartWebServer), "InterceptInput" => Some(Self::InterceptInput), "ReadPaneContents" => Some(Self::ReadPaneContents), + "RunActionsAsUser" => Some(Self::RunActionsAsUser), + "WriteToClipboard" => Some(Self::WriteToClipboard), _ => None, } } diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs index c977170ce..8fa4187c6 100644 --- a/zellij-utils/assets/prost_ipc/client_server_contract.rs +++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs @@ -578,6 +578,8 @@ pub struct NewPaneAction { pub pane_name: ::core::option::Option<::prost::alloc::string::String>, #[prost(bool, tag="3")] pub start_suppressed: bool, + #[prost(bool, tag="4")] + pub near_current_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -594,6 +596,8 @@ pub struct EditFileAction { pub start_suppressed: bool, #[prost(message, optional, tag="6")] pub coordinates: ::core::option::Option, + #[prost(bool, tag="7")] + pub near_current_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -604,6 +608,8 @@ pub struct NewFloatingPaneAction { pub pane_name: ::core::option::Option<::prost::alloc::string::String>, #[prost(message, optional, tag="3")] pub coordinates: ::core::option::Option, + #[prost(bool, tag="7")] + pub near_current_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -614,6 +620,8 @@ pub struct NewTiledPaneAction { pub command: ::core::option::Option, #[prost(string, optional, tag="3")] pub pane_name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(bool, tag="7")] + pub near_current_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -622,6 +630,12 @@ pub struct NewInPlacePaneAction { pub command: ::core::option::Option, #[prost(string, optional, tag="2")] pub pane_name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(bool, tag="3")] + pub near_current_pane: bool, + #[prost(message, optional, tag="4")] + pub pane_id_to_replace: ::core::option::Option, + #[prost(bool, tag="5")] + pub close_replace_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -630,6 +644,8 @@ pub struct NewStackedPaneAction { pub command: ::core::option::Option, #[prost(string, optional, tag="2")] pub pane_name: ::core::option::Option<::prost::alloc::string::String>, + #[prost(bool, tag="3")] + pub near_current_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -640,6 +656,10 @@ pub struct NewBlockingPaneAction { pub pane_name: ::core::option::Option<::prost::alloc::string::String>, #[prost(message, optional, tag="3")] pub command: ::core::option::Option, + #[prost(enumeration="UnblockCondition", optional, tag="4")] + pub unblock_condition: ::core::option::Option, + #[prost(bool, tag="5")] + pub near_current_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -664,6 +684,10 @@ pub struct NewTabAction { pub should_change_focus_to_new_tab: bool, #[prost(string, optional, tag="7")] pub cwd: ::core::option::Option<::prost::alloc::string::String>, + #[prost(message, repeated, tag="8")] + pub initial_panes: ::prost::alloc::vec::Vec, + #[prost(enumeration="UnblockCondition", optional, tag="9")] + pub first_pane_unblock_condition: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -696,6 +720,8 @@ pub struct MoveTabAction { pub struct RunAction { #[prost(message, optional, tag="1")] pub command: ::core::option::Option, + #[prost(bool, tag="2")] + pub near_current_pane: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -816,6 +842,8 @@ pub struct FocusTerminalPaneWithIdAction { pub pane_id: u32, #[prost(bool, tag="2")] pub should_float_if_hidden: bool, + #[prost(bool, tag="3")] + pub should_be_in_place_if_hidden: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -824,6 +852,8 @@ pub struct FocusPluginPaneWithIdAction { pub pane_id: u32, #[prost(bool, tag="2")] pub should_float_if_hidden: bool, + #[prost(bool, tag="3")] + pub should_be_in_place_if_hidden: bool, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -1254,6 +1284,23 @@ pub struct PluginAlias { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct CommandOrPlugin { + #[prost(oneof="command_or_plugin::CommandOrPluginType", tags="1, 2")] + pub command_or_plugin_type: ::core::option::Option, +} +/// Nested message and enum types in `CommandOrPlugin`. +pub mod command_or_plugin { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum CommandOrPluginType { + #[prost(message, tag="1")] + Command(super::RunCommandAction), + #[prost(message, tag="2")] + Plugin(super::RunPluginOrAlias), + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct MouseEvent { #[prost(enumeration="MouseEventType", tag="1")] pub event_type: i32, @@ -1834,6 +1881,38 @@ impl Direction { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] +pub enum UnblockCondition { + Unspecified = 0, + OnExitSuccess = 1, + OnExitFailure = 2, + OnAnyExit = 3, +} +impl UnblockCondition { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + UnblockCondition::Unspecified => "UNBLOCK_CONDITION_UNSPECIFIED", + UnblockCondition::OnExitSuccess => "UNBLOCK_CONDITION_ON_EXIT_SUCCESS", + UnblockCondition::OnExitFailure => "UNBLOCK_CONDITION_ON_EXIT_FAILURE", + UnblockCondition::OnAnyExit => "UNBLOCK_CONDITION_ON_ANY_EXIT", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "UNBLOCK_CONDITION_UNSPECIFIED" => Some(Self::Unspecified), + "UNBLOCK_CONDITION_ON_EXIT_SUCCESS" => Some(Self::OnExitSuccess), + "UNBLOCK_CONDITION_ON_EXIT_FAILURE" => Some(Self::OnExitFailure), + "UNBLOCK_CONDITION_ON_ANY_EXIT" => Some(Self::OnAnyExit), + _ => None, + } + } +} +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] pub enum ResizeType { Unspecified = 0, Increase = 1, diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 0fd524735..e59ba71fa 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -1,4 +1,4 @@ -use crate::data::{Direction, InputMode, Resize}; +use crate::data::{Direction, InputMode, Resize, UnblockCondition}; use crate::setup::Setup; use crate::{ consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV}, @@ -388,9 +388,49 @@ pub enum Sessions { takes_value(false) )] stacked: bool, - /// Whether to block until this command has finished + /// Block until the command has finished and its pane has been closed #[clap(long, value_parser, default_value("false"), takes_value(false))] blocking: bool, + + /// Block until the command exits successfully (exit status 0) OR its pane has been closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + conflicts_with("blocking"), + conflicts_with("block-until-exit-failure"), + conflicts_with("block-until-exit") + )] + block_until_exit_success: bool, + + /// Block until the command exits with failure (non-zero exit status) OR its pane has been + /// closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + conflicts_with("blocking"), + conflicts_with("block-until-exit-success"), + conflicts_with("block-until-exit") + )] + block_until_exit_failure: bool, + + /// Block until the command exits (regardless of exit status) OR its pane has been closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + conflicts_with("blocking"), + conflicts_with("block-until-exit-success"), + conflicts_with("block-until-exit-failure") + )] + block_until_exit: bool, + /// if set, will open the pane near the current one rather than following the user's focus + #[clap(long)] + near_current_pane: bool, }, /// Load a plugin #[clap(visible_alias = "p")] @@ -484,6 +524,9 @@ pub enum Sessions { /// Whether to pin a floating pane so that it is always on top #[clap(long, requires("floating"))] pinned: Option, + /// if set, will open the pane near the current one rather than following the user's focus + #[clap(long)] + near_current_pane: bool, }, ConvertConfig { old_config_file: PathBuf, @@ -691,6 +734,14 @@ pub enum CliAction { stacked: bool, #[clap(short, long)] blocking: bool, + + // TODO: clean this up + #[clap(skip)] + unblock_condition: Option, + + /// if set, will open the pane near the current one rather than following the user's focus + #[clap(long)] + near_current_pane: bool, }, /// Open the specified file in a new zellij pane with your default EDITOR Edit { @@ -738,6 +789,9 @@ pub enum CliAction { /// Whether to pin a floating pane so that it is always on top #[clap(long, requires("floating"))] pinned: Option, + /// if set, will open the pane near the current one rather than following the user's focus + #[clap(long)] + near_current_pane: bool, }, /// Switch input mode of all connected clients [locked|pane|tab|resize|move|search|session] SwitchMode { @@ -795,6 +849,76 @@ pub enum CliAction { /// Change the working directory of the new tab #[clap(short, long, value_parser)] cwd: Option, + + /// Optional initial command to run in the new tab + #[clap( + value_parser, + conflicts_with("initial-plugin"), + multiple_values(true), + takes_value(true), + last(true) + )] + initial_command: Vec, + + /// Initial plugin to load in the new tab + #[clap(long, value_parser, conflicts_with("initial-command"))] + initial_plugin: Option, + + /// Close the pane immediately when its command exits + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + requires("initial-command") + )] + close_on_exit: bool, + + /// Start the command suspended, only running it after you first press ENTER + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + requires("initial-command") + )] + start_suspended: bool, + + /// Block until the command exits successfully (exit status 0) OR its pane has been closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + requires("initial-command"), + conflicts_with("block-until-exit-failure"), + conflicts_with("block-until-exit") + )] + block_until_exit_success: bool, + + /// Block until the command exits with failure (non-zero exit status) OR its pane has been closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + requires("initial-command"), + conflicts_with("block-until-exit-success"), + conflicts_with("block-until-exit") + )] + block_until_exit_failure: bool, + + /// Block until the command exits (regardless of exit status) OR its pane has been closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + requires("initial-command"), + conflicts_with("block-until-exit-success"), + conflicts_with("block-until-exit-failure") + )] + block_until_exit: bool, }, /// Move the focused tab in the specified direction. [right|left] MoveTab { diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto index 254a2a75f..e4561989d 100644 --- a/zellij-utils/src/client_server_contract/common_types.proto +++ b/zellij-utils/src/client_server_contract/common_types.proto @@ -333,6 +333,7 @@ message NewPaneAction { optional Direction direction = 1; optional string pane_name = 2; bool start_suppressed = 3; + bool near_current_pane = 4; } message EditFileAction { @@ -342,34 +343,43 @@ message EditFileAction { bool in_place = 4; bool start_suppressed = 5; optional FloatingPaneCoordinates coordinates = 6; + bool near_current_pane = 7; } message NewFloatingPaneAction { optional RunCommandAction command = 1; optional string pane_name = 2; optional FloatingPaneCoordinates coordinates = 3; + bool near_current_pane = 7; } message NewTiledPaneAction { optional Direction direction = 1; optional RunCommandAction command = 2; optional string pane_name = 3; + bool near_current_pane = 7; } message NewInPlacePaneAction { optional RunCommandAction command = 1; optional string pane_name = 2; + bool near_current_pane = 3; + optional PaneId pane_id_to_replace = 4; + bool close_replace_pane = 5; } message NewStackedPaneAction { optional RunCommandAction command = 1; optional string pane_name = 2; + bool near_current_pane = 3; } message NewBlockingPaneAction { NewPanePlacement placement = 1; optional string pane_name = 2; optional RunCommandAction command = 3; + optional UnblockCondition unblock_condition = 4; + bool near_current_pane = 5; } message PaneNameInputAction { @@ -384,6 +394,8 @@ message NewTabAction { optional string tab_name = 5; bool should_change_focus_to_new_tab = 6; optional string cwd = 7; + repeated CommandOrPlugin initial_panes = 8; + optional UnblockCondition first_pane_unblock_condition = 9; } message GoToTabAction { @@ -405,6 +417,7 @@ message MoveTabAction { message RunAction { RunCommandAction command = 1; + bool near_current_pane = 2; } message LaunchOrFocusPluginAction { @@ -479,11 +492,13 @@ message ClosePluginPaneAction { message FocusTerminalPaneWithIdAction { uint32 pane_id = 1; bool should_float_if_hidden = 2; + bool should_be_in_place_if_hidden = 3; } message FocusPluginPaneWithIdAction { uint32 pane_id = 1; bool should_float_if_hidden = 2; + bool should_be_in_place_if_hidden = 3; } message RenameTerminalPaneAction { @@ -571,6 +586,13 @@ enum Direction { DIRECTION_DOWN = 4; } +enum UnblockCondition { + UNBLOCK_CONDITION_UNSPECIFIED = 0; + UNBLOCK_CONDITION_ON_EXIT_SUCCESS = 1; + UNBLOCK_CONDITION_ON_EXIT_FAILURE = 2; + UNBLOCK_CONDITION_ON_ANY_EXIT = 3; +} + enum ResizeType { RESIZE_TYPE_UNSPECIFIED = 0; RESIZE_TYPE_INCREASE = 1; @@ -761,6 +783,13 @@ message PluginAlias { optional RunPlugin run_plugin = 4; } +message CommandOrPlugin { + oneof command_or_plugin_type { + RunCommandAction command = 1; + RunPluginOrAlias plugin = 2; + } +} + message MouseEvent { MouseEventType event_type = 1; bool left = 2; diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs index 4f5b729de..e5b93e1a3 100644 --- a/zellij-utils/src/consts.rs +++ b/zellij-utils/src/consts.rs @@ -138,6 +138,7 @@ mod not_wasm { add_plugin!(assets, "about.wasm"); add_plugin!(assets, "share.wasm"); add_plugin!(assets, "multiple-select.wasm"); + add_plugin!(assets, "sequence.wasm"); assets }; } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 7f700d4dd..62571fd4d 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -1,8 +1,8 @@ use crate::home::default_layout_dir; -use crate::input::actions::Action; +use crate::input::actions::{Action, RunCommandAction}; use crate::input::config::ConversionError; use crate::input::keybinds::Keybinds; -use crate::input::layout::{RunPlugin, SplitSize}; +use crate::input::layout::{RunPlugin, RunPluginOrAlias, SplitSize}; use crate::pane_size::PaneGeom; use crate::position::Position; use crate::shared::{colors as default_colors, eightbit_to_rgb}; @@ -15,7 +15,7 @@ use std::net::IpAddr; use std::path::{Path, PathBuf}; use std::str::{self, FromStr}; use std::time::Duration; -use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString, ToString}; +use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString}; use unicode_width::UnicodeWidthChar; #[cfg(not(target_family = "wasm"))] @@ -26,6 +26,39 @@ use termwiz::{ pub type ClientId = u16; // TODO: merge with crate type? +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum UnblockCondition { + /// Unblock only when exit status is 0 (success) + OnExitSuccess, + /// Unblock only when exit status is non-zero (failure) + OnExitFailure, + /// Unblock on any exit (success or failure) + OnAnyExit, +} + +impl UnblockCondition { + /// Check if the condition is met for the given exit status + pub fn is_met(&self, exit_status: i32) -> bool { + match self { + UnblockCondition::OnExitSuccess => exit_status == 0, + UnblockCondition::OnExitFailure => exit_status != 0, + UnblockCondition::OnAnyExit => true, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum CommandOrPlugin { + Command(RunCommandAction), + Plugin(RunPluginOrAlias), +} + +impl CommandOrPlugin { + pub fn new_command(command: Vec) -> Self { + CommandOrPlugin::Command(RunCommandAction::new(command)) + } +} + pub fn client_id_to_colors( client_id: ClientId, colors: MultiplayerColors, @@ -252,7 +285,7 @@ impl FromStr for BareKey { } #[derive( - Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, ToString, + Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, Display, )] pub enum KeyModifier { Ctrl, @@ -589,6 +622,12 @@ pub enum Direction { Down, } +impl Default for Direction { + fn default() -> Self { + Direction::Left + } +} + impl Direction { pub fn invert(&self) -> Direction { match *self { @@ -642,6 +681,12 @@ pub enum Resize { Decrease, } +impl Default for Resize { + fn default() -> Self { + Resize::Increase + } +} + impl Resize { pub fn invert(&self) -> Self { match self { @@ -877,7 +922,7 @@ impl From for FileMetadata { /// These events can be subscribed to with subscribe method exported by `zellij-tile`. /// Once subscribed to, they will trigger the `update` method of the `ZellijPlugin` trait. -#[derive(Debug, Clone, PartialEq, EnumDiscriminants, ToString, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, EnumDiscriminants, Display, Serialize, Deserialize)] #[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))] #[strum_discriminants(name(EventType))] #[non_exhaustive] @@ -946,10 +991,14 @@ pub enum Event { FailedToStartWebServer(String), BeforeClose, InterceptedKeyPress(KeyWithModifier), + /// An action was performed by the user (requires InterceptInput permission) + UserAction(Action, ClientId, Option, Option), // Action, client_id, terminal_id, cli_client_id PaneRenderReport(HashMap), + ActionComplete(Action, Option, BTreeMap), // Action, pane_id, context + CwdChanged(PaneId, PathBuf, Vec), // pane_id, cwd, focused_client_ids } -#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants, ToString, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants, Display, Serialize, Deserialize)] pub enum WebServerStatus { Online(String), // String -> base url Offline, @@ -964,7 +1013,7 @@ pub enum WebServerStatus { Copy, Clone, EnumDiscriminants, - ToString, + Display, Serialize, Deserialize, PartialOrd, @@ -988,6 +1037,8 @@ pub enum Permission { StartWebServer, InterceptInput, ReadPaneContents, + RunActionsAsUser, + WriteToClipboard, } impl PermissionType { @@ -1017,6 +1068,8 @@ impl PermissionType { PermissionType::ReadPaneContents => { "Read pane contents (viewport and selection)".to_owned() }, + PermissionType::RunActionsAsUser => "Execute actions as the user".to_owned(), + PermissionType::WriteToClipboard => "Write to clipboard".to_owned(), } } } @@ -1643,6 +1696,7 @@ pub struct SessionInfo { pub web_clients_allowed: bool, pub web_client_count: usize, pub tab_history: BTreeMap>, + pub pane_history: BTreeMap>, } #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] @@ -2053,6 +2107,12 @@ pub enum PaneScrollbackResponse { Err(String), } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum GetPanePidResponse { + Ok(i32), + Err(String), +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SelectedText { pub start: Position, @@ -2226,6 +2286,12 @@ pub enum PaneId { Plugin(u32), } +impl Default for PaneId { + fn default() -> Self { + PaneId::Terminal(0) + } +} + impl FromStr for PaneId { type Err = Box; fn from_str(stringified_pane_id: &str) -> Result { @@ -2660,13 +2726,14 @@ impl NewPanePlacement { type Context = BTreeMap; -#[derive(Debug, Clone, EnumDiscriminants, ToString)] +#[derive(Debug, Clone, EnumDiscriminants, Display)] #[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))] #[strum_discriminants(name(CommandType))] pub enum PluginCommand { Subscribe(HashSet), Unsubscribe(HashSet), SetSelectable(bool), + ShowCursor(Option<(usize, usize)>), GetPluginIds, GetZellijVersion, OpenFile(FileToOpen, Context), @@ -2723,16 +2790,16 @@ pub enum PluginCommand { NextSwapLayout, GoToTabName(String), FocusOrCreateTab(String), - GoToTab(u32), // tab index - StartOrReloadPlugin(String), // plugin url (eg. file:/path/to/plugin.wasm) - CloseTerminalPane(u32), // terminal pane id - ClosePluginPane(u32), // plugin pane id - FocusTerminalPane(u32, bool), // terminal pane id, should_float_if_hidden - FocusPluginPane(u32, bool), // plugin pane id, should_float_if_hidden - RenameTerminalPane(u32, String), // terminal pane id, new name - RenamePluginPane(u32, String), // plugin pane id, new name - RenameTab(u32, String), // tab index, new name - ReportPanic(String), // stringified panic + GoToTab(u32), // tab index + StartOrReloadPlugin(String), // plugin url (eg. file:/path/to/plugin.wasm) + CloseTerminalPane(u32), // terminal pane id + ClosePluginPane(u32), // plugin pane id + FocusTerminalPane(u32, bool, bool), // terminal pane id, should_float_if_hidden, should_be_in_place_if_hidden + FocusPluginPane(u32, bool, bool), // plugin pane id, should_float_if_hidden, should_be_in_place_if_hidden + RenameTerminalPane(u32, String), // terminal pane id, new name + RenamePluginPane(u32, String), // plugin pane id, new name + RenameTab(u32, String), // tab index, new name + ReportPanic(String), // stringified panic RequestPluginPermissions(Vec), SwitchSession(ConnectToSession), DeleteDeadSession(String), // String -> session name @@ -2768,7 +2835,7 @@ pub enum PluginCommand { Reconfigure(String, bool), // String -> stringified configuration, bool -> save configuration // file to disk HidePaneWithId(PaneId), - ShowPaneWithId(PaneId, bool), // bool -> should_float_if_hidden + ShowPaneWithId(PaneId, bool, bool), // bools -> should_float_if_hidden, should_focus_pane OpenCommandPaneBackground(CommandToRun, Context), RerunCommandPane(u32), // u32 - terminal pane id ResizePaneIdWithDirection(ResizeStrategy, PaneId), @@ -2779,6 +2846,11 @@ pub enum PluginCommand { }, WriteToPaneId(Vec, PaneId), WriteCharsToPaneId(String, PaneId), + SendSigintToPaneId(PaneId), + SendSigkillToPaneId(PaneId), + GetPanePid { + pane_id: PaneId, + }, MovePaneWithPaneId(PaneId), MovePaneWithPaneIdInDirection(PaneId, Direction), ClearScreenForPaneId(PaneId), @@ -2844,5 +2916,8 @@ pub enum PluginCommand { RenameWebLoginToken(String, String), // (original_name, new_name) InterceptKeyPresses, ClearKeyPressesIntercepts, - ReplacePaneWithExistingPane(PaneId, PaneId), // (pane id to replace, pane id of existing) + ReplacePaneWithExistingPane(PaneId, PaneId, bool), // (pane id to replace, pane id of existing, + // suppress_replaced_pane) + RunAction(Action, BTreeMap), + CopyToClipboard(String), // text to copy } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index bcd059870..74c40534d 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -277,6 +277,7 @@ pub enum ScreenContext { ToggleActiveTerminalFullscreen, TogglePaneFrames, SetSelectable, + ShowPluginCursor, SetInvisibleBorders, SetFixedHeight, SetFixedWidth, @@ -313,10 +314,6 @@ pub enum ScreenContext { ToggleTab, AddClient, RemoveClient, - AddOverlay, - RemoveOverlay, - ConfirmPrompt, - DenyPrompt, UpdateSearch, SearchDown, SearchUp, @@ -339,6 +336,8 @@ pub enum ScreenContext { LaunchOrFocusPlugin, LaunchPlugin, SuppressPane, + UnsuppressPane, + UnsuppressOrExpandPane, FocusPaneWithId, RenamePane, RenameTab, @@ -358,6 +357,7 @@ pub enum ScreenContext { ResizePaneWithId, EditScrollbackForPaneWithId, WriteToPaneId, + CopyTextToClipboard, MovePaneWithPaneId, MovePaneWithPaneIdInDirection, ClearScreenForPaneId, @@ -418,6 +418,10 @@ pub enum PtyContext { Reconfigure, ListClientsToPlugin, ReportPluginCwd, + SendSigintToPaneId, + SendSigkillToPaneId, + GetPanePid, + UpdateAndReportCwds, Exit, } @@ -460,6 +464,7 @@ pub enum PluginContext { WebServerStarted, FailedToStartWebServer, PaneRenderReport, + UserInput, } /// Stack call representations corresponding to the different types of [`ClientInstruction`]s. diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index d623197bc..92d697a90 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -1,12 +1,15 @@ //! Definition of the actions that can be bound to keys. -use super::command::{OpenFilePayload, RunCommandAction}; +pub use super::command::{OpenFilePayload, RunCommandAction}; use super::layout::{ FloatingPaneLayout, Layout, PluginAlias, RunPlugin, RunPluginLocation, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }; use crate::cli::CliAction; -use crate::data::{Direction, KeyWithModifier, LayoutInfo, NewPanePlacement, PaneId, Resize}; +use crate::data::{ + CommandOrPlugin, Direction, KeyWithModifier, LayoutInfo, NewPanePlacement, OriginatingPlugin, + PaneId, Resize, UnblockCondition, +}; use crate::data::{FloatingPaneCoordinates, InputMode}; use crate::home::{find_default_config_dir, get_layout_dir}; use crate::input::config::{Config, ConfigError, KdlError}; @@ -99,7 +102,18 @@ impl FromStr for SearchOption { // They might need to be adjusted in the default config // as well `../../assets/config/default.yaml` /// Actions that can be bound to keys. -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, strum_macros::Display)] +#[derive( + Clone, + Debug, + PartialEq, + Eq, + Deserialize, + Serialize, + strum_macros::Display, + strum_macros::EnumString, + strum_macros::EnumIter, +)] +#[strum(ascii_case_insensitive)] pub enum Action { /// Quit Zellij. Quit, @@ -194,6 +208,8 @@ pub enum Action { placement: NewPanePlacement, pane_name: Option, command: Option, + unblock_condition: Option, + near_current_pane: bool, }, /// Open the file in a new pane using the default editor EditFile { @@ -203,27 +219,34 @@ pub enum Action { in_place: bool, start_suppressed: bool, coordinates: Option, + near_current_pane: bool, }, /// Open a new floating pane NewFloatingPane { command: Option, pane_name: Option, coordinates: Option, + near_current_pane: bool, }, /// Open a new tiled (embedded, non-floating) pane NewTiledPane { direction: Option, command: Option, pane_name: Option, + near_current_pane: bool, }, /// Open a new pane in place of the focused one, suppressing it instead NewInPlacePane { command: Option, pane_name: Option, + near_current_pane: bool, + pane_id_to_replace: Option, + close_replace_pane: bool, }, NewStackedPane { command: Option, pane_name: Option, + near_current_pane: bool, }, /// Embed focused pane in tab if floating or float focused pane if embedded TogglePaneEmbedOrFloating, @@ -244,6 +267,8 @@ pub enum Action { tab_name: Option, should_change_focus_to_new_tab: bool, cwd: Option, + initial_panes: Option>, + first_pane_unblock_condition: Option, }, /// Do nothing. NoOp, @@ -271,6 +296,7 @@ pub enum Action { /// Run specified command in new pane. Run { command: RunCommandAction, + near_current_pane: bool, }, /// Detach session and exit Detach, @@ -356,10 +382,12 @@ pub enum Action { FocusTerminalPaneWithId { pane_id: u32, should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, }, FocusPluginPaneWithId { pane_id: u32, should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, }, RenameTerminalPane { pane_id: u32, @@ -420,6 +448,24 @@ pub enum Action { ToggleGroupMarking, } +impl Default for Action { + fn default() -> Self { + Action::NoOp + } +} + +impl Default for SearchDirection { + fn default() -> Self { + SearchDirection::Down + } +} + +impl Default for SearchOption { + fn default() -> Self { + SearchOption::CaseSensitivity + } +} + impl Action { /// Checks that two Action are match except their mutable attributes. pub fn shallow_eq(&self, other_action: &Action) -> bool { @@ -492,6 +538,8 @@ impl Action { pinned, stacked, blocking, + unblock_condition, + near_current_pane, } => { let current_dir = get_current_dir(); // cwd should only be specified in a plugin alias if it was explicitly given to us, @@ -500,7 +548,7 @@ impl Action { let cwd = cwd .map(|cwd| current_dir.join(cwd)) .or_else(|| Some(current_dir.clone())); - if blocking { + if blocking || unblock_condition.is_some() { // For blocking panes, we don't support plugins if plugin.is_some() { return Err("Blocking panes do not support plugin variants".to_string()); @@ -543,6 +591,8 @@ impl Action { placement, pane_name: name, command, + unblock_condition, + near_current_pane, }]) } else if let Some(plugin) = plugin { let plugin = match RunPluginLocation::parse(&plugin, cwd.clone()) { @@ -614,22 +664,28 @@ impl Action { command: Some(run_command_action), pane_name: name, coordinates: FloatingPaneCoordinates::new(x, y, width, height, pinned), + near_current_pane, }]) } else if in_place { Ok(vec![Action::NewInPlacePane { command: Some(run_command_action), pane_name: name, + near_current_pane, + pane_id_to_replace: None, // TODO: support this + close_replace_pane: false, }]) } else if stacked { Ok(vec![Action::NewStackedPane { command: Some(run_command_action), pane_name: name, + near_current_pane, }]) } else { Ok(vec![Action::NewTiledPane { direction, command: Some(run_command_action), pane_name: name, + near_current_pane, }]) } } else { @@ -638,22 +694,28 @@ impl Action { command: None, pane_name: name, coordinates: FloatingPaneCoordinates::new(x, y, width, height, pinned), + near_current_pane, }]) } else if in_place { Ok(vec![Action::NewInPlacePane { command: None, pane_name: name, + near_current_pane, + pane_id_to_replace: None, // TODO: support this + close_replace_pane: false, }]) } else if stacked { Ok(vec![Action::NewStackedPane { command: None, pane_name: name, + near_current_pane, }]) } else { Ok(vec![Action::NewTiledPane { direction, command: None, pane_name: name, + near_current_pane, }]) } } @@ -670,6 +732,7 @@ impl Action { width, height, pinned, + near_current_pane, } => { let mut file = file; let current_dir = get_current_dir(); @@ -689,6 +752,7 @@ impl Action { in_place, start_suppressed, coordinates: FloatingPaneCoordinates::new(x, y, width, height, pinned), + near_current_pane, }]) }, CliAction::SwitchMode { input_mode } => Ok(vec![Action::SwitchToMode { input_mode }]), @@ -721,11 +785,68 @@ impl Action { layout, layout_dir, cwd, + initial_command, + initial_plugin, + close_on_exit, + start_suspended, + block_until_exit_success, + block_until_exit_failure, + block_until_exit, } => { let current_dir = get_current_dir(); let cwd = cwd .map(|cwd| current_dir.join(cwd)) - .or_else(|| Some(current_dir)); + .or_else(|| Some(current_dir.clone())); + + // Map CLI flags to UnblockCondition + let first_pane_unblock_condition = if block_until_exit_success { + Some(UnblockCondition::OnExitSuccess) + } else if block_until_exit_failure { + Some(UnblockCondition::OnExitFailure) + } else if block_until_exit { + Some(UnblockCondition::OnAnyExit) + } else { + None + }; + + // Parse initial_panes from initial_command or initial_plugin + let initial_panes = if let Some(plugin_url) = initial_plugin { + let plugin = match RunPluginLocation::parse(&plugin_url, cwd.clone()) { + Ok(location) => RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location, + configuration: Default::default(), + initial_cwd: cwd.clone(), + }), + Err(_) => { + let mut plugin_alias = + PluginAlias::new(&plugin_url, &None, cwd.clone()); + plugin_alias.set_caller_cwd_if_not_set(Some(current_dir.clone())); + RunPluginOrAlias::Alias(plugin_alias) + }, + }; + Some(vec![CommandOrPlugin::Plugin(plugin)]) + } else if !initial_command.is_empty() { + let mut command: Vec = initial_command.clone(); + let (command, args) = ( + PathBuf::from(command.remove(0)), + command.into_iter().collect(), + ); + let hold_on_close = !close_on_exit; + let hold_on_start = start_suspended; + let run_command_action = RunCommandAction { + command, + args, + cwd: cwd.clone(), + direction: None, + hold_on_close, + hold_on_start, + ..Default::default() + }; + Some(vec![CommandOrPlugin::Command(run_command_action)]) + } else { + None + }; if let Some(layout_path) = layout { let layout_dir = layout_dir .or_else(|| config.and_then(|c| c.options.layout_dir)) @@ -813,6 +934,8 @@ impl Action { tab_name: name, should_change_focus_to_new_tab, cwd: None, // the cwd is done through the layout + initial_panes: initial_panes.clone(), + first_pane_unblock_condition, }); } Ok(new_tab_actions) @@ -829,6 +952,8 @@ impl Action { tab_name: name, should_change_focus_to_new_tab, cwd: None, // the cwd is done through the layout + initial_panes, + first_pane_unblock_condition, }]) } } else { @@ -841,6 +966,8 @@ impl Action { tab_name: name, should_change_focus_to_new_tab, cwd, + initial_panes, + first_pane_unblock_condition, }]) } }, @@ -1047,6 +1174,38 @@ impl Action { }, } } + pub fn populate_originating_plugin(&mut self, originating_plugin: OriginatingPlugin) { + match self { + Action::NewBlockingPane { command, .. } + | Action::NewFloatingPane { command, .. } + | Action::NewTiledPane { command, .. } + | Action::NewInPlacePane { command, .. } + | Action::NewStackedPane { command, .. } => { + command + .as_mut() + .map(|c| c.populate_originating_plugin(originating_plugin)); + }, + Action::Run { command, .. } => { + command.populate_originating_plugin(originating_plugin); + }, + Action::EditFile { payload, .. } => { + payload.originating_plugin = Some(originating_plugin); + }, + Action::NewTab { initial_panes, .. } => { + if let Some(initial_panes) = initial_panes.as_mut() { + for pane in initial_panes.iter_mut() { + match pane { + CommandOrPlugin::Command(run_command) => { + run_command.populate_originating_plugin(originating_plugin.clone()); + }, + _ => {}, + } + } + } + }, + _ => {}, + } + } pub fn launches_plugin(&self, plugin_url: &str) -> bool { match self { Action::LaunchPlugin { plugin, .. } => &plugin.location_string() == plugin_url, diff --git a/zellij-utils/src/input/command.rs b/zellij-utils/src/input/command.rs index 0169f799c..11a34f59f 100644 --- a/zellij-utils/src/input/command.rs +++ b/zellij-utils/src/input/command.rs @@ -30,6 +30,17 @@ pub struct OpenFilePayload { pub originating_plugin: Option, } +impl Default for OpenFilePayload { + fn default() -> Self { + OpenFilePayload { + path: PathBuf::new(), + line_number: None, + cwd: None, + originating_plugin: None, + } + } +} + impl OpenFilePayload { pub fn new(path: PathBuf, line_number: Option, cwd: Option) -> Self { OpenFilePayload { @@ -129,6 +140,23 @@ impl From for RunCommandAction { } } +impl RunCommandAction { + pub fn new(mut command: Vec) -> Self { + if command.is_empty() { + Default::default() + } else { + RunCommandAction { + command: PathBuf::from(command.remove(0)), + args: command, + ..Default::default() + } + } + } + pub fn populate_originating_plugin(&mut self, originating_plugin: OriginatingPlugin) { + self.originating_plugin = Some(originating_plugin); + } +} + impl RunCommand { pub fn new(command: PathBuf) -> Self { RunCommand { diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index de555a00b..d925f7c5c 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -995,6 +995,44 @@ impl TiledPaneLayout { } run_instructions } + pub fn replace_next_empty_slot_with_run(&mut self, run_to_insert: Run) -> bool { + // Replaces the first empty slot (None or Run::Cwd) with the given Run instruction. + // Returns true if a replacement was made, false if no empty slot was found. + // Traversal order matches extract_run_instructions (breadth-first). + + if self.children.is_empty() { + // This is a leaf node - check if it's an empty slot + match &self.run { + None | Some(Run::Cwd(_)) => { + self.run = Some(run_to_insert); + return true; + }, + _ => return false, + } + } + + // Check first child of each child (breadth-first first level) + for child in self.children.iter_mut() { + if child.children.is_empty() { + match &child.run { + None | Some(Run::Cwd(_)) => { + child.run = Some(run_to_insert); + return true; + }, + _ => {}, + } + } + } + + // Recursively check deeper levels (breadth-first continuation) + for child in self.children.iter_mut() { + if child.replace_next_empty_slot_with_run(run_to_insert.clone()) { + return true; + } + } + + false + } pub fn ignore_run_instruction(&mut self, run_instruction: Option) { self.run_instructions_to_ignore.push(run_instruction); } diff --git a/zellij-utils/src/input/mouse.rs b/zellij-utils/src/input/mouse.rs index d920495d3..0977d5e73 100644 --- a/zellij-utils/src/input/mouse.rs +++ b/zellij-utils/src/input/mouse.rs @@ -239,3 +239,9 @@ impl MouseEvent { event } } + +impl Default for MouseEvent { + fn default() -> Self { + MouseEvent::new() + } +} diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs index c47552050..892c7a660 100644 --- a/zellij-utils/src/input/plugins.rs +++ b/zellij-utils/src/input/plugins.rs @@ -67,6 +67,7 @@ impl PluginConfig { || tag == "about" || tag == "share" || tag == "multiple-select" + || tag == "sequence" { Some(PluginConfig { path: PathBuf::from(&tag), diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index 53dd4d9fb..96e2c747e 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -251,18 +251,14 @@ impl IpcSenderWithContext { pub fn send_client_msg(&mut self, msg: ClientToServerMsg) -> Result<()> { let proto_msg: ProtoClientToServerMsg = msg.into(); write_protobuf_message(&mut self.sender, &proto_msg)?; - if let Err(e) = self.sender.flush() { - log::error!("Failed to flush ipc sender: {}", e); - } + let _ = self.sender.flush(); Ok(()) } pub fn send_server_msg(&mut self, msg: ServerToClientMsg) -> Result<()> { let proto_msg: ProtoServerToClientMsg = msg.into(); write_protobuf_message(&mut self.sender, &proto_msg)?; - if let Err(e) = self.sender.flush() { - log::error!("Failed to flush ipc sender: {}", e); - } + let _ = self.sender.flush(); Ok(()) } @@ -370,9 +366,7 @@ pub fn send_protobuf_client_to_server( ) -> Result<()> { let proto_msg: ProtoClientToServerMsg = msg.into(); write_protobuf_message(&mut sender.sender, &proto_msg)?; - if let Err(e) = sender.sender.flush() { - log::error!("Failed to flush ipc sender: {}", e); - } + let _ = sender.sender.flush(); Ok(()) } @@ -382,9 +376,7 @@ pub fn send_protobuf_server_to_client( ) -> Result<()> { let proto_msg: ProtoServerToClientMsg = msg.into(); write_protobuf_message(&mut sender.sender, &proto_msg)?; - if let Err(e) = sender.sender.flush() { - log::error!("Failed to flush ipc sender: {}", e); - } + let _ = sender.sender.flush(); Ok(()) } diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs index ab06faae6..f7ab632f2 100644 --- a/zellij-utils/src/ipc/protobuf_conversion.rs +++ b/zellij-utils/src/ipc/protobuf_conversion.rs @@ -858,6 +858,7 @@ impl From direction: direction.map(|d| direction_to_proto_i32(d)), pane_name, start_suppressed, + near_current_pane: false, }), crate::input::actions::Action::EditFile { payload, @@ -866,6 +867,7 @@ impl From in_place, start_suppressed, coordinates, + near_current_pane, } => ActionType::EditFile(EditFileAction { payload: Some(payload.into()), direction: direction.map(|d| direction_to_proto_i32(d)), @@ -873,45 +875,64 @@ impl From in_place, start_suppressed, coordinates: coordinates.map(|c| c.into()), + near_current_pane, }), crate::input::actions::Action::NewFloatingPane { command, pane_name, coordinates, + near_current_pane, } => ActionType::NewFloatingPane(NewFloatingPaneAction { command: command.map(|c| c.into()), pane_name, coordinates: coordinates.map(|c| c.into()), + near_current_pane, }), crate::input::actions::Action::NewTiledPane { direction, command, pane_name, + near_current_pane, } => ActionType::NewTiledPane(NewTiledPaneAction { direction: direction.map(|d| direction_to_proto_i32(d)), command: command.map(|c| c.into()), pane_name, + near_current_pane, + }), + crate::input::actions::Action::NewInPlacePane { + command, + pane_name, + near_current_pane, + pane_id_to_replace, + close_replace_pane, + } => ActionType::NewInPlacePane(NewInPlacePaneAction { + command: command.map(|c| c.into()), + pane_name, + near_current_pane, + pane_id_to_replace: pane_id_to_replace.and_then(|p| p.try_into().ok()), + close_replace_pane, + }), + crate::input::actions::Action::NewStackedPane { + command, + pane_name, + near_current_pane, + } => ActionType::NewStackedPane(NewStackedPaneAction { + command: command.map(|c| c.into()), + pane_name, + near_current_pane, }), - crate::input::actions::Action::NewInPlacePane { command, pane_name } => { - ActionType::NewInPlacePane(NewInPlacePaneAction { - command: command.map(|c| c.into()), - pane_name, - }) - }, - crate::input::actions::Action::NewStackedPane { command, pane_name } => { - ActionType::NewStackedPane(NewStackedPaneAction { - command: command.map(|c| c.into()), - pane_name, - }) - }, crate::input::actions::Action::NewBlockingPane { placement, pane_name, command, + unblock_condition, + near_current_pane, } => ActionType::NewBlockingPane(NewBlockingPaneAction { placement: Some(placement.into()), pane_name, command: command.map(|c| c.into()), + unblock_condition: unblock_condition.map(|c| unblock_condition_to_proto_i32(c)), + near_current_pane, }), crate::input::actions::Action::TogglePaneEmbedOrFloating => { ActionType::TogglePaneEmbedOrFloating(TogglePaneEmbedOrFloatingAction {}) @@ -938,6 +959,8 @@ impl From tab_name, should_change_focus_to_new_tab, cwd, + initial_panes, + first_pane_unblock_condition, } => ActionType::NewTab(NewTabAction { tiled_layout: tiled_layout.map(|l| l.into()), floating_layouts: floating_layouts.into_iter().map(|l| l.into()).collect(), @@ -950,6 +973,11 @@ impl From tab_name, should_change_focus_to_new_tab, cwd: cwd.map(|p| p.to_string_lossy().to_string()), + initial_panes: initial_panes + .map(|panes| panes.into_iter().map(|p| p.into()).collect()) + .unwrap_or_default(), + first_pane_unblock_condition: first_pane_unblock_condition + .map(|c| unblock_condition_to_proto_i32(c)), }), crate::input::actions::Action::NoOp => ActionType::NoOp(NoOpAction {}), crate::input::actions::Action::GoToNextTab => { @@ -979,8 +1007,12 @@ impl From direction: direction_to_proto_i32(direction), }) }, - crate::input::actions::Action::Run { command } => ActionType::Run(RunAction { + crate::input::actions::Action::Run { + command, + near_current_pane, + } => ActionType::Run(RunAction { command: Some(command.into()), + near_current_pane, }), crate::input::actions::Action::Detach => ActionType::Detach(DetachAction {}), crate::input::actions::Action::SwitchSession { @@ -1112,16 +1144,20 @@ impl From crate::input::actions::Action::FocusTerminalPaneWithId { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => ActionType::FocusTerminalPaneWithId(FocusTerminalPaneWithIdAction { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }), crate::input::actions::Action::FocusPluginPaneWithId { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => ActionType::FocusPluginPaneWithId(FocusPluginPaneWithIdAction { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }), crate::input::actions::Action::RenameTerminalPane { pane_id, name } => { ActionType::RenameTerminalPane(RenameTerminalPaneAction { @@ -1380,6 +1416,7 @@ impl TryFrom .coordinates .map(|c| c.try_into()) .transpose()?, + near_current_pane: edit_file_action.near_current_pane, }), ActionType::NewFloatingPane(new_floating_action) => { Ok(crate::input::actions::Action::NewFloatingPane { @@ -1392,6 +1429,7 @@ impl TryFrom .coordinates .map(|c| c.try_into()) .transpose()?, + near_current_pane: new_floating_action.near_current_pane, }) }, ActionType::NewTiledPane(new_tiled_action) => { @@ -1402,6 +1440,7 @@ impl TryFrom .transpose()?, command: new_tiled_action.command.map(|c| c.try_into()).transpose()?, pane_name: new_tiled_action.pane_name, + near_current_pane: new_tiled_action.near_current_pane, }) }, ActionType::NewInPlacePane(new_in_place_action) => { @@ -1411,6 +1450,11 @@ impl TryFrom .map(|c| c.try_into()) .transpose()?, pane_name: new_in_place_action.pane_name, + near_current_pane: new_in_place_action.near_current_pane, + pane_id_to_replace: new_in_place_action + .pane_id_to_replace + .and_then(|p| p.try_into().ok()), + close_replace_pane: new_in_place_action.close_replace_pane, }) }, ActionType::NewStackedPane(new_stacked_action) => { @@ -1420,6 +1464,7 @@ impl TryFrom .map(|c| c.try_into()) .transpose()?, pane_name: new_stacked_action.pane_name, + near_current_pane: new_stacked_action.near_current_pane, }) }, ActionType::NewBlockingPane(new_blocking_action) => { @@ -1433,6 +1478,11 @@ impl TryFrom .command .map(|c| c.try_into()) .transpose()?, + unblock_condition: new_blocking_action + .unblock_condition + .map(|c| proto_i32_to_unblock_condition(c)) + .transpose()?, + near_current_pane: new_blocking_action.near_current_pane, }) }, ActionType::TogglePaneEmbedOrFloating(_) => { @@ -1487,6 +1537,22 @@ impl TryFrom tab_name: new_tab_action.tab_name, should_change_focus_to_new_tab: new_tab_action.should_change_focus_to_new_tab, cwd: new_tab_action.cwd.map(PathBuf::from), + initial_panes: if new_tab_action.initial_panes.is_empty() { + None + } else { + Some( + new_tab_action + .initial_panes + .into_iter() + .map(|p| p.try_into()) + .collect::>>()?, + ) + }, + + first_pane_unblock_condition: new_tab_action + .first_pane_unblock_condition + .map(|c| proto_i32_to_unblock_condition(c)) + .transpose()?, }), ActionType::NoOp(_) => Ok(crate::input::actions::Action::NoOp), ActionType::GoToNextTab(_) => Ok(crate::input::actions::Action::GoToNextTab), @@ -1516,6 +1582,7 @@ impl TryFrom .command .ok_or_else(|| anyhow!("Run missing command"))? .try_into()?, + near_current_pane: run_action.near_current_pane, }), ActionType::Detach(_) => Ok(crate::input::actions::Action::Detach), ActionType::SwitchSession(switch_session_action) => { @@ -1661,12 +1728,14 @@ impl TryFrom Ok(crate::input::actions::Action::FocusTerminalPaneWithId { pane_id: focus_pane_action.pane_id, should_float_if_hidden: focus_pane_action.should_float_if_hidden, + should_be_in_place_if_hidden: focus_pane_action.should_be_in_place_if_hidden, }) }, ActionType::FocusPluginPaneWithId(focus_pane_action) => { Ok(crate::input::actions::Action::FocusPluginPaneWithId { pane_id: focus_pane_action.pane_id, should_float_if_hidden: focus_pane_action.should_float_if_hidden, + should_be_in_place_if_hidden: focus_pane_action.should_be_in_place_if_hidden, }) }, ActionType::RenameTerminalPane(rename_pane_action) => { @@ -2037,6 +2106,15 @@ fn search_option_to_proto_i32(option: crate::input::actions::SearchOption) -> i3 } } +fn unblock_condition_to_proto_i32(condition: crate::data::UnblockCondition) -> i32 { + use crate::client_server_contract::client_server_contract::UnblockCondition as ProtoUnblockCondition; + match condition { + crate::data::UnblockCondition::OnExitSuccess => ProtoUnblockCondition::OnExitSuccess as i32, + crate::data::UnblockCondition::OnExitFailure => ProtoUnblockCondition::OnExitFailure as i32, + crate::data::UnblockCondition::OnAnyExit => ProtoUnblockCondition::OnAnyExit as i32, + } +} + // Reverse helper functions for Action conversion fn proto_i32_to_resize(resize: i32) -> Result { @@ -2103,6 +2181,26 @@ fn proto_i32_to_search_option(option: i32) -> Result Result { + use crate::client_server_contract::client_server_contract::UnblockCondition as ProtoUnblockCondition; + let proto_condition = match condition { + x if x == ProtoUnblockCondition::OnExitSuccess as i32 => { + ProtoUnblockCondition::OnExitSuccess + }, + x if x == ProtoUnblockCondition::OnExitFailure as i32 => { + ProtoUnblockCondition::OnExitFailure + }, + x if x == ProtoUnblockCondition::OnAnyExit as i32 => ProtoUnblockCondition::OnAnyExit, + _ => return Err(anyhow!("Invalid UnblockCondition: {}", condition)), + }; + match proto_condition { + ProtoUnblockCondition::OnExitSuccess => Ok(crate::data::UnblockCondition::OnExitSuccess), + ProtoUnblockCondition::OnExitFailure => Ok(crate::data::UnblockCondition::OnExitFailure), + ProtoUnblockCondition::OnAnyExit => Ok(crate::data::UnblockCondition::OnAnyExit), + ProtoUnblockCondition::Unspecified => Err(anyhow!("Unspecified unblock condition")), + } +} + // Position conversion impl From for crate::client_server_contract::client_server_contract::Position @@ -2722,6 +2820,47 @@ impl From } } +// CommandOrPlugin conversion +impl From + for crate::client_server_contract::client_server_contract::CommandOrPlugin +{ + fn from(cmd_or_plugin: crate::data::CommandOrPlugin) -> Self { + use crate::client_server_contract::client_server_contract::command_or_plugin::CommandOrPluginType; + match cmd_or_plugin { + crate::data::CommandOrPlugin::Command(cmd) => Self { + command_or_plugin_type: Some(CommandOrPluginType::Command(cmd.into())), + }, + crate::data::CommandOrPlugin::Plugin(plugin) => Self { + command_or_plugin_type: Some(CommandOrPluginType::Plugin(plugin.into())), + }, + } + } +} + +impl TryFrom + for crate::data::CommandOrPlugin +{ + type Error = anyhow::Error; + + fn try_from( + proto: crate::client_server_contract::client_server_contract::CommandOrPlugin, + ) -> Result { + use crate::client_server_contract::client_server_contract::command_or_plugin::CommandOrPluginType; + + let cmd_or_plugin_type = proto + .command_or_plugin_type + .ok_or_else(|| anyhow!("CommandOrPlugin missing command_or_plugin_type"))?; + match cmd_or_plugin_type { + CommandOrPluginType::Command(cmd) => { + Ok(crate::data::CommandOrPlugin::Command(cmd.try_into()?)) + }, + CommandOrPluginType::Plugin(plugin) => { + Ok(crate::data::CommandOrPlugin::Plugin(plugin.try_into()?)) + }, + } + } +} + // Run reverse conversion impl TryFrom for crate::input::layout::Run diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs index 2ec513da6..61981f38e 100644 --- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs +++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs @@ -1,7 +1,8 @@ use super::test_framework::*; use crate::data::{ - BareKey, ConnectToSession, Direction, FloatingPaneCoordinates, InputMode, KeyModifier, - KeyWithModifier, LayoutInfo, OriginatingPlugin, PaneId, PluginTag, Resize, WebSharing, + BareKey, CommandOrPlugin, ConnectToSession, Direction, FloatingPaneCoordinates, InputMode, + KeyModifier, KeyWithModifier, LayoutInfo, OriginatingPlugin, PaneId, PluginTag, Resize, + WebSharing, }; use crate::input::actions::{Action, SearchDirection, SearchOption}; use crate::input::cli_assets::CliAssets; @@ -1019,6 +1020,7 @@ fn test_client_messages() { in_place: false, start_suppressed: false, coordinates: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1041,6 +1043,7 @@ fn test_client_messages() { in_place: false, start_suppressed: false, coordinates: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1063,6 +1066,7 @@ fn test_client_messages() { in_place: false, start_suppressed: false, coordinates: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1085,6 +1089,7 @@ fn test_client_messages() { in_place: true, start_suppressed: true, coordinates: FloatingPaneCoordinates::new(None, None, None, None, None), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1113,6 +1118,7 @@ fn test_client_messages() { None, None ), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1141,6 +1147,7 @@ fn test_client_messages() { None, None ), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1169,6 +1176,7 @@ fn test_client_messages() { Some("20".to_owned()), Some(true) ), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1191,6 +1199,7 @@ fn test_client_messages() { in_place: true, start_suppressed: true, coordinates: FloatingPaneCoordinates::new(None, None, None, None, Some(false)), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1201,6 +1210,7 @@ fn test_client_messages() { command: None, pane_name: None, coordinates: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1225,7 +1235,8 @@ fn test_client_messages() { None, None, None - ) + ), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1254,7 +1265,8 @@ fn test_client_messages() { None, None, None - ) + ), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1265,6 +1277,7 @@ fn test_client_messages() { command: None, direction: None, pane_name: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1288,6 +1301,7 @@ fn test_client_messages() { }), direction: Some(Direction::Right), pane_name: Some("my_pane_name".to_owned()), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1310,6 +1324,9 @@ fn test_client_messages() { use_terminal_title: false, }), pane_name: Some("my_pane_name".to_owned()), + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1319,6 +1336,9 @@ fn test_client_messages() { action: Action::NewInPlacePane { command: None, pane_name: None, + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1341,6 +1361,7 @@ fn test_client_messages() { use_terminal_title: false, }), pane_name: Some("my_pane_name".to_owned()), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1350,6 +1371,7 @@ fn test_client_messages() { action: Action::NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1396,6 +1418,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1410,6 +1434,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1427,6 +1453,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1444,6 +1472,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1462,6 +1492,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1492,6 +1524,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1509,6 +1543,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1526,6 +1562,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1545,6 +1583,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1565,6 +1605,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1585,6 +1627,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1605,6 +1649,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1625,6 +1671,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1645,6 +1693,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1665,6 +1715,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1685,6 +1737,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1705,6 +1759,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1722,6 +1778,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1745,6 +1803,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1770,6 +1830,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1799,6 +1861,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1816,6 +1880,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1837,6 +1903,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1854,6 +1922,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1891,6 +1961,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1913,6 +1985,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1927,6 +2001,8 @@ fn test_client_messages() { tab_name: Some("tab_name".to_owned()), should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1941,6 +2017,36 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: true, cwd: Some(PathBuf::from("relative/path/to/cwd")), + initial_panes: None, + first_pane_unblock_condition: None, + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::NewTab { + tiled_layout: None, + floating_layouts: vec![], + swap_tiled_layouts: None, + swap_floating_layouts: None, + tab_name: None, + should_change_focus_to_new_tab: true, + cwd: None, + initial_panes: Some(vec![ + CommandOrPlugin::Command(RunCommandAction { + command: PathBuf::from("/path/to/command"), + args: vec![], + cwd: None, + direction: None, + hold_on_close: false, + hold_on_start: false, + originating_plugin: None, + use_terminal_title: false, + }), + CommandOrPlugin::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin::default())), + ]), + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -2035,6 +2141,7 @@ fn test_client_messages() { originating_plugin: None, use_terminal_title: false, }, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -2328,6 +2435,7 @@ fn test_client_messages() { action: Action::FocusTerminalPaneWithId { pane_id: 12, should_float_if_hidden: false, + should_be_in_place_if_hidden: false, }, terminal_id: Some(1), client_id: Some(100), @@ -2337,6 +2445,7 @@ fn test_client_messages() { action: Action::FocusTerminalPaneWithId { pane_id: 12, should_float_if_hidden: true, + should_be_in_place_if_hidden: false, }, terminal_id: Some(1), client_id: Some(100), @@ -2346,6 +2455,7 @@ fn test_client_messages() { action: Action::FocusPluginPaneWithId { pane_id: 12, should_float_if_hidden: false, + should_be_in_place_if_hidden: true, }, terminal_id: Some(1), client_id: Some(100), @@ -2355,6 +2465,7 @@ fn test_client_messages() { action: Action::FocusPluginPaneWithId { pane_id: 12, should_float_if_hidden: true, + should_be_in_place_if_hidden: false, }, terminal_id: Some(1), client_id: Some(100), diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 789c3d96a..5ca02d86a 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -1,8 +1,8 @@ mod kdl_layout_parser; use crate::data::{ BareKey, Direction, FloatingPaneCoordinates, InputMode, KeyWithModifier, LayoutInfo, - MultiplayerColors, Palette, PaletteColor, PaneInfo, PaneManifest, PermissionType, Resize, - SessionInfo, StyleDeclaration, Styling, TabInfo, WebSharing, DEFAULT_STYLES, + MultiplayerColors, Palette, PaletteColor, PaneId, PaneInfo, PaneManifest, PermissionType, + Resize, SessionInfo, StyleDeclaration, Styling, TabInfo, WebSharing, DEFAULT_STYLES, }; use crate::envs::EnvironmentVariables; use crate::home::{find_default_config_dir, get_layout_dir}; @@ -551,6 +551,7 @@ impl Action { return Ok(Action::NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }); } else { let direction = Direction::from_str(string.as_str()).map_err(|_| { @@ -742,6 +743,8 @@ impl Action { tab_name: name, should_change_focus_to_new_tab, cwd, + initial_panes: _, + first_pane_unblock_condition: _, } => { let mut node = KdlNode::new("NewTab"); let mut children = KdlDocument::new(); @@ -800,6 +803,7 @@ impl Action { direction, command: run_command_action, pane_name: name, + near_current_pane: false, } => { let mut node = KdlNode::new("Run"); let mut node_children = KdlDocument::new(); @@ -849,6 +853,7 @@ impl Action { command: run_command_action, pane_name: name, coordinates: floating_pane_coordinates, + near_current_pane: false, } => { let mut node = KdlNode::new("Run"); let mut node_children = KdlDocument::new(); @@ -939,6 +944,9 @@ impl Action { Action::NewInPlacePane { command: run_command_action, pane_name: name, + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, } => { let mut node = KdlNode::new("Run"); let mut node_children = KdlDocument::new(); @@ -979,6 +987,7 @@ impl Action { Action::NewStackedPane { command: run_command_action, pane_name: name, + near_current_pane: _, } => match run_command_action { Some(run_command_action) => { let mut node = KdlNode::new("Run"); @@ -1623,6 +1632,8 @@ impl TryFrom<(&KdlNode, &Options)> for Action { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }); } @@ -1692,6 +1703,8 @@ impl TryFrom<(&KdlNode, &Options)> for Action { tab_name: name, should_change_focus_to_new_tab, cwd, + initial_panes: None, + first_pane_unblock_condition: None, }) } else { let (layout, floating_panes_layout) = layout.new_tab(); @@ -1705,6 +1718,8 @@ impl TryFrom<(&KdlNode, &Options)> for Action { tab_name: name, should_change_focus_to_new_tab, cwd, + initial_panes: None, + first_pane_unblock_condition: None, }) } }, @@ -1785,22 +1800,28 @@ impl TryFrom<(&KdlNode, &Options)> for Action { command: Some(run_command_action), pane_name: name, coordinates: FloatingPaneCoordinates::new(x, y, width, height, pinned), + near_current_pane: false, }) } else if in_place { Ok(Action::NewInPlacePane { command: Some(run_command_action), pane_name: name, + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, }) } else if stacked { Ok(Action::NewStackedPane { command: Some(run_command_action), pane_name: name, + near_current_pane: false, }) } else { Ok(Action::NewTiledPane { direction, command: Some(run_command_action), pane_name: name, + near_current_pane: false, }) } }, @@ -5073,6 +5094,50 @@ impl SessionInfo { } } } + let mut pane_history = BTreeMap::new(); + if let Some(kdl_pane_history) = kdl_document.get("pane_history").and_then(|p| p.children()) + { + for client_node in kdl_pane_history.nodes() { + if let Some(client_id) = client_node.children().and_then(|c| { + c.get("id") + .and_then(|c| c.entries().iter().next().and_then(|e| e.value().as_i64())) + }) { + let mut history = vec![]; + if let Some(history_node) = + client_node.children().and_then(|c| c.get("history")) + { + if let Some(history_children) = history_node.children() { + for pane_id_node in history_children.nodes() { + if pane_id_node.name().value() == "pane_id" { + let pane_type = pane_id_node + .entries() + .iter() + .find(|e| e.name().map(|n| n.value()) == Some("type")) + .and_then(|e| e.value().as_string()); + let id = pane_id_node + .entries() + .iter() + .find(|e| e.name().is_none()) + .and_then(|e| e.value().as_i64()) + .map(|i| i as u32); + if let (Some(pane_type), Some(id)) = (pane_type, id) { + let pane_id = match pane_type { + "terminal" => Some(PaneId::Terminal(id)), + "plugin" => Some(PaneId::Plugin(id)), + _ => None, + }; + if let Some(pane_id) = pane_id { + history.push(pane_id); + } + } + } + } + } + } + pane_history.insert(client_id as u16, history); + } + } + } Ok(SessionInfo { name, tabs, @@ -5084,6 +5149,7 @@ impl SessionInfo { web_clients_allowed, plugins: Default::default(), // we do not serialize plugin information tab_history, + pane_history, }) } pub fn to_string(&self) -> String { @@ -5148,6 +5214,35 @@ impl SessionInfo { } tab_history.set_children(tab_history_children); + let mut pane_history = KdlNode::new("pane_history"); + let mut pane_history_children = KdlDocument::new(); + for (client_id, client_pane_history) in &self.pane_history { + let mut client_document = KdlDocument::new(); + let mut client_node = KdlNode::new("client"); + let mut id = KdlNode::new("id"); + id.push(*client_id as i64); + client_document.nodes_mut().push(id); + let mut history = KdlNode::new("history"); + for pane_id in client_pane_history { + let mut pane_id_node = KdlNode::new("pane_id"); + match pane_id { + PaneId::Terminal(id) => { + pane_id_node.push(KdlEntry::new_prop("type", "terminal")); + pane_id_node.push(*id as i64); + }, + PaneId::Plugin(id) => { + pane_id_node.push(KdlEntry::new_prop("type", "plugin")); + pane_id_node.push(*id as i64); + }, + } + history.ensure_children().nodes_mut().push(pane_id_node); + } + client_document.nodes_mut().push(history); + client_node.set_children(client_document); + pane_history_children.nodes_mut().push(client_node); + } + pane_history.set_children(pane_history_children); + kdl_document.nodes_mut().push(name); kdl_document.nodes_mut().push(tabs); kdl_document.nodes_mut().push(panes); @@ -5156,6 +5251,7 @@ impl SessionInfo { kdl_document.nodes_mut().push(web_client_count); kdl_document.nodes_mut().push(available_layouts); kdl_document.nodes_mut().push(tab_history); + kdl_document.nodes_mut().push(pane_history); kdl_document.fmt(); kdl_document.to_string() } @@ -5714,6 +5810,7 @@ fn serialize_and_deserialize_session_info_with_data() { web_client_count: 2, web_clients_allowed: true, tab_history: Default::default(), + pane_history: Default::default(), }; let serialized = session_info.to_string(); let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap(); 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 index 11f6e53d2..6353ee0ec 100644 --- 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 @@ -122,6 +122,12 @@ keybinds clear-defaults=true { } SwitchToMode "normal" } + bind "q" { + LaunchPlugin "zellij:sequence" { + floating true + } + SwitchToMode "normal" + } bind "s" { LaunchOrFocusPlugin "zellij:share" { floating true @@ -255,6 +261,7 @@ plugins { cwd "/" } plugin-manager location="zellij:plugin-manager" + sequence location="zellij:sequence" session-manager location="zellij:session-manager" status-bar location="zellij:status-bar" strider location="zellij:strider" 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 index ddf36a403..e562e030c 100644 --- 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 @@ -122,6 +122,12 @@ keybinds clear-defaults=true { } SwitchToMode "normal" } + bind "q" { + LaunchPlugin "zellij:sequence" { + floating true + } + SwitchToMode "normal" + } bind "s" { LaunchOrFocusPlugin "zellij:share" { floating true @@ -258,6 +264,7 @@ plugins { cwd "/" } plugin-manager location="zellij:plugin-manager" + sequence location="zellij:sequence" session-manager location="zellij:session-manager" status-bar location="zellij:status-bar" strider location="zellij:strider" diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap index 088504af3..e43fcf3b7 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap @@ -14,3 +14,5 @@ available_layouts { } tab_history { } +pane_history { +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap index cd4ff687b..44b2e8bf7 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap @@ -98,3 +98,5 @@ available_layouts { } tab_history { } +pane_history { +} diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index 2772e7252..c29884afc 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -5,6 +5,163 @@ import "resize.proto"; package api.action; +// Layout and related types + +enum SplitDirection { + SPLIT_DIRECTION_UNSPECIFIED = 0; + SPLIT_DIRECTION_HORIZONTAL = 1; + SPLIT_DIRECTION_VERTICAL = 2; +} + +message PercentOrFixed { + oneof size_type { + uint32 percent = 1; + uint32 fixed = 2; + } +} + +enum LayoutConstraint { + LAYOUT_CONSTRAINT_UNSPECIFIED = 0; + LAYOUT_CONSTRAINT_MAX_PANES = 1; + LAYOUT_CONSTRAINT_MIN_PANES = 2; + LAYOUT_CONSTRAINT_EXACT_PANES = 3; + LAYOUT_CONSTRAINT_NO_CONSTRAINT = 4; +} + +message LayoutConstraintWithValue { + LayoutConstraint constraint_type = 1; + optional uint32 value = 2; +} + +message PluginUserConfiguration { + map configuration = 1; +} + +enum RunPluginLocation { + RUN_PLUGIN_LOCATION_UNSPECIFIED = 0; + RUN_PLUGIN_LOCATION_FILE = 1; + RUN_PLUGIN_LOCATION_ZELLIJ = 2; + RUN_PLUGIN_LOCATION_REMOTE = 3; +} + +message RunPluginLocationData { + RunPluginLocation location_type = 1; + oneof location_data { + string file_path = 2; + PluginTag zellij_tag = 3; + string remote_url = 4; + } +} + +message PluginTag { + string tag = 1; +} + +message RunPlugin { + bool _allow_exec_host_cmd = 1; + RunPluginLocationData location = 2; + PluginUserConfiguration configuration = 3; + optional string initial_cwd = 4; +} + +message PluginAlias { + string name = 1; + optional PluginUserConfiguration configuration = 2; + optional string initial_cwd = 3; + optional RunPlugin run_plugin = 4; +} + +message RunPluginOrAlias { + oneof plugin_type { + RunPlugin plugin = 1; + PluginAlias alias = 2; + } +} + +message RunEditFileAction { + string file_path = 1; + optional uint32 line_number = 2; + optional string cwd = 3; +} + +message PaneRun { + oneof run_type { + RunCommandAction command = 1; + RunPluginOrAlias plugin = 2; + RunEditFileAction edit_file = 3; + string cwd = 4; + } +} + +message TiledPaneLayout { + SplitDirection children_split_direction = 1; + optional string name = 2; + repeated TiledPaneLayout children = 3; + optional SplitSize split_size = 4; + optional PaneRun run = 5; + bool borderless = 6; + optional string focus = 7; + optional bool exclude_from_sync = 8; + bool children_are_stacked = 9; + optional uint32 external_children_index = 10; + bool is_expanded_in_stack = 11; + bool hide_floating_panes = 12; + optional string pane_initial_contents = 13; +} + +message FloatingPaneLayout { + optional string name = 1; + optional PercentOrFixed height = 2; + optional PercentOrFixed width = 3; + optional PercentOrFixed x = 4; + optional PercentOrFixed y = 5; + optional bool pinned = 6; + optional PaneRun run = 7; + optional bool focus = 8; + bool already_running = 9; + optional string pane_initial_contents = 10; + optional uint32 logical_position = 11; +} + +message SwapTiledLayout { + repeated LayoutConstraintTiledPair constraint_map = 1; + optional string name = 2; +} + +message SwapFloatingLayout { + repeated LayoutConstraintFloatingPair constraint_map = 1; + optional string name = 2; +} + +message LayoutConstraintTiledPair { + LayoutConstraintWithValue constraint = 1; + TiledPaneLayout layout = 2; +} + +message LayoutConstraintFloatingPair { + LayoutConstraintWithValue constraint = 1; + repeated FloatingPaneLayout layouts = 2; +} + +message CommandOrPlugin { + oneof command_or_plugin_type { + RunCommandAction command = 1; + RunPluginOrAlias plugin = 2; + } +} + +message NewTabPayload { + optional TiledPaneLayout tiled_layout = 1; + repeated FloatingPaneLayout floating_layouts = 2; + repeated SwapTiledLayout swap_tiled_layouts = 3; + repeated SwapFloatingLayout swap_floating_layouts = 4; + optional string tab_name = 5; + bool should_change_focus_to_new_tab = 6; + optional string cwd = 7; + repeated CommandOrPlugin initial_panes = 8; + optional UnblockCondition first_pane_unblock_condition = 9; +} + message Action { ActionName name = 1; oneof optional_payload { @@ -53,6 +210,9 @@ message Action { CliPipePayload message_payload = 47; MoveTabDirection move_tab_payload = 48; MouseEventPayload mouse_event_payload = 49; + NewBlockingPanePayload new_blocking_pane_payload = 50; + NewTabPayload new_tab_payload = 51; + NewInPlacePanePayload new_in_place_pane_payload = 52; } } @@ -71,6 +231,7 @@ message IdAndName { message PaneIdAndShouldFloat { uint32 pane_id = 1; bool should_float = 2; + bool should_be_in_place = 3; } message NewPluginPanePayload { @@ -111,11 +272,13 @@ message GoToTabNamePayload { message NewFloatingPanePayload { optional RunCommandAction command = 1; + bool near_current_pane = 2; } message NewTiledPanePayload { optional RunCommandAction command = 1; optional resize.ResizeDirection direction = 2; + bool near_current_pane = 3; } message MovePanePayload { @@ -128,6 +291,7 @@ message EditFilePayload { optional string cwd = 3; optional resize.ResizeDirection direction = 4; bool should_float = 5; + bool near_current_pane = 6; } message ScrollAtPayload { @@ -139,12 +303,30 @@ message NewPanePayload { optional string pane_name = 2; } +message NewBlockingPanePayload { + NewPanePlacement placement = 1; + optional string pane_name = 2; + optional RunCommandAction command = 3; + optional UnblockCondition unblock_condition = 4; + bool near_current_pane = 5; +} + +message NewInPlacePanePayload { + optional RunCommandAction command = 1; + optional string pane_name = 2; + bool near_current_pane = 3; + optional PaneId pane_id_to_replace = 4; + bool close_replace_pane = 5; +} + message SwitchToModePayload { input_mode.InputMode input_mode = 1; } message WritePayload { bytes bytes_to_write = 1; + optional KeyWithModifier key_with_modifier = 2; + bool is_kitty_keyboard_protocol = 3; } message WriteCharsPayload { @@ -245,6 +427,8 @@ enum ActionName { ToggleGroupMarking = 88; NewStackedPane = 89; SwitchSession = 90; + NewBlockingPane = 91; + NewInPlacePane = 92; } message Position { @@ -252,6 +436,66 @@ message Position { int64 column = 2; } +// SplitSize represents a dimension that can be either a percentage or fixed size +message SplitSize { + oneof split_size_variant { + uint32 percent = 1; // 1 to 100 + uint32 fixed = 2; // absolute number of columns or rows + } +} + +// PaneId identifies either a terminal or plugin pane +message PaneId { + oneof pane_id_variant { + uint32 terminal = 1; + uint32 plugin = 2; + } +} + +// FloatingPaneCoordinates specifies the position and size of a floating pane +message FloatingPaneCoordinates { + optional SplitSize x = 1; + optional SplitSize y = 2; + optional SplitSize width = 3; + optional SplitSize height = 4; + optional bool pinned = 5; +} + +// UnblockCondition specifies when a blocking pane should unblock +enum UnblockCondition { + UNBLOCK_ON_EXIT_SUCCESS = 0; + UNBLOCK_ON_EXIT_FAILURE = 1; + UNBLOCK_ON_ANY_EXIT = 2; +} + +// NewPanePlacement specifies where a new pane should be placed +message NewPanePlacement { + oneof placement_variant { + bool no_preference = 1; + TiledPlacement tiled = 2; + FloatingPlacement floating = 3; + InPlaceConfig in_place = 4; + StackedPlacement stacked = 5; + } +} + +message TiledPlacement { + optional resize.ResizeDirection direction = 1; +} + +message FloatingPlacement { + optional FloatingPaneCoordinates coordinates = 1; +} + +message InPlaceConfig { + optional PaneId pane_id_to_replace = 1; + bool close_replaced_pane = 2; +} + +message StackedPlacement { + optional PaneId pane_id = 1; +} + message MouseEventPayload { uint32 event_type = 1; bool left = 2; @@ -284,3 +528,54 @@ message NameAndValue { string name = 1; string value = 2; } + +message KeyWithModifier { + BareKey bare_key = 1; + repeated KeyModifier key_modifiers = 2; + optional string character = 3; // Only set when bare_key is CHAR +} + +enum BareKey { + BARE_KEY_UNSPECIFIED = 0; + BARE_KEY_PAGE_DOWN = 1; + BARE_KEY_PAGE_UP = 2; + BARE_KEY_LEFT = 3; + BARE_KEY_DOWN = 4; + BARE_KEY_UP = 5; + BARE_KEY_RIGHT = 6; + BARE_KEY_HOME = 7; + BARE_KEY_END = 8; + BARE_KEY_BACKSPACE = 9; + BARE_KEY_DELETE = 10; + BARE_KEY_INSERT = 11; + BARE_KEY_F1 = 12; + BARE_KEY_F2 = 13; + BARE_KEY_F3 = 14; + BARE_KEY_F4 = 15; + BARE_KEY_F5 = 16; + BARE_KEY_F6 = 17; + BARE_KEY_F7 = 18; + BARE_KEY_F8 = 19; + BARE_KEY_F9 = 20; + BARE_KEY_F10 = 21; + BARE_KEY_F11 = 22; + BARE_KEY_F12 = 23; + BARE_KEY_CHAR = 24; + BARE_KEY_TAB = 25; + BARE_KEY_ESC = 26; + BARE_KEY_ENTER = 27; + BARE_KEY_CAPS_LOCK = 28; + BARE_KEY_SCROLL_LOCK = 29; + BARE_KEY_NUM_LOCK = 30; + BARE_KEY_PRINT_SCREEN = 31; + BARE_KEY_PAUSE = 32; + BARE_KEY_MENU = 33; +} + +enum KeyModifier { + KEY_MODIFIER_UNSPECIFIED = 0; + KEY_MODIFIER_CTRL = 1; + KEY_MODIFIER_ALT = 2; + KEY_MODIFIER_SHIFT = 3; + KEY_MODIFIER_SUPER = 4; +} diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index c442144a0..f8de7b768 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -1,26 +1,88 @@ pub use super::generated_api::api::{ action::{ - action::OptionalPayload, Action as ProtobufAction, ActionName as ProtobufActionName, - DumpScreenPayload, EditFilePayload, GoToTabNamePayload, IdAndName, - LaunchOrFocusPluginPayload, MouseEventPayload as ProtobufMouseEventPayload, - MovePanePayload, MoveTabDirection as ProtobufMoveTabDirection, - NameAndValue as ProtobufNameAndValue, NewFloatingPanePayload, NewPanePayload, - NewPluginPanePayload, NewTiledPanePayload, PaneIdAndShouldFloat, - PluginConfiguration as ProtobufPluginConfiguration, Position as ProtobufPosition, - RunCommandAction as ProtobufRunCommandAction, ScrollAtPayload, - SearchDirection as ProtobufSearchDirection, SearchOption as ProtobufSearchOption, - SwitchToModePayload, WriteCharsPayload, WritePayload, + action::OptionalPayload, + command_or_plugin::CommandOrPluginType, + pane_run::RunType, + run_plugin_location_data::LocationData, + run_plugin_or_alias::PluginType, + Action as ProtobufAction, + ActionName as ProtobufActionName, + BareKey as ProtobufBareKey, + // New layout-related types + CommandOrPlugin as ProtobufCommandOrPlugin, + DumpScreenPayload, + EditFilePayload, + FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, + FloatingPaneLayout as ProtobufFloatingPaneLayout, + FloatingPlacement as ProtobufFloatingPlacement, + GoToTabNamePayload, + IdAndName, + InPlaceConfig as ProtobufInPlaceConfig, + KeyModifier as ProtobufKeyModifier, + KeyWithModifier as ProtobufKeyWithModifier, + LaunchOrFocusPluginPayload, + LayoutConstraint as ProtobufLayoutConstraint, + LayoutConstraintFloatingPair as ProtobufLayoutConstraintFloatingPair, + LayoutConstraintTiledPair as ProtobufLayoutConstraintTiledPair, + LayoutConstraintWithValue as ProtobufLayoutConstraintWithValue, + MouseEventPayload as ProtobufMouseEventPayload, + MovePanePayload, + MoveTabDirection as ProtobufMoveTabDirection, + NameAndValue as ProtobufNameAndValue, + NewBlockingPanePayload, + NewFloatingPanePayload, + NewInPlacePanePayload, + NewPanePayload, + NewPanePlacement as ProtobufNewPanePlacement, + NewPluginPanePayload, + NewTabPayload, + NewTiledPanePayload, + PaneId as ProtobufPaneId, + PaneIdAndShouldFloat, + PaneRun as ProtobufPaneRun, + PercentOrFixed as ProtobufPercentOrFixed, + PluginAlias as ProtobufPluginAlias, + PluginConfiguration as ProtobufPluginConfiguration, + PluginTag as ProtobufPluginTag, + PluginUserConfiguration as ProtobufPluginUserConfiguration, + Position as ProtobufPosition, + RunCommandAction as ProtobufRunCommandAction, + RunEditFileAction as ProtobufRunEditFileAction, + RunPlugin as ProtobufRunPlugin, + RunPluginLocation as ProtobufRunPluginLocation, + RunPluginLocationData as ProtobufRunPluginLocationData, + RunPluginOrAlias as ProtobufRunPluginOrAlias, + ScrollAtPayload, + SearchDirection as ProtobufSearchDirection, + SearchOption as ProtobufSearchOption, + SplitDirection as ProtobufSplitDirection, + SplitSize as ProtobufSplitSize, + StackedPlacement as ProtobufStackedPlacement, + SwapFloatingLayout as ProtobufSwapFloatingLayout, + SwapTiledLayout as ProtobufSwapTiledLayout, + SwitchToModePayload, + TiledPaneLayout as ProtobufTiledPaneLayout, + TiledPlacement as ProtobufTiledPlacement, + UnblockCondition as ProtobufUnblockCondition, + WriteCharsPayload, + WritePayload, }, input_mode::InputMode as ProtobufInputMode, resize::{Resize as ProtobufResize, ResizeDirection as ProtobufResizeDirection}, }; -use crate::data::{Direction, InputMode, ResizeStrategy}; +use crate::data::{ + CommandOrPlugin, Direction, FloatingPaneCoordinates, InputMode, KeyWithModifier, + NewPanePlacement, PaneId, PluginTag, ResizeStrategy, UnblockCondition, +}; use crate::errors::prelude::*; use crate::input::actions::Action; use crate::input::actions::{SearchDirection, SearchOption}; use crate::input::command::{OpenFilePayload, RunCommandAction}; +use crate::input::layout::SplitSize; use crate::input::layout::{ - PluginUserConfiguration, RunPlugin, RunPluginLocation, RunPluginOrAlias, + FloatingPaneLayout, LayoutConstraint, PercentOrFixed, PluginAlias, PluginUserConfiguration, + Run, RunPlugin, RunPluginLocation, RunPluginOrAlias, SplitDirection, SwapFloatingLayout, + SwapTiledLayout, TiledPaneLayout, }; use crate::input::mouse::{MouseEvent, MouseEventType}; use crate::position::Position; @@ -38,11 +100,16 @@ impl TryFrom for Action { None => Ok(Action::Quit), }, Some(ProtobufActionName::Write) => match protobuf_action.optional_payload { - Some(OptionalPayload::WritePayload(write_payload)) => Ok(Action::Write { - key_with_modifier: None, - bytes: write_payload.bytes_to_write, - is_kitty_keyboard_protocol: false, - }), + Some(OptionalPayload::WritePayload(write_payload)) => { + let key_with_modifier = write_payload + .key_with_modifier + .and_then(|k| k.try_into().ok()); + Ok(Action::Write { + key_with_modifier, + bytes: write_payload.bytes_to_write, + is_kitty_keyboard_protocol: write_payload.is_kitty_keyboard_protocol, + }) + }, _ => Err("Wrong payload for Action::Write"), }, Some(ProtobufActionName::WriteChars) => match protobuf_action.optional_payload { @@ -246,6 +313,7 @@ impl TryFrom for Action { .direction .and_then(|d| ProtobufResizeDirection::from_i32(d)) .and_then(|d| d.try_into().ok()); + let near_current_pane = payload.near_current_pane; let should_float = payload.should_float; let should_be_in_place = false; Ok(Action::EditFile { @@ -255,12 +323,14 @@ impl TryFrom for Action { in_place: should_be_in_place, start_suppressed: false, coordinates: None, + near_current_pane, }) }, _ => Err("Wrong payload for Action::NewPane"), }, Some(ProtobufActionName::NewFloatingPane) => match protobuf_action.optional_payload { Some(OptionalPayload::NewFloatingPanePayload(payload)) => { + let near_current_pane = payload.near_current_pane; if let Some(payload) = payload.command { let pane_name = payload.pane_name.clone(); let run_command_action: RunCommandAction = payload.try_into()?; @@ -268,12 +338,14 @@ impl TryFrom for Action { command: Some(run_command_action), pane_name, coordinates: None, + near_current_pane, }) } else { Ok(Action::NewFloatingPane { command: None, pane_name: None, coordinates: None, + near_current_pane, }) } }, @@ -285,6 +357,7 @@ impl TryFrom for Action { .direction .and_then(|d| ProtobufResizeDirection::from_i32(d)) .and_then(|d| d.try_into().ok()); + let near_current_pane = payload.near_current_pane; if let Some(payload) = payload.command { let pane_name = payload.pane_name.clone(); let run_command_action: RunCommandAction = payload.try_into()?; @@ -292,12 +365,14 @@ impl TryFrom for Action { direction, command: Some(run_command_action), pane_name, + near_current_pane, }) } else { Ok(Action::NewTiledPane { direction, command: None, pane_name: None, + near_current_pane, }) } }, @@ -331,9 +406,77 @@ impl TryFrom for Action { }, Some(ProtobufActionName::NewTab) => { match protobuf_action.optional_payload { - Some(_) => Err("NewTab should not have a payload"), + Some(OptionalPayload::NewTabPayload(payload)) => { + // New behavior: extract all fields from payload + let tiled_layout = + payload.tiled_layout.map(|l| l.try_into()).transpose()?; + + let floating_layouts = payload + .floating_layouts + .into_iter() + .map(|l| l.try_into()) + .collect::, _>>()?; + + let swap_tiled_layouts = if payload.swap_tiled_layouts.is_empty() { + None + } else { + Some( + payload + .swap_tiled_layouts + .into_iter() + .map(|l| l.try_into()) + .collect::, _>>()?, + ) + }; + + let swap_floating_layouts = if payload.swap_floating_layouts.is_empty() { + None + } else { + Some( + payload + .swap_floating_layouts + .into_iter() + .map(|l| l.try_into()) + .collect::, _>>()?, + ) + }; + + let tab_name = payload.tab_name; + let should_change_focus_to_new_tab = payload.should_change_focus_to_new_tab; + let cwd = payload.cwd.map(PathBuf::from); + + let initial_panes = if payload.initial_panes.is_empty() { + None + } else { + Some( + payload + .initial_panes + .into_iter() + .map(|p| p.try_into()) + .collect::, _>>()?, + ) + }; + + let first_pane_unblock_condition = payload + .first_pane_unblock_condition + .and_then(|uc| ProtobufUnblockCondition::from_i32(uc)) + .and_then(|uc| uc.try_into().ok()); + + Ok(Action::NewTab { + tiled_layout, + floating_layouts, + swap_tiled_layouts, + swap_floating_layouts, + tab_name, + should_change_focus_to_new_tab, + cwd, + initial_panes, + first_pane_unblock_condition, + }) + }, None => { - // we do not serialize the layouts of this action + // Backwards compatibility: accept None payload for existing plugins + // Return the same defaults as before Ok(Action::NewTab { tiled_layout: None, floating_layouts: vec![], @@ -342,8 +485,11 @@ impl TryFrom for Action { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }) }, + _ => Err("Wrong payload for Action::NewTab"), } }, Some(ProtobufActionName::NoOp) => match protobuf_action.optional_payload { @@ -405,6 +551,7 @@ impl TryFrom for Action { let run_command_action = run_command_action.try_into()?; Ok(Action::Run { command: run_command_action, + near_current_pane: false, }) }, _ => Err("Wrong payload for Action::Run"), @@ -657,9 +804,11 @@ impl TryFrom for Action { Some(OptionalPayload::FocusTerminalPaneWithIdPayload(payload)) => { let terminal_pane_id = payload.pane_id; let should_float_if_hidden = payload.should_float; + let should_be_in_place_if_hidden = payload.should_be_in_place; Ok(Action::FocusTerminalPaneWithId { pane_id: terminal_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }) }, _ => Err("Wrong payload for Action::FocusTerminalPaneWithId"), @@ -670,9 +819,11 @@ impl TryFrom for Action { Some(OptionalPayload::FocusPluginPaneWithIdPayload(payload)) => { let plugin_pane_id = payload.pane_id; let should_float_if_hidden = payload.should_float; + let should_be_in_place_if_hidden = payload.should_be_in_place; Ok(Action::FocusPluginPaneWithId { pane_id: plugin_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }) }, _ => Err("Wrong payload for Action::FocusPluginPaneWithId"), @@ -768,8 +919,60 @@ impl TryFrom for Action { None => Ok(Action::NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }), }, + Some(ProtobufActionName::NewBlockingPane) => match protobuf_action.optional_payload { + Some(OptionalPayload::NewBlockingPanePayload(payload)) => { + let placement: NewPanePlacement = payload + .placement + .ok_or("NewBlockingPanePayload must have a placement")? + .try_into()?; + let pane_name = payload.pane_name; + let command = payload.command.and_then(|c| c.try_into().ok()); + let unblock_condition = payload + .unblock_condition + .and_then(|uc| ProtobufUnblockCondition::from_i32(uc)) + .and_then(|uc| uc.try_into().ok()); + let near_current_pane = payload.near_current_pane; + Ok(Action::NewBlockingPane { + placement, + pane_name, + command, + unblock_condition, + near_current_pane, + }) + }, + _ => Err("Wrong payload for Action::NewBlockingPane"), + }, + Some(ProtobufActionName::NewInPlacePane) => match protobuf_action.optional_payload { + Some(OptionalPayload::NewInPlacePanePayload(payload)) => { + let near_current_pane = payload.near_current_pane; + let pane_id_to_replace = + payload.pane_id_to_replace.and_then(|p| p.try_into().ok()); + let close_replace_pane = payload.close_replace_pane; + if let Some(command) = payload.command { + let pane_name = command.pane_name.clone(); + let run_command_action: RunCommandAction = command.try_into()?; + Ok(Action::NewInPlacePane { + command: Some(run_command_action), + pane_name, + near_current_pane, + pane_id_to_replace, + close_replace_pane, + }) + } else { + Ok(Action::NewInPlacePane { + command: None, + pane_name: payload.pane_name, + near_current_pane, + pane_id_to_replace, + close_replace_pane, + }) + } + }, + _ => Err("Wrong payload for Action::NewInPlacePane"), + }, _ => Err("Unknown Action"), } } @@ -784,15 +987,20 @@ impl TryFrom for ProtobufAction { optional_payload: None, }), Action::Write { - key_with_modifier: _, + key_with_modifier, bytes, - is_kitty_keyboard_protocol: _, - } => Ok(ProtobufAction { - name: ProtobufActionName::Write as i32, - optional_payload: Some(OptionalPayload::WritePayload(WritePayload { - bytes_to_write: bytes, - })), - }), + is_kitty_keyboard_protocol, + } => { + let protobuf_key_with_modifier = key_with_modifier.and_then(|k| k.try_into().ok()); + Ok(ProtobufAction { + name: ProtobufActionName::Write as i32, + optional_payload: Some(OptionalPayload::WritePayload(WritePayload { + key_with_modifier: protobuf_key_with_modifier, + bytes_to_write: bytes, + is_kitty_keyboard_protocol, + })), + }) + }, Action::WriteChars { chars: chars_to_write, } => Ok(ProtobufAction { @@ -982,6 +1190,7 @@ impl TryFrom for ProtobufAction { in_place: _should_be_in_place, start_suppressed: _start_suppressed, coordinates: _floating_pane_coordinates, + near_current_pane, } => { let file_to_edit = open_file_payload.path.display().to_string(); let cwd = open_file_payload.cwd.map(|cwd| cwd.display().to_string()); @@ -997,6 +1206,7 @@ impl TryFrom for ProtobufAction { should_float, direction, cwd, + near_current_pane, })), }) }, @@ -1004,6 +1214,7 @@ impl TryFrom for ProtobufAction { command: run_command_action, pane_name, coordinates: _coordinates, + near_current_pane, } => { let command = run_command_action.and_then(|r| { let mut protobuf_run_command_action: ProtobufRunCommandAction = @@ -1014,7 +1225,10 @@ impl TryFrom for ProtobufAction { Ok(ProtobufAction { name: ProtobufActionName::NewFloatingPane as i32, optional_payload: Some(OptionalPayload::NewFloatingPanePayload( - NewFloatingPanePayload { command }, + NewFloatingPanePayload { + command, + near_current_pane, + }, )), }) }, @@ -1022,6 +1236,7 @@ impl TryFrom for ProtobufAction { direction, command: run_command_action, pane_name, + near_current_pane, } => { let direction = direction.and_then(|direction| { let protobuf_direction: ProtobufResizeDirection = direction.try_into().ok()?; @@ -1037,7 +1252,11 @@ impl TryFrom for ProtobufAction { Ok(ProtobufAction { name: ProtobufActionName::NewTiledPane as i32, optional_payload: Some(OptionalPayload::NewTiledPanePayload( - NewTiledPanePayload { direction, command }, + NewTiledPanePayload { + direction, + command, + near_current_pane, + }, )), }) }, @@ -1062,18 +1281,82 @@ impl TryFrom for ProtobufAction { optional_payload: None, }), Action::NewTab { - tiled_layout: _, - floating_layouts: _, - swap_tiled_layouts: _, - swap_floating_layouts: _, - tab_name: _, - should_change_focus_to_new_tab: _, - cwd: _, + tiled_layout, + floating_layouts, + swap_tiled_layouts, + swap_floating_layouts, + tab_name, + should_change_focus_to_new_tab, + cwd, + initial_panes, + first_pane_unblock_condition, } => { - // we do not serialize the various newtab payloads + // Always send payload (even if all fields are default) + let protobuf_tiled_layout = tiled_layout + .as_ref() + .map(|l| l.clone().try_into()) + .transpose()?; + + let protobuf_floating_layouts = floating_layouts + .iter() + .map(|l| l.clone().try_into()) + .collect::, _>>()?; + + let protobuf_swap_tiled_layouts = swap_tiled_layouts + .as_ref() + .map(|layouts| { + layouts + .iter() + .map(|l| l.clone().try_into()) + .collect::, _>>() + }) + .transpose()? + .unwrap_or_default(); + + let protobuf_swap_floating_layouts = swap_floating_layouts + .as_ref() + .map(|layouts| { + layouts + .iter() + .map(|l| l.clone().try_into()) + .collect::, _>>() + }) + .transpose()? + .unwrap_or_default(); + + let cwd_string = cwd.as_ref().map(|p| p.display().to_string()); + + let protobuf_initial_panes = initial_panes + .as_ref() + .map(|panes| { + panes + .iter() + .map(|p| p.clone().try_into()) + .collect::, _>>() + }) + .transpose()? + .unwrap_or_default(); + + let protobuf_first_pane_unblock_condition = first_pane_unblock_condition + .map(|uc| { + let protobuf_uc: ProtobufUnblockCondition = uc.try_into().ok()?; + Some(protobuf_uc as i32) + }) + .flatten(); + Ok(ProtobufAction { name: ProtobufActionName::NewTab as i32, - optional_payload: None, + optional_payload: Some(OptionalPayload::NewTabPayload(NewTabPayload { + tiled_layout: protobuf_tiled_layout, + floating_layouts: protobuf_floating_layouts, + swap_tiled_layouts: protobuf_swap_tiled_layouts, + swap_floating_layouts: protobuf_swap_floating_layouts, + tab_name: tab_name.clone(), + should_change_focus_to_new_tab, + cwd: cwd_string, + initial_panes: protobuf_initial_panes, + first_pane_unblock_condition: protobuf_first_pane_unblock_condition, + })), }) }, Action::GoToNextTab => Ok(ProtobufAction { @@ -1123,6 +1406,7 @@ impl TryFrom for ProtobufAction { }, Action::Run { command: run_command_action, + near_current_pane: _, } => { let run_command_action: ProtobufRunCommandAction = run_command_action.try_into()?; Ok(ProtobufAction { @@ -1277,24 +1561,28 @@ impl TryFrom for ProtobufAction { Action::FocusTerminalPaneWithId { pane_id: terminal_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => Ok(ProtobufAction { name: ProtobufActionName::FocusTerminalPaneWithId as i32, optional_payload: Some(OptionalPayload::FocusTerminalPaneWithIdPayload( PaneIdAndShouldFloat { pane_id: terminal_pane_id, should_float: should_float_if_hidden, + should_be_in_place: should_be_in_place_if_hidden, }, )), }), Action::FocusPluginPaneWithId { pane_id: plugin_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => Ok(ProtobufAction { name: ProtobufActionName::FocusPluginPaneWithId as i32, optional_payload: Some(OptionalPayload::FocusPluginPaneWithIdPayload( PaneIdAndShouldFloat { pane_id: plugin_pane_id, should_float: should_float_if_hidden, + should_be_in_place: should_be_in_place_if_hidden, }, )), }), @@ -1363,16 +1651,72 @@ impl TryFrom for ProtobufAction { Action::NewStackedPane { command: _, pane_name: _, + near_current_pane: _, } => Ok(ProtobufAction { name: ProtobufActionName::NewStackedPane as i32, optional_payload: None, }), + Action::NewBlockingPane { + placement, + pane_name, + command, + unblock_condition, + near_current_pane, + } => { + let placement: ProtobufNewPanePlacement = placement.try_into()?; + let command = command.and_then(|c| { + let protobuf_command: ProtobufRunCommandAction = c.try_into().ok()?; + Some(protobuf_command) + }); + let unblock_condition = unblock_condition + .map(|uc| { + let protobuf_uc: ProtobufUnblockCondition = uc.try_into().ok()?; + Some(protobuf_uc as i32) + }) + .flatten(); + Ok(ProtobufAction { + name: ProtobufActionName::NewBlockingPane as i32, + optional_payload: Some(OptionalPayload::NewBlockingPanePayload( + NewBlockingPanePayload { + placement: Some(placement), + pane_name, + command, + unblock_condition, + near_current_pane, + }, + )), + }) + }, + Action::NewInPlacePane { + command: run_command_action, + pane_name, + near_current_pane, + pane_id_to_replace, + close_replace_pane, + } => { + let command = run_command_action.and_then(|r| { + let mut protobuf_run_command_action: ProtobufRunCommandAction = + r.try_into().ok()?; + let pane_name = pane_name.and_then(|n| n.try_into().ok()); + protobuf_run_command_action.pane_name = pane_name; + Some(protobuf_run_command_action) + }); + let pane_id_to_replace = pane_id_to_replace.and_then(|p| p.try_into().ok()); + Ok(ProtobufAction { + name: ProtobufActionName::NewInPlacePane as i32, + optional_payload: Some(OptionalPayload::NewInPlacePanePayload( + NewInPlacePanePayload { + command, + pane_name: None, // pane_name is already embedded in command + near_current_pane, + pane_id_to_replace, + close_replace_pane, + }, + )), + }) + }, Action::NoOp | Action::Confirm - | Action::NewInPlacePane { - command: _, - pane_name: _, - } | Action::NewInPlacePluginPane { plugin: _, pane_name: _, @@ -1389,7 +1733,6 @@ impl TryFrom for ProtobufAction { coordinates: _, } | Action::SkipConfirm { action: _ } - | Action::NewBlockingPane { .. } | Action::SwitchSession { .. } => Err("Unsupported action"), } } @@ -1616,3 +1959,914 @@ impl TryFrom<&ProtobufPluginConfiguration> for BTreeMap { Ok(converted) } } + +impl TryFrom for KeyWithModifier { + type Error = &'static str; + fn try_from(protobuf_key: ProtobufKeyWithModifier) -> Result { + let bare_key = match ProtobufBareKey::from_i32(protobuf_key.bare_key) { + Some(ProtobufBareKey::PageDown) => crate::data::BareKey::PageDown, + Some(ProtobufBareKey::PageUp) => crate::data::BareKey::PageUp, + Some(ProtobufBareKey::Left) => crate::data::BareKey::Left, + Some(ProtobufBareKey::Down) => crate::data::BareKey::Down, + Some(ProtobufBareKey::Up) => crate::data::BareKey::Up, + Some(ProtobufBareKey::Right) => crate::data::BareKey::Right, + Some(ProtobufBareKey::Home) => crate::data::BareKey::Home, + Some(ProtobufBareKey::End) => crate::data::BareKey::End, + Some(ProtobufBareKey::Backspace) => crate::data::BareKey::Backspace, + Some(ProtobufBareKey::Delete) => crate::data::BareKey::Delete, + Some(ProtobufBareKey::Insert) => crate::data::BareKey::Insert, + Some(ProtobufBareKey::F1) => crate::data::BareKey::F(1), + Some(ProtobufBareKey::F2) => crate::data::BareKey::F(2), + Some(ProtobufBareKey::F3) => crate::data::BareKey::F(3), + Some(ProtobufBareKey::F4) => crate::data::BareKey::F(4), + Some(ProtobufBareKey::F5) => crate::data::BareKey::F(5), + Some(ProtobufBareKey::F6) => crate::data::BareKey::F(6), + Some(ProtobufBareKey::F7) => crate::data::BareKey::F(7), + Some(ProtobufBareKey::F8) => crate::data::BareKey::F(8), + Some(ProtobufBareKey::F9) => crate::data::BareKey::F(9), + Some(ProtobufBareKey::F10) => crate::data::BareKey::F(10), + Some(ProtobufBareKey::F11) => crate::data::BareKey::F(11), + Some(ProtobufBareKey::F12) => crate::data::BareKey::F(12), + Some(ProtobufBareKey::Char) => { + if let Some(character) = protobuf_key.character { + let ch = character + .chars() + .next() + .ok_or("BareKey::Char requires a character")?; + crate::data::BareKey::Char(ch) + } else { + return Err("BareKey::Char requires a character"); + } + }, + Some(ProtobufBareKey::Tab) => crate::data::BareKey::Tab, + Some(ProtobufBareKey::Esc) => crate::data::BareKey::Esc, + Some(ProtobufBareKey::Enter) => crate::data::BareKey::Enter, + Some(ProtobufBareKey::CapsLock) => crate::data::BareKey::CapsLock, + Some(ProtobufBareKey::ScrollLock) => crate::data::BareKey::ScrollLock, + Some(ProtobufBareKey::NumLock) => crate::data::BareKey::NumLock, + Some(ProtobufBareKey::PrintScreen) => crate::data::BareKey::PrintScreen, + Some(ProtobufBareKey::Pause) => crate::data::BareKey::Pause, + Some(ProtobufBareKey::Menu) => crate::data::BareKey::Menu, + _ => return Err("Unknown BareKey"), + }; + + let mut key_modifiers = std::collections::BTreeSet::new(); + for modifier in protobuf_key.key_modifiers { + let key_modifier = match ProtobufKeyModifier::from_i32(modifier) { + Some(ProtobufKeyModifier::Ctrl) => crate::data::KeyModifier::Ctrl, + Some(ProtobufKeyModifier::Alt) => crate::data::KeyModifier::Alt, + Some(ProtobufKeyModifier::Shift) => crate::data::KeyModifier::Shift, + Some(ProtobufKeyModifier::Super) => crate::data::KeyModifier::Super, + _ => continue, + }; + key_modifiers.insert(key_modifier); + } + + Ok(KeyWithModifier { + bare_key, + key_modifiers, + }) + } +} + +impl TryFrom for ProtobufKeyWithModifier { + type Error = &'static str; + fn try_from(key: KeyWithModifier) -> Result { + let (bare_key, character) = match key.bare_key { + crate::data::BareKey::PageDown => (ProtobufBareKey::PageDown as i32, None), + crate::data::BareKey::PageUp => (ProtobufBareKey::PageUp as i32, None), + crate::data::BareKey::Left => (ProtobufBareKey::Left as i32, None), + crate::data::BareKey::Down => (ProtobufBareKey::Down as i32, None), + crate::data::BareKey::Up => (ProtobufBareKey::Up as i32, None), + crate::data::BareKey::Right => (ProtobufBareKey::Right as i32, None), + crate::data::BareKey::Home => (ProtobufBareKey::Home as i32, None), + crate::data::BareKey::End => (ProtobufBareKey::End as i32, None), + crate::data::BareKey::Backspace => (ProtobufBareKey::Backspace as i32, None), + crate::data::BareKey::Delete => (ProtobufBareKey::Delete as i32, None), + crate::data::BareKey::Insert => (ProtobufBareKey::Insert as i32, None), + crate::data::BareKey::F(1) => (ProtobufBareKey::F1 as i32, None), + crate::data::BareKey::F(2) => (ProtobufBareKey::F2 as i32, None), + crate::data::BareKey::F(3) => (ProtobufBareKey::F3 as i32, None), + crate::data::BareKey::F(4) => (ProtobufBareKey::F4 as i32, None), + crate::data::BareKey::F(5) => (ProtobufBareKey::F5 as i32, None), + crate::data::BareKey::F(6) => (ProtobufBareKey::F6 as i32, None), + crate::data::BareKey::F(7) => (ProtobufBareKey::F7 as i32, None), + crate::data::BareKey::F(8) => (ProtobufBareKey::F8 as i32, None), + crate::data::BareKey::F(9) => (ProtobufBareKey::F9 as i32, None), + crate::data::BareKey::F(10) => (ProtobufBareKey::F10 as i32, None), + crate::data::BareKey::F(11) => (ProtobufBareKey::F11 as i32, None), + crate::data::BareKey::F(12) => (ProtobufBareKey::F12 as i32, None), + crate::data::BareKey::Char(c) => (ProtobufBareKey::Char as i32, Some(c.to_string())), + crate::data::BareKey::Tab => (ProtobufBareKey::Tab as i32, None), + crate::data::BareKey::Esc => (ProtobufBareKey::Esc as i32, None), + crate::data::BareKey::Enter => (ProtobufBareKey::Enter as i32, None), + crate::data::BareKey::CapsLock => (ProtobufBareKey::CapsLock as i32, None), + crate::data::BareKey::ScrollLock => (ProtobufBareKey::ScrollLock as i32, None), + crate::data::BareKey::NumLock => (ProtobufBareKey::NumLock as i32, None), + crate::data::BareKey::PrintScreen => (ProtobufBareKey::PrintScreen as i32, None), + crate::data::BareKey::Pause => (ProtobufBareKey::Pause as i32, None), + crate::data::BareKey::Menu => (ProtobufBareKey::Menu as i32, None), + _ => return Err("Unsupported BareKey"), + }; + + let key_modifiers: Vec = key + .key_modifiers + .iter() + .map(|m| match m { + crate::data::KeyModifier::Ctrl => ProtobufKeyModifier::Ctrl as i32, + crate::data::KeyModifier::Alt => ProtobufKeyModifier::Alt as i32, + crate::data::KeyModifier::Shift => ProtobufKeyModifier::Shift as i32, + crate::data::KeyModifier::Super => ProtobufKeyModifier::Super as i32, + }) + .collect(); + + Ok(ProtobufKeyWithModifier { + bare_key, + key_modifiers, + character, + }) + } +} + +// UnblockCondition conversions +impl TryFrom for UnblockCondition { + type Error = &'static str; + fn try_from(protobuf_uc: ProtobufUnblockCondition) -> Result { + match protobuf_uc { + ProtobufUnblockCondition::UnblockOnExitSuccess => Ok(UnblockCondition::OnExitSuccess), + ProtobufUnblockCondition::UnblockOnExitFailure => Ok(UnblockCondition::OnExitFailure), + ProtobufUnblockCondition::UnblockOnAnyExit => Ok(UnblockCondition::OnAnyExit), + } + } +} + +impl TryFrom for ProtobufUnblockCondition { + type Error = &'static str; + fn try_from(uc: UnblockCondition) -> Result { + match uc { + UnblockCondition::OnExitSuccess => Ok(ProtobufUnblockCondition::UnblockOnExitSuccess), + UnblockCondition::OnExitFailure => Ok(ProtobufUnblockCondition::UnblockOnExitFailure), + UnblockCondition::OnAnyExit => Ok(ProtobufUnblockCondition::UnblockOnAnyExit), + } + } +} + +impl TryFrom for UnblockCondition { + type Error = &'static str; + fn try_from(value: i32) -> Result { + match ProtobufUnblockCondition::from_i32(value) { + Some(uc) => uc.try_into(), + None => Err("Invalid UnblockCondition value"), + } + } +} + +// PaneId conversions +impl TryFrom for PaneId { + type Error = &'static str; + fn try_from(protobuf_pane_id: ProtobufPaneId) -> Result { + use super::generated_api::api::action::pane_id::PaneIdVariant; + + match protobuf_pane_id.pane_id_variant { + Some(PaneIdVariant::Terminal(id)) => Ok(PaneId::Terminal(id)), + Some(PaneIdVariant::Plugin(id)) => Ok(PaneId::Plugin(id)), + None => Err("PaneId must have either terminal or plugin id"), + } + } +} + +impl TryFrom for ProtobufPaneId { + type Error = &'static str; + fn try_from(pane_id: PaneId) -> Result { + use super::generated_api::api::action::pane_id::PaneIdVariant; + + let pane_id_variant = match pane_id { + PaneId::Terminal(id) => Some(PaneIdVariant::Terminal(id)), + PaneId::Plugin(id) => Some(PaneIdVariant::Plugin(id)), + }; + Ok(ProtobufPaneId { pane_id_variant }) + } +} + +// SplitSize conversions +impl TryFrom for SplitSize { + type Error = &'static str; + fn try_from(protobuf_split_size: ProtobufSplitSize) -> Result { + use super::generated_api::api::action::split_size::SplitSizeVariant; + + match protobuf_split_size.split_size_variant { + Some(SplitSizeVariant::Percent(p)) => Ok(SplitSize::Percent(p as usize)), + Some(SplitSizeVariant::Fixed(f)) => Ok(SplitSize::Fixed(f as usize)), + None => Err("SplitSize must have either percent or fixed value"), + } + } +} + +impl TryFrom for ProtobufSplitSize { + type Error = &'static str; + fn try_from(split_size: SplitSize) -> Result { + use super::generated_api::api::action::split_size::SplitSizeVariant; + + let split_size_variant = match split_size { + SplitSize::Percent(p) => Some(SplitSizeVariant::Percent(p as u32)), + SplitSize::Fixed(f) => Some(SplitSizeVariant::Fixed(f as u32)), + }; + Ok(ProtobufSplitSize { split_size_variant }) + } +} + +// FloatingPaneCoordinates conversions +impl TryFrom for FloatingPaneCoordinates { + type Error = &'static str; + fn try_from(protobuf_coords: ProtobufFloatingPaneCoordinates) -> Result { + Ok(FloatingPaneCoordinates { + x: protobuf_coords.x.and_then(|x| x.try_into().ok()), + y: protobuf_coords.y.and_then(|y| y.try_into().ok()), + width: protobuf_coords.width.and_then(|w| w.try_into().ok()), + height: protobuf_coords.height.and_then(|h| h.try_into().ok()), + pinned: protobuf_coords.pinned, + }) + } +} + +impl TryFrom for ProtobufFloatingPaneCoordinates { + type Error = &'static str; + fn try_from(coords: FloatingPaneCoordinates) -> Result { + Ok(ProtobufFloatingPaneCoordinates { + x: coords.x.and_then(|x| x.try_into().ok()), + y: coords.y.and_then(|y| y.try_into().ok()), + width: coords.width.and_then(|w| w.try_into().ok()), + height: coords.height.and_then(|h| h.try_into().ok()), + pinned: coords.pinned, + }) + } +} + +// NewPanePlacement conversions +impl TryFrom for NewPanePlacement { + type Error = &'static str; + fn try_from(protobuf_placement: ProtobufNewPanePlacement) -> Result { + use super::generated_api::api::action::new_pane_placement::PlacementVariant; + + match protobuf_placement.placement_variant { + Some(PlacementVariant::NoPreference(_)) => Ok(NewPanePlacement::NoPreference), + Some(PlacementVariant::Tiled(tiled)) => { + let direction = tiled + .direction + .and_then(|d| ProtobufResizeDirection::from_i32(d)) + .and_then(|d| d.try_into().ok()); + Ok(NewPanePlacement::Tiled(direction)) + }, + Some(PlacementVariant::Floating(floating)) => { + let coords = floating.coordinates.and_then(|c| c.try_into().ok()); + Ok(NewPanePlacement::Floating(coords)) + }, + Some(PlacementVariant::InPlace(config)) => { + let pane_id_to_replace = + config.pane_id_to_replace.and_then(|id| id.try_into().ok()); + Ok(NewPanePlacement::InPlace { + pane_id_to_replace, + close_replaced_pane: config.close_replaced_pane, + }) + }, + Some(PlacementVariant::Stacked(stacked)) => { + let pane_id = stacked.pane_id.and_then(|id| id.try_into().ok()); + Ok(NewPanePlacement::Stacked(pane_id)) + }, + None => Err("NewPanePlacement must have a placement variant"), + } + } +} + +impl TryFrom for ProtobufNewPanePlacement { + type Error = &'static str; + fn try_from(placement: NewPanePlacement) -> Result { + use super::generated_api::api::action::new_pane_placement::PlacementVariant; + + let placement_variant = match placement { + NewPanePlacement::NoPreference => Some(PlacementVariant::NoPreference(true)), + NewPanePlacement::Tiled(direction) => { + let direction = direction.and_then(|d| { + let protobuf_direction: ProtobufResizeDirection = d.try_into().ok()?; + Some(protobuf_direction as i32) + }); + Some(PlacementVariant::Tiled(ProtobufTiledPlacement { + direction, + })) + }, + NewPanePlacement::Floating(coords) => { + let coordinates = coords.and_then(|c| c.try_into().ok()); + Some(PlacementVariant::Floating(ProtobufFloatingPlacement { + coordinates, + })) + }, + NewPanePlacement::InPlace { + pane_id_to_replace, + close_replaced_pane, + } => { + let pane_id_to_replace = pane_id_to_replace.and_then(|id| id.try_into().ok()); + Some(PlacementVariant::InPlace(ProtobufInPlaceConfig { + pane_id_to_replace, + close_replaced_pane, + })) + }, + NewPanePlacement::Stacked(pane_id) => { + let pane_id = pane_id.and_then(|id| id.try_into().ok()); + Some(PlacementVariant::Stacked(ProtobufStackedPlacement { + pane_id, + })) + }, + }; + + Ok(ProtobufNewPanePlacement { placement_variant }) + } +} + +// Layout type conversions + +impl TryFrom for PercentOrFixed { + type Error = &'static str; + fn try_from(protobuf: ProtobufPercentOrFixed) -> Result { + use super::generated_api::api::action::percent_or_fixed::SizeType; + match protobuf.size_type { + Some(SizeType::Percent(p)) => Ok(PercentOrFixed::Percent(p as usize)), + Some(SizeType::Fixed(f)) => Ok(PercentOrFixed::Fixed(f as usize)), + None => Err("PercentOrFixed must have a size_type"), + } + } +} + +impl TryFrom for ProtobufPercentOrFixed { + type Error = &'static str; + fn try_from(internal: PercentOrFixed) -> Result { + use super::generated_api::api::action::percent_or_fixed::SizeType; + let size_type = match internal { + PercentOrFixed::Percent(p) => Some(SizeType::Percent(p as u32)), + PercentOrFixed::Fixed(f) => Some(SizeType::Fixed(f as u32)), + }; + Ok(ProtobufPercentOrFixed { size_type }) + } +} + +impl TryFrom for SplitDirection { + type Error = &'static str; + fn try_from(protobuf: ProtobufSplitDirection) -> Result { + match protobuf { + ProtobufSplitDirection::Horizontal => Ok(SplitDirection::Horizontal), + ProtobufSplitDirection::Vertical => Ok(SplitDirection::Vertical), + ProtobufSplitDirection::Unspecified => Err("SplitDirection cannot be unspecified"), + } + } +} + +impl TryFrom for ProtobufSplitDirection { + type Error = &'static str; + fn try_from(internal: SplitDirection) -> Result { + Ok(match internal { + SplitDirection::Horizontal => ProtobufSplitDirection::Horizontal, + SplitDirection::Vertical => ProtobufSplitDirection::Vertical, + }) + } +} + +impl TryFrom for LayoutConstraint { + type Error = &'static str; + fn try_from(protobuf: ProtobufLayoutConstraint) -> Result { + match protobuf { + ProtobufLayoutConstraint::MaxPanes => Ok(LayoutConstraint::MaxPanes(0)), + ProtobufLayoutConstraint::MinPanes => Ok(LayoutConstraint::MinPanes(0)), + ProtobufLayoutConstraint::ExactPanes => Ok(LayoutConstraint::ExactPanes(0)), + ProtobufLayoutConstraint::NoConstraint => Ok(LayoutConstraint::NoConstraint), + ProtobufLayoutConstraint::Unspecified => Err("LayoutConstraint cannot be unspecified"), + } + } +} + +impl TryFrom for ProtobufLayoutConstraint { + type Error = &'static str; + fn try_from(internal: LayoutConstraint) -> Result { + Ok(match internal { + LayoutConstraint::MaxPanes(_) => ProtobufLayoutConstraint::MaxPanes, + LayoutConstraint::MinPanes(_) => ProtobufLayoutConstraint::MinPanes, + LayoutConstraint::ExactPanes(_) => ProtobufLayoutConstraint::ExactPanes, + LayoutConstraint::NoConstraint => ProtobufLayoutConstraint::NoConstraint, + }) + } +} + +impl TryFrom for LayoutConstraint { + type Error = &'static str; + fn try_from(protobuf: ProtobufLayoutConstraintWithValue) -> Result { + let constraint_type = ProtobufLayoutConstraint::from_i32(protobuf.constraint_type) + .ok_or("Invalid constraint type")?; + match constraint_type { + ProtobufLayoutConstraint::MaxPanes => Ok(LayoutConstraint::MaxPanes( + protobuf.value.unwrap_or(0) as usize, + )), + ProtobufLayoutConstraint::MinPanes => Ok(LayoutConstraint::MinPanes( + protobuf.value.unwrap_or(0) as usize, + )), + ProtobufLayoutConstraint::ExactPanes => Ok(LayoutConstraint::ExactPanes( + protobuf.value.unwrap_or(0) as usize, + )), + ProtobufLayoutConstraint::NoConstraint => Ok(LayoutConstraint::NoConstraint), + ProtobufLayoutConstraint::Unspecified => Err("LayoutConstraint cannot be unspecified"), + } + } +} + +impl TryFrom for ProtobufLayoutConstraintWithValue { + type Error = &'static str; + fn try_from(internal: LayoutConstraint) -> Result { + let (constraint_type, value) = match internal { + LayoutConstraint::MaxPanes(v) => { + (ProtobufLayoutConstraint::MaxPanes as i32, Some(v as u32)) + }, + LayoutConstraint::MinPanes(v) => { + (ProtobufLayoutConstraint::MinPanes as i32, Some(v as u32)) + }, + LayoutConstraint::ExactPanes(v) => { + (ProtobufLayoutConstraint::ExactPanes as i32, Some(v as u32)) + }, + LayoutConstraint::NoConstraint => (ProtobufLayoutConstraint::NoConstraint as i32, None), + }; + Ok(ProtobufLayoutConstraintWithValue { + constraint_type, + value, + }) + } +} + +impl TryFrom for PluginTag { + type Error = &'static str; + fn try_from(protobuf: ProtobufPluginTag) -> Result { + Ok(PluginTag::new(protobuf.tag)) + } +} + +impl TryFrom for ProtobufPluginTag { + type Error = &'static str; + fn try_from(internal: PluginTag) -> Result { + Ok(ProtobufPluginTag { + tag: internal.into(), + }) + } +} + +impl TryFrom for PluginUserConfiguration { + type Error = &'static str; + fn try_from(protobuf: ProtobufPluginUserConfiguration) -> Result { + let btree_map: BTreeMap = protobuf.configuration.into_iter().collect(); + Ok(PluginUserConfiguration::new(btree_map)) + } +} + +impl TryFrom for ProtobufPluginUserConfiguration { + type Error = &'static str; + fn try_from(internal: PluginUserConfiguration) -> Result { + let configuration = internal.inner().clone().into_iter().collect(); + Ok(ProtobufPluginUserConfiguration { configuration }) + } +} + +impl TryFrom for RunPluginLocation { + type Error = &'static str; + fn try_from(protobuf: ProtobufRunPluginLocationData) -> Result { + use super::generated_api::api::action::run_plugin_location_data::LocationData; + match protobuf.location_data { + Some(LocationData::FilePath(path)) => Ok(RunPluginLocation::File(PathBuf::from(path))), + Some(LocationData::ZellijTag(tag)) => Ok(RunPluginLocation::Zellij(tag.try_into()?)), + Some(LocationData::RemoteUrl(url)) => Ok(RunPluginLocation::Remote(url)), + None => Err("RunPluginLocationData must have location_data"), + } + } +} + +impl TryFrom for ProtobufRunPluginLocationData { + type Error = &'static str; + fn try_from(internal: RunPluginLocation) -> Result { + use super::generated_api::api::action::{ + run_plugin_location_data::LocationData, + RunPluginLocation as ProtobufRunPluginLocationType, + }; + let (location_type, location_data) = match internal { + RunPluginLocation::File(path) => ( + ProtobufRunPluginLocationType::File as i32, + Some(LocationData::FilePath(path.display().to_string())), + ), + RunPluginLocation::Zellij(tag) => ( + ProtobufRunPluginLocationType::Zellij as i32, + Some(LocationData::ZellijTag(tag.try_into()?)), + ), + RunPluginLocation::Remote(url) => ( + ProtobufRunPluginLocationType::Remote as i32, + Some(LocationData::RemoteUrl(url)), + ), + }; + Ok(ProtobufRunPluginLocationData { + location_type, + location_data, + }) + } +} + +impl TryFrom for RunPlugin { + type Error = &'static str; + fn try_from(protobuf: ProtobufRunPlugin) -> Result { + let location = protobuf + .location + .ok_or("RunPlugin must have location")? + .try_into()?; + let configuration = protobuf + .configuration + .ok_or("RunPlugin must have configuration")? + .try_into()?; + let initial_cwd = protobuf.initial_cwd.map(PathBuf::from); + Ok(RunPlugin { + _allow_exec_host_cmd: protobuf.allow_exec_host_cmd, + location, + configuration, + initial_cwd, + }) + } +} + +impl TryFrom for ProtobufRunPlugin { + type Error = &'static str; + fn try_from(internal: RunPlugin) -> Result { + Ok(ProtobufRunPlugin { + allow_exec_host_cmd: internal._allow_exec_host_cmd, + location: Some(internal.location.try_into()?), + configuration: Some(internal.configuration.try_into()?), + initial_cwd: internal.initial_cwd.map(|p| p.display().to_string()), + }) + } +} + +impl TryFrom for PluginAlias { + type Error = &'static str; + fn try_from(protobuf: ProtobufPluginAlias) -> Result { + let configuration = protobuf.configuration.map(|c| c.try_into()).transpose()?; + let initial_cwd = protobuf.initial_cwd.map(PathBuf::from); + let run_plugin = protobuf.run_plugin.map(|r| r.try_into()).transpose()?; + Ok(PluginAlias { + name: protobuf.name, + configuration, + initial_cwd, + run_plugin, + }) + } +} + +impl TryFrom for ProtobufPluginAlias { + type Error = &'static str; + fn try_from(internal: PluginAlias) -> Result { + Ok(ProtobufPluginAlias { + name: internal.name, + configuration: internal.configuration.map(|c| c.try_into()).transpose()?, + initial_cwd: internal.initial_cwd.map(|p| p.display().to_string()), + run_plugin: internal.run_plugin.map(|r| r.try_into()).transpose()?, + }) + } +} + +impl TryFrom for RunPluginOrAlias { + type Error = &'static str; + fn try_from(protobuf: ProtobufRunPluginOrAlias) -> Result { + use super::generated_api::api::action::run_plugin_or_alias::PluginType; + match protobuf.plugin_type { + Some(PluginType::Plugin(plugin)) => Ok(RunPluginOrAlias::RunPlugin(plugin.try_into()?)), + Some(PluginType::Alias(alias)) => Ok(RunPluginOrAlias::Alias(alias.try_into()?)), + None => Err("RunPluginOrAlias must have plugin_type"), + } + } +} + +impl TryFrom for ProtobufRunPluginOrAlias { + type Error = &'static str; + fn try_from(internal: RunPluginOrAlias) -> Result { + use super::generated_api::api::action::run_plugin_or_alias::PluginType; + let plugin_type = match internal { + RunPluginOrAlias::RunPlugin(plugin) => Some(PluginType::Plugin(plugin.try_into()?)), + RunPluginOrAlias::Alias(alias) => Some(PluginType::Alias(alias.try_into()?)), + }; + Ok(ProtobufRunPluginOrAlias { plugin_type }) + } +} + +impl TryFrom for Run { + type Error = &'static str; + fn try_from(protobuf: ProtobufRunEditFileAction) -> Result { + let file_path = PathBuf::from(protobuf.file_path); + let line_number = protobuf.line_number.map(|n| n as usize); + let cwd = protobuf.cwd.map(PathBuf::from); + Ok(Run::EditFile(file_path, line_number, cwd)) + } +} + +impl TryFrom for Run { + type Error = &'static str; + fn try_from(protobuf: ProtobufPaneRun) -> Result { + use super::generated_api::api::action::pane_run::RunType; + use crate::input::command::RunCommand; + match protobuf.run_type { + Some(RunType::Command(cmd)) => { + let run_command_action: RunCommandAction = cmd.try_into()?; + let run_command: RunCommand = run_command_action.into(); + Ok(Run::Command(run_command)) + }, + Some(RunType::Plugin(plugin)) => Ok(Run::Plugin(plugin.try_into()?)), + Some(RunType::EditFile(edit)) => edit.try_into(), + Some(RunType::Cwd(cwd)) => Ok(Run::Cwd(PathBuf::from(cwd))), + None => Err("PaneRun must have run_type"), + } + } +} + +impl TryFrom for ProtobufPaneRun { + type Error = &'static str; + fn try_from(internal: Run) -> Result { + use super::generated_api::api::action::pane_run::RunType; + let run_type = match internal { + Run::Command(cmd) => { + let run_command_action: RunCommandAction = cmd.into(); + Some(RunType::Command(run_command_action.try_into()?)) + }, + Run::Plugin(plugin) => Some(RunType::Plugin(plugin.try_into()?)), + Run::EditFile(file_path, line_number, cwd) => { + Some(RunType::EditFile(ProtobufRunEditFileAction { + file_path: file_path.display().to_string(), + line_number: line_number.map(|n| n as u32), + cwd: cwd.map(|p| p.display().to_string()), + })) + }, + Run::Cwd(cwd) => Some(RunType::Cwd(cwd.display().to_string())), + }; + Ok(ProtobufPaneRun { run_type }) + } +} + +impl TryFrom for CommandOrPlugin { + type Error = &'static str; + fn try_from(protobuf: ProtobufCommandOrPlugin) -> Result { + use super::generated_api::api::action::command_or_plugin::CommandOrPluginType; + match protobuf.command_or_plugin_type { + Some(CommandOrPluginType::Command(cmd)) => { + Ok(CommandOrPlugin::Command(cmd.try_into()?)) + }, + Some(CommandOrPluginType::Plugin(plugin)) => { + Ok(CommandOrPlugin::Plugin(plugin.try_into()?)) + }, + None => Err("CommandOrPlugin must have command_or_plugin_type"), + } + } +} + +impl TryFrom for ProtobufCommandOrPlugin { + type Error = &'static str; + fn try_from(internal: CommandOrPlugin) -> Result { + use super::generated_api::api::action::command_or_plugin::CommandOrPluginType; + let command_or_plugin_type = match internal { + CommandOrPlugin::Command(cmd) => Some(CommandOrPluginType::Command(cmd.try_into()?)), + CommandOrPlugin::Plugin(plugin) => { + Some(CommandOrPluginType::Plugin(plugin.try_into()?)) + }, + }; + Ok(ProtobufCommandOrPlugin { + command_or_plugin_type, + }) + } +} + +impl TryFrom for TiledPaneLayout { + type Error = &'static str; + fn try_from(protobuf: ProtobufTiledPaneLayout) -> Result { + let children_split_direction = + ProtobufSplitDirection::from_i32(protobuf.children_split_direction) + .ok_or("Invalid split direction")? + .try_into()?; + let children = protobuf + .children + .into_iter() + .map(|c| c.try_into()) + .collect::, _>>()?; + let split_size = protobuf.split_size.map(|s| s.try_into()).transpose()?; + let run = protobuf.run.map(|r| r.try_into()).transpose()?; + let focus = protobuf.focus.and_then(|f| { + if f == "true" { + Some(true) + } else if f == "false" { + Some(false) + } else { + None + } + }); + let run_instructions_to_ignore = vec![]; // Not serialized in protobuf + Ok(TiledPaneLayout { + children_split_direction, + name: protobuf.name, + children, + split_size, + run, + borderless: protobuf.borderless, + focus, + external_children_index: protobuf.external_children_index.map(|i| i as usize), + children_are_stacked: protobuf.children_are_stacked, + is_expanded_in_stack: protobuf.is_expanded_in_stack, + exclude_from_sync: protobuf.exclude_from_sync, + run_instructions_to_ignore, + hide_floating_panes: protobuf.hide_floating_panes, + pane_initial_contents: protobuf.pane_initial_contents, + }) + } +} + +impl TryFrom for ProtobufTiledPaneLayout { + type Error = &'static str; + fn try_from(internal: TiledPaneLayout) -> Result { + let children_split_direction: ProtobufSplitDirection = + internal.children_split_direction.try_into()?; + let children = internal + .children + .into_iter() + .map(|c| c.try_into()) + .collect::, _>>()?; + let split_size = internal.split_size.map(|s| s.try_into()).transpose()?; + let run = internal.run.map(|r| r.try_into()).transpose()?; + let focus = internal.focus.map(|f| f.to_string()); + Ok(ProtobufTiledPaneLayout { + children_split_direction: children_split_direction as i32, + name: internal.name, + children, + split_size, + run, + borderless: internal.borderless, + focus, + external_children_index: internal.external_children_index.map(|i| i as u32), + children_are_stacked: internal.children_are_stacked, + is_expanded_in_stack: internal.is_expanded_in_stack, + exclude_from_sync: internal.exclude_from_sync, + hide_floating_panes: internal.hide_floating_panes, + pane_initial_contents: internal.pane_initial_contents, + }) + } +} + +impl TryFrom for FloatingPaneLayout { + type Error = &'static str; + fn try_from(protobuf: ProtobufFloatingPaneLayout) -> Result { + let height = protobuf.height.map(|h| h.try_into()).transpose()?; + let width = protobuf.width.map(|w| w.try_into()).transpose()?; + let x = protobuf.x.map(|x| x.try_into()).transpose()?; + let y = protobuf.y.map(|y| y.try_into()).transpose()?; + let run = protobuf.run.map(|r| r.try_into()).transpose()?; + Ok(FloatingPaneLayout { + name: protobuf.name, + height, + width, + x, + y, + pinned: protobuf.pinned, + run, + focus: protobuf.focus, + already_running: protobuf.already_running, + pane_initial_contents: protobuf.pane_initial_contents, + logical_position: protobuf.logical_position.map(|p| p as usize), + }) + } +} + +impl TryFrom for ProtobufFloatingPaneLayout { + type Error = &'static str; + fn try_from(internal: FloatingPaneLayout) -> Result { + let height = internal.height.map(|h| h.try_into()).transpose()?; + let width = internal.width.map(|w| w.try_into()).transpose()?; + let x = internal.x.map(|x| x.try_into()).transpose()?; + let y = internal.y.map(|y| y.try_into()).transpose()?; + let run = internal.run.map(|r| r.try_into()).transpose()?; + Ok(ProtobufFloatingPaneLayout { + name: internal.name, + height, + width, + x, + y, + pinned: internal.pinned, + run, + focus: internal.focus, + already_running: internal.already_running, + pane_initial_contents: internal.pane_initial_contents, + logical_position: internal.logical_position.map(|p| p as u32), + }) + } +} + +impl TryFrom for (LayoutConstraint, TiledPaneLayout) { + type Error = &'static str; + fn try_from(protobuf: ProtobufLayoutConstraintTiledPair) -> Result { + let constraint = protobuf + .constraint + .ok_or("LayoutConstraintTiledPair must have constraint")? + .try_into()?; + let layout = protobuf + .layout + .ok_or("LayoutConstraintTiledPair must have layout")? + .try_into()?; + Ok((constraint, layout)) + } +} + +impl TryFrom<(LayoutConstraint, TiledPaneLayout)> for ProtobufLayoutConstraintTiledPair { + type Error = &'static str; + fn try_from(internal: (LayoutConstraint, TiledPaneLayout)) -> Result { + Ok(ProtobufLayoutConstraintTiledPair { + constraint: Some(internal.0.try_into()?), + layout: Some(internal.1.try_into()?), + }) + } +} + +impl TryFrom for (LayoutConstraint, Vec) { + type Error = &'static str; + fn try_from(protobuf: ProtobufLayoutConstraintFloatingPair) -> Result { + let constraint = protobuf + .constraint + .ok_or("LayoutConstraintFloatingPair must have constraint")? + .try_into()?; + let layouts = protobuf + .layouts + .into_iter() + .map(|l| l.try_into()) + .collect::, _>>()?; + Ok((constraint, layouts)) + } +} + +impl TryFrom<(LayoutConstraint, Vec)> for ProtobufLayoutConstraintFloatingPair { + type Error = &'static str; + fn try_from( + internal: (LayoutConstraint, Vec), + ) -> Result { + Ok(ProtobufLayoutConstraintFloatingPair { + constraint: Some(internal.0.try_into()?), + layouts: internal + .1 + .into_iter() + .map(|l| l.try_into()) + .collect::, _>>()?, + }) + } +} + +impl TryFrom for SwapTiledLayout { + type Error = &'static str; + fn try_from(protobuf: ProtobufSwapTiledLayout) -> Result { + let constraint_map: BTreeMap = protobuf + .constraint_map + .into_iter() + .map(|pair| pair.try_into()) + .collect::, _>>()?; + Ok((constraint_map, protobuf.name)) + } +} + +impl TryFrom for ProtobufSwapTiledLayout { + type Error = &'static str; + fn try_from(internal: SwapTiledLayout) -> Result { + let constraint_map = internal + .0 + .into_iter() + .map(|(constraint, layout)| (constraint, layout).try_into()) + .collect::, _>>()?; + Ok(ProtobufSwapTiledLayout { + constraint_map, + name: internal.1, + }) + } +} + +impl TryFrom for SwapFloatingLayout { + type Error = &'static str; + fn try_from(protobuf: ProtobufSwapFloatingLayout) -> Result { + let constraint_map: BTreeMap> = protobuf + .constraint_map + .into_iter() + .map(|pair| pair.try_into()) + .collect::, _>>()?; + Ok((constraint_map, protobuf.name)) + } +} + +impl TryFrom for ProtobufSwapFloatingLayout { + type Error = &'static str; + fn try_from(internal: SwapFloatingLayout) -> Result { + let constraint_map = internal + .0 + .into_iter() + .map(|(constraint, layouts)| (constraint, layouts).try_into()) + .collect::, _>>()?; + Ok(ProtobufSwapFloatingLayout { + constraint_map, + name: internal.1, + }) + } +} diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index f50591317..3bdeea19b 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -59,6 +59,9 @@ enum EventType { FailedToStartWebServer = 34; InterceptedKeyPress = 35; PaneRenderReport = 36; + UserAction = 37; + ActionComplete = 38; + CwdChanged = 39; } message EventNameList { @@ -97,9 +100,18 @@ message Event { FailedToStartWebServerPayload failed_to_start_web_server_payload = 28; key.Key intercepted_key_payload = 29; PaneRenderReportPayload pane_render_report_payload = 30; + UserActionPayload user_action_payload = 31; + ActionCompletePayload action_complete_payload = 32; + CwdChangedPayload cwd_changed_payload = 33; } } +message CwdChangedPayload { + PaneId pane_id = 1; + string new_cwd = 2; + repeated uint32 focused_client_ids = 3; +} + message FailedToStartWebServerPayload { string error = 1; } @@ -284,6 +296,7 @@ message SessionManifest { bool web_clients_allowed = 8; uint32 web_client_count = 9; repeated ClientTabHistory tab_history = 10; + repeated ClientPaneHistory pane_history = 11; } message ClientTabHistory { @@ -291,6 +304,11 @@ message ClientTabHistory { repeated uint32 tab_history = 2; } +message ClientPaneHistory { + uint32 client_id = 1; + repeated PaneId pane_history = 2; +} + message PluginInfo { uint32 plugin_id = 1; string plugin_url = 2; @@ -418,3 +436,16 @@ message SelectedText { action.Position start = 1; action.Position end = 2; } + +message UserActionPayload { + action.Action action = 1; + uint32 client_id = 2; + optional uint32 terminal_id = 3; + optional uint32 cli_client_id = 4; +} + +message ActionCompletePayload { + action.Action action = 1; + optional PaneId pane_id = 2; + repeated ContextItem context = 3; +} diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index 02ef1842b..4e430b0b9 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -2,20 +2,24 @@ pub use super::generated_api::api::{ action::{Action as ProtobufAction, Position as ProtobufPosition}, event::{ event::Payload as ProtobufEventPayload, pane_scrollback_response, - ClientInfo as ProtobufClientInfo, ClientTabHistory as ProtobufClientTabHistory, - CopyDestination as ProtobufCopyDestination, Event as ProtobufEvent, - EventNameList as ProtobufEventNameList, EventType as ProtobufEventType, - FileMetadata as ProtobufFileMetadata, InputModeKeybinds as ProtobufInputModeKeybinds, - KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo, - ModeUpdatePayload as ProtobufModeUpdatePayload, PaneContents as ProtobufPaneContents, - PaneContentsEntry as ProtobufPaneContentsEntry, PaneId as ProtobufPaneId, - PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest, + ActionCompletePayload as ProtobufActionCompletePayload, ClientInfo as ProtobufClientInfo, + ClientPaneHistory as ProtobufClientPaneHistory, + ClientTabHistory as ProtobufClientTabHistory, ContextItem as ProtobufContextItem, + CopyDestination as ProtobufCopyDestination, CwdChangedPayload as ProtobufCwdChangedPayload, + Event as ProtobufEvent, EventNameList as ProtobufEventNameList, + EventType as ProtobufEventType, FileMetadata as ProtobufFileMetadata, + InputModeKeybinds as ProtobufInputModeKeybinds, KeyBind as ProtobufKeyBind, + LayoutInfo as ProtobufLayoutInfo, ModeUpdatePayload as ProtobufModeUpdatePayload, + PaneContents as ProtobufPaneContents, PaneContentsEntry as ProtobufPaneContentsEntry, + PaneId as ProtobufPaneId, PaneInfo as ProtobufPaneInfo, + PaneManifest as ProtobufPaneManifest, PaneRenderReportPayload as ProtobufPaneRenderReportPayload, PaneScrollbackResponse as ProtobufPaneScrollbackResponse, PaneType as ProtobufPaneType, PluginInfo as ProtobufPluginInfo, ResurrectableSession as ProtobufResurrectableSession, SelectedText as ProtobufSelectedText, SessionManifest as ProtobufSessionManifest, - TabInfo as ProtobufTabInfo, WebServerStatusPayload as ProtobufWebServerStatusPayload, - WebSharing as ProtobufWebSharing, *, + TabInfo as ProtobufTabInfo, UserActionPayload as ProtobufUserActionPayload, + WebServerStatusPayload as ProtobufWebServerStatusPayload, WebSharing as ProtobufWebSharing, + *, }, input_mode::InputMode as ProtobufInputMode, key::Key as ProtobufKey, @@ -23,8 +27,8 @@ pub use super::generated_api::api::{ }; #[allow(hidden_glob_reexports)] use crate::data::{ - ClientInfo, CopyDestination, Event, EventType, FileMetadata, InputMode, KeyWithModifier, - LayoutInfo, ModeInfo, Mouse, PaneContents, PaneId, PaneInfo, PaneManifest, + ClientId, ClientInfo, CopyDestination, Event, EventType, FileMetadata, InputMode, + KeyWithModifier, LayoutInfo, ModeInfo, Mouse, PaneContents, PaneId, PaneInfo, PaneManifest, PaneScrollbackResponse, PermissionStatus, PluginCapabilities, PluginInfo, SelectedText, SessionInfo, Style, TabInfo, WebServerStatus, WebSharing, }; @@ -397,6 +401,63 @@ impl TryFrom for Event { }, _ => Err("Malformed payload for the PaneRenderReport Event"), }, + Some(ProtobufEventType::UserAction) => match protobuf_event.payload { + Some(ProtobufEventPayload::UserActionPayload(protobuf_payload)) => { + let action: Action = protobuf_payload + .action + .ok_or("Missing action in UserAction payload")? + .try_into() + .map_err(|_| "Failed to convert Action in UserAction payload")?; + let client_id = protobuf_payload.client_id as u16; + let terminal_id = protobuf_payload.terminal_id; + let cli_client_id = protobuf_payload.cli_client_id.map(|id| id as u16); + Ok(Event::UserAction( + action, + client_id, + terminal_id, + cli_client_id, + )) + }, + _ => Err("Malformed payload for the UserAction Event"), + }, + Some(ProtobufEventType::ActionComplete) => match protobuf_event.payload { + Some(ProtobufEventPayload::ActionCompletePayload(protobuf_payload)) => { + let action: Action = protobuf_payload + .action + .ok_or("Missing action in ActionComplete payload")? + .try_into() + .map_err(|_| "Failed to convert Action in ActionComplete payload")?; + let pane_id = protobuf_payload + .pane_id + .map(|id| id.try_into()) + .transpose() + .map_err(|_| "Failed to convert PaneId in ActionComplete payload")?; + let context: BTreeMap = protobuf_payload + .context + .into_iter() + .map(|item| (item.name, item.value)) + .collect(); + Ok(Event::ActionComplete(action, pane_id, context)) + }, + _ => Err("Malformed payload for the ActionComplete Event"), + }, + Some(ProtobufEventType::CwdChanged) => match protobuf_event.payload { + Some(ProtobufEventPayload::CwdChangedPayload(protobuf_payload)) => { + let pane_id: PaneId = protobuf_payload + .pane_id + .ok_or("Missing pane_id in CwdChanged payload")? + .try_into() + .map_err(|_| "Failed to convert PaneId in CwdChanged payload")?; + let new_cwd = PathBuf::from(protobuf_payload.new_cwd); + let focused_client_ids: Vec = protobuf_payload + .focused_client_ids + .into_iter() + .map(|id| id as u16) + .collect(); + Ok(Event::CwdChanged(pane_id, new_cwd, focused_client_ids)) + }, + _ => Err("Malformed payload for the CwdChanged Event"), + }, None => Err("Unknown Protobuf Event"), } } @@ -797,6 +858,58 @@ impl TryFrom for ProtobufEvent { pane_contents_map.try_into()?, )), }), + Event::UserAction(action, client_id, terminal_id, cli_client_id) => { + let protobuf_action: ProtobufAction = action + .try_into() + .map_err(|_| "Failed to convert Action to protobuf")?; + let protobuf_payload = ProtobufUserActionPayload { + action: Some(protobuf_action), + client_id: client_id as u32, + terminal_id, + cli_client_id: cli_client_id.map(|id| id as u32), + }; + Ok(ProtobufEvent { + name: ProtobufEventType::UserAction as i32, + payload: Some(event::Payload::UserActionPayload(protobuf_payload)), + }) + }, + Event::ActionComplete(action, pane_id, context) => { + let protobuf_action = action.try_into()?; + let protobuf_pane_id = pane_id.map(|id| id.try_into()).transpose()?; + let context_items: Vec = context + .into_iter() + .map(|(name, value)| ProtobufContextItem { name, value }) + .collect(); + let action_complete_payload = ProtobufActionCompletePayload { + action: Some(protobuf_action), + pane_id: protobuf_pane_id, + context: context_items, + }; + Ok(ProtobufEvent { + name: ProtobufEventType::ActionComplete as i32, + payload: Some(event::Payload::ActionCompletePayload( + action_complete_payload, + )), + }) + }, + Event::CwdChanged(pane_id, new_cwd, focused_client_ids) => { + let protobuf_pane_id: ProtobufPaneId = pane_id.try_into()?; + let new_cwd_string = new_cwd + .to_str() + .ok_or("Failed to convert PathBuf to string")? + .to_string(); + let focused_client_ids_u32: Vec = + focused_client_ids.into_iter().map(|id| id as u32).collect(); + let cwd_changed_payload = ProtobufCwdChangedPayload { + pane_id: Some(protobuf_pane_id), + new_cwd: new_cwd_string, + focused_client_ids: focused_client_ids_u32, + }; + Ok(ProtobufEvent { + name: ProtobufEventType::CwdChanged as i32, + payload: Some(event::Payload::CwdChangedPayload(cwd_changed_payload)), + }) + }, } } } @@ -842,6 +955,11 @@ impl TryFrom for ProtobufSessionManifest { .into_iter() .map(|t| ProtobufClientTabHistory::from(t)) .collect(), + pane_history: session_info + .pane_history + .into_iter() + .map(|p| ProtobufClientPaneHistory::from(p)) + .collect(), }) } } @@ -854,6 +972,18 @@ impl From<(u16, Vec)> for ProtobufClientTabHistory { } } } + +impl From<(u16, Vec)> for ProtobufClientPaneHistory { + fn from((client_id, pane_history): (u16, Vec)) -> ProtobufClientPaneHistory { + ProtobufClientPaneHistory { + client_id: client_id as u32, + pane_history: pane_history + .into_iter() + .filter_map(|p| p.try_into().ok()) + .collect(), + } + } +} impl From<(u32, PluginInfo)> for ProtobufPluginInfo { fn from((plugin_id, plugin_info): (u32, PluginInfo)) -> ProtobufPluginInfo { ProtobufPluginInfo { @@ -910,6 +1040,16 @@ impl TryFrom for SessionInfo { .collect(); tab_history.insert(client_id as u16, tab_history_for_client); } + let mut pane_history = BTreeMap::new(); + for client_pane_history in protobuf_session_manifest.pane_history.into_iter() { + let client_id = client_pane_history.client_id; + let pane_history_for_client = client_pane_history + .pane_history + .into_iter() + .filter_map(|p| p.try_into().ok()) + .collect(); + pane_history.insert(client_id as u16, pane_history_for_client); + } Ok(SessionInfo { name: protobuf_session_manifest.name, tabs: protobuf_session_manifest @@ -929,6 +1069,7 @@ impl TryFrom for SessionInfo { web_clients_allowed: protobuf_session_manifest.web_clients_allowed, web_client_count: protobuf_session_manifest.web_client_count as usize, tab_history, + pane_history, }) } } @@ -1476,6 +1617,9 @@ impl TryFrom for EventType { ProtobufEventType::FailedToStartWebServer => EventType::FailedToStartWebServer, ProtobufEventType::InterceptedKeyPress => EventType::InterceptedKeyPress, ProtobufEventType::PaneRenderReport => EventType::PaneRenderReport, + ProtobufEventType::UserAction => EventType::UserAction, + ProtobufEventType::ActionComplete => EventType::ActionComplete, + ProtobufEventType::CwdChanged => EventType::CwdChanged, }) } } @@ -1520,6 +1664,9 @@ impl TryFrom for ProtobufEventType { EventType::FailedToStartWebServer => ProtobufEventType::FailedToStartWebServer, EventType::InterceptedKeyPress => ProtobufEventType::InterceptedKeyPress, EventType::PaneRenderReport => ProtobufEventType::PaneRenderReport, + EventType::UserAction => ProtobufEventType::UserAction, + EventType::ActionComplete => ProtobufEventType::ActionComplete, + EventType::CwdChanged => ProtobufEventType::CwdChanged, }) } } @@ -2129,6 +2276,7 @@ fn serialize_session_update_event_with_non_default_values() { web_clients_allowed: false, web_client_count: 1, tab_history, + pane_history: Default::default(), }; let session_info_2 = SessionInfo { name: "session 2".to_owned(), @@ -2147,6 +2295,7 @@ fn serialize_session_update_event_with_non_default_values() { web_clients_allowed: false, web_client_count: 0, tab_history: Default::default(), + pane_history: Default::default(), }; let session_infos = vec![session_info_1, session_info_2]; let resurrectable_sessions = vec![]; diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 7ffe2fc1d..f662c93ba 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -160,6 +160,12 @@ enum CommandName { ClearKeyPressesIntercepts = 144; ReplacePaneWithExistingPane = 155; GetPaneScrollback = 163; + RunAction = 164; + ShowCursor = 169; + CopyToClipboard = 170; + SendSigintToPaneId = 171; + SendSigkillToPaneId = 172; + GetPanePid = 173; } message PluginCommand { @@ -268,6 +274,12 @@ message PluginCommand { ReplacePaneWithExistingPanePayload replace_pane_with_existing_pane_payload = 111; NewTabPayload new_tab_payload = 112; GetPaneScrollbackPayload get_pane_scrollback_payload = 113; + RunActionPayload run_action_payload = 114; + ShowCursorPayload show_cursor_payload = 119; + CopyToClipboardPayload copy_to_clipboard_payload = 120; + PaneId send_sigint_to_pane_id_payload = 121; + PaneId send_sigkill_to_pane_id_payload = 122; + GetPanePidPayload get_pane_pid_payload = 123; } } @@ -276,9 +288,15 @@ message NewTabPayload { optional string cwd = 2; } +message RunActionPayload { + action.Action action = 1; + repeated ContextItem context = 2; +} + message ReplacePaneWithExistingPanePayload { PaneId pane_id_to_replace = 1; PaneId existing_pane_id = 2; + bool suppress_replaced_pane = 3; } message RenameWebLoginTokenPayload { @@ -516,6 +534,7 @@ message HidePaneWithIdPayload { message ShowPaneWithIdPayload { PaneId pane_id = 1; bool should_float_if_hidden = 2; + bool should_focus_pane = 3; } message NewTabsWithLayoutInfoPayload { @@ -710,3 +729,27 @@ message GetPaneScrollbackPayload { PaneId pane_id = 1; bool get_full_scrollback = 2; } + +message ShowCursorPayload { + optional CursorPosition position = 1; +} + +message CursorPosition { + uint32 x = 1; + uint32 y = 2; +} + +message CopyToClipboardPayload { + string text = 1; +} + +message GetPanePidPayload { + PaneId pane_id = 1; +} + +message GetPanePidResponse { + oneof result { + int32 pid = 1; + string error = 2; + } +} diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index c1ba877e2..771bb9ecc 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -1,17 +1,19 @@ pub use super::generated_api::api::{ - action::{PaneIdAndShouldFloat, SwitchToModePayload}, + action::{Action as ProtobufAction, PaneIdAndShouldFloat, SwitchToModePayload}, event::{EventNameList as ProtobufEventNameList, Header}, input_mode::InputMode as ProtobufInputMode, plugin_command::{ - plugin_command::Payload, BreakPanesToNewTabPayload, BreakPanesToTabWithIndexPayload, - ChangeFloatingPanesCoordinatesPayload, ChangeHostFolderPayload, - ClearScreenForPaneIdPayload, CliPipeOutputPayload, CloseMultiplePanesPayload, - CloseTabWithIndexPayload, CommandName, ContextItem, - CreateTokenResponse as ProtobufCreateTokenResponse, CreateTokenResponse, - EditScrollbackForPaneWithIdPayload, EmbedMultiplePanesPayload, EnvVariable, ExecCmdPayload, + get_pane_pid_response, plugin_command::Payload, BreakPanesToNewTabPayload, + BreakPanesToTabWithIndexPayload, ChangeFloatingPanesCoordinatesPayload, + ChangeHostFolderPayload, ClearScreenForPaneIdPayload, CliPipeOutputPayload, + CloseMultiplePanesPayload, CloseTabWithIndexPayload, CommandName, ContextItem, + CopyToClipboardPayload, CreateTokenResponse as ProtobufCreateTokenResponse, + CreateTokenResponse, CursorPosition, EditScrollbackForPaneWithIdPayload, + EmbedMultiplePanesPayload, EnvVariable, ExecCmdPayload, FixedOrPercent as ProtobufFixedOrPercent, FixedOrPercentValue as ProtobufFixedOrPercentValue, FloatMultiplePanesPayload, FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, GenerateWebLoginTokenPayload, + GetPanePidPayload, GetPanePidResponse as ProtobufGetPanePidResponse, GetPaneScrollbackPayload, GroupAndUngroupPanesPayload, HidePaneWithIdPayload, HighlightAndUnhighlightPanesPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload, ListTokensResponse, LoadNewPluginPayload, @@ -29,20 +31,21 @@ pub use super::generated_api::api::{ RenameWebTokenResponse, ReplacePaneWithExistingPanePayload, RequestPluginPermissionPayload, RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload, RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload, - RunCommandPayload, ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, - ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SetFloatingPanePinnedPayload, - SetSelfMouseSelectionSupportPayload, SetTimeoutPayload, ShowPaneWithIdPayload, - StackPanesPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, - TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, UnsubscribePayload, - WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload, + RunActionPayload, RunCommandPayload, ScrollDownInPaneIdPayload, + ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, + SetFloatingPanePinnedPayload, SetSelfMouseSelectionSupportPayload, SetTimeoutPayload, + ShowCursorPayload, ShowPaneWithIdPayload, StackPanesPayload, SubscribePayload, + SwitchSessionPayload, SwitchTabToPayload, TogglePaneEmbedOrEjectForPaneIdPayload, + TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload, + WriteCharsToPaneIdPayload, WriteToPaneIdPayload, }, plugin_permission::PermissionType as ProtobufPermissionType, resize::ResizeAction as ProtobufResizeAction, }; use crate::data::{ - ConnectToSession, FloatingPaneCoordinates, HttpVerb, InputMode, KeyWithModifier, - MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand, + ConnectToSession, FloatingPaneCoordinates, GetPanePidResponse, HttpVerb, InputMode, + KeyWithModifier, MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand, }; use crate::input::actions::Action; use crate::input::layout::SplitSize; @@ -200,6 +203,30 @@ impl TryFrom for ProtobufPaneId { } } +impl TryFrom for GetPanePidResponse { + type Error = &'static str; + fn try_from(protobuf_response: ProtobufGetPanePidResponse) -> Result { + match protobuf_response.result { + Some(get_pane_pid_response::Result::Pid(pid)) => Ok(GetPanePidResponse::Ok(pid)), + Some(get_pane_pid_response::Result::Error(error)) => Ok(GetPanePidResponse::Err(error)), + None => Err("Empty GetPanePidResponse"), + } + } +} + +impl From for ProtobufGetPanePidResponse { + fn from(response: GetPanePidResponse) -> Self { + match response { + GetPanePidResponse::Ok(pid) => ProtobufGetPanePidResponse { + result: Some(get_pane_pid_response::Result::Pid(pid)), + }, + GetPanePidResponse::Err(error) => ProtobufGetPanePidResponse { + result: Some(get_pane_pid_response::Result::Error(error)), + }, + } + } +} + impl TryFrom<(InputMode, KeyWithModifier, Vec)> for KeyToRebind { type Error = &'static str; fn try_from( @@ -288,6 +315,14 @@ impl TryFrom for PluginCommand { }, _ => Err("Mismatched payload for SetSelectable"), }, + Some(CommandName::ShowCursor) => match protobuf_plugin_command.payload { + Some(Payload::ShowCursorPayload(payload)) => { + let cursor_position = + payload.position.map(|pos| (pos.x as usize, pos.y as usize)); + Ok(PluginCommand::ShowCursor(cursor_position)) + }, + _ => Err("Mismatched payload for ShowCursor"), + }, Some(CommandName::GetPluginIds) => { if protobuf_plugin_command.payload.is_some() { Err("GetPluginIds should not have a payload") @@ -734,7 +769,12 @@ impl TryFrom for PluginCommand { Some(Payload::FocusTerminalPanePayload(payload)) => { let pane_id = payload.pane_id as u32; let should_float = payload.should_float; - Ok(PluginCommand::FocusTerminalPane(pane_id, should_float)) + let should_be_in_place = payload.should_be_in_place; + Ok(PluginCommand::FocusTerminalPane( + pane_id, + should_float, + should_be_in_place, + )) }, _ => Err("Mismatched payload for ClosePluginPane"), }, @@ -742,7 +782,12 @@ impl TryFrom for PluginCommand { Some(Payload::FocusPluginPanePayload(payload)) => { let pane_id = payload.pane_id as u32; let should_float = payload.should_float; - Ok(PluginCommand::FocusPluginPane(pane_id, should_float)) + let should_be_in_place = payload.should_be_in_place; + Ok(PluginCommand::FocusPluginPane( + pane_id, + should_float, + should_be_in_place, + )) }, _ => Err("Mismatched payload for ClosePluginPane"), }, @@ -1048,9 +1093,11 @@ impl TryFrom for PluginCommand { .and_then(|p_id| PaneId::try_from(p_id).ok()) .ok_or("Failed to parse ShowPaneWithId command")?; let should_float_if_hidden = show_pane_with_id_payload.should_float_if_hidden; + let should_focus_pane = show_pane_with_id_payload.should_focus_pane; Ok(PluginCommand::ShowPaneWithId( pane_id, should_float_if_hidden, + should_focus_pane, )) }, _ => Err("Mismatched payload for ShowPaneWithId"), @@ -1145,6 +1192,29 @@ impl TryFrom for PluginCommand { }, _ => Err("Mismatched payload for WriteCharsCharsToPaneId"), }, + Some(CommandName::SendSigintToPaneId) => match protobuf_plugin_command.payload { + Some(Payload::SendSigintToPaneIdPayload(pane_id)) => { + Ok(PluginCommand::SendSigintToPaneId(pane_id.try_into()?)) + }, + _ => Err("Mismatched payload for SendSigintToPaneId"), + }, + Some(CommandName::SendSigkillToPaneId) => match protobuf_plugin_command.payload { + Some(Payload::SendSigkillToPaneIdPayload(pane_id)) => { + Ok(PluginCommand::SendSigkillToPaneId(pane_id.try_into()?)) + }, + _ => Err("Mismatched payload for SendSigkillToPaneId"), + }, + Some(CommandName::GetPanePid) => match protobuf_plugin_command.payload { + Some(Payload::GetPanePidPayload(get_pane_pid_payload)) => { + match get_pane_pid_payload.pane_id { + Some(pane_id) => Ok(PluginCommand::GetPanePid { + pane_id: pane_id.try_into()?, + }), + _ => Err("Malformed get_pane_pid payload"), + } + }, + _ => Err("Mismatched payload for GetPanePid"), + }, Some(CommandName::MovePaneWithPaneId) => match protobuf_plugin_command.payload { Some(Payload::MovePaneWithPaneIdPayload(move_pane_with_pane_id_payload)) => { match move_pane_with_pane_id_payload.pane_id { @@ -1745,9 +1815,35 @@ impl TryFrom for PluginCommand { .existing_pane_id .and_then(|p_id| PaneId::try_from(p_id).ok()) .ok_or("Failed to parse ReplacePaneWithExistingPanePayload")?, + replace_pane_with_other_pane_payload.suppress_replaced_pane, )), _ => Err("Mismatched payload for ReplacePaneWithExistingPane"), }, + Some(CommandName::RunAction) => match protobuf_plugin_command.payload { + Some(Payload::RunActionPayload(protobuf_payload)) => { + let action = Action::try_from( + protobuf_payload + .action + .ok_or("Missing action in RunAction payload")?, + ) + .map_err(|_| "Failed to convert protobuf action")?; + + let context: BTreeMap = protobuf_payload + .context + .into_iter() + .map(|item| (item.name, item.value)) + .collect(); + + Ok(PluginCommand::RunAction(action, context)) + }, + _ => Err("Mismatched payload for RunAction"), + }, + Some(CommandName::CopyToClipboard) => match protobuf_plugin_command.payload { + Some(Payload::CopyToClipboardPayload(payload)) => { + Ok(PluginCommand::CopyToClipboard(payload.text)) + }, + _ => Err("Mismatched payload for CopyToClipboard"), + }, None => Err("Unrecognized plugin command"), } } @@ -1779,6 +1875,16 @@ impl TryFrom for ProtobufPluginCommand { name: CommandName::SetSelectable as i32, payload: Some(Payload::SetSelectablePayload(should_be_selectable)), }), + PluginCommand::ShowCursor(cursor_position) => { + let position = cursor_position.map(|(x, y)| CursorPosition { + x: x as u32, + y: y as u32, + }); + Ok(ProtobufPluginCommand { + name: CommandName::ShowCursor as i32, + payload: Some(Payload::ShowCursorPayload(ShowCursorPayload { position })), + }) + }, PluginCommand::GetPluginIds => Ok(ProtobufPluginCommand { name: CommandName::GetPluginIds as i32, payload: None, @@ -2077,24 +2183,30 @@ impl TryFrom for ProtobufPluginCommand { name: CommandName::ClosePluginPane as i32, payload: Some(Payload::ClosePluginPanePayload(pane_id)), }), - PluginCommand::FocusTerminalPane(pane_id, should_float_if_hidden) => { - Ok(ProtobufPluginCommand { - name: CommandName::FocusTerminalPane as i32, - payload: Some(Payload::FocusTerminalPanePayload(PaneIdAndShouldFloat { - pane_id: pane_id, - should_float: should_float_if_hidden, - })), - }) - }, - PluginCommand::FocusPluginPane(pane_id, should_float_if_hidden) => { - Ok(ProtobufPluginCommand { - name: CommandName::FocusPluginPane as i32, - payload: Some(Payload::FocusPluginPanePayload(PaneIdAndShouldFloat { - pane_id: pane_id, - should_float: should_float_if_hidden, - })), - }) - }, + PluginCommand::FocusTerminalPane( + pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ) => Ok(ProtobufPluginCommand { + name: CommandName::FocusTerminalPane as i32, + payload: Some(Payload::FocusTerminalPanePayload(PaneIdAndShouldFloat { + pane_id: pane_id, + should_float: should_float_if_hidden, + should_be_in_place: should_be_in_place_if_hidden, + })), + }), + PluginCommand::FocusPluginPane( + pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ) => Ok(ProtobufPluginCommand { + name: CommandName::FocusPluginPane as i32, + payload: Some(Payload::FocusPluginPanePayload(PaneIdAndShouldFloat { + pane_id: pane_id, + should_float: should_float_if_hidden, + should_be_in_place: should_be_in_place_if_hidden, + })), + }), PluginCommand::RenameTerminalPane(pane_id, new_name) => Ok(ProtobufPluginCommand { name: CommandName::RenameTerminalPane as i32, payload: Some(Payload::RenameTerminalPanePayload(IdAndNewName { @@ -2335,15 +2447,18 @@ impl TryFrom for ProtobufPluginCommand { pane_id: ProtobufPaneId::try_from(pane_id_to_hide).ok(), })), }), - PluginCommand::ShowPaneWithId(pane_id_to_show, should_float_if_hidden) => { - Ok(ProtobufPluginCommand { - name: CommandName::ShowPaneWithId as i32, - payload: Some(Payload::ShowPaneWithIdPayload(ShowPaneWithIdPayload { - pane_id: ProtobufPaneId::try_from(pane_id_to_show).ok(), - should_float_if_hidden, - })), - }) - }, + PluginCommand::ShowPaneWithId( + pane_id_to_show, + should_float_if_hidden, + should_focus_pane, + ) => Ok(ProtobufPluginCommand { + name: CommandName::ShowPaneWithId as i32, + payload: Some(Payload::ShowPaneWithIdPayload(ShowPaneWithIdPayload { + pane_id: ProtobufPaneId::try_from(pane_id_to_show).ok(), + should_float_if_hidden, + should_focus_pane, + })), + }), PluginCommand::OpenCommandPaneBackground(command_to_run, context) => { let context: Vec<_> = context .into_iter() @@ -2415,6 +2530,20 @@ impl TryFrom for ProtobufPluginCommand { )), }) }, + PluginCommand::SendSigintToPaneId(pane_id) => Ok(ProtobufPluginCommand { + name: CommandName::SendSigintToPaneId as i32, + payload: Some(Payload::SendSigintToPaneIdPayload(pane_id.try_into()?)), + }), + PluginCommand::SendSigkillToPaneId(pane_id) => Ok(ProtobufPluginCommand { + name: CommandName::SendSigkillToPaneId as i32, + payload: Some(Payload::SendSigkillToPaneIdPayload(pane_id.try_into()?)), + }), + PluginCommand::GetPanePid { pane_id } => Ok(ProtobufPluginCommand { + name: CommandName::GetPanePid as i32, + payload: Some(Payload::GetPanePidPayload(GetPanePidPayload { + pane_id: Some(pane_id.try_into()?), + })), + }), PluginCommand::MovePaneWithPaneId(pane_id) => Ok(ProtobufPluginCommand { name: CommandName::MovePaneWithPaneId as i32, payload: Some(Payload::MovePaneWithPaneIdPayload( @@ -2901,17 +3030,43 @@ impl TryFrom for ProtobufPluginCommand { name: CommandName::ClearKeyPressesIntercepts as i32, payload: None, }), - PluginCommand::ReplacePaneWithExistingPane(pane_id_to_replace, existing_pane_id) => { + PluginCommand::ReplacePaneWithExistingPane( + pane_id_to_replace, + existing_pane_id, + suppress_replaced_pane, + ) => Ok(ProtobufPluginCommand { + name: CommandName::ReplacePaneWithExistingPane as i32, + payload: Some(Payload::ReplacePaneWithExistingPanePayload( + ReplacePaneWithExistingPanePayload { + pane_id_to_replace: ProtobufPaneId::try_from(pane_id_to_replace).ok(), + existing_pane_id: ProtobufPaneId::try_from(existing_pane_id).ok(), + suppress_replaced_pane, + }, + )), + }), + PluginCommand::RunAction(action, context) => { + let protobuf_action = ProtobufAction::try_from(action) + .map_err(|_| "Failed to convert action to protobuf")?; + + let context_items: Vec = context + .into_iter() + .map(|(name, value)| ContextItem { name, value }) + .collect(); + Ok(ProtobufPluginCommand { - name: CommandName::ReplacePaneWithExistingPane as i32, - payload: Some(Payload::ReplacePaneWithExistingPanePayload( - ReplacePaneWithExistingPanePayload { - pane_id_to_replace: ProtobufPaneId::try_from(pane_id_to_replace).ok(), - existing_pane_id: ProtobufPaneId::try_from(existing_pane_id).ok(), - }, - )), + name: CommandName::RunAction as i32, + payload: Some(Payload::RunActionPayload(RunActionPayload { + action: Some(protobuf_action), + context: context_items, + })), }) }, + PluginCommand::CopyToClipboard(text) => Ok(ProtobufPluginCommand { + name: CommandName::CopyToClipboard as i32, + payload: Some(Payload::CopyToClipboardPayload(CopyToClipboardPayload { + text, + })), + }), } } } diff --git a/zellij-utils/src/plugin_api/plugin_permission.proto b/zellij-utils/src/plugin_api/plugin_permission.proto index 851268301..779f6b321 100644 --- a/zellij-utils/src/plugin_api/plugin_permission.proto +++ b/zellij-utils/src/plugin_api/plugin_permission.proto @@ -17,4 +17,6 @@ enum PermissionType { StartWebServer = 11; InterceptInput = 12; ReadPaneContents = 13; + RunActionsAsUser = 14; + WriteToClipboard = 15; } diff --git a/zellij-utils/src/plugin_api/plugin_permission.rs b/zellij-utils/src/plugin_api/plugin_permission.rs index 307d35766..37782a086 100644 --- a/zellij-utils/src/plugin_api/plugin_permission.rs +++ b/zellij-utils/src/plugin_api/plugin_permission.rs @@ -29,6 +29,8 @@ impl TryFrom for PermissionType { ProtobufPermissionType::StartWebServer => Ok(PermissionType::StartWebServer), ProtobufPermissionType::InterceptInput => Ok(PermissionType::InterceptInput), ProtobufPermissionType::ReadPaneContents => Ok(PermissionType::ReadPaneContents), + ProtobufPermissionType::RunActionsAsUser => Ok(PermissionType::RunActionsAsUser), + ProtobufPermissionType::WriteToClipboard => Ok(PermissionType::WriteToClipboard), } } } @@ -59,6 +61,8 @@ impl TryFrom for ProtobufPermissionType { PermissionType::StartWebServer => Ok(ProtobufPermissionType::StartWebServer), PermissionType::InterceptInput => Ok(ProtobufPermissionType::InterceptInput), PermissionType::ReadPaneContents => Ok(ProtobufPermissionType::ReadPaneContents), + PermissionType::RunActionsAsUser => Ok(ProtobufPermissionType::RunActionsAsUser), + PermissionType::WriteToClipboard => Ok(ProtobufPermissionType::WriteToClipboard), } } } diff --git a/zellij-utils/src/position.rs b/zellij-utils/src/position.rs index 3566625bd..95987c27c 100644 --- a/zellij-utils/src/position.rs +++ b/zellij-utils/src/position.rs @@ -37,6 +37,12 @@ impl Position { } } +impl Default for Position { + fn default() -> Self { + Position::new(0, 0) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd)] pub struct Line(pub isize); #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd)] diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap index e01b74102..3e60c3179 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap @@ -1348,6 +1348,7 @@ Config { NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }, SwitchToMode { input_mode: Normal, @@ -1853,6 +1854,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -4442,6 +4445,36 @@ Config { }: [ ToggleGroupMarking, ], + KeyWithModifier { + bare_key: Char( + 'q', + ), + key_modifiers: {}, + }: [ + LaunchPlugin { + plugin: RunPlugin( + RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, + ), + should_float: true, + should_open_in_place: false, + skip_cache: false, + cwd: None, + }, + SwitchToMode { + input_mode: Normal, + }, + ], KeyWithModifier { bare_key: Char( 'q', @@ -5584,6 +5617,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -6036,6 +6071,18 @@ Config { ), initial_cwd: None, }, + "sequence": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap index 9c280d9c6..e0fa22f9d 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap @@ -1348,6 +1348,7 @@ Config { NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }, SwitchToMode { input_mode: Normal, @@ -1853,6 +1854,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -4442,6 +4445,36 @@ Config { }: [ ToggleGroupMarking, ], + KeyWithModifier { + bare_key: Char( + 'q', + ), + key_modifiers: {}, + }: [ + LaunchPlugin { + plugin: RunPlugin( + RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, + ), + should_float: true, + should_open_in_place: false, + skip_cache: false, + cwd: None, + }, + SwitchToMode { + input_mode: Normal, + }, + ], KeyWithModifier { bare_key: Char( 'q', @@ -5584,6 +5617,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -6036,6 +6071,18 @@ Config { ), initial_cwd: None, }, + "sequence": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap index 773ae7e4c..1d245a3e6 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap @@ -195,6 +195,18 @@ Config { ), initial_cwd: None, }, + "sequence": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( 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 0406faa82..e9935ef3c 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 @@ -1348,6 +1348,7 @@ Config { NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }, SwitchToMode { input_mode: Normal, @@ -1853,6 +1854,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -4442,6 +4445,36 @@ Config { }: [ ToggleGroupMarking, ], + KeyWithModifier { + bare_key: Char( + 'q', + ), + key_modifiers: {}, + }: [ + LaunchPlugin { + plugin: RunPlugin( + RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, + ), + should_float: true, + should_open_in_place: false, + skip_cache: false, + cwd: None, + }, + SwitchToMode { + input_mode: Normal, + }, + ], KeyWithModifier { bare_key: Char( 'q', @@ -5584,6 +5617,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -7807,6 +7842,18 @@ Config { ), initial_cwd: None, }, + "sequence": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap index ca2b7baed..368b5d883 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap @@ -1348,6 +1348,7 @@ Config { NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }, SwitchToMode { input_mode: Normal, @@ -1853,6 +1854,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -4442,6 +4445,36 @@ Config { }: [ ToggleGroupMarking, ], + KeyWithModifier { + bare_key: Char( + 'q', + ), + key_modifiers: {}, + }: [ + LaunchPlugin { + plugin: RunPlugin( + RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, + ), + should_float: true, + should_open_in_place: false, + skip_cache: false, + cwd: None, + }, + SwitchToMode { + input_mode: Normal, + }, + ], KeyWithModifier { bare_key: Char( 'q', @@ -5584,6 +5617,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -6036,6 +6071,18 @@ Config { ), initial_cwd: None, }, + "sequence": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij(