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 | |
| 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')
| -rw-r--r-- | default-plugins/sequence/src/state/command_entry.rs | 10 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/command_parser.rs | 177 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/editing.rs | 80 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/execution.rs | 185 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/layout.rs | 54 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/mod.rs | 461 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/positioning.rs | 49 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/selection.rs | 175 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/sequence_mode.rs | 6 |
9 files changed, 245 insertions, 952 deletions
diff --git a/default-plugins/sequence/src/state/command_entry.rs b/default-plugins/sequence/src/state/command_entry.rs index 96a0020b..4ca9350a 100644 --- a/default-plugins/sequence/src/state/command_entry.rs +++ b/default-plugins/sequence/src/state/command_entry.rs @@ -1,6 +1,5 @@ use super::{ChainType, CommandStatus}; use std::path::PathBuf; -use std::time::Instant; use zellij_tile::prelude::PaneId; #[derive(Debug, Clone)] @@ -9,7 +8,6 @@ pub struct CommandEntry { cwd: Option<PathBuf>, pub(super) chain_type: ChainType, pub(super) status: CommandStatus, - pub(super) start_time: std::time::Instant, } impl Default for CommandEntry { @@ -19,7 +17,6 @@ impl Default for CommandEntry { cwd: None, chain_type: ChainType::default(), status: CommandStatus::default(), - start_time: Instant::now(), } } } @@ -32,10 +29,6 @@ impl CommandEntry { ..Default::default() } } - pub fn with_and(mut self) -> Self { - self.chain_type = ChainType::And; - self - } pub fn get_text(&self) -> String { self.text.clone() } @@ -74,9 +67,6 @@ impl CommandEntry { 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() } diff --git a/default-plugins/sequence/src/state/command_parser.rs b/default-plugins/sequence/src/state/command_parser.rs index 7f0898f5..35935cd3 100644 --- a/default-plugins/sequence/src/state/command_parser.rs +++ b/default-plugins/sequence/src/state/command_parser.rs @@ -1,4 +1,4 @@ -use super::ChainType; +use super::{ChainType, CommandEntry}; #[derive(Debug, Clone, Copy, PartialEq)] enum ParseState { @@ -104,63 +104,105 @@ pub fn split_by_chain_operators(text: &str) -> Vec<(String, Option<ChainType>)> 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; - } +pub fn detect_cd_command(text: &str) -> Option<String> { + let trimmed = text.trim(); - if segments.len() == 1 { - if let Some((text, Some(chain_type))) = segments.first() { - return Some((text.clone(), *chain_type)); - } - return None; + if trimmed == "cd" { + return Some("~".to_string()); } - if let Some((first_text, Some(chain_type))) = segments.first() { - return Some((first_text.clone(), *chain_type)); + if trimmed.starts_with("cd ") { + let path = trimmed[3..].trim(); + return Some(path.to_string()); } None } -pub fn get_remaining_after_first_segment(text: &str) -> Option<String> { - let segments = split_by_chain_operators(text); +/// Serialize a sequence of commands to editor format. +/// CWD changes are emitted as `cd <path>` lines; chain type as trailing ` &&`, ` ||`, ` ;`. +pub fn serialize_sequence_to_editor(commands: &[CommandEntry]) -> String { + let mut result = String::new(); - if segments.len() <= 1 { - return None; - } + result.push_str("# One cmd per line. Use && || ; to chain. cd <path> sets cwd.\n"); + result.push_str("\n"); - 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())); - } + let mut prev_cwd: Option<std::path::PathBuf> = None; + + for cmd in commands { + if cmd.get_text().trim().is_empty() { + continue; } - result.push_str(segment_text); - if let Some(chain_type) = chain_type_opt { - result.push_str(&format!(" {} ", chain_type.as_str())); + + let cmd_cwd = cmd.get_cwd(); + if cmd_cwd != prev_cwd && cmd_cwd.is_some() { + let path = cmd_cwd.as_ref().unwrap(); + result.push_str(&format!("cd {}\n", path.display())); } + prev_cwd = cmd_cwd; + + let op_suffix = match cmd.get_chain_type() { + ChainType::And => " &&", + ChainType::Or => " ||", + ChainType::Then => " ;", + ChainType::None => "", + }; + + result.push_str(&format!("{}{}\n", cmd.get_text(), op_suffix)); } - Some(result.trim().to_string()) + result } -pub fn detect_cd_command(text: &str) -> Option<String> { - let trimmed = text.trim(); +/// Parse command text into a Vec<CommandEntry>. +/// `cd <path>` lines set the cwd for subsequent commands. +/// Blank lines and `#` comment lines are ignored. +pub fn parse_commands( + contents: &str, + initial_cwd: Option<std::path::PathBuf>, +) -> Vec<CommandEntry> { + use crate::path_formatting; + let mut commands = Vec::new(); + let mut current_cwd = initial_cwd; - if trimmed == "cd" { - return Some("~".to_string()); + for line in contents.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + + // Split by chain operators first so that "cd /path &&" doesn't absorb the operator + // into the path. Each resulting segment is then checked for cd independently. + let segments = split_by_chain_operators(trimmed); + for (text, chain_type_opt) in segments { + let text = text.trim().to_string(); + if text.is_empty() { + continue; + } + if let Some(path) = detect_cd_command(&text) { + if let Some(new_cwd) = path_formatting::resolve_path(current_cwd.as_ref(), &path) { + current_cwd = Some(new_cwd); + } + // cd segments are not added as command entries; their chain type is discarded + } else { + let mut entry = CommandEntry::new(&text, current_cwd.clone()); + if let Some(chain_type) = chain_type_opt { + entry.set_chain_type(chain_type); + } + commands.push(entry); + } + } } - if trimmed.starts_with("cd ") { - let path = trimmed[3..].trim(); - return Some(path.to_string()); + // Commands with no explicit operator default to && + if !commands.is_empty() { + let last_idx = commands.len() - 1; + for cmd in &mut commands[..last_idx] { + cmd.fill_chain_type_if_empty(); + } } - None + commands } #[cfg(test)] @@ -278,36 +320,6 @@ mod tests { } #[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!( @@ -324,4 +336,35 @@ mod tests { let result = split_by_chain_operators("ls && "); assert_eq!(result, vec![("ls".to_string(), Some(ChainType::And))]); } + + #[test] + fn test_editor_file_no_operator_defaults_to_and() { + let contents = "cmd1\ncmd2\ncmd3"; + let result = parse_commands(contents, None); + assert_eq!(result.len(), 3); + assert_eq!(result[0].get_chain_type(), ChainType::And); + assert_eq!(result[1].get_chain_type(), ChainType::And); + assert_eq!(result[2].get_chain_type(), ChainType::None); + } + + #[test] + fn test_editor_file_explicit_operator_preserved() { + let contents = "cmd1 ||\ncmd2"; + let result = parse_commands(contents, None); + assert_eq!(result.len(), 2); + assert_eq!(result[0].get_chain_type(), ChainType::Or); + assert_eq!(result[1].get_chain_type(), ChainType::None); + } + + #[test] + fn test_cd_with_chain_operator_does_not_absorb_operator_into_path() { + use super::super::CommandEntry; + use std::path::PathBuf; + + let contents = "cd /some/path && cargo build"; + let result = parse_commands(contents, None); + assert_eq!(result.len(), 1); + assert_eq!(result[0].get_text(), "cargo build"); + assert_eq!(result[0].get_cwd(), Some(PathBuf::from("/some/path"))); + } } diff --git a/default-plugins/sequence/src/state/editing.rs b/default-plugins/sequence/src/state/editing.rs deleted file mode 100644 index 3deac619..00000000 --- a/default-plugins/sequence/src/state/editing.rs +++ /dev/null @@ -1,80 +0,0 @@ -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 index fb194f61..42d4112a 100644 --- a/default-plugins/sequence/src/state/execution.rs +++ b/default-plugins/sequence/src/state/execution.rs @@ -1,16 +1,13 @@ use crate::path_formatting::format_cwd; -use crate::state::{ChainType, CommandEntry, CommandStatus}; -use std::collections::BTreeMap; +use crate::state::{CommandEntry, CommandStatus}; 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>, + pub displayed_pane_id: Option<PaneId>, } impl Execution { @@ -19,8 +16,7 @@ impl Execution { all_commands: vec![CommandEntry::default()], current_running_command_index: 0, is_running: false, - sequence_id: 0, - primary_pane_id_before_sequence: None, + displayed_pane_id: None, } } @@ -82,181 +78,6 @@ impl Execution { 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, - borderless: Some(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 { diff --git a/default-plugins/sequence/src/state/layout.rs b/default-plugins/sequence/src/state/layout.rs index 65a267e0..6b070778 100644 --- a/default-plugins/sequence/src/state/layout.rs +++ b/default-plugins/sequence/src/state/layout.rs @@ -1,10 +1,4 @@ -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, } @@ -12,58 +6,10 @@ pub struct Layout { 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 { 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 - } } diff --git a/default-plugins/sequence/src/state/positioning.rs b/default-plugins/sequence/src/state/positioning.rs deleted file mode 100644 index b724df62..00000000 --- a/default-plugins/sequence/src/state/positioning.rs +++ /dev/null @@ -1,49 +0,0 @@ -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), - Some(false), - ); - 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 index 18f83bcd..2137f47d 100644 --- a/default-plugins/sequence/src/state/selection.rs +++ b/default-plugins/sequence/src/state/selection.rs @@ -1,115 +1,17 @@ -use crate::state::{CommandEntry, Editing}; -use std::path::PathBuf; -use zellij_tile::prelude::*; +use super::CommandEntry; 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, + current_selected_command_index: None, } } - 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); + pub fn move_up(&mut self, all_commands: &[CommandEntry]) { match self.current_selected_command_index.as_mut() { Some(i) if *i > 0 => { *i -= 1; @@ -121,11 +23,9 @@ impl Selection { 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); + pub fn move_down(&mut self, all_commands: &[CommandEntry]) { match self.current_selected_command_index.as_mut() { Some(i) if *i < all_commands.len().saturating_sub(1) => { *i += 1; @@ -137,73 +37,6 @@ impl Selection { 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); - } } } diff --git a/default-plugins/sequence/src/state/sequence_mode.rs b/default-plugins/sequence/src/state/sequence_mode.rs new file mode 100644 index 00000000..3d079d70 --- /dev/null +++ b/default-plugins/sequence/src/state/sequence_mode.rs @@ -0,0 +1,6 @@ +#[derive(Default, Debug, Clone, PartialEq)] +pub enum SequenceMode { + #[default] + SinglePane, + Spread, +} |
