aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/sequence/src/state
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-02-17 15:43:28 +0100
committerGitHub <noreply@github.com>2026-02-17 15:43:28 +0100
commit4e66a0beff5e41bda1dbde521fa1d2858c46fe7b (patch)
tree380bee5eeac56bea2ebf341cccbe69615585e394 /default-plugins/sequence/src/state
parentFix startup message and cache file creation (#4705) (diff)
downloadsl0ppij-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.rs10
-rw-r--r--default-plugins/sequence/src/state/command_parser.rs177
-rw-r--r--default-plugins/sequence/src/state/editing.rs80
-rw-r--r--default-plugins/sequence/src/state/execution.rs185
-rw-r--r--default-plugins/sequence/src/state/layout.rs54
-rw-r--r--default-plugins/sequence/src/state/mod.rs461
-rw-r--r--default-plugins/sequence/src/state/positioning.rs49
-rw-r--r--default-plugins/sequence/src/state/selection.rs175
-rw-r--r--default-plugins/sequence/src/state/sequence_mode.rs6
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(&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();
+ 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,
+}