diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-02-17 15:43:28 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-17 15:43:28 +0100 |
| commit | 4e66a0beff5e41bda1dbde521fa1d2858c46fe7b (patch) | |
| tree | 380bee5eeac56bea2ebf341cccbe69615585e394 /default-plugins/sequence/src/state/mod.rs | |
| parent | Fix startup message and cache file creation (#4705) (diff) | |
| download | sl0ppij-4e66a0beff5e41bda1dbde521fa1d2858c46fe7b.tar.gz sl0ppij-4e66a0beff5e41bda1dbde521fa1d2858c46fe7b.tar.xz sl0ppij-4e66a0beff5e41bda1dbde521fa1d2858c46fe7b.zip | |
Revamp the sequence plugin (#4713)
* initial pipe interface
* initial open_x_pane_in_new_tab plugin api implementation
* fix: send CommandPaneExited for command panes opened as initial_terminals in new tabs from plugins
* open panes in place of specific pane id plugin commands - initial implementation
* sequence and plugin api ux
* focus self when starting
* combine ui and pipe paths
* highlight selected pane
* initial sequence removal of editing capabilities
* fix: translate plugin paths in api
* properly trim commands when loaded from pipe or pasted text
* minor ux fixes
* get ctrl c to work properly
* behavior fixes
* more ui fixes
* fix tests
* close tab and end sequence properly
* default to AND
* zellij sequence alias
* add alias
* consolidate paths
* rustfmt
* fix ui padding
* add pr
* fix: reset scrolling when resetting terminal state
Diffstat (limited to 'default-plugins/sequence/src/state/mod.rs')
| -rw-r--r-- | default-plugins/sequence/src/state/mod.rs | 461 |
1 files changed, 122 insertions, 339 deletions
diff --git a/default-plugins/sequence/src/state/mod.rs b/default-plugins/sequence/src/state/mod.rs index e3fbf3ff..148e6702 100644 --- a/default-plugins/sequence/src/state/mod.rs +++ b/default-plugins/sequence/src/state/mod.rs @@ -2,28 +2,25 @@ mod chain_type; mod command_entry; mod command_parser; mod command_status; -mod editing; mod execution; mod layout; -pub mod positioning; mod selection; +mod sequence_mode; 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_parser::{parse_commands, serialize_sequence_to_editor}; pub use command_status::CommandStatus; -pub use editing::Editing; pub use execution::Execution; pub use layout::Layout; pub use selection::Selection; +pub use sequence_mode::SequenceMode; -use crate::ui::text_input::TextInput; +use std::collections::BTreeMap; use std::path::PathBuf; use zellij_tile::prelude::*; +#[derive(Default)] pub struct State { pub shell: Option<PathBuf>, pub plugin_id: Option<u32>, @@ -31,291 +28,85 @@ pub struct State { 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>, + pub mode: SequenceMode, + pub editor_pane_id: Option<PaneId>, + pub editor_temp_file: Option<PathBuf>, + pub sequence_tab_id: Option<usize>, + pub blocking_pipe_id: Option<String>, } 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); - } + self.selection.move_up(&self.execution.all_commands); + self.show_selected_pane(); } 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); - } - }); + self.selection.move_down(&self.execution.all_commands); + self.show_selected_pane(); } - 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; + pub fn show_selected_pane(&mut self) { + if self.mode == SequenceMode::Spread { + let all_pane_ids: Vec<PaneId> = self + .execution + .all_commands + .iter() + .filter_map(|c| c.get_pane_id()) + .collect(); + let selected_pane_id = self + .current_selected_command() + .and_then(|c| c.get_pane_id()); + if let Some(selected) = selected_pane_id { + let to_unhighlight: Vec<PaneId> = all_pane_ids + .into_iter() + .filter(|&p| p != selected) + .collect(); + highlight_and_unhighlight_panes(vec![selected], to_unhighlight); } - } - - let Some(new_selected_index) = self.selection.current_selected_command_index.map(|i| i + 1) - else { return; - }; - - let mut new_command = CommandEntry::new(segment_text, self.cwd.clone()); - if let Some(chain_type) = chain_type_opt { - new_command.set_chain_type(*chain_type); - } else if !is_last_line { - new_command.set_chain_type(ChainType::And); } - - self.execution - .all_commands - .insert(new_selected_index, new_command); - self.selection.current_selected_command_index = Some(new_selected_index); - } - - fn ensure_line_end_chain_type(&mut self) { - if let Some(last_cmd_index) = self.selection.current_selected_command_index { - self.execution - .all_commands - .get_mut(last_cmd_index) - .map(|c| { - if matches!(c.get_chain_type(), ChainType::None) { - c.set_chain_type(ChainType::And); - } - }); - } - } - - pub fn pasted_lines(&mut self, lines: Vec<&str>) { - let Some(current_text) = self.editing_input_text() else { - return; - }; - - for (line_index, line) in lines.iter().enumerate() { - let segments = split_by_chain_operators(line); - let is_last_line = line_index == lines.len().saturating_sub(1); - - for (seg_index, (segment_text, chain_type_opt)) in segments.iter().enumerate() { - if line_index == 0 && seg_index == 0 { - self.handle_first_pasted_segment(¤t_text, segment_text, chain_type_opt); - } else { - self.insert_new_pasted_segment(segment_text, chain_type_opt, is_last_line); - } - } - - if !is_last_line { - self.ensure_line_end_chain_type(); + let target_pane_id = self + .current_selected_command() + .and_then(|c| c.get_pane_id()); + if let (Some(displayed), Some(target)) = (self.execution.displayed_pane_id, target_pane_id) + { + if displayed != target { + replace_pane_with_existing_pane(displayed, target, true); + self.execution.displayed_pane_id = Some(target); } } - - 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.selection.current_selected_command_index = None; 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 load_commands(&mut self, command_string: &str, cwd_override: Option<PathBuf>) { + let effective_cwd = cwd_override.or_else(|| self.cwd.clone()); + let commands = parse_commands(command_string, effective_cwd); + if commands.is_empty() { + return; + } + self.execution.all_commands = commands; + self.execution.current_running_command_index = 0; + self.selection.current_selected_command_index = None; } pub fn can_run_sequence(&self) -> bool { @@ -332,6 +123,31 @@ impl State { }; self.execution.all_commands.get_mut(i) } + + /// Serialize current sequence to a temp file and open it in the user's $EDITOR. + /// Stores the editor pane id and temp file path for later retrieval. + pub fn open_editor(&mut self) { + if self.execution.is_running { + return; + } + let Some(plugin_id) = self.plugin_id else { + return; + }; + + let serialized = serialize_sequence_to_editor(&self.execution.all_commands); + let temp_path = PathBuf::from(format!("/tmp/zellij-sequence-{}.sh", plugin_id)); + + if let Err(e) = std::fs::write(&temp_path, &serialized) { + eprintln!("Failed to write sequence to temp file: {}", e); + return; + } + + let file_to_open = FileToOpen::new(temp_path.clone()); + let editor_pane_id = open_file_in_place_of_plugin(file_to_open, false, BTreeMap::new()); + self.editor_pane_id = editor_pane_id; + self.editor_temp_file = Some(temp_path); + } + pub fn reposition_plugin(&mut self) -> bool { let mut repositioned = false; let Some(plugin_id) = self.plugin_id else { @@ -343,82 +159,62 @@ impl State { let Some(total_viewport_columns) = self.total_viewport_columns else { return repositioned; }; + if self.all_commands_are_pending() { + return false; + } + 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 - Some(false), - ); - 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, - ); + // 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 longest_line = crate::ui::components::calculate_longest_line( - &longest_cwd_display, - longest_command, - max_chain_width, - max_status_width, - ); + let (max_chain_width, max_status_width) = crate::ui::components::calculate_max_widths( + &self.execution.all_commands, + self.layout.spinner_frame, + ); - 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 longest_line = crate::ui::components::calculate_longest_line( + &longest_cwd_display, + longest_command, + max_chain_width, + max_status_width, + ); - let height = (height_padding + total_commands).min((total_viewport_rows * 25) / 100); + 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.saturating_sub(2)); - // Position: top-right with 1-space margins - let x = total_viewport_columns.saturating_sub(width); - let y = 1; + let height = (height_padding + total_commands).min((total_viewport_rows * 50) / 100); - 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 - Some(false), - ); - 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, - )]); - } - }); - } + // 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 + Some(false), + ); + 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 @@ -432,17 +228,4 @@ impl State { }; 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 - } } |
