diff options
| author | Aram Drevekenin <aram@poor.dev> | 2025-12-11 14:25:42 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-11 14:25:42 +0100 |
| commit | 693fa082def5ef0fe030cd7fdc3b1df051b35dc6 (patch) | |
| tree | b3f651462bbc1cd2fa5b2d79207343413ef6b83c /default-plugins/sequence/src/state | |
| parent | fix(zellij-server): unblock building on 32bit targets (#4484) (diff) | |
| download | sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.tar.gz sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.tar.xz sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.zip | |
Command Sequences, Blocking CLI Commands, New Plugin APIs (#4546)
* fix deadlocks
* initial logging to plugin implementation
* only log actions, include keys as Action::Write
* send actions to plugins as a new Event::UserAction
* add Action::Write metadata to plugin API
* run_action plugin command and permission
* block-until-exit/success/failure in the cli
* fix tests
* add NewBlockingPane to the plugin API
* run actions in a different thread so as not to block plugin execution
* add custom context to run_action flow
* fixes for floating pane z_index sorting and unknown protobuf events not crashing plugins
* always render the frame of pinned panes
* macros module
* add macros to config
* KDL macro parsing
* thread macros to the plugin thread
* get/update/delete macros from plugins
* add macros updated event
* add ability to rename macros
* fix deadlock
* fix z_indices with pinned unselectable panes bug
* allow plugins to opt-in to show_cursor
* change show_cursor(bool) to show_cursor(Option<coordinates>)
* test
* upgrade strum to allow proper enum iteration and case insensitive fromstr implementation for Actions
* better cwd tracking for exited command panes
* add initial panes to NewTab Action (no-op for now)
* add initial panes to cli newtab action
* make initial panes work
* add close_on_exit and start_suspended to the cli
* properly convert newtab action (potentially breaking for plugins)
* allow first pane in tab to block
* make distinction between opening panes with the last active client and the current pane in the cli
* improve some apis
* fix fixed size closing pane bug
* fix plugin cursor coordinates
* fix issues with launch-or-focus-plugin having to do with in-place and suppressed panes
* fix: set affected pane in replace pane
* allow setting a pane in place when it is suppressed and being focused (tests failing)
* fix tests
* some fixes for unsuppression of panes
* some fixes for completion_tx in new tab action
* allow replace/suppressed rather than replace/close only
* fixes for inserting suppressed panes without conflicts
* pane focus history and explicit unsuppress plugin api
* fix: prefer explicitly provided pane id in placement in actions that provide it
* fix floating pane coordinate change and z_index
* fix plugin cache dir to persist between sessions
* fix some stack focusing issues when adding panes without a client id
* make sure pinned panes are always above non-pinned floating panes
* allow plugins to copy to clipboard
* initial text styling additions implementations - buggy
* fix dimming issues
* remove extras, stay with just dim and unbold
* unbold and dim as indices
* allow ui components to render success/error colors
* allow plugins to send sigint/sigkill to processes, as well as receive their pid
* ui component fixes and send originating plugin context with run_action
* originating plugin for new tab too
* make plugn show pane command also expand stacked panes
* add sequence plugin
* remove macros
* remove snapshots
* add shortcut for sequence plugin
* fix plugin tests
* plugin tests for new apis
* update snapshots for sequence shortcut
* cleanups and formatting
* Event::CwdChanged
* moar cleanups
* remove some warnings
* remove mroe warnings
* remove dead code
* remove more dead code
* fix minor bugs in sequence plugin
* update sequence help and format codebase
* fix conditional blocking cli commands
* fix initial-command new-tab cli flag
* add client_ids to CwdChanged Event
* last fixes
* add pr
Diffstat (limited to 'default-plugins/sequence/src/state')
| -rw-r--r-- | default-plugins/sequence/src/state/chain_type.rs | 45 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/command_entry.rs | 86 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/command_parser.rs | 327 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/command_status.rs | 26 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/editing.rs | 80 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/execution.rs | 265 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/layout.rs | 73 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/mod.rs | 446 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/positioning.rs | 48 | ||||
| -rw-r--r-- | default-plugins/sequence/src/state/selection.rs | 214 |
10 files changed, 1610 insertions, 0 deletions
diff --git a/default-plugins/sequence/src/state/chain_type.rs b/default-plugins/sequence/src/state/chain_type.rs new file mode 100644 index 00000000..3da3316c --- /dev/null +++ b/default-plugins/sequence/src/state/chain_type.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; +use zellij_tile::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ChainType { + And, + Or, + Then, + None, +} + +impl Default for ChainType { + fn default() -> Self { + ChainType::None + } +} + +impl ChainType { + pub fn to_unblock_condition(&self) -> Option<UnblockCondition> { + match self { + ChainType::And => Some(UnblockCondition::OnExitSuccess), + ChainType::Or => Some(UnblockCondition::OnExitFailure), + ChainType::Then => Some(UnblockCondition::OnAnyExit), + ChainType::None => None, + } + } + + pub fn as_str(&self) -> &str { + match self { + ChainType::And => "&&", + ChainType::Or => "||", + ChainType::Then => ";", + ChainType::None => "", + } + } + + pub fn cycle_next(&mut self) { + *self = match self { + ChainType::And => ChainType::Or, + ChainType::Or => ChainType::Then, + ChainType::Then => ChainType::And, + ChainType::None => ChainType::And, + }; + } +} diff --git a/default-plugins/sequence/src/state/command_entry.rs b/default-plugins/sequence/src/state/command_entry.rs new file mode 100644 index 00000000..96a0020b --- /dev/null +++ b/default-plugins/sequence/src/state/command_entry.rs @@ -0,0 +1,86 @@ +use super::{ChainType, CommandStatus}; +use std::path::PathBuf; +use std::time::Instant; +use zellij_tile::prelude::PaneId; + +#[derive(Debug, Clone)] +pub struct CommandEntry { + text: String, + cwd: Option<PathBuf>, + pub(super) chain_type: ChainType, + pub(super) status: CommandStatus, + pub(super) start_time: std::time::Instant, +} + +impl Default for CommandEntry { + fn default() -> Self { + CommandEntry { + text: String::default(), + cwd: None, + chain_type: ChainType::default(), + status: CommandStatus::default(), + start_time: Instant::now(), + } + } +} + +impl CommandEntry { + pub fn new(text: &str, cwd: Option<PathBuf>) -> Self { + CommandEntry { + text: text.to_owned(), + cwd, + ..Default::default() + } + } + pub fn with_and(mut self) -> Self { + self.chain_type = ChainType::And; + self + } + pub fn get_text(&self) -> String { + self.text.clone() + } + pub fn set_text(&mut self, text: String) { + self.text = text; + } + pub fn clear_text(&mut self) { + self.text.clear(); + } + pub fn get_chain_type(&self) -> ChainType { + self.chain_type + } + pub fn set_chain_type(&mut self, chain_type: ChainType) { + self.chain_type = chain_type; + } + pub fn get_status(&self) -> CommandStatus { + self.status.clone() + } + pub fn set_status(&mut self, status: CommandStatus) { + self.status = status; + } + pub fn get_pane_id(&self) -> Option<PaneId> { + self.status.get_pane_id() + } + pub fn is_empty(&self) -> bool { + self.text.trim().is_empty() + } + pub fn fill_chain_type_if_empty(&mut self) { + if let ChainType::None = self.chain_type { + self.chain_type = ChainType::And; + } + } + pub fn clear_chain_type(&mut self) { + self.chain_type = ChainType::None; + } + pub fn clear_status(&mut self) { + self.status = CommandStatus::Pending; + } + pub fn cycle_chain_type(&mut self) { + self.chain_type.cycle_next(); + } + pub fn get_cwd(&self) -> Option<PathBuf> { + self.cwd.clone() + } + pub fn set_cwd(&mut self, cwd: Option<PathBuf>) { + self.cwd = cwd; + } +} diff --git a/default-plugins/sequence/src/state/command_parser.rs b/default-plugins/sequence/src/state/command_parser.rs new file mode 100644 index 00000000..7f0898f5 --- /dev/null +++ b/default-plugins/sequence/src/state/command_parser.rs @@ -0,0 +1,327 @@ +use super::ChainType; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum ParseState { + Normal, + InSingleQuote, + InDoubleQuote, + Escaped(EscapeContext), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum EscapeContext { + Normal, + DoubleQuote, +} + +pub fn split_by_chain_operators(text: &str) -> Vec<(String, Option<ChainType>)> { + let mut segments = Vec::new(); + let mut current_segment = String::new(); + let mut state = ParseState::Normal; + let chars: Vec<char> = text.chars().collect(); + let mut i = 0; + + while i < chars.len() { + let ch = chars[i]; + + match state { + ParseState::Normal => { + if ch == '\\' { + state = ParseState::Escaped(EscapeContext::Normal); + current_segment.push(ch); + i += 1; + } else if ch == '\'' { + state = ParseState::InSingleQuote; + current_segment.push(ch); + i += 1; + } else if ch == '"' { + state = ParseState::InDoubleQuote; + current_segment.push(ch); + i += 1; + } else if ch == '&' && i + 1 < chars.len() && chars[i + 1] == '&' { + let segment_text = current_segment.trim().to_string(); + if !segment_text.is_empty() { + segments.push((segment_text, Some(ChainType::And))); + } + current_segment.clear(); + i += 2; + } else if ch == '|' && i + 1 < chars.len() && chars[i + 1] == '|' { + let segment_text = current_segment.trim().to_string(); + if !segment_text.is_empty() { + segments.push((segment_text, Some(ChainType::Or))); + } + current_segment.clear(); + i += 2; + } else if ch == ';' { + let segment_text = current_segment.trim().to_string(); + if !segment_text.is_empty() { + segments.push((segment_text, Some(ChainType::Then))); + } + current_segment.clear(); + i += 1; + } else { + current_segment.push(ch); + i += 1; + } + }, + ParseState::InSingleQuote => { + current_segment.push(ch); + if ch == '\'' { + state = ParseState::Normal; + } + i += 1; + }, + ParseState::InDoubleQuote => { + if ch == '\\' { + state = ParseState::Escaped(EscapeContext::DoubleQuote); + current_segment.push(ch); + i += 1; + } else if ch == '"' { + current_segment.push(ch); + state = ParseState::Normal; + i += 1; + } else { + current_segment.push(ch); + i += 1; + } + }, + ParseState::Escaped(context) => { + current_segment.push(ch); + state = match context { + EscapeContext::Normal => ParseState::Normal, + EscapeContext::DoubleQuote => ParseState::InDoubleQuote, + }; + i += 1; + }, + } + } + + let final_segment = current_segment.trim().to_string(); + if !final_segment.is_empty() { + segments.push((final_segment, None)); + } + + segments +} + +pub fn detect_chain_operator_at_end(text: &str) -> Option<(String, ChainType)> { + let segments = split_by_chain_operators(text); + + if segments.is_empty() { + return None; + } + + if segments.len() == 1 { + if let Some((text, Some(chain_type))) = segments.first() { + return Some((text.clone(), *chain_type)); + } + return None; + } + + if let Some((first_text, Some(chain_type))) = segments.first() { + return Some((first_text.clone(), *chain_type)); + } + + None +} + +pub fn get_remaining_after_first_segment(text: &str) -> Option<String> { + let segments = split_by_chain_operators(text); + + if segments.len() <= 1 { + return None; + } + + let mut result = String::new(); + for (i, (segment_text, chain_type_opt)) in segments.iter().enumerate().skip(1) { + if i > 1 { + if let Some(chain_type) = chain_type_opt { + result.push_str(&format!(" {} ", chain_type.as_str())); + } + } + result.push_str(segment_text); + if let Some(chain_type) = chain_type_opt { + result.push_str(&format!(" {} ", chain_type.as_str())); + } + } + + Some(result.trim().to_string()) +} + +pub fn detect_cd_command(text: &str) -> Option<String> { + let trimmed = text.trim(); + + if trimmed == "cd" { + return Some("~".to_string()); + } + + if trimmed.starts_with("cd ") { + let path = trimmed[3..].trim(); + return Some(path.to_string()); + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_single_command_no_operator() { + let result = split_by_chain_operators("ls"); + assert_eq!(result, vec![("ls".to_string(), None)]); + } + + #[test] + fn test_two_commands_with_and() { + let result = split_by_chain_operators("ls && pwd"); + assert_eq!( + result, + vec![ + ("ls".to_string(), Some(ChainType::And)), + ("pwd".to_string(), None) + ] + ); + } + + #[test] + fn test_two_commands_with_or() { + let result = split_by_chain_operators("cmd1 || cmd2"); + assert_eq!( + result, + vec![ + ("cmd1".to_string(), Some(ChainType::Or)), + ("cmd2".to_string(), None) + ] + ); + } + + #[test] + fn test_two_commands_with_semicolon() { + let result = split_by_chain_operators("echo hi ; ls"); + assert_eq!( + result, + vec![ + ("echo hi".to_string(), Some(ChainType::Then)), + ("ls".to_string(), None) + ] + ); + } + + #[test] + fn test_three_commands_mixed() { + let result = split_by_chain_operators("a && b || c"); + assert_eq!( + result, + vec![ + ("a".to_string(), Some(ChainType::And)), + ("b".to_string(), Some(ChainType::Or)), + ("c".to_string(), None) + ] + ); + } + + #[test] + fn test_operator_in_single_quotes() { + let result = split_by_chain_operators("echo '&&' && ls"); + assert_eq!( + result, + vec![ + ("echo '&&'".to_string(), Some(ChainType::And)), + ("ls".to_string(), None) + ] + ); + } + + #[test] + fn test_operator_in_double_quotes() { + let result = split_by_chain_operators("echo \"||\" || pwd"); + assert_eq!( + result, + vec![ + ("echo \"||\"".to_string(), Some(ChainType::Or)), + ("pwd".to_string(), None) + ] + ); + } + + #[test] + fn test_escaped_operator() { + let result = split_by_chain_operators("echo \\&& test"); + assert_eq!(result, vec![("echo \\&& test".to_string(), None)]); + } + + #[test] + fn test_complex_quoting() { + let result = split_by_chain_operators("cmd1 && echo \"a || b\" && cmd2"); + assert_eq!( + result, + vec![ + ("cmd1".to_string(), Some(ChainType::And)), + ("echo \"a || b\"".to_string(), Some(ChainType::And)), + ("cmd2".to_string(), None) + ] + ); + } + + #[test] + fn test_empty_segments_filtered() { + let result = split_by_chain_operators("cmd1 && && cmd2"); + assert_eq!( + result, + vec![ + ("cmd1".to_string(), Some(ChainType::And)), + ("cmd2".to_string(), None) + ] + ); + } + + #[test] + fn test_detect_operator_just_typed_and() { + let result = detect_chain_operator_at_end("ls &&"); + assert_eq!(result, Some(("ls".to_string(), ChainType::And))); + } + + #[test] + fn test_detect_operator_just_typed_or() { + let result = detect_chain_operator_at_end("cmd ||"); + assert_eq!(result, Some(("cmd".to_string(), ChainType::Or))); + } + + #[test] + fn test_detect_operator_just_typed_semicolon() { + let result = detect_chain_operator_at_end("echo ;"); + assert_eq!(result, Some(("echo".to_string(), ChainType::Then))); + } + + #[test] + fn test_detect_operator_no_operator() { + let result = detect_chain_operator_at_end("ls"); + assert_eq!(result, None); + } + + #[test] + fn test_detect_operator_in_quotes() { + let result = detect_chain_operator_at_end("echo '&&'"); + assert_eq!(result, None); + } + + #[test] + fn test_whitespace_trimming() { + let result = split_by_chain_operators(" cmd1 && cmd2 "); + assert_eq!( + result, + vec![ + ("cmd1".to_string(), Some(ChainType::And)), + ("cmd2".to_string(), None) + ] + ); + } + + #[test] + fn test_trailing_operator_with_space() { + let result = split_by_chain_operators("ls && "); + assert_eq!(result, vec![("ls".to_string(), Some(ChainType::And))]); + } +} diff --git a/default-plugins/sequence/src/state/command_status.rs b/default-plugins/sequence/src/state/command_status.rs new file mode 100644 index 00000000..39717e8b --- /dev/null +++ b/default-plugins/sequence/src/state/command_status.rs @@ -0,0 +1,26 @@ +use zellij_tile::prelude::*; + +#[derive(Debug, Clone)] +pub enum CommandStatus { + Exited(Option<i32>, Option<PaneId>), + Running(Option<PaneId>), + Pending, + Interrupted(Option<PaneId>), +} + +impl CommandStatus { + pub fn get_pane_id(&self) -> Option<PaneId> { + match self { + CommandStatus::Exited(_, pane_id) => *pane_id, + CommandStatus::Running(pane_id) => *pane_id, + CommandStatus::Pending => None, + CommandStatus::Interrupted(pane_id) => *pane_id, + } + } +} + +impl Default for CommandStatus { + fn default() -> Self { + CommandStatus::Pending + } +} diff --git a/default-plugins/sequence/src/state/editing.rs b/default-plugins/sequence/src/state/editing.rs new file mode 100644 index 00000000..3deac619 --- /dev/null +++ b/default-plugins/sequence/src/state/editing.rs @@ -0,0 +1,80 @@ +use crate::path_formatting; +use crate::state::CommandEntry; +use crate::ui::text_input::TextInput; +use std::path::PathBuf; + +pub struct Editing { + pub editing_input: Option<TextInput>, +} + +impl Editing { + pub fn new() -> Self { + Self { + editing_input: Some(TextInput::new("".to_owned())), + } + } + + pub fn start_editing(&mut self, text: String) { + self.editing_input = Some(TextInput::new(text)); + } + + pub fn cancel_editing(&mut self) { + self.editing_input = None; + } + + pub fn input_text(&self) -> Option<String> { + self.editing_input.as_ref().map(|e| e.get_text().to_owned()) + } + + pub fn set_input_text(&mut self, text: String) { + self.editing_input.as_mut().map(|e| e.set_text(text)); + } + + pub fn handle_submit( + &mut self, + selection_index: Option<usize>, + all_commands: &mut Vec<CommandEntry>, + current_cwd: &Option<PathBuf>, + ) -> (bool, Option<usize>) { + let mut handled_internally = false; + let mut new_selection_index = None; + + if let Some(current_text) = self.editing_input.as_ref().map(|i| i.get_text()) { + if let Some(index) = selection_index { + if let Some(command) = all_commands.get_mut(index) { + if current_text.starts_with("cd ") || current_text == "cd" { + let path = if current_text == "cd" { + "~" + } else { + current_text[3..].trim() + }; + + if let Some(new_cwd) = + path_formatting::resolve_path(current_cwd.as_ref(), path) + { + command.set_cwd(Some(new_cwd)); + } + + command.set_text("".to_owned()); + self.editing_input.as_mut().map(|c| c.clear()); + new_selection_index = selection_index; // remain editing after a cd command + handled_internally = true; + return (handled_internally, new_selection_index); // avoid setting edit_input to None below, we + // still want to be in editing mode after + // changing directory with cd + } else { + command.set_text(current_text.to_owned()); + } + } + } + } + self.editing_input = None; + (handled_internally, new_selection_index) + } +} + +impl Default for Editing { + fn default() -> Self { + Self::new() + } +} diff --git a/default-plugins/sequence/src/state/execution.rs b/default-plugins/sequence/src/state/execution.rs new file mode 100644 index 00000000..ff630ec0 --- /dev/null +++ b/default-plugins/sequence/src/state/execution.rs @@ -0,0 +1,265 @@ +use crate::path_formatting::format_cwd; +use crate::state::{ChainType, CommandEntry, CommandStatus}; +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::time::Duration; +use zellij_tile::prelude::*; + +pub struct Execution { + pub all_commands: Vec<CommandEntry>, + pub current_running_command_index: usize, + pub is_running: bool, + pub sequence_id: u64, + pub primary_pane_id_before_sequence: Option<PaneId>, +} + +impl Execution { + pub fn new() -> Self { + Self { + all_commands: vec![CommandEntry::default()], + current_running_command_index: 0, + is_running: false, + sequence_id: 0, + primary_pane_id_before_sequence: None, + } + } + + pub fn longest_cwd_display(&self, global_cwd: &Option<PathBuf>) -> String { + self.all_commands + .iter() + .map(|cmd| { + let cwd = cmd.get_cwd().or_else(|| global_cwd.clone()); + if let Some(cwd) = &cwd { + format_cwd(cwd) + } else { + "~".to_string() + } + }) + .max_by_key(|s| s.len()) + .unwrap_or_else(|| "~".to_string()) + } + + pub fn remove_empty_commands(&mut self) { + if self.all_commands.iter().len() > 1 { + self.all_commands.retain(|c| !c.get_text().is_empty()); + } + } + + pub fn get_first_command(&self) -> Option<CommandEntry> { + self.all_commands.iter().next().cloned() + } + + pub fn set_command_status(&mut self, command_index: usize, status: CommandStatus) { + self.all_commands + .get_mut(command_index) + .map(|c| c.set_status(status)); + } + + pub fn set_current_running_command_status(&mut self, status: CommandStatus) { + self.all_commands + .get_mut(self.current_running_command_index) + .map(|c| c.set_status(status)); + } + + pub fn get_current_running_command_status(&self) -> Option<CommandStatus> { + self.all_commands + .get(self.current_running_command_index) + .map(|c| c.get_status()) + } + + pub fn can_run_sequence(&self) -> bool { + self.all_commands.iter().any(|command| !command.is_empty()) + } + + pub fn copy_to_clipboard(&self) { + let text_to_copy = self + .all_commands + .iter() + .map(|c| format!("{}", c.get_text())) + .collect::<Vec<_>>() + .join("\n"); + if !text_to_copy.is_empty() { + copy_to_clipboard(text_to_copy); + } + } + + pub fn update_pane_id_for_command(&mut self, pane_id: PaneId, command_text: &str) { + for command in self.all_commands.iter_mut() { + if let CommandStatus::Pending | CommandStatus::Running(None) = command.get_status() { + let cmd_text = command.get_text(); + if cmd_text == command_text { + command.set_status(CommandStatus::Running(Some(pane_id))); + break; + } + } + } + } + + pub fn update_exited_command_statuses(&mut self, pane_manifest: &PaneManifest) -> bool { + let mut updated = false; + for command in self.all_commands.iter_mut() { + let status = command.get_status(); + let pane_id_opt = match &status { + CommandStatus::Running(pid) => *pid, + CommandStatus::Exited(_, pid) => *pid, + CommandStatus::Interrupted(pid) => *pid, + _ => None, + }; + + if let Some(pane_id) = pane_id_opt { + let mut pane_found = false; + for (_tab_index, panes) in &pane_manifest.panes { + for pane_info in panes { + let pane_matches = match pane_id { + PaneId::Terminal(id) => !pane_info.is_plugin && pane_info.id == id, + PaneId::Plugin(id) => pane_info.is_plugin && pane_info.id == id, + }; + + if pane_matches { + pane_found = true; + if pane_info.exited { + command.set_status(CommandStatus::Exited( + pane_info.exit_status, + Some(pane_id), + )); + updated = true; + } + break; + } + } + if pane_found { + break; + } + } + + if !pane_found && command.start_time.elapsed() > Duration::from_millis(400) { + match command.get_status() { + CommandStatus::Running(_) => { + eprintln!( + "Pane {:?} was closed while running, setting pane_id to None", + pane_id + ); + command.set_status(CommandStatus::Running(None)); + updated = true; + }, + CommandStatus::Exited(exit_code, _) => { + eprintln!( + "Pane {:?} was closed after exiting, setting pane_id to None", + pane_id + ); + command.set_status(CommandStatus::Exited(exit_code, None)); + updated = true; + }, + CommandStatus::Interrupted(_) => { + eprintln!("Pane {:?} was closed after being interrupted, setting pane_id to None", pane_id); + command.set_status(CommandStatus::Interrupted(None)); + updated = true; + }, + _ => {}, + } + } + } + } + updated + } + + pub fn update_sequence_stopped_state(&mut self) -> bool { + let mut needs_rerender = false; + if self.is_running { + let current_idx = self.current_running_command_index; + if let Some(command) = self.all_commands.get_mut(current_idx) { + if let CommandStatus::Exited(exit_code, _) = command.get_status() { + let should_stop; + if current_idx >= self.all_commands.len().saturating_sub(1) { + should_stop = true; + } else { + if let Some(chain_type) = &self + .all_commands + .get(current_idx) + .map(|c| c.get_chain_type()) + { + match chain_type { + ChainType::And => { + should_stop = exit_code.unwrap_or(0) != 0; + }, + ChainType::Or => { + should_stop = exit_code.unwrap_or(0) == 0; + }, + ChainType::Then => { + should_stop = false; + }, + ChainType::None => { + should_stop = true; + }, + } + } else { + should_stop = true; + } + }; + if should_stop { + self.is_running = false; + } + } + } + needs_rerender = true; + } + needs_rerender + } + + pub fn execute_command_sequence( + &mut self, + shell: &Option<PathBuf>, + global_cwd: &Option<PathBuf>, + primary_pane_id: Option<PaneId>, + ) { + use zellij_tile::prelude::actions::{Action, RunCommandAction}; + + self.all_commands.retain(|c| !c.is_empty()); + + let Some(first_active_sequence_command) = self.get_first_command() else { + return; + }; + + let shell = shell.clone().unwrap_or_else(|| PathBuf::from("/bin/bash")); + + let first_command = first_active_sequence_command.get_text(); + let first_chain_type = first_active_sequence_command.get_chain_type(); + let command_cwd = first_active_sequence_command + .get_cwd() + .or_else(|| global_cwd.clone()); + + let command = RunCommandAction { + command: shell.clone(), + args: vec!["-ic".to_string(), first_command.trim().to_string()], + cwd: command_cwd, + hold_on_close: true, + ..Default::default() + }; + + let placement = NewPanePlacement::InPlace { + pane_id_to_replace: primary_pane_id, + close_replaced_pane: false, + }; + + let action = Action::NewBlockingPane { + placement, + command: Some(command), + pane_name: Some(first_command.trim().to_string()), + unblock_condition: first_chain_type.to_unblock_condition(), + near_current_pane: true, + }; + + self.sequence_id += 1; + + let mut context = BTreeMap::new(); + context.insert("sequence_id".to_string(), self.sequence_id.to_string()); + run_action(action, context); + self.set_command_status(0, CommandStatus::Running(None)); + } +} + +impl Default for Execution { + fn default() -> Self { + Self::new() + } +} diff --git a/default-plugins/sequence/src/state/layout.rs b/default-plugins/sequence/src/state/layout.rs new file mode 100644 index 00000000..65a267e0 --- /dev/null +++ b/default-plugins/sequence/src/state/layout.rs @@ -0,0 +1,73 @@ +use crate::state::positioning; +use std::path::PathBuf; +use zellij_tile::prelude::*; + +pub struct Layout { + pub needs_reposition: bool, + pub cached_cursor_position: Option<(usize, usize)>, + pub spinner_frame: usize, + pub spinner_timer_scheduled: bool, +} + +impl Layout { + pub fn new() -> Self { + Self { + needs_reposition: false, + cached_cursor_position: None, + spinner_frame: 0, + spinner_timer_scheduled: false, + } + } + + pub fn reposition_plugin_based_on_sequence_state( + &mut self, + is_running: bool, + first_command_pane_id: Option<PaneId>, + pane_manifest: &PaneManifest, + plugin_id: Option<u32>, + total_viewport_columns: Option<usize>, + total_viewport_rows: Option<usize>, + total_commands: usize, + _selected_index: Option<usize>, + _running_index: usize, + _cwd: &Option<PathBuf>, + ) { + let Some(plugin_id) = plugin_id else { + return; + }; + let (Some(total_cols), Some(total_rows)) = (total_viewport_columns, total_viewport_rows) + else { + return; + }; + + if !self.needs_reposition || !is_running { + return; + } + + let Some(first_pane_id) = first_command_pane_id else { + return; + }; + + for (_tab_index, panes) in &pane_manifest.panes { + for pane in panes { + if !pane.is_plugin && PaneId::Terminal(pane.id) == first_pane_id { + positioning::reposition_plugin_for_sequence( + plugin_id, + total_cols, + total_rows, + total_commands, + ); + + self.needs_reposition = false; + return; + } + } + } + } +} + +impl Default for Layout { + fn default() -> Self { + Self::new() + } +} diff --git a/default-plugins/sequence/src/state/mod.rs b/default-plugins/sequence/src/state/mod.rs new file mode 100644 index 00000000..19738be7 --- /dev/null +++ b/default-plugins/sequence/src/state/mod.rs @@ -0,0 +1,446 @@ +mod chain_type; +mod command_entry; +mod command_parser; +mod command_status; +mod editing; +mod execution; +mod layout; +pub mod positioning; +mod selection; + +pub use chain_type::ChainType; +pub use command_entry::CommandEntry; +pub use command_parser::{ + detect_cd_command, detect_chain_operator_at_end, get_remaining_after_first_segment, + split_by_chain_operators, +}; +pub use command_status::CommandStatus; +pub use editing::Editing; +pub use execution::Execution; +pub use layout::Layout; +pub use selection::Selection; + +use crate::ui::text_input::TextInput; +use std::path::PathBuf; +use zellij_tile::prelude::*; + +pub struct State { + pub shell: Option<PathBuf>, + pub plugin_id: Option<u32>, + pub client_id: Option<u16>, + pub own_rows: Option<usize>, + pub own_columns: Option<usize>, + pub cwd: Option<PathBuf>, + pub original_pane_id: Option<PaneId>, + pub primary_pane_id: Option<PaneId>, + pub is_first_run: bool, + pub pane_manifest: Option<PaneManifest>, + pub total_viewport_columns: Option<usize>, + pub total_viewport_rows: Option<usize>, + pub selection: Selection, + pub editing: Editing, + pub execution: Execution, + pub layout: Layout, + pub current_position: Option<FloatingPaneCoordinates>, +} + +impl State { + pub fn new() -> Self { + Self { + shell: None, + plugin_id: None, + client_id: None, + own_rows: None, + own_columns: None, + cwd: None, + original_pane_id: None, + primary_pane_id: None, + is_first_run: true, + pane_manifest: None, + total_viewport_columns: None, + total_viewport_rows: None, + selection: Selection::new(), + editing: Editing::new(), + execution: Execution::new(), + layout: Layout::new(), + current_position: None, + } + } + + pub fn set_plugin_id(&mut self, plugin_id: u32) { + self.plugin_id = Some(plugin_id); + } + + /// Check if the sequence has finished executing (all commands are done) + pub fn has_finished(&self) -> bool { + self.execution + .all_commands + .iter() + .all(|command| matches!(command.status, CommandStatus::Exited(_, _))) + } + pub fn add_empty_command_after_current_selected(&mut self) { + self.selection.add_empty_command_after_current_selected( + &mut self.execution.all_commands, + &mut self.editing, + &self.cwd, + ); + } + + pub fn current_selected_command_is_empty(&self) -> bool { + self.selection + .current_selected_command_is_empty(&self.execution.all_commands, &self.editing) + } + + pub fn remove_current_selected_command(&mut self) { + let removed_pane_id = self + .selection + .remove_current_selected_command(&mut self.execution.all_commands, &mut self.editing); + if let Some(removed_pane_id) = removed_pane_id { + close_pane_with_id(removed_pane_id); + } + } + + pub fn clear_current_selected_command(&mut self) { + self.selection + .clear_current_selected_command(&mut self.execution.all_commands, &mut self.editing); + } + + pub fn move_selection_up(&mut self) { + self.selection + .move_up(&mut self.execution.all_commands, &mut self.editing); + if let Some(pane_id) = self + .current_selected_command() + .and_then(|c| c.get_pane_id()) + { + show_pane_with_id(pane_id, true, false); + } + } + + pub fn move_selection_down(&mut self) { + self.selection + .move_down(&mut self.execution.all_commands, &mut self.editing); + if let Some(pane_id) = self + .current_selected_command() + .and_then(|c| c.get_pane_id()) + { + show_pane_with_id(pane_id, true, false); + } + } + pub fn start_editing_selected(&mut self) { + if let Some(current_command_text) = self.current_selected_command().map(|c| c.get_text()) { + self.editing.start_editing(current_command_text); + } + } + + pub fn cancel_editing_selected(&mut self) { + self.editing.cancel_editing(); + } + + pub fn editing_input_text(&self) -> Option<String> { + self.editing.input_text() + } + + pub fn set_editing_input_text(&mut self, text: String) { + self.editing.set_input_text(text); + } + fn handle_first_pasted_segment( + &mut self, + current_text: &str, + segment_text: &str, + chain_type_opt: &Option<ChainType>, + ) { + let new_text = if current_text.trim().is_empty() { + segment_text.to_string() + } else { + format!("{}{}", current_text, segment_text) + }; + + if let Some(path) = detect_cd_command(&new_text) { + use crate::path_formatting; + if let Some(new_cwd) = path_formatting::resolve_path(self.cwd.as_ref(), &path) { + self.current_selected_command_mut().map(|c| { + c.set_cwd(Some(new_cwd)); + c.set_text("".to_owned()); + if let Some(chain_type) = chain_type_opt { + c.set_chain_type(*chain_type); + } + }); + return; + } + } + + self.current_selected_command_mut().map(|c| { + c.set_text(new_text); + if let Some(chain_type) = chain_type_opt { + c.set_chain_type(*chain_type); + } + }); + } + + fn insert_new_pasted_segment( + &mut self, + segment_text: &str, + chain_type_opt: &Option<ChainType>, + is_last_line: bool, + ) { + use crate::path_formatting; + + let cd_path = detect_cd_command(segment_text); + + if let Some(path) = cd_path { + if let Some(new_cwd) = path_formatting::resolve_path(self.cwd.as_ref(), &path) { + self.current_selected_command_mut().map(|c| { + c.set_cwd(Some(new_cwd)); + if let Some(chain_type) = chain_type_opt { + c.set_chain_type(*chain_type); + } + }); + return; + } + } + + let Some(new_selected_index) = self.selection.current_selected_command_index.map(|i| i + 1) + else { + return; + }; + + let mut new_command = CommandEntry::new(segment_text, self.cwd.clone()); + if let Some(chain_type) = chain_type_opt { + new_command.set_chain_type(*chain_type); + } else if !is_last_line { + new_command.set_chain_type(ChainType::And); + } + + self.execution + .all_commands + .insert(new_selected_index, new_command); + self.selection.current_selected_command_index = Some(new_selected_index); + } + + fn ensure_line_end_chain_type(&mut self) { + if let Some(last_cmd_index) = self.selection.current_selected_command_index { + self.execution + .all_commands + .get_mut(last_cmd_index) + .map(|c| { + if matches!(c.get_chain_type(), ChainType::None) { + c.set_chain_type(ChainType::And); + } + }); + } + } + + pub fn pasted_lines(&mut self, lines: Vec<&str>) { + let Some(current_text) = self.editing_input_text() else { + return; + }; + + for (line_index, line) in lines.iter().enumerate() { + let segments = split_by_chain_operators(line); + let is_last_line = line_index == lines.len().saturating_sub(1); + + for (seg_index, (segment_text, chain_type_opt)) in segments.iter().enumerate() { + if line_index == 0 && seg_index == 0 { + self.handle_first_pasted_segment(¤t_text, segment_text, chain_type_opt); + } else { + self.insert_new_pasted_segment(segment_text, chain_type_opt, is_last_line); + } + } + + if !is_last_line { + self.ensure_line_end_chain_type(); + } + } + + self.start_editing_selected(); + } + pub fn remove_empty_commands(&mut self) { + self.execution.remove_empty_commands(); + } + + pub fn get_first_command(&self) -> Option<CommandEntry> { + self.execution.get_first_command() + } + + pub fn set_command_status(&mut self, command_index: usize, status: CommandStatus) { + self.execution.set_command_status(command_index, status); + } + + pub fn set_current_running_command_status(&mut self, status: CommandStatus) { + self.execution.set_current_running_command_status(status); + } + + pub fn get_current_running_command_status(&mut self) -> Option<CommandStatus> { + self.execution.get_current_running_command_status() + } + + pub fn handle_editing_submit(&mut self, current_cwd: &Option<PathBuf>) -> bool { + let (handled_internally, new_selection_index) = self.editing.handle_submit( + self.selection.current_selected_command_index, + &mut self.execution.all_commands, + current_cwd, + ); + self.selection.current_selected_command_index = new_selection_index; + handled_internally + } + + pub fn update_pane_id_for_command(&mut self, pane_id: PaneId, command_text: &str) { + self.execution + .update_pane_id_for_command(pane_id, command_text); + } + pub fn update_exited_command_statuses(&mut self, pane_manifest: &PaneManifest) -> bool { + self.execution.update_exited_command_statuses(pane_manifest) + } + pub fn update_sequence_stopped_state(&mut self) -> bool { + self.execution.update_sequence_stopped_state() + } + + pub fn cycle_chain_type(&mut self) { + self.current_selected_command_mut() + .map(|c| c.cycle_chain_type()); + } + + pub fn set_primary_pane_id_before_sequence(&mut self, pane_id: Option<PaneId>) { + if pane_id.is_some() { + self.execution.primary_pane_id_before_sequence = pane_id; + } + } + pub fn clear_all_commands(&mut self) { + self.execution.all_commands = vec![CommandEntry::new("", self.cwd.clone())]; + self.editing.editing_input = Some(TextInput::new("".to_owned())); + self.selection.current_selected_command_index = Some(0); + self.execution.current_running_command_index = 0; + } + pub fn update_running_state(&mut self, primary_pane_id: Option<PaneId>) { + self.remove_empty_commands(); + self.set_primary_pane_id_before_sequence(primary_pane_id); + self.layout.needs_reposition = true; + self.execution.is_running = true; + } + + pub fn can_run_sequence(&self) -> bool { + self.execution.can_run_sequence() + } + + pub fn copy_to_clipboard(&mut self) { + self.execution.copy_to_clipboard(); + } + + pub fn current_selected_command_mut(&mut self) -> Option<&mut CommandEntry> { + let Some(i) = self.selection.current_selected_command_index else { + return None; + }; + self.execution.all_commands.get_mut(i) + } + pub fn reposition_plugin(&mut self) -> bool { + let mut repositioned = false; + let Some(plugin_id) = self.plugin_id else { + return repositioned; + }; + let Some(total_viewport_rows) = self.total_viewport_rows else { + return repositioned; + }; + let Some(total_viewport_columns) = self.total_viewport_columns else { + return repositioned; + }; + let total_commands = std::cmp::max(self.execution.all_commands.iter().len(), 1); + let height_padding = 7; + if self.all_commands_are_pending() { + let initial_height = total_commands + height_padding; + let y = total_viewport_rows.saturating_sub(initial_height) / 2; + let coordinates = FloatingPaneCoordinates::new( + Some(format!("25%")), + Some(format!("{}", y)), + Some(format!("50%")), + Some(format!("{}", initial_height)), + Some(false), // should not be pinned when running sequence + ); + coordinates.map(|coordinates| { + if Some(&coordinates) != self.current_position.as_ref() { + repositioned = true; + self.current_position = Some(coordinates.clone()); + change_floating_panes_coordinates(vec![( + PaneId::Plugin(plugin_id), + coordinates, + )]); + } + }); + } else { + // Calculate longest table row width + let longest_cwd_display = self.execution.longest_cwd_display(&self.cwd); + let longest_command = self + .execution + .all_commands + .iter() + .map(|cmd| cmd.get_text().chars().count()) + .max() + .unwrap_or(1); + + let (max_chain_width, max_status_width) = crate::ui::components::calculate_max_widths( + &self.execution.all_commands, + self.layout.spinner_frame, + ); + + let longest_line = crate::ui::components::calculate_longest_line( + &longest_cwd_display, + longest_command, + max_chain_width, + max_status_width, + ); + + let ui_width = std::cmp::max(longest_line, 50) + 4; // 2 for ui-padding, 2 for pane frame + let width = std::cmp::min(ui_width, total_viewport_columns / 2); + + let height = (height_padding + total_commands).min((total_viewport_rows * 25) / 100); + + // Position: top-right with 1-space margins + let x = total_viewport_columns.saturating_sub(width); + let y = 1; + + let coordinates = FloatingPaneCoordinates::new( + Some(x.to_string()), + Some(y.to_string()), + Some(width.to_string()), + Some(height.to_string()), + Some(true), // should be pinned for sequence + ); + coordinates.map(|coordinates| { + if Some(&coordinates) != self.current_position.as_ref() { + self.current_position = Some(coordinates.clone()); + repositioned = true; + change_floating_panes_coordinates(vec![( + PaneId::Plugin(plugin_id), + coordinates, + )]); + } + }); + } + repositioned + } + pub fn all_commands_are_pending(&self) -> bool { + self.execution + .all_commands + .iter() + .all(|command| matches!(command.get_status(), CommandStatus::Pending)) + } + + fn current_selected_command(&self) -> Option<&CommandEntry> { + let Some(i) = self.selection.current_selected_command_index else { + return None; + }; + self.execution.all_commands.get(i) + } + + pub fn execute_command_sequence(&mut self) { + self.execution + .execute_command_sequence(&self.shell, &self.cwd, self.primary_pane_id); + } +} + +impl Default for State { + fn default() -> Self { + let mut state = Self::new(); + state.is_first_run = true; + state + } +} diff --git a/default-plugins/sequence/src/state/positioning.rs b/default-plugins/sequence/src/state/positioning.rs new file mode 100644 index 00000000..6acadfff --- /dev/null +++ b/default-plugins/sequence/src/state/positioning.rs @@ -0,0 +1,48 @@ +use zellij_tile::prelude::*; + +pub fn reposition_plugin_for_sequence( + plugin_id: u32, + total_cols: usize, + total_rows: usize, + total_commands: usize, +) { + // Width: 30% of viewport + let width = std::cmp::max((total_cols * 30) / 100, 50); + + // Height: UI overhead + commands, capped at 25% of viewport + let overhead_rows = 6; + let height = (overhead_rows + total_commands).min((total_rows * 25) / 100); + + // Position: top-right with 1-space margins + let x = total_cols.saturating_sub(width); + let y = 1; + + change_floating_pane_coordinates_absolute( + plugin_id, + Some(x), + Some(y), + Some(width), + Some(height), + true, + ); +} + +pub fn change_floating_pane_coordinates_absolute( + own_plugin_id: u32, + x: Option<usize>, + y: Option<usize>, + width: Option<usize>, + height: Option<usize>, + should_be_pinned: bool, +) { + let coordinates = FloatingPaneCoordinates::new( + x.map(|x| x.to_string()), + y.map(|y| y.to_string()), + width.map(|width| width.to_string()), + height.map(|height| height.to_string()), + Some(should_be_pinned), + ); + if let Some(coordinates) = coordinates { + change_floating_panes_coordinates(vec![(PaneId::Plugin(own_plugin_id), coordinates)]); + } +} diff --git a/default-plugins/sequence/src/state/selection.rs b/default-plugins/sequence/src/state/selection.rs new file mode 100644 index 00000000..18f83bcd --- /dev/null +++ b/default-plugins/sequence/src/state/selection.rs @@ -0,0 +1,214 @@ +use crate::state::{CommandEntry, Editing}; +use std::path::PathBuf; +use zellij_tile::prelude::*; + +pub struct Selection { + pub current_selected_command_index: Option<usize>, + pub scroll_offset: usize, +} + +impl Selection { + pub fn new() -> Self { + Self { + current_selected_command_index: Some(0), + scroll_offset: 0, + } + } + + pub fn add_empty_command_after_current_selected( + &mut self, + all_commands: &mut Vec<CommandEntry>, + editing: &mut Editing, + cwd: &Option<PathBuf>, + ) { + self.save_editing_buffer_to_current_selected(all_commands, editing); + editing.editing_input.as_mut().map(|i| i.clear()); + if !self.current_selected_command_is_empty(all_commands, editing) { + self.current_selected_command_mut(all_commands) + .map(|c| c.fill_chain_type_if_empty()); + + if let Some(current_selected_command_index) = self.current_selected_command_index.take() + { + let new_command = CommandEntry::new("", cwd.clone()); + if current_selected_command_index == all_commands.len().saturating_sub(1) { + all_commands.push(new_command); + } else { + all_commands.insert(current_selected_command_index + 1, new_command); + } + + self.current_selected_command_index = Some(current_selected_command_index + 1); + } + } + } + + pub fn current_selected_command_is_empty( + &self, + all_commands: &[CommandEntry], + editing: &Editing, + ) -> bool { + let text_input_is_empty = editing + .editing_input + .as_ref() + .map(|i| i.is_empty()) + .unwrap_or(true); + let current_selected_command_is_empty = self + .current_selected_command_index + .and_then(|i| all_commands.get(i).map(|c| c.is_empty())) + .unwrap_or(true); + text_input_is_empty && current_selected_command_is_empty + } + + pub fn remove_current_selected_command( + &mut self, + all_commands: &mut Vec<CommandEntry>, + editing: &mut Editing, + ) -> Option<PaneId> { + // returns the pane_id of the removed command, if any + let mut removed_pane_id = None; + let Some(mut current_selected_command_index) = self.current_selected_command_index else { + return removed_pane_id; + }; + if all_commands.len() > 1 && all_commands.len() > current_selected_command_index { + let command = all_commands.remove(current_selected_command_index); + removed_pane_id = command.get_pane_id(); + } else { + self.clear_current_selected_command(all_commands, editing); + } + if current_selected_command_index >= all_commands.len() + && current_selected_command_index > 0 + { + current_selected_command_index -= 1; + } + self.update_current_selected_command_index( + current_selected_command_index, + all_commands, + editing, + ); + if current_selected_command_index == all_commands.len().saturating_sub(1) { + self.current_selected_command_mut(all_commands) + .map(|c| c.clear_chain_type()); + } + removed_pane_id + } + + pub fn clear_current_selected_command( + &mut self, + all_commands: &mut [CommandEntry], + editing: &mut Editing, + ) { + let Some(current_selected_command_index) = self.current_selected_command_index else { + return; + }; + self.current_selected_command_mut(all_commands) + .map(|c| c.clear_text()); + self.clear_editing_buffer(editing); + if current_selected_command_index == all_commands.len().saturating_sub(1) { + self.current_selected_command_mut(all_commands) + .map(|c| c.clear_chain_type()); + } + } + + pub fn move_up(&mut self, all_commands: &mut [CommandEntry], editing: &mut Editing) { + self.save_editing_buffer_to_current_selected(all_commands, editing); + match self.current_selected_command_index.as_mut() { + Some(i) if *i > 0 => { + *i -= 1; + }, + None => { + self.current_selected_command_index = Some(all_commands.len().saturating_sub(1)); + }, + _ => { + self.current_selected_command_index = None; + }, + } + self.update_editing_buffer_to_current_selected(all_commands, editing); + } + + pub fn move_down(&mut self, all_commands: &mut [CommandEntry], editing: &mut Editing) { + self.save_editing_buffer_to_current_selected(all_commands, editing); + match self.current_selected_command_index.as_mut() { + Some(i) if *i < all_commands.len().saturating_sub(1) => { + *i += 1; + }, + None => { + self.current_selected_command_index = Some(0); + }, + _ => { + self.current_selected_command_index = None; + }, + } + self.update_editing_buffer_to_current_selected(all_commands, editing); + } + + fn current_selected_command_mut<'a>( + &self, + all_commands: &'a mut [CommandEntry], + ) -> Option<&'a mut CommandEntry> { + let Some(i) = self.current_selected_command_index else { + return None; + }; + all_commands.get_mut(i) + } + + fn current_selected_command<'a>( + &self, + all_commands: &'a [CommandEntry], + ) -> Option<&'a CommandEntry> { + let Some(i) = self.current_selected_command_index else { + return None; + }; + all_commands.get(i) + } + + fn clear_editing_buffer(&self, editing: &mut Editing) { + editing.editing_input.as_mut().map(|c| c.clear()); + } + + fn update_current_selected_command_index( + &mut self, + new_index: usize, + all_commands: &[CommandEntry], + editing: &mut Editing, + ) { + self.current_selected_command_index = Some(new_index); + self.update_editing_buffer_to_current_selected(all_commands, editing); + } + + fn get_text_of_current_selected_command( + &self, + all_commands: &[CommandEntry], + ) -> Option<String> { + self.current_selected_command(all_commands) + .map(|c| c.get_text()) + } + + fn save_editing_buffer_to_current_selected( + &self, + all_commands: &mut [CommandEntry], + editing: &mut Editing, + ) { + if let Some(text_input) = editing.editing_input.as_ref().map(|i| i.get_text()) { + self.current_selected_command_mut(all_commands) + .map(|c| c.set_text(text_input.to_owned())); + } + } + + fn update_editing_buffer_to_current_selected( + &self, + all_commands: &[CommandEntry], + editing: &mut Editing, + ) { + let new_text_of_current_selected_command = self + .get_text_of_current_selected_command(all_commands) + .unwrap_or_else(|| String::new()); + if let Some(editing_input) = editing.editing_input.as_mut() { + editing_input.set_text(new_text_of_current_selected_command); + } + } +} + +impl Default for Selection { + fn default() -> Self { + Self::new() + } +} |
