aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/sequence/src/state/command_parser.rs
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-12-11 14:25:42 +0100
committerGitHub <noreply@github.com>2025-12-11 14:25:42 +0100
commit693fa082def5ef0fe030cd7fdc3b1df051b35dc6 (patch)
treeb3f651462bbc1cd2fa5b2d79207343413ef6b83c /default-plugins/sequence/src/state/command_parser.rs
parentfix(zellij-server): unblock building on 32bit targets (#4484) (diff)
downloadsl0ppij-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.rs327
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))]);
+ }
+}