aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-12-11 14:25:42 +0100
committerGitHub <noreply@github.com>2025-12-11 14:25:42 +0100
commit693fa082def5ef0fe030cd7fdc3b1df051b35dc6 (patch)
treeb3f651462bbc1cd2fa5b2d79207343413ef6b83c
parentfix(zellij-server): unblock building on 32bit targets (#4484) (diff)
downloadsl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.tar.gz
sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.tar.xz
sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.zip
Command Sequences, Blocking CLI Commands, New Plugin APIs (#4546)
* fix deadlocks * initial logging to plugin implementation * only log actions, include keys as Action::Write * send actions to plugins as a new Event::UserAction * add Action::Write metadata to plugin API * run_action plugin command and permission * block-until-exit/success/failure in the cli * fix tests * add NewBlockingPane to the plugin API * run actions in a different thread so as not to block plugin execution * add custom context to run_action flow * fixes for floating pane z_index sorting and unknown protobuf events not crashing plugins * always render the frame of pinned panes * macros module * add macros to config * KDL macro parsing * thread macros to the plugin thread * get/update/delete macros from plugins * add macros updated event * add ability to rename macros * fix deadlock * fix z_indices with pinned unselectable panes bug * allow plugins to opt-in to show_cursor * change show_cursor(bool) to show_cursor(Option<coordinates>) * test * upgrade strum to allow proper enum iteration and case insensitive fromstr implementation for Actions * better cwd tracking for exited command panes * add initial panes to NewTab Action (no-op for now) * add initial panes to cli newtab action * make initial panes work * add close_on_exit and start_suspended to the cli * properly convert newtab action (potentially breaking for plugins) * allow first pane in tab to block * make distinction between opening panes with the last active client and the current pane in the cli * improve some apis * fix fixed size closing pane bug * fix plugin cursor coordinates * fix issues with launch-or-focus-plugin having to do with in-place and suppressed panes * fix: set affected pane in replace pane * allow setting a pane in place when it is suppressed and being focused (tests failing) * fix tests * some fixes for unsuppression of panes * some fixes for completion_tx in new tab action * allow replace/suppressed rather than replace/close only * fixes for inserting suppressed panes without conflicts * pane focus history and explicit unsuppress plugin api * fix: prefer explicitly provided pane id in placement in actions that provide it * fix floating pane coordinate change and z_index * fix plugin cache dir to persist between sessions * fix some stack focusing issues when adding panes without a client id * make sure pinned panes are always above non-pinned floating panes * allow plugins to copy to clipboard * initial text styling additions implementations - buggy * fix dimming issues * remove extras, stay with just dim and unbold * unbold and dim as indices * allow ui components to render success/error colors * allow plugins to send sigint/sigkill to processes, as well as receive their pid * ui component fixes and send originating plugin context with run_action * originating plugin for new tab too * make plugn show pane command also expand stacked panes * add sequence plugin * remove macros * remove snapshots * add shortcut for sequence plugin * fix plugin tests * plugin tests for new apis * update snapshots for sequence shortcut * cleanups and formatting * Event::CwdChanged * moar cleanups * remove some warnings * remove mroe warnings * remove dead code * remove more dead code * fix minor bugs in sequence plugin * update sequence help and format codebase * fix conditional blocking cli commands * fix initial-command new-tab cli flag * add client_ids to CwdChanged Event * last fixes * add pr
-rw-r--r--CHANGELOG.md1
-rw-r--r--Cargo.lock35
-rw-r--r--Cargo.toml5
-rw-r--r--default-plugins/compact-bar/src/keybind_utils.rs7
-rw-r--r--default-plugins/configuration/src/presets.rs36
-rw-r--r--default-plugins/fixture-plugin-for-tests/src/main.rs42
-rw-r--r--default-plugins/plugin-manager/src/main.rs2
-rw-r--r--default-plugins/sequence/.cargo/config.toml2
-rw-r--r--default-plugins/sequence/.gitignore1
-rw-r--r--default-plugins/sequence/Cargo.toml16
l---------default-plugins/sequence/LICENSE.md1
-rw-r--r--default-plugins/sequence/src/lib.rs5
-rw-r--r--default-plugins/sequence/src/main.rs1131
-rw-r--r--default-plugins/sequence/src/path_formatting.rs71
-rw-r--r--default-plugins/sequence/src/state/chain_type.rs45
-rw-r--r--default-plugins/sequence/src/state/command_entry.rs86
-rw-r--r--default-plugins/sequence/src/state/command_parser.rs327
-rw-r--r--default-plugins/sequence/src/state/command_status.rs26
-rw-r--r--default-plugins/sequence/src/state/editing.rs80
-rw-r--r--default-plugins/sequence/src/state/execution.rs265
-rw-r--r--default-plugins/sequence/src/state/layout.rs73
-rw-r--r--default-plugins/sequence/src/state/mod.rs446
-rw-r--r--default-plugins/sequence/src/state/positioning.rs48
-rw-r--r--default-plugins/sequence/src/state/selection.rs214
-rw-r--r--default-plugins/sequence/src/ui/components.rs596
-rw-r--r--default-plugins/sequence/src/ui/fuzzy_complete.rs321
-rw-r--r--default-plugins/sequence/src/ui/layout_calculations.rs28
-rw-r--r--default-plugins/sequence/src/ui/mod.rs5
-rw-r--r--default-plugins/sequence/src/ui/text_input.rs997
-rw-r--r--default-plugins/sequence/src/ui/truncation.rs373
-rw-r--r--default-plugins/session-manager/src/main.rs4
-rw-r--r--default-plugins/status-bar/src/one_line_ui.rs26
-rw-r--r--default-plugins/status-bar/src/second_line.rs24
-rw-r--r--example/default.kdl1
-rw-r--r--src/main.rs31
-rw-r--r--xtask/src/main.rs4
-rw-r--r--zellij-client/src/old_config_converter/old_config.rs49
-rw-r--r--zellij-client/src/web_client/types.rs16
-rw-r--r--zellij-server/src/background_jobs.rs2
-rw-r--r--zellij-server/src/lib.rs2
-rw-r--r--zellij-server/src/os_input_output.rs6
-rw-r--r--zellij-server/src/panes/floating_panes/mod.rs59
-rw-r--r--zellij-server/src/panes/plugin_pane.rs29
-rw-r--r--zellij-server/src/panes/terminal_pane.rs22
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs8
-rw-r--r--zellij-server/src/panes/tiled_panes/tiled_pane_grid.rs8
-rw-r--r--zellij-server/src/panes/tiled_panes/unit/stacked_panes_tests.rs2
-rw-r--r--zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__table_ui_component.snap14
-rw-r--r--zellij-server/src/plugins/mod.rs70
-rw-r--r--zellij-server/src/plugins/unit/plugin_tests.rs667
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__clear_screen_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focus_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_focused_tab_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_plugin_pane_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__close_terminal_pane_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_plugin_command.snap10
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__copy_to_clipboard_without_permission.snap5
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__detach_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__edit_scrollback_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_next_pane_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_or_create_tab_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_plugin_pane_plugin_command.snap3
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_previous_pane_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_terminal_pane_plugin_command.snap3
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_next_tab_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_previous_tab_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap3
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__hide_cursor_plugin_command.snap11
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_or_tab_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_focus_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__move_pane_with_direction_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tab_plugin_command.snap4
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap4
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap4
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__next_swap_layout_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_floating_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_command_pane_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_floating_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_floating_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_file_with_line_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_floating_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__open_terminal_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_down_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__page_scroll_up_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__previous_swap_layout_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__quit_zellij_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_plugin_pane_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_tab_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rename_terminal_pane_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap3
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__resize_focused_pane_with_direction_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_plugin_command.snap17
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_action_without_permission.snap5
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_down_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_bottom_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_to_top_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__scroll_up_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigint_to_pane_id_plugin_command.snap9
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_sigkill_to_pane_id_plugin_command.snap9
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_cursor_plugin_command.snap16
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_pane_with_id_plugin_command.snap1
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__show_self_plugin_command.snap3
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_active_tab_sync_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_focus_fullscreen_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_embed_or_eject_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_pane_frames_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__toggle_tab_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_pane_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__undo_rename_tab_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_plugin_command.snap2
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs109
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs320
-rw-r--r--zellij-server/src/pty.rs318
-rw-r--r--zellij-server/src/route.rs435
-rw-r--r--zellij-server/src/screen.rs273
-rw-r--r--zellij-server/src/tab/layout_applier.rs19
-rw-r--r--zellij-server/src/tab/mod.rs262
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs18
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs6
-rw-r--r--zellij-server/src/ui/components/nested_list.rs1
-rw-r--r--zellij-server/src/ui/components/ribbon.rs1
-rw-r--r--zellij-server/src/ui/components/table.rs24
-rw-r--r--zellij-server/src/ui/components/text.rs71
-rw-r--r--zellij-server/src/ui/mod.rs1
-rw-r--r--zellij-server/src/ui/overlay/mod.rs97
-rw-r--r--zellij-server/src/ui/overlay/prompt.rs65
-rw-r--r--zellij-server/src/ui/pane_contents_and_ui.rs3
-rw-r--r--zellij-server/src/unit/screen_tests.rs42
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_default_parameters.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_line_number.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_edit_action_with_split_direction.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_command_and_cwd.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_default_parameters.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_floating_pane_and_coordinates.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_pane_action_with_split_direction.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap2
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap4
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_query_tab_names_action.snap2
-rw-r--r--zellij-tile/src/shim.rs140
-rw-r--r--zellij-tile/src/ui_components/nested_list.rs52
-rw-r--r--zellij-tile/src/ui_components/text.rs221
-rw-r--r--zellij-utils/assets/config/default.kdl7
-rwxr-xr-xzellij-utils/assets/plugins/about.wasmbin1231744 -> 1390199 bytes
-rwxr-xr-xzellij-utils/assets/plugins/compact-bar.wasmbin1343193 -> 1548034 bytes
-rwxr-xr-xzellij-utils/assets/plugins/configuration.wasmbin1304492 -> 1450980 bytes
-rwxr-xr-xzellij-utils/assets/plugins/fixture-plugin-for-tests.wasmbin1245311 -> 1445407 bytes
-rwxr-xr-xzellij-utils/assets/plugins/multiple-select.wasmbin1224537 -> 1382984 bytes
-rwxr-xr-xzellij-utils/assets/plugins/plugin-manager.wasmbin1189169 -> 1363473 bytes
-rwxr-xr-xzellij-utils/assets/plugins/sequence.wasmbin0 -> 1419868 bytes
-rwxr-xr-xzellij-utils/assets/plugins/session-manager.wasmbin1273228 -> 1445706 bytes
-rwxr-xr-xzellij-utils/assets/plugins/share.wasmbin1184473 -> 1353298 bytes
-rwxr-xr-xzellij-utils/assets/plugins/status-bar.wasmbin1432227 -> 1558406 bytes
-rwxr-xr-xzellij-utils/assets/plugins/strider.wasmbin1308344 -> 1512515 bytes
-rwxr-xr-xzellij-utils/assets/plugins/tab-bar.wasmbin1261905 -> 1438990 bytes
-rw-r--r--zellij-utils/assets/prost/api.action.rs710
-rw-r--r--zellij-utils/assets/prost/api.event.rs59
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs87
-rw-r--r--zellij-utils/assets/prost/api.plugin_permission.rs6
-rw-r--r--zellij-utils/assets/prost_ipc/client_server_contract.rs79
-rw-r--r--zellij-utils/src/cli.rs128
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto29
-rw-r--r--zellij-utils/src/consts.rs1
-rw-r--r--zellij-utils/src/data.rs115
-rw-r--r--zellij-utils/src/errors.rs13
-rw-r--r--zellij-utils/src/input/actions.rs169
-rw-r--r--zellij-utils/src/input/command.rs28
-rw-r--r--zellij-utils/src/input/layout.rs38
-rw-r--r--zellij-utils/src/input/mouse.rs6
-rw-r--r--zellij-utils/src/input/plugins.rs1
-rw-r--r--zellij-utils/src/ipc.rs16
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs165
-rw-r--r--zellij-utils/src/ipc/tests/roundtrip_tests.rs119
-rw-r--r--zellij-utils/src/kdl/mod.rs101
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap7
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap7
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap2
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap2
-rw-r--r--zellij-utils/src/plugin_api/action.proto295
-rw-r--r--zellij-utils/src/plugin_api/action.rs1340
-rw-r--r--zellij-utils/src/plugin_api/event.proto31
-rw-r--r--zellij-utils/src/plugin_api/event.rs173
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto43
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs259
-rw-r--r--zellij-utils/src/plugin_api/plugin_permission.proto2
-rw-r--r--zellij-utils/src/plugin_api/plugin_permission.rs4
-rw-r--r--zellij-utils/src/position.rs6
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap47
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap47
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap12
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap47
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap47
201 files changed, 12403 insertions, 890 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 828f43c5..d28657f0 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 e7fc87d2..3c7b4b87 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1620,15 +1620,6 @@ dependencies = [
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
@@ -3380,6 +3371,20 @@ dependencies = [
]
[[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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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 41f22088..3fb2b188 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 aeb862d1..2f84497c 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 0c972a1d..7e875073 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 01489860..9f5b7e93 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 8e1c91a8..5cd243f5 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 00000000..6b509f5b
--- /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 00000000..ea8c4bf7
--- /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 00000000..4f97ae6c
--- /dev/null
+++ b/default-plugins/sequence/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "sequence"
+version = "0.1.0"
+authors = ["Aram Drevekenin <aram@poor.dev>"]
+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 00000000..f0608a63
--- /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 00000000..1e124979
--- /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 00000000..0ac286d9
--- /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<String, String>) {
+ 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(&current_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(
+ &current_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(&current_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(&current_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<PaneId>,
+ context: BTreeMap<String, String>,
+) -> 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<String, String>, running_sequence_id: u64) -> bool {
+ if let Some(context_sequence_id) = context.get("sequence_id") {
+ if let Ok(context_id) = context_sequence_id.parse::<u64>() {
+ 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<PaneId>,
+) -> Option<PaneId> {
+ 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<usize>,
+ y: Option<usize>,
+ width: Option<usize>,
+ height: Option<usize>,
+ 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 00000000..01bf50e6
--- /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<PathBuf> {
+ 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<PathBuf> {
+ 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 00000000..3da3316c
--- /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<UnblockCondition> {
+ 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 00000000..96a0020b
--- /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<PathBuf>,
+ 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<PathBuf>) -> 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<PaneId> {
+ 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<PathBuf> {
+ self.cwd.clone()
+ }
+ pub fn set_cwd(&mut self, cwd: Option<PathBuf>) {
+ 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 00000000..7f0898f5
--- /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<ChainType>)> {
+ let mut segments = Vec::new();
+ let mut current_segment = String::new();
+ let mut state = ParseState::Normal;
+ let chars: Vec<char> = 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<String> {
+ 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<String> {
+ 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 00000000..39717e8b
--- /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<i32>, Option<PaneId>),
+ Running(Option<PaneId>),
+ Pending,
+ Interrupted(Option<PaneId>),
+}
+
+impl CommandStatus {
+ pub fn get_pane_id(&self) -> Option<PaneId> {
+ 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 00000000..3deac619
--- /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<TextInput>,
+}
+
+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<String> {
+ 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<usize>,
+ all_commands: &mut Vec<CommandEntry>,
+ current_cwd: &Option<PathBuf>,
+ ) -> (bool, Option<usize>) {
+ 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 00000000..ff630ec0
--- /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<CommandEntry>,
+ pub current_running_command_index: usize,
+ pub is_running: bool,
+ pub sequence_id: u64,
+ pub primary_pane_id_before_sequence: Option<PaneId>,
+}
+
+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<PathBuf>) -> 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<CommandEntry> {
+ 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<CommandStatus> {
+ 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::<Vec<_>>()
+ .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<PathBuf>,
+ global_cwd: &Option<PathBuf>,
+ primary_pane_id: Option<PaneId>,
+ ) {
+ 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 00000000..65a267e0
--- /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<PaneId>,
+ pane_manifest: &PaneManifest,
+ plugin_id: Option<u32>,
+ total_viewport_columns: Option<usize>,
+ total_viewport_rows: Option<usize>,
+ total_commands: usize,
+ _selected_index: Option<usize>,
+ _running_index: usize,
+ _cwd: &Option<PathBuf>,
+ ) {
+ 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 00000000..19738be7
--- /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<PathBuf>,
+ pub plugin_id: Option<u32>,
+ pub client_id: Option<u16>,
+ pub own_rows: Option<usize>,
+ pub own_columns: Option<usize>,
+ pub cwd: Option<PathBuf>,
+ pub original_pane_id: Option<PaneId>,
+ pub primary_pane_id: Option<PaneId>,
+ pub is_first_run: bool,
+ pub pane_manifest: Option<PaneManifest>,
+ pub total_viewport_columns: Option<usize>,
+ pub total_viewport_rows: Option<usize>,
+ pub selection: Selection,
+ pub editing: Editing,
+ pub execution: Execution,
+ pub layout: Layout,
+ pub current_position: Option<FloatingPaneCoordinates>,
+}
+
+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<String> {
+ 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<ChainType>,
+ ) {
+ 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<ChainType>,
+ 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(&current_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<CommandEntry> {
+ 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<CommandStatus> {
+ self.execution.get_current_running_command_status()
+ }
+
+ pub fn handle_editing_submit(&mut self, current_cwd: &Option<PathBuf>) -> 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<PaneId>) {
+ 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<PaneId>) {
+ 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 00000000..6acadfff
--- /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<usize>,
+ y: Option<usize>,
+ width: Option<usize>,
+ height: Option<usize>,
+ 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 00000000..18f83bcd
--- /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<usize>,
+ 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<CommandEntry>,
+ editing: &mut Editing,
+ cwd: &Option<PathBuf>,
+ ) {
+ 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<CommandEntry>,
+ editing: &mut Editing,
+ ) -> Option<PaneId> {
+ // 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<String> {
+ 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 00000000..3fe2ae1a
--- /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 = "<Ctrl c> - interrupt, <Enter> - run from selected";
+const HELP_RUNNING_NO_SELECTION: &str = "<Ctrl c> - interrupt, <↓↑> - navigate";
+const HELP_STOPPED_WITH_SELECTION: &str =
+ "<Ctrl w> - close all, <Enter> - run from selected, <e> - edit";
+const HELP_STOPPED_NO_SELECTION: &str =
+ "<Ctrl w> - close all, <↓↑> - navigate, <Enter> - run from first";
+
+const HELP_ONE_PENDING_COMMAND: &str = "<Enter> - run, <Ctrl Enter> - add command";
+
+const HELP_ALL_COMMANDS_PENDING: &str =
+ "<Enter> - run, <Ctrl Enter> - add command, <↓↑> - navigate";
+const HELP_ALL_COMMANDS_PENDING_WITH_SELECTION: &str =
+ "<Enter> - run, <Ctrl Enter> - add command, <↓↑> - navigate, <e> - edit selected";
+
+const HELP_EDITING_FIRST_LINE: &str =
+ "<Enter> - accept, <Ctrl Enter> - 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, "<Ctrl c>")
+ .color_substring(3, "<Ctrl w>")
+ .color_substring(3, "<↓↑>")
+ .color_substring(3, "<Esc>")
+ .color_substring(3, "<Enter>")
+ .color_substring(3, "<Ctrl Enter>")
+ .color_substring(3, "<e>")
+}
+
+pub fn render_help_lines(
+ sequence: &State,
+ max_width: Option<usize>,
+) -> (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<String> = keybindings
+ .iter()
+ .take(split_point)
+ .map(|(key, _, action)| format!("{} - {}", key, action))
+ .collect();
+ let first_line = first_part.join(", ");
+
+ let second_part: Vec<String> = 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<PathBuf>) -> (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<Text>, is_selected: bool) -> Vec<Text> {
+ 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<PathBuf>,
+ truncated_cmd_text: &str,
+ chain_type: &crate::state::ChainType,
+ status: &crate::state::CommandStatus,
+ is_selected: bool,
+ overflow_indicator: Option<String>,
+) -> (Vec<Text>, 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<String> {
+ 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<String>, 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::<Vec<_>>()
+ .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<String> = 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::<Vec<_>>()
+ .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::<Vec<_>>()
+ .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 00000000..0feae7e4
--- /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<String, PathBuf>,
+) -> Option<CompletionResult> {
+ 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<CompletionResult> {
+ 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<String, PathBuf>,
+ cwd: Option<&PathBuf>,
+) -> Option<String> {
+ // 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 00000000..911dbc62
--- /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<usize>,
+ 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 00000000..6d1ec303
--- /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 00000000..2694e17a
--- /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<char> = 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<char> = 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<char> = 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 00000000..0c4073a3
--- /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<usize>,
+) -> (String, Option<usize>) {
+ 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<usize>,
+) -> (String, Option<usize>) {
+ 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<usize>) {
+ 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<char> = 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<usize>) {
+ let chars: Vec<char> = text.chars().collect();
+ let char_widths: Vec<usize> = 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<usize>) {
+ 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 6be29dc5..5c014779 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 a3fdec77..a73c7b80 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<KeyWithModifier
(s("Toggle Embed"), s("Embed"), single_action_key(&km, &[A::TogglePaneEmbedOrFloating, TO_NORMAL])),
(s("Split Right"), s("Right"), single_action_key(&km, &[A::NewPane{direction: Some(Direction::Right), pane_name: None, start_suppressed: false}, TO_NORMAL])),
(s("Split Down"), s("Down"), single_action_key(&km, &[A::NewPane{direction: Some(Direction::Down), pane_name: None, start_suppressed: false}, TO_NORMAL])),
- (s("Stack"), s("Stack"), single_action_key(&km, &[A::NewStackedPane{command: None, pane_name: None}, TO_NORMAL])),
+ (s("Stack"), s("Stack"), single_action_key(&km, &[A::NewStackedPane{command: None, pane_name: None, near_current_pane: false}, TO_NORMAL])),
(s("Select pane"), s("Select"), to_basemode_key),
]} else if mi.mode == IM::Tab {
// With the default bindings, "Move focus" for tabs is tricky: It binds all the arrow keys
@@ -1406,7 +1406,17 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
};
vec![
- (s("New"), s("New"), single_action_key(&km, &[A::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}, TO_NORMAL])),
+ (s("New"), s("New"), single_action_key(&km, &[A::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: None,
+ first_pane_unblock_condition: None,
+ }, TO_NORMAL])),
(s("Change focus"), s("Move"), focus_keys),
(s("Close"), s("Close"), single_action_key(&km, &[A::CloseTab, TO_NORMAL])),
(s("Rename"), s("Rename"),
@@ -1491,7 +1501,17 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
(s("Split down"), s("Down"), action_key(&km, &[A::NewPane{direction: Some(Dir::Down), pane_name: None, start_suppressed: false}, TO_NORMAL])),
(s("Split right"), s("Right"), action_key(&km, &[A::NewPane{direction: Some(Dir::Right), pane_name: None, start_suppressed: false}, TO_NORMAL])),
(s("Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])),
- (s("New tab"), s("New"), action_key(&km, &[A::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}, TO_NORMAL])),
+ (s("New tab"), s("New"), action_key(&km, &[A::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: None,
+ first_pane_unblock_condition: None,
+ }, TO_NORMAL])),
(s("Rename tab"), s("Rename"),
action_key(&km, &[A::SwitchToMode{input_mode: IM::RenameTab}, A::TabNameInput{input: vec![0]}])),
(s("Previous Tab"), s("Previous"), action_key(&km, &[A::GoToPreviousTab, TO_NORMAL])),
diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs
index 6f3fb02c..00e2b83b 100644
--- a/default-plugins/status-bar/src/second_line.rs
+++ b/default-plugins/status-bar/src/second_line.rs
@@ -173,7 +173,17 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
};
vec![
- (s("New"), s("New"), action_key(&km, &[A::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}, TO_NORMAL])),
+ (s("New"), s("New"), action_key(&km, &[A::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: None,
+ first_pane_unblock_condition: None,
+ }, TO_NORMAL])),
(s("Change focus"), s("Move"), focus_keys),
(s("Close"), s("Close"), action_key(&km, &[A::CloseTab, TO_NORMAL])),
(s("Rename"), s("Rename"),
@@ -253,7 +263,17 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
(s("Split down"), s("Down"), action_key(&km, &[A::NewPane{direction: Some(Dir::Down), pane_name: None, start_suppressed: false}, TO_NORMAL])),
(s("Split right"), s("Right"), action_key(&km, &[A::NewPane{direction: Some(Dir::Right), pane_name: None, start_suppressed: false}, TO_NORMAL])),
(s("Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])),
- (s("New tab"), s("New"), action_key(&km, &[A::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}, TO_NORMAL])),
+ (s("New tab"), s("New"), action_key(&km, &[A::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: None,
+ first_pane_unblock_condition: None,
+ }, TO_NORMAL])),
(s("Rename tab"), s("Rename"),
action_key(&km, &[A::SwitchToMode{input_mode: IM::RenameTab}, A::TabNameInput{input: vec![0]}])),
(s("Previous Tab"), s("Previous"), action_key(&km, &[A::GoToPreviousTab, TO_NORMAL])),
diff --git a/example/default.kdl b/example/default.kdl
index 017e3407..e11be799 100644
--- a/example/default.kdl
+++ b/example/default.kdl
@@ -235,6 +235,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/src/main.rs b/src/main.rs
index f077c199..2a5db3c5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,6 +6,7 @@ use clap::Parser;
use zellij_utils::{
cli::{CliAction, CliArgs, Command, Sessions},
consts::{create_config_and_cache_folders, VERSION},
+ data::UnblockCondition,
envs,
input::config::Config,
logging::*,
@@ -40,10 +41,26 @@ fn main() {
pinned,
stacked,
blocking,
+ block_until_exit_success,
+ block_until_exit_failure,
+ block_until_exit,
+ near_current_pane,
})) = opts.command
{
let cwd = cwd.or_else(|| std::env::current_dir().ok());
let skip_plugin_cache = false; // N/A for this action
+
+ // Compute the unblock condition
+ let 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
+ };
+
let command_cli_action = CliAction::NewPane {
command,
plugin: None,
@@ -63,6 +80,8 @@ fn main() {
pinned,
stacked,
blocking,
+ unblock_condition,
+ near_current_pane,
};
commands::send_action_to_session(command_cli_action, opts.session, config);
std::process::exit(0);
@@ -83,6 +102,7 @@ fn main() {
let cwd = None;
let stacked = false;
let blocking = false;
+ let unblock_condition = None;
let command_cli_action = CliAction::NewPane {
command: vec![],
plugin: Some(url),
@@ -102,6 +122,8 @@ fn main() {
pinned,
stacked,
blocking,
+ unblock_condition,
+ near_current_pane: false,
};
commands::send_action_to_session(command_cli_action, opts.session, config);
std::process::exit(0);
@@ -118,6 +140,7 @@ fn main() {
width,
height,
pinned,
+ near_current_pane,
})) = opts.command
{
let mut file = file;
@@ -139,6 +162,7 @@ fn main() {
width,
height,
pinned,
+ near_current_pane,
};
commands::send_action_to_session(command_cli_action, opts.session, config);
std::process::exit(0);
@@ -224,6 +248,13 @@ fn main() {
layout_dir: options.as_ref().and_then(|o| o.layout_dir.clone()),
name: None,
cwd: options.as_ref().and_then(|o| o.default_cwd.clone()),
+ initial_command: vec![],
+ initial_plugin: None,
+ close_on_exit: Default::default(),
+ start_suspended: Default::default(),
+ block_until_exit_success: false,
+ block_until_exit_failure: false,
+ block_until_exit: false,
};
commands::send_action_to_session(new_layout_cli_action, Some(session_name), config);
} else {
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 1dac0e2f..54c2e068 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -79,6 +79,10 @@ fn workspace_members() -> &'static Vec<WorkspaceMember> {
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 bfe345df..b5083c17 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<OldThemesFromYamlIntermediate>,
- pub plugins: OldPluginsConfig,
- pub ui: Option<OldUiConfigFromYaml>,
- pub env: OldEnvironmentVariablesFromYaml,
-}
-
-#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
-struct OldKeybinds(HashMap<OldInputMode, OldModeKeybinds>);
-
-#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
-struct OldModeKeybinds(BTreeMap<OldKey, Vec<OldAction>>);
-
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct OldThemesFromYamlIntermediate(HashMap<String, OldTheme>);
@@ -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<OldPluginTag, OldPluginConfig>);
-
-#[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<usize>), // 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 201a0cf4..32676fd3 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 9bc8a938..62907048 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 1861fb68..dd093919 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<dyn ServerOsApi>, 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 adcca974..46010493 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<dyn ServerOsApi>;
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 6ae71b43..d347dcd0 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 0e74a9de..ffad3d84 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<RefCell<Option<SizeInPixels>>>,
vte_parsers: HashMap<ClientId, vte::Parser>,
grids: HashMap<ClientId, Grid>,
+ cursor_visibility: HashMap<ClientId, Option<(usize, usize)>>,
prev_pane_name: String,
frame: HashMap<ClientId, PaneFrame>,
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<ClientId>) -> 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<PluginPermission>) {
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 1c5667a5..97bf5dcc 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<ClientId>) -> 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<NotificationEnd>,
+ mut notification_end: Option<NotificationEnd>,
) -> 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 fb73a90d..4df68db4 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 ddb225a4..0c989a9e 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 6b4edf40..e4adfbe4 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<ClientId>) -> 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 7ab7a56c..53059240 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 cede7901..24a1e811 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<TerminalAction>,
Option<TiledPaneLayout>,
Vec<FloatingPaneLayout>,
- usize, // tab_index
- bool, // should change focus to new tab
- (ClientId, bool), // bool -> is_web_client
- Option<NotificationEnd>, // completion signal
+ usize, // tab_index
+ Option<Vec<CommandOrPlugin>>, // initial_panes
+ bool, // block_on_first_terminal
+ bool, // should change focus to new tab
+ (ClientId, bool), // bool -> is_web_client
+ Option<NotificationEnd>, // completion signal
),
ApplyCachedEvents {
plugin_ids: Vec<PluginId>,
@@ -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<u32>,
+ cli_client_id: Option<ClientId>,
+ },
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<RunPluginOrAlias, Vec<PluginId>> = 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 5f90ebbe..c0368fbc 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 975e20a3..88023c4b 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 133b23ff..38179b85 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 cdaa2362..d0312218 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 cbf3c8c2..b795da46 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 1041e20f..97e61a45 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 00000000..cc4717e3
--- /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 00000000..333c0389
--- /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 ea71beac..1de9477d 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 cc572e0c..370a7bbf 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 f8cf4623..3ef17f9d 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 305eb5a5..fbd45fd0 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 6b98856a..31a63f73 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 0b37d318..8073ceb8 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 345f0479..0a3a1d23 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 ca2edc5f..c76a5ecc 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 1451fa50..f4653b62 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 13bbdb4a..48cf0870 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 3fd2e40f..7e45c1fc 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 28c7dedc..91ec8a9b 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 00000000..31880c73
--- /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 c3b1d5f2..94419199 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 72b7dc15..f901ba75 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 b9306571..1c2a8fc6 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 9da82855..51f2cc15 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 1213308e..128c7abe 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 bc4faa99..1c74a94b 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 2ba4de2a..889a6e9d 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 21c4257c..87c3264c 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 ac0838cc..3afcead6 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 ca751a22..596700c5 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 eb598cf0..be8503de 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 d235c416..b899e596 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 cde191a5..fc2087d6 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 89986fd2..4d2689db 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 b2f0f9ae..b42943fc 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 5cc2ce7c..dd197b76 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 d85ffc61..5292281b 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 014ac3d3..5709731c 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 b35b3b0b..523e76fd 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 d00a7663..5057701a 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 5816c283..119444e2 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 9450f7c0..4846cd65 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 f23a792c..53b81db8 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 75f47da7..1400f081 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 37275f43..0184c9a6 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 d9fa542d..71766b1e 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 00000000..dcbf7004
--- /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 00000000..102abab0
--- /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 c8605a1c..738a7625 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 410902d5..339ea8c4 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 9f2effac..5c0b9088 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 acc0d6a6..d0250b34 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 aa9592c2..14438cf8 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 00000000..a7558487
--- /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 00000000..fb7dc0a1
--- /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 00000000..5b53f360
--- /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 24f6c12f..457bebdc 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 a99b0230..41396ddb 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 290247c0..845f37ba 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 6df0b5df..b4ff3c00 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 f8959104..06cde256 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 34e96dae..44747b66 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 8f375b9c..8e24bff8 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 e1bded4d..c91f66aa 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 e088770b..32965d58 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 6c500286..99df8475 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 54566d68..7836e7d2 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 8bb05ce1..abf3d1a2 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 8bb05ce1..abf3d1a2 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 5dd78d13..2453a0a1 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<ProtobufEvent, _> = 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 526b4bcb..f64a9e18 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<PermissionType>) -> 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<String
in_place,
start_suppressed,
coordinates: None,
+ near_current_pane: false,
};
apply_action!(action, error_msg, env);
}
+fn run_action(env: &PluginEnv, mut action: Action, context: BTreeMap<String, String>) {
+ // 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<String>, cwd: Option<String>) {
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 39bf4668..fee6054e 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<FloatingPaneLayout>,
usize, // tab_index
HashMap<RunPluginOrAlias, Vec<u32>>, // plugin_ids
+ Option<Vec<CommandOrPlugin>>, // initial_panes
+ bool, // block_on_first_terminal
bool, // should change focus to new tab
(ClientId, bool), // bool -> is_web_client
Option<NotificationEnd>, // 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<GetPanePidResponse>,
+ },
+ 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<u32, JoinHandle<()>>, // terminal_id to join-handle
default_editor: Option<PathBuf>,
post_command_discovery_hook: Option<String>,
- plugin_cwds: HashMap<u32, PathBuf>, // plugin_id -> cwd
+ plugin_cwds: HashMap<u32, PathBuf>, // plugin_id -> cwd
+ terminal_cwds: HashMap<u32, PathBuf>, // terminal_id -> cwd
}
pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> Result<()> {
@@ -430,6 +447,8 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> 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<Layout>) -> 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<Layout>) -> 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<FloatingPaneLayout>,
default_shell: Option<TerminalAction>,
plugin_ids: HashMap<RunPluginOrAlias, Vec<u32>>,
+ initial_panes: Option<Vec<CommandOrPlugin>>,
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<NotificationEnd>,
@@ -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<RunCommand>, Result<RawFd>)> =
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<RunCommand> should only be Some if the pane starts held
let new_tab_pane_ids: Vec<(u32, Option<RunCommand>)> = 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<Run>,
@@ -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::<ZellijError>() {
@@ -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::<ZellijError>() {
@@ -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::<ZellijError>() {
@@ -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<u32, Vec<String>> = HashMap::new();
let mut terminal_ids_to_cwds: HashMap<u32, PathBuf> = 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<u32> = 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<ClientId> = 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<PathBuf>,
@@ -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 7d11ba65..ead4282d 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<i32>,
+ pub affected_pane_id: Option<PaneId>,
+}
+
fn wait_for_action_completion(
- receiver: oneshot::Receiver<Option<i32>>, // currently i32 signals an exit status, can be expanded as
- // needed
+ receiver: oneshot::Receiver<ActionCompletionResult>,
action_name: &str,
wait_forever: bool,
-) -> Option<i32> {
+) -> 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<oneshot::Sender<Option<i32>>>,
+ channel: Option<oneshot::Sender<ActionCompletionResult>>,
exit_status: Option<i32>,
+ unblock_condition: Option<UnblockCondition>,
+ affected_pane_id: Option<PaneId>, // 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<Option<i32>>) -> Self {
+ pub fn new(sender: oneshot::Sender<ActionCompletionResult>) -> Self {
+ NotificationEnd {
+ channel: Some(sender),
+ exit_status: None,
+ unblock_condition: None,
+ affected_pane_id: None,
+ }
+ }
+
+ pub fn new_with_condition(
+ sender: oneshot::Sender<ActionCompletionResult>,
+ 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<UnblockCondition> {
+ 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<Box<dyn ServerOsApi>>,
-) -> Result<bool> {
+) -> Result<(bool, Option<ActionCompletionResult>)> {
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<bool> {
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 3c83d6b0..c8998831 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<NotificationEnd>),
TogglePaneFrames(Option<NotificationEnd>),
SetSelectable(PaneId, bool),
+ ShowPluginCursor(u32, ClientId, Option<(usize, usize)>),
ClosePane(
PaneId,
Option<ClientId>,
@@ -235,6 +233,8 @@ pub enum ScreenInstruction {
Vec<FloatingPaneLayout>,
Option<String>,
(Vec<SwapTiledLayout>, Vec<SwapFloatingLayout>), // swap layouts
+ Option<Vec<CommandOrPlugin>>, // initial_panes
+ bool, // block_on_first_terminal
bool, // should_change_focus_to_new_tab
(ClientId, bool), // bool -> is_web_client
Option<NotificationEnd>, // completion signal
@@ -245,10 +245,11 @@ pub enum ScreenInstruction {
Vec<(u32, HoldForCommand)>, // new pane pids
Vec<(u32, HoldForCommand)>, // new floating pane pids
HashMap<RunPluginOrAlias, Vec<u32>>,
- usize, // tab_index
- bool, // should change focus to new tab
- (ClientId, bool), // bool -> is_web_client
- Option<NotificationEnd>, // completion signal
+ usize, // tab_index
+ bool, // should change focus to new tab
+ (ClientId, bool), // bool -> is_web_client
+ Option<NotificationEnd>, // regular completion signal
+ Option<(u32, NotificationEnd)>, // blocking_terminal (terminal_id, completion_tx)
),
SwitchTabNext(ClientId, Option<NotificationEnd>),
SwitchTabPrev(ClientId, Option<NotificationEnd>),
@@ -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<NotificationEnd>),
- DenyPrompt(ClientId, Option<NotificationEnd>),
UpdateSearch(Vec<u8>, ClientId, Option<NotificationEnd>),
SearchDown(ClientId, Option<NotificationEnd>),
SearchUp(ClientId, Option<NotificationEnd>),
@@ -368,8 +365,12 @@ pub enum ScreenInstruction {
ClientId,
Option<NotificationEnd>,
), // bools are: should_float, should_open_in_place Option<PaneId> is the pane id to replace, Option<PathBuf> is an optional cwd, bool after is skip_cache
- SuppressPane(PaneId, ClientId), // bool is should_float
- FocusPaneWithId(PaneId, bool, ClientId, Option<NotificationEnd>), // 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<NotificationEnd>), // bools:
+ // should_float_if_hidden,
+ // should_be_in_place_if_hidden
RenamePane(PaneId, Vec<u8>, Option<NotificationEnd>),
RenameTab(usize, Vec<u8>, Option<NotificationEnd>),
RequestPluginPermissions(
@@ -422,6 +423,7 @@ pub enum ScreenInstruction {
ResizePaneWithId(ResizeStrategy, PaneId),
EditScrollbackForPaneWithId(PaneId, Option<NotificationEnd>),
WriteToPaneId(Vec<u8>, 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<RefCell<Option<SizeInPixels>>>,
stacked_resize: Rc<RefCell<bool>>,
sixel_image_store: Rc<RefCell<SixelImageStore>>,
- /// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`]
- overlay: OverlayWindow,
terminal_emulator_colors: Rc<RefCell<Palette>>,
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
connected_clients: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client
/// The indices of this [`Screen`]'s active [`Tab`]s.
active_tab_indices: BTreeMap<ClientId, usize>,
tab_history: BTreeMap<ClientId, Vec<usize>>,
+ pane_history: BTreeMap<ClientId, Vec<PaneId>>,
mode_info: BTreeMap<ClientId, ModeInfo>,
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<Overlay> {
- &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<Size> {
- self.watcher_clients
- .get(client_id)
- .map(|state| state.size())
- }
-
- // Optional: get all watcher sizes
- pub fn get_all_watcher_sizes(&self) -> &HashMap<ClientId, WatcherState> {
- &self.watcher_clients
- }
-
pub fn generate_and_report_tab_state(&mut self) -> Result<Vec<TabInfo>> {
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<bool> {
// 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<ClientId> =
+ 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 5e52888a..559dc5db 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 cd87b817..7e441a0e 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<ClientId>) -> 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<RunPluginOrAlias, Vec<u32>>,
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<dyn Pane>,
+ _completion_tx: Option<NotificationEnd>,
+ ) {
+ 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<dyn Pane>),
+ ) {
+ // 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<dyn Pane>)| -> Option<(bool, Box<dyn Pane>)> {
+ 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<PaneInfo> {
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(&current_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, &current_pane_group);
+ pane_info_for_pane(&pane.pid(), pane, &current_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 029df49d..6eb39eae 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 f5284b88..085f8211 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 c2aa2860..b4b76b9b 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 98b681a5..adfa3a79 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 978b741e..80ab2a37 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 c0088e60..eb8667f8 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<Coordina
style.colors.text_unselected
};
+ // Start with base style from declaration
let base_text_style = CharacterStyles::from(declaration).bold(Some(AnsiCode::On));
let (text, _text_width) = stringify_text(
@@ -22,6 +23,7 @@ pub fn text(content: Text, style: &Style, component_coordinates: Option<Coordina
None,
&component_coordinates,
&declaration,
+ &style.colors,
base_text_style,
);
match component_coordinates {
@@ -39,6 +41,7 @@ pub fn stringify_text(
left_padding: Option<usize>,
coordinates: &Option<Coordinates>,
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<PaletteColor> {
+ 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<PaletteColor> {
+ 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 d9d5e3a0..1a17e743 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 7e3f821b..00000000
--- 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<String>;
-}
-
-#[derive(Clone, Debug)]
-pub enum OverlayType {
- Prompt(prompt::Prompt),
-}
-
-impl Overlayable for OverlayType {
- fn generate_overlay(&self, size: Size) -> Result<String> {
- 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<Overlay>,
-}
-
-impl Overlayable for OverlayWindow {
- fn generate_overlay(&self, size: Size) -> Result<String> {
- 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<Box<ServerInstruction>> {
- match self.overlay_type {
- OverlayType::Prompt(p) => p.confirm(),
- }
- }
- pub fn prompt_deny(self) -> Option<Box<ServerInstruction>> {
- match self.overlay_type {
- OverlayType::Prompt(p) => p.deny(),
- }
- }
-}
-
-impl Overlayable for Overlay {
- fn generate_overlay(&self, size: Size) -> Result<String> {
- 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 0cbfe351..00000000
--- 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<Box<ServerInstruction>>,
- on_deny: Option<Box<ServerInstruction>>,
-}
-
-impl Prompt {
- pub fn new(
- message: String,
- on_confirm: Option<Box<ServerInstruction>>,
- on_deny: Option<Box<ServerInstruction>>,
- ) -> Self {
- Self {
- message,
- on_confirm,
- on_deny,
- }
- }
- pub fn confirm(self) -> Option<Box<ServerInstruction>> {
- self.on_confirm
- }
- pub fn deny(self) -> Option<Box<ServerInstruction>> {
- self.on_deny
- }
-}
-
-impl Overlayable for Prompt {
- fn generate_overlay(&self, size: Size) -> Result<String> {
- 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 c17a9b66..326dd96f 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 171096f7..6345d6b8 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 e36f78b8..d322d6c1 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 73fec393..b21357e7 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 c09ce65b..fc5e1af5 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 ca824efe..2428bce0 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 de7e1ed5..fcb5c135 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 75f460d5..57aaa650 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 4d5b11c8..3c3d85e7 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 7ac623b8..2bdd6975 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 30640244..0bdf3e1d 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 1a0677e6..d61ae4d7 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 ed703453..5c157762 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 6030dcc7..189f1f8f 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<String>) {
+ 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<i32, String> {
+ 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<String, String>) {
+ // 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 f9a31632..0c0eb9a1 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<usize>) -> Self {
+ self.content = self.content.error_color_indices(indices);
+ self
+ }
+ pub fn error_color_range<R: RangeBounds<usize>>(mut self, indices: R) -> Self {
+ self.content = self.content.error_color_range(indices);
+ self
+ }
+ pub fn error_color_substring<S: AsRef<str>>(mut self, substr: S) -> Self {
+ self.content = self.content.error_color_substring(substr);
+ self
+ }
+ pub fn error_color_nth_substring<S: AsRef<str>>(
+ 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<usize>) -> Self {
+ self.content = self.content.success_color_indices(indices);
+ self
+ }
+ pub fn success_color_range<R: RangeBounds<usize>>(mut self, indices: R) -> Self {
+ self.content = self.content.success_color_range(indices);
+ self
+ }
+ pub fn success_color_substring<S: AsRef<str>>(mut self, substr: S) -> Self {
+ self.content = self.content.success_color_substring(substr);
+ self
+ }
+ pub fn success_color_nth_substring<S: AsRef<str>>(
+ 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 68e8c0a1..492e2352 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<usize>) -> 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<R: RangeBounds<usize>>(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<S: AsRef<str>>(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<usize>) -> 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<R: RangeBounds<usize>>(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<S: AsRef<str>>(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<usize>) -> 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<R: RangeBounds<usize>>(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<S: AsRef<str>>(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<S: AsRef<str>>(
+ 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<usize>) -> 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<R: RangeBounds<usize>>(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<S: AsRef<str>>(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<S: AsRef<str>>(
+ 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<usize>) -> 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<S: AsRef<str>>(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 017e3407..e06dbe5c 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 d5e4ee66..ae99b4a1 100755
--- a/zellij-utils/assets/plugins/about.wasm
+++ b/zellij-utils/assets/plugins/about.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm
index 7e854b27..0d6ab290 100755
--- a/zellij-utils/assets/plugins/compact-bar.wasm
+++ b/zellij-utils/assets/plugins/compact-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/configuration.wasm b/zellij-utils/assets/plugins/configuration.wasm
index 9510473b..b448d343 100755
--- a/zellij-utils/assets/plugins/configuration.wasm
+++ b/zellij-utils/assets/plugins/configuration.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
index dfd22a4d..407f41f1 100755
--- a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
+++ b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/multiple-select.wasm b/zellij-utils/assets/plugins/multiple-select.wasm
index 92c10d9e..77f0310b 100755
--- a/zellij-utils/assets/plugins/multiple-select.wasm
+++ b/zellij-utils/assets/plugins/multiple-select.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/plugin-manager.wasm b/zellij-utils/assets/plugins/plugin-manager.wasm
index 71a7526c..5c9fb461 100755
--- a/zellij-utils/assets/plugins/plugin-manager.wasm
+++ b/zellij-utils/assets/plugins/plugin-manager.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/sequence.wasm b/zellij-utils/assets/plugins/sequence.wasm
new file mode 100755
index 00000000..0a982532
--- /dev/null
+++ b/zellij-utils/assets/plugins/sequence.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/session-manager.wasm b/zellij-utils/assets/plugins/session-manager.wasm
index 17d95bbd..0a8460d0 100755
--- a/zellij-utils/assets/plugins/session-manager.wasm
+++ b/zellij-utils/assets/plugins/session-manager.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/share.wasm b/zellij-utils/assets/plugins/share.wasm
index 6918fb7f..360e55a2 100755
--- a/zellij-utils/assets/plugins/share.wasm
+++ b/zellij-utils/assets/plugins/share.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm
index 1ee3c21f..ac1b076a 100755
--- a/zellij-utils/assets/plugins/status-bar.wasm
+++ b/zellij-utils/assets/plugins/status-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm
index 8f581fee..ec6de885 100755
--- a/zellij-utils/assets/plugins/strider.wasm
+++ b/zellij-utils/assets/plugins/strider.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm
index 73bebb9a..4a31b561 100755
--- a/zellij-utils/assets/plugins/tab-bar.wasm
+++ b/zellij-utils/assets/plugins/tab-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs
index 8f381ee0..46435021 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<percent_or_fixed::SizeType>,
+}
+/// 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<u32>,
+}
+#[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<run_plugin_location_data::LocationData>,
+}
+/// 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<RunPluginLocationData>,
+ #[prost(message, optional, tag="3")]
+ pub configuration: ::core::option::Option<PluginUserConfiguration>,
+ #[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<PluginUserConfiguration>,
+ #[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<RunPlugin>,
+}
+#[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<run_plugin_or_alias::PluginType>,
+}
+/// 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<u32>,
+ #[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<pane_run::RunType>,
+}
+/// 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<TiledPaneLayout>,
+ #[prost(message, optional, tag="4")]
+ pub split_size: ::core::option::Option<SplitSize>,
+ #[prost(message, optional, tag="5")]
+ pub run: ::core::option::Option<PaneRun>,
+ #[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<bool>,
+ #[prost(bool, tag="9")]
+ pub children_are_stacked: bool,
+ #[prost(uint32, optional, tag="10")]
+ pub external_children_index: ::core::option::Option<u32>,
+ #[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<PercentOrFixed>,
+ #[prost(message, optional, tag="3")]
+ pub width: ::core::option::Option<PercentOrFixed>,
+ #[prost(message, optional, tag="4")]
+ pub x: ::core::option::Option<PercentOrFixed>,
+ #[prost(message, optional, tag="5")]
+ pub y: ::core::option::Option<PercentOrFixed>,
+ #[prost(bool, optional, tag="6")]
+ pub pinned: ::core::option::Option<bool>,
+ #[prost(message, optional, tag="7")]
+ pub run: ::core::option::Option<PaneRun>,
+ #[prost(bool, optional, tag="8")]
+ pub focus: ::core::option::Option<bool>,
+ #[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<u32>,
+}
+#[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<LayoutConstraintTiledPair>,
+ #[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<LayoutConstraintFloatingPair>,
+ #[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<LayoutConstraintWithValue>,
+ #[prost(message, optional, tag="2")]
+ pub layout: ::core::option::Option<TiledPaneLayout>,
+}
+#[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<LayoutConstraintWithValue>,
+ #[prost(message, repeated, tag="2")]
+ pub layouts: ::prost::alloc::vec::Vec<FloatingPaneLayout>,
+}
+#[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<command_or_plugin::CommandOrPluginType>,
+}
+/// 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<TiledPaneLayout>,
+ #[prost(message, repeated, tag="2")]
+ pub floating_layouts: ::prost::alloc::vec::Vec<FloatingPaneLayout>,
+ #[prost(message, repeated, tag="3")]
+ pub swap_tiled_layouts: ::prost::alloc::vec::Vec<SwapTiledLayout>,
+ #[prost(message, repeated, tag="4")]
+ pub swap_floating_layouts: ::prost::alloc::vec::Vec<SwapFloatingLayout>,
+ #[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<CommandOrPlugin>,
+ #[prost(enumeration="UnblockCondition", optional, tag="9")]
+ pub first_pane_unblock_condition: ::core::option::Option<i32>,
+}
+#[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<action::OptionalPayload>,
}
/// 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<RunCommandAction>,
+ #[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<RunCommandAction>,
#[prost(enumeration="super::resize::ResizeDirection", optional, tag="2")]
pub direction: ::core::option::Option<i32>,
+ #[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<i32>,
#[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<NewPanePlacement>,
+ #[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<RunCommandAction>,
+ #[prost(enumeration="UnblockCondition", optional, tag="4")]
+ pub unblock_condition: ::core::option::Option<i32>,
+ #[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<RunCommandAction>,
+ #[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<PaneId>,
+ #[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<u8>,
+ #[prost(message, optional, tag="2")]
+ pub key_with_modifier: ::core::option::Option<KeyWithModifier>,
+ #[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<split_size::SplitSizeVariant>,
+}
+/// 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<pane_id::PaneIdVariant>,
+}
+/// 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<SplitSize>,
+ #[prost(message, optional, tag="2")]
+ pub y: ::core::option::Option<SplitSize>,
+ #[prost(message, optional, tag="3")]
+ pub width: ::core::option::Option<SplitSize>,
+ #[prost(message, optional, tag="4")]
+ pub height: ::core::option::Option<SplitSize>,
+ #[prost(bool, optional, tag="5")]
+ pub pinned: ::core::option::Option<bool>,
+}
+/// 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<new_pane_placement::PlacementVariant>,
+}
+/// 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<i32>,
+}
+#[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<FloatingPaneCoordinates>,
+}
+#[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<PaneId>,
+ #[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<PaneId>,
+}
#[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<i32>,
+ /// 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<Self> {
+ 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<Self> {
+ 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<Self> {
+ 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<Self> {
+ 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<Self> {
+ 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<Self> {
+ 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 7ce91771..5e1d7d1c 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<event::Payload>,
}
/// 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<PaneId>,
+ #[prost(string, tag="2")]
+ pub new_cwd: ::prost::alloc::string::String,
+ #[prost(uint32, repeated, tag="3")]
+ pub focused_client_ids: ::prost::alloc::vec::Vec<u32>,
+}
+#[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<ClientTabHistory>,
+ #[prost(message, repeated, tag="11")]
+ pub pane_history: ::prost::alloc::vec::Vec<ClientPaneHistory>,
}
#[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<PaneId>,
+}
+#[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<super::action::Position>,
}
+#[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<super::action::Action>,
+ #[prost(uint32, tag="2")]
+ pub client_id: u32,
+ #[prost(uint32, optional, tag="3")]
+ pub terminal_id: ::core::option::Option<u32>,
+ #[prost(uint32, optional, tag="4")]
+ pub cli_client_id: ::core::option::Option<u32>,
+}
+#[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<super::action::Action>,
+ #[prost(message, optional, tag="2")]
+ pub pane_id: ::core::option::Option<PaneId>,
+ #[prost(message, repeated, tag="3")]
+ pub context: ::prost::alloc::vec::Vec<ContextItem>,
+}
#[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 b33d0b8e..931a3a73 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<plugin_command::Payload>,
}
/// 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<super::action::Action>,
+ #[prost(message, repeated, tag="2")]
+ pub context: ::prost::alloc::vec::Vec<ContextItem>,
+}
+#[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<PaneId>,
#[prost(message, optional, tag="2")]
pub existing_pane_id: ::core::option::Option<PaneId>,
+ #[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<PaneId>,
#[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<CursorPosition>,
+}
+#[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<PaneId>,
+}
+#[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<get_pane_pid_response::Result>,
+}
+/// 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 643f5b99..e113273a 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 c977170c..8fa4187c 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<FloatingPaneCoordinates>,
+ #[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<FloatingPaneCoordinates>,
+ #[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<RunCommandAction>,
#[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<RunCommandAction>,
#[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<PaneId>,
+ #[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<RunCommandAction>,
#[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<RunCommandAction>,
+ #[prost(enumeration="UnblockCondition", optional, tag="4")]
+ pub unblock_condition: ::core::option::Option<i32>,
+ #[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<CommandOrPlugin>,
+ #[prost(enumeration="UnblockCondition", optional, tag="9")]
+ pub first_pane_unblock_condition: ::core::option::Option<i32>,
}
#[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<RunCommandAction>,
+ #[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<command_or_plugin::CommandOrPluginType>,
+}
+/// 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<Self> {
+ 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 0fd52473..e59ba71f 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<bool>,
+ /// 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<UnblockCondition>,
+
+ /// 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<bool>,
+ /// 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<PathBuf>,
+
+ /// 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<String>,
+
+ /// Initial plugin to load in the new tab
+ #[clap(long, value_parser, conflicts_with("initial-command"))]
+ initial_plugin: Option<String>,
+
+ /// 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 254a2a75..e4561989 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 4f5b729d..e5b93e1a 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 7f700d4d..62571fd4 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<String>) -> 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<Metadata> 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<u32>, Option<ClientId>), // Action, client_id, terminal_id, cli_client_id
PaneRenderReport(HashMap<PaneId, PaneContents>),
+ ActionComplete(Action, Option<PaneId>, BTreeMap<String, String>), // Action, pane_id, context
+ CwdChanged(PaneId, PathBuf, Vec<ClientId>), // 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<ClientId, Vec<usize>>,
+ pub pane_history: BTreeMap<ClientId, Vec<PaneId>>,
}
#[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<dyn std::error::Error>;
fn from_str(stringified_pane_id: &str) -> Result<Self, Self::Err> {
@@ -2660,13 +2726,14 @@ impl NewPanePlacement {
type Context = BTreeMap<String, String>;
-#[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<EventType>),
Unsubscribe(HashSet<EventType>),
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<PermissionType>),
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<u8>, 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<String, String>),
+ CopyToClipboard(String), // text to copy
}
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index bcd05987..74c40534 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 d623197b..92d697a9 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<String>,
command: Option<RunCommandAction>,
+ unblock_condition: Option<UnblockCondition>,
+ 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<FloatingPaneCoordinates>,
+ near_current_pane: bool,
},
/// Open a new floating pane
NewFloatingPane {
command: Option<RunCommandAction>,
pane_name: Option<String>,
coordinates: Option<FloatingPaneCoordinates>,
+ near_current_pane: bool,
},
/// Open a new tiled (embedded, non-floating) pane
NewTiledPane {
direction: Option<Direction>,
command: Option<RunCommandAction>,
pane_name: Option<String>,
+ near_current_pane: bool,
},
/// Open a new pane in place of the focused one, suppressing it instead
NewInPlacePane {
command: Option<RunCommandAction>,
pane_name: Option<String>,
+ near_current_pane: bool,
+ pane_id_to_replace: Option<PaneId>,
+ close_replace_pane: bool,
},
NewStackedPane {
command: Option<RunCommandAction>,
pane_name: Option<String>,
+ 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<String>,
should_change_focus_to_new_tab: bool,
cwd: Option<PathBuf>,
+ initial_panes: Option<Vec<CommandOrPlugin>>,
+ first_pane_unblock_condition: Option<UnblockCondition>,
},
/// 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<String> = 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 0169f799..11a34f59 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<OriginatingPlugin>,
}
+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<usize>, cwd: Option<PathBuf>) -> Self {
OpenFilePayload {
@@ -129,6 +140,23 @@ impl From<RunCommand> for RunCommandAction {
}
}
+impl RunCommandAction {
+ pub fn new(mut command: Vec<String>) -> 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 de555a00..d925f7c5 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<Run>) {
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 d920495d..0977d5e7 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 c4755205..892c7a66 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 53dd4d9f..96e2c747 100644
--- a/zellij-utils/src/ipc.rs
+++ b/zellij-utils/src/ipc.rs
@@ -251,18 +251,14 @@ impl<T: Serialize> IpcSenderWithContext<T> {
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 ab06faae..f7ab632f 100644
--- a/zellij-utils/src/ipc/protobuf_conversion.rs
+++ b/zellij-utils/src/ipc/protobuf_conversion.rs
@@ -858,6 +858,7 @@ impl From<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
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<crate::input::actions::Action>
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>
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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
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::<Result<Vec<_>>>()?,
+ )
+ },
+
+ 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<crate::client_server_contract::client_server_contract::Action>
.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<crate::client_server_contract::client_server_contract::Action>
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<crate::data::Resize> {
@@ -2103,6 +2181,26 @@ fn proto_i32_to_search_option(option: i32) -> Result<crate::input::actions::Sear
}
}
+fn proto_i32_to_unblock_condition(condition: i32) -> Result<crate::data::UnblockCondition> {
+ 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<crate::position::Position>
for crate::client_server_contract::client_server_contract::Position
@@ -2722,6 +2820,47 @@ impl From<crate::input::layout::RunPluginOrAlias>
}
}
+// CommandOrPlugin conversion
+impl From<crate::data::CommandOrPlugin>
+ 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<crate::client_server_contract::client_server_contract::CommandOrPlugin>
+ for crate::data::CommandOrPlugin
+{
+ type Error = anyhow::Error;
+
+ fn try_from(
+ proto: crate::client_server_contract::client_server_contract::CommandOrPlugin,
+ ) -> Result<Self> {
+ 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<crate::client_server_contract::client_server_contract::Run>
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 2ec513da..61981f38 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 789c3d96..5ca02d86 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 11f6e53d..6353ee0e 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 ddf36a40..e562e030 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 088504af..e43fcf3b 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 cd4ff687..44b2e8bf 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 2772e725..c29884af 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<string, string> 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 c442144a..f8de7b76 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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::<Result<Vec<_>, _>>()?;
+
+ 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::<Result<Vec<_>, _>>()?,
+ )
+ };
+
+ 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::<Result<Vec<_>, _>>()?,
+ )
+ };
+
+ 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::<Result<Vec<_>, _>>()?,
+ )
+ };
+
+ 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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<ProtobufAction> 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<Action> 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<Action> 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<Action> for ProtobufAction {
should_float,
direction,
cwd,
+ near_current_pane,
})),
})
},
@@ -1004,6 +1214,7 @@ impl TryFrom<Action> 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<Action> 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<Action> 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<Action> 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<Action> 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::<Result<Vec<_>, _>>()?;
+
+ let protobuf_swap_tiled_layouts = swap_tiled_layouts
+ .as_ref()
+ .map(|layouts| {
+ layouts
+ .iter()
+ .map(|l| l.clone().try_into())
+ .collect::<Result<Vec<_>, _>>()
+ })
+ .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::<Result<Vec<_>, _>>()
+ })
+ .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::<Result<Vec<_>, _>>()
+ })
+ .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<Action> 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<Action> 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<Action> 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<Action> for ProtobufAction {
coordinates: _,
}
| Action::SkipConfirm { action: _ }
- | Action::NewBlockingPane { .. }
| Action::SwitchSession { .. } => Err("Unsupported action"),
}
}
@@ -1616,3 +1959,914 @@ impl TryFrom<&ProtobufPluginConfiguration> for BTreeMap<String, String> {
Ok(converted)
}
}
+
+impl TryFrom<ProtobufKeyWithModifier> for KeyWithModifier {
+ type Error = &'static str;
+ fn try_from(protobuf_key: ProtobufKeyWithModifier) -> Result<Self, &'static str> {
+ 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<KeyWithModifier> for ProtobufKeyWithModifier {
+ type Error = &'static str;
+ fn try_from(key: KeyWithModifier) -> Result<Self, &'static str> {
+ 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<i32> = 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<ProtobufUnblockCondition> for UnblockCondition {
+ type Error = &'static str;
+ fn try_from(protobuf_uc: ProtobufUnblockCondition) -> Result<Self, &'static str> {
+ match protobuf_uc {
+ ProtobufUnblockCondition::UnblockOnExitSuccess => Ok(UnblockCondition::OnExitSuccess),
+ ProtobufUnblockCondition::UnblockOnExitFailure => Ok(UnblockCondition::OnExitFailure),
+ ProtobufUnblockCondition::UnblockOnAnyExit => Ok(UnblockCondition::OnAnyExit),
+ }
+ }
+}
+
+impl TryFrom<UnblockCondition> for ProtobufUnblockCondition {
+ type Error = &'static str;
+ fn try_from(uc: UnblockCondition) -> Result<Self, &'static str> {
+ match uc {
+ UnblockCondition::OnExitSuccess => Ok(ProtobufUnblockCondition::UnblockOnExitSuccess),
+ UnblockCondition::OnExitFailure => Ok(ProtobufUnblockCondition::UnblockOnExitFailure),
+ UnblockCondition::OnAnyExit => Ok(ProtobufUnblockCondition::UnblockOnAnyExit),
+ }
+ }
+}
+
+impl TryFrom<i32> for UnblockCondition {
+ type Error = &'static str;
+ fn try_from(value: i32) -> Result<Self, &'static str> {
+ match ProtobufUnblockCondition::from_i32(value) {
+ Some(uc) => uc.try_into(),
+ None => Err("Invalid UnblockCondition value"),
+ }
+ }
+}
+
+// PaneId conversions
+impl TryFrom<ProtobufPaneId> for PaneId {
+ type Error = &'static str;
+ fn try_from(protobuf_pane_id: ProtobufPaneId) -> Result<Self, &'static str> {
+ 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<PaneId> for ProtobufPaneId {
+ type Error = &'static str;
+ fn try_from(pane_id: PaneId) -> Result<Self, &'static str> {
+ 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<ProtobufSplitSize> for SplitSize {
+ type Error = &'static str;
+ fn try_from(protobuf_split_size: ProtobufSplitSize) -> Result<Self, &'static str> {
+ 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<SplitSize> for ProtobufSplitSize {
+ type Error = &'static str;
+ fn try_from(split_size: SplitSize) -> Result<Self, &'static str> {
+ 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<ProtobufFloatingPaneCoordinates> for FloatingPaneCoordinates {
+ type Error = &'static str;
+ fn try_from(protobuf_coords: ProtobufFloatingPaneCoordinates) -> Result<Self, &'static str> {
+ 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<FloatingPaneCoordinates> for ProtobufFloatingPaneCoordinates {
+ type Error = &'static str;
+ fn try_from(coords: FloatingPaneCoordinates) -> Result<Self, &'static str> {
+ 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<ProtobufNewPanePlacement> for NewPanePlacement {
+ type Error = &'static str;
+ fn try_from(protobuf_placement: ProtobufNewPanePlacement) -> Result<Self, &'static str> {
+ 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<NewPanePlacement> for ProtobufNewPanePlacement {
+ type Error = &'static str;
+ fn try_from(placement: NewPanePlacement) -> Result<Self, &'static str> {
+ 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<ProtobufPercentOrFixed> for PercentOrFixed {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufPercentOrFixed) -> Result<Self, Self::Error> {
+ 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<PercentOrFixed> for ProtobufPercentOrFixed {
+ type Error = &'static str;
+ fn try_from(internal: PercentOrFixed) -> Result<Self, Self::Error> {
+ 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<ProtobufSplitDirection> for SplitDirection {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufSplitDirection) -> Result<Self, Self::Error> {
+ match protobuf {
+ ProtobufSplitDirection::Horizontal => Ok(SplitDirection::Horizontal),
+ ProtobufSplitDirection::Vertical => Ok(SplitDirection::Vertical),
+ ProtobufSplitDirection::Unspecified => Err("SplitDirection cannot be unspecified"),
+ }
+ }
+}
+
+impl TryFrom<SplitDirection> for ProtobufSplitDirection {
+ type Error = &'static str;
+ fn try_from(internal: SplitDirection) -> Result<Self, Self::Error> {
+ Ok(match internal {
+ SplitDirection::Horizontal => ProtobufSplitDirection::Horizontal,
+ SplitDirection::Vertical => ProtobufSplitDirection::Vertical,
+ })
+ }
+}
+
+impl TryFrom<ProtobufLayoutConstraint> for LayoutConstraint {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufLayoutConstraint) -> Result<Self, Self::Error> {
+ 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<LayoutConstraint> for ProtobufLayoutConstraint {
+ type Error = &'static str;
+ fn try_from(internal: LayoutConstraint) -> Result<Self, Self::Error> {
+ Ok(match internal {
+ LayoutConstraint::MaxPanes(_) => ProtobufLayoutConstraint::MaxPanes,
+ LayoutConstraint::MinPanes(_) => ProtobufLayoutConstraint::MinPanes,
+ LayoutConstraint::ExactPanes(_) => ProtobufLayoutConstraint::ExactPanes,
+ LayoutConstraint::NoConstraint => ProtobufLayoutConstraint::NoConstraint,
+ })
+ }
+}
+
+impl TryFrom<ProtobufLayoutConstraintWithValue> for LayoutConstraint {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufLayoutConstraintWithValue) -> Result<Self, Self::Error> {
+ 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<LayoutConstraint> for ProtobufLayoutConstraintWithValue {
+ type Error = &'static str;
+ fn try_from(internal: LayoutConstraint) -> Result<Self, Self::Error> {
+ 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<ProtobufPluginTag> for PluginTag {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufPluginTag) -> Result<Self, Self::Error> {
+ Ok(PluginTag::new(protobuf.tag))
+ }
+}
+
+impl TryFrom<PluginTag> for ProtobufPluginTag {
+ type Error = &'static str;
+ fn try_from(internal: PluginTag) -> Result<Self, Self::Error> {
+ Ok(ProtobufPluginTag {
+ tag: internal.into(),
+ })
+ }
+}
+
+impl TryFrom<ProtobufPluginUserConfiguration> for PluginUserConfiguration {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufPluginUserConfiguration) -> Result<Self, Self::Error> {
+ let btree_map: BTreeMap<String, String> = protobuf.configuration.into_iter().collect();
+ Ok(PluginUserConfiguration::new(btree_map))
+ }
+}
+
+impl TryFrom<PluginUserConfiguration> for ProtobufPluginUserConfiguration {
+ type Error = &'static str;
+ fn try_from(internal: PluginUserConfiguration) -> Result<Self, Self::Error> {
+ let configuration = internal.inner().clone().into_iter().collect();
+ Ok(ProtobufPluginUserConfiguration { configuration })
+ }
+}
+
+impl TryFrom<ProtobufRunPluginLocationData> for RunPluginLocation {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufRunPluginLocationData) -> Result<Self, Self::Error> {
+ 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<RunPluginLocation> for ProtobufRunPluginLocationData {
+ type Error = &'static str;
+ fn try_from(internal: RunPluginLocation) -> Result<Self, Self::Error> {
+ 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<ProtobufRunPlugin> for RunPlugin {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufRunPlugin) -> Result<Self, Self::Error> {
+ 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<RunPlugin> for ProtobufRunPlugin {
+ type Error = &'static str;
+ fn try_from(internal: RunPlugin) -> Result<Self, Self::Error> {
+ 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<ProtobufPluginAlias> for PluginAlias {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufPluginAlias) -> Result<Self, Self::Error> {
+ 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<PluginAlias> for ProtobufPluginAlias {
+ type Error = &'static str;
+ fn try_from(internal: PluginAlias) -> Result<Self, Self::Error> {
+ 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<ProtobufRunPluginOrAlias> for RunPluginOrAlias {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufRunPluginOrAlias) -> Result<Self, Self::Error> {
+ 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<RunPluginOrAlias> for ProtobufRunPluginOrAlias {
+ type Error = &'static str;
+ fn try_from(internal: RunPluginOrAlias) -> Result<Self, Self::Error> {
+ 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<ProtobufRunEditFileAction> for Run {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufRunEditFileAction) -> Result<Self, Self::Error> {
+ 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<ProtobufPaneRun> for Run {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufPaneRun) -> Result<Self, Self::Error> {
+ 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<Run> for ProtobufPaneRun {
+ type Error = &'static str;
+ fn try_from(internal: Run) -> Result<Self, Self::Error> {
+ 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<ProtobufCommandOrPlugin> for CommandOrPlugin {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufCommandOrPlugin) -> Result<Self, Self::Error> {
+ 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<CommandOrPlugin> for ProtobufCommandOrPlugin {
+ type Error = &'static str;
+ fn try_from(internal: CommandOrPlugin) -> Result<Self, Self::Error> {
+ 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<ProtobufTiledPaneLayout> for TiledPaneLayout {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufTiledPaneLayout) -> Result<Self, Self::Error> {
+ 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::<Result<Vec<_>, _>>()?;
+ 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<TiledPaneLayout> for ProtobufTiledPaneLayout {
+ type Error = &'static str;
+ fn try_from(internal: TiledPaneLayout) -> Result<Self, Self::Error> {
+ let children_split_direction: ProtobufSplitDirection =
+ internal.children_split_direction.try_into()?;
+ let children = internal
+ .children
+ .into_iter()
+ .map(|c| c.try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ 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<ProtobufFloatingPaneLayout> for FloatingPaneLayout {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufFloatingPaneLayout) -> Result<Self, Self::Error> {
+ 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<FloatingPaneLayout> for ProtobufFloatingPaneLayout {
+ type Error = &'static str;
+ fn try_from(internal: FloatingPaneLayout) -> Result<Self, Self::Error> {
+ 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<ProtobufLayoutConstraintTiledPair> for (LayoutConstraint, TiledPaneLayout) {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufLayoutConstraintTiledPair) -> Result<Self, Self::Error> {
+ 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<Self, Self::Error> {
+ Ok(ProtobufLayoutConstraintTiledPair {
+ constraint: Some(internal.0.try_into()?),
+ layout: Some(internal.1.try_into()?),
+ })
+ }
+}
+
+impl TryFrom<ProtobufLayoutConstraintFloatingPair> for (LayoutConstraint, Vec<FloatingPaneLayout>) {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufLayoutConstraintFloatingPair) -> Result<Self, Self::Error> {
+ let constraint = protobuf
+ .constraint
+ .ok_or("LayoutConstraintFloatingPair must have constraint")?
+ .try_into()?;
+ let layouts = protobuf
+ .layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok((constraint, layouts))
+ }
+}
+
+impl TryFrom<(LayoutConstraint, Vec<FloatingPaneLayout>)> for ProtobufLayoutConstraintFloatingPair {
+ type Error = &'static str;
+ fn try_from(
+ internal: (LayoutConstraint, Vec<FloatingPaneLayout>),
+ ) -> Result<Self, Self::Error> {
+ Ok(ProtobufLayoutConstraintFloatingPair {
+ constraint: Some(internal.0.try_into()?),
+ layouts: internal
+ .1
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ })
+ }
+}
+
+impl TryFrom<ProtobufSwapTiledLayout> for SwapTiledLayout {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufSwapTiledLayout) -> Result<Self, Self::Error> {
+ let constraint_map: BTreeMap<LayoutConstraint, TiledPaneLayout> = protobuf
+ .constraint_map
+ .into_iter()
+ .map(|pair| pair.try_into())
+ .collect::<Result<BTreeMap<_, _>, _>>()?;
+ Ok((constraint_map, protobuf.name))
+ }
+}
+
+impl TryFrom<SwapTiledLayout> for ProtobufSwapTiledLayout {
+ type Error = &'static str;
+ fn try_from(internal: SwapTiledLayout) -> Result<Self, Self::Error> {
+ let constraint_map = internal
+ .0
+ .into_iter()
+ .map(|(constraint, layout)| (constraint, layout).try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(ProtobufSwapTiledLayout {
+ constraint_map,
+ name: internal.1,
+ })
+ }
+}
+
+impl TryFrom<ProtobufSwapFloatingLayout> for SwapFloatingLayout {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufSwapFloatingLayout) -> Result<Self, Self::Error> {
+ let constraint_map: BTreeMap<LayoutConstraint, Vec<FloatingPaneLayout>> = protobuf
+ .constraint_map
+ .into_iter()
+ .map(|pair| pair.try_into())
+ .collect::<Result<BTreeMap<_, _>, _>>()?;
+ Ok((constraint_map, protobuf.name))
+ }
+}
+
+impl TryFrom<SwapFloatingLayout> for ProtobufSwapFloatingLayout {
+ type Error = &'static str;
+ fn try_from(internal: SwapFloatingLayout) -> Result<Self, Self::Error> {
+ let constraint_map = internal
+ .0
+ .into_iter()
+ .map(|(constraint, layouts)| (constraint, layouts).try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ 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 f5059131..3bdeea19 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 02ef1842..4e430b0b 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<ProtobufEvent> 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<String, String> = 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<ClientId> = 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<Event> 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<ProtobufContextItem> = 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<u32> =
+ 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<SessionInfo> 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<usize>)> for ProtobufClientTabHistory {
}
}
}
+
+impl From<(u16, Vec<PaneId>)> for ProtobufClientPaneHistory {
+ fn from((client_id, pane_history): (u16, Vec<PaneId>)) -> 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<ProtobufSessionManifest> 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<ProtobufSessionManifest> 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<ProtobufEventType> 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<EventType> 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 7ffe2fc1..f662c93b 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 c1ba877e..771bb9ec 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<PaneId> for ProtobufPaneId {
}
}
+impl TryFrom<ProtobufGetPanePidResponse> for GetPanePidResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufGetPanePidResponse) -> Result<Self, &'static str> {
+ 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<GetPanePidResponse> 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<Action>)> for KeyToRebind {
type Error = &'static str;
fn try_from(
@@ -288,6 +315,14 @@ impl TryFrom<ProtobufPluginCommand> 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<ProtobufPluginCommand> 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<ProtobufPluginCommand> 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<ProtobufPluginCommand> 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<ProtobufPluginCommand> 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<ProtobufPluginCommand> 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<String, String> = 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<PluginCommand> 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<PluginCommand> 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<PluginCommand> 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<PluginCommand> 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<PluginCommand> 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<ContextItem> = 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 85126830..779f6b32 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 307d3576..37782a08 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<ProtobufPermissionType> 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<PermissionType> 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 3566625b..95987c27 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 e01b7410..3e60c317 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,
@@ -4446,6 +4449,36 @@ Config {
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',
+ ),
key_modifiers: {
Ctrl,
},
@@ -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 9c280d9c..e0fa22f9 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,
@@ -4446,6 +4449,36 @@ Config {
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',
+ ),
key_modifiers: {
Ctrl,
},
@@ -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 773ae7e4..1d245a3e 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 0406faa8..e9935ef3 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,
@@ -4446,6 +4449,36 @@ Config {
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',
+ ),
key_modifiers: {
Ctrl,
},
@@ -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 ca2b7bae..368b5d88 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,
@@ -4446,6 +4449,36 @@ Config {
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',
+ ),
key_modifiers: {
Ctrl,
},
@@ -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(