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/command_parser.rs | |
| 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/command_parser.rs')
| -rw-r--r-- | default-plugins/sequence/src/state/command_parser.rs | 327 |
1 files changed, 327 insertions, 0 deletions
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))]); + } +} |
