diff options
author | Aram Drevekenin <aram@poor.dev> | 2022-10-05 07:44:00 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-05 07:44:00 +0200 |
commit | 79bf6ab868cbdab1f9a3827c9b70198f54548b44 (patch) | |
tree | 2d6fc4c1d8a79ebd727a1a5f8b6406617dd0de55 /zellij-utils/src/kdl/mod.rs | |
parent | 917e9b2ff0f583183c0155060d243afd295770b9 (diff) |
feat(config): switch to kdl (#1759)
* chore(config): default kdl keybindings config
* tests
* work
* refactor(config): move stuff around
* work
* tab merge layout
* work
* work
* layouts working
* work
* layout tests
* work
* work
* feat(parsing): kdl layouts without config
* refactor(kdl): move stuff around
* work
* tests(layout): add cases and fix bugs
* work
* fix(kdl): various bugs
* chore(layouts): move all layouts to kdl
* feat(kdl): shared keybidns
* fix(layout): do not count fixed panes toward percentile
* fix(keybinds): missing keybinds and actions
* fix(config): adjust default tips
* refactor(config): move stuff around
* fix(tests): make e2e tests pass
* fix(kdl): add verbose parsing errors
* fix(kdl): focused tab
* fix(layout): corret default_tab_template behavior
* style(code): fix compile warnings
* feat(cli): send actions through the cli
* fix(cli): exit only when action is done
* fix(cli): open embedded pane from floating pane
* fix(cli): send actions to other sessions
* feat(cli): command alias
* feat(converter): convert old config
* feat(converter): convert old layout and theme files
* feat(kdl): pretty errors
* feat(client): convert old YAML files on startup
* fix: various bugs and styling issues
* fix: e2e tests
* fix(screen): propagate errors after merge
* style(clippy): lower clippy level
* fix(tests): own session_name variable
* style(fmt): rustfmt
* fix(cli): various action fixes
* style(fmt): rustfmt
* fix(themes): loading of theme files
* style(fmt): rustfmt
* fix(tests): theme fixtures
* fix(layouts): better errors on unknown nodes
* fix(kdl): clarify valid node terminator error
* fix(e2e): adjust close tab test
* fix(e2e): adjust close tab test again
* style(code): cleanup some comments
Diffstat (limited to 'zellij-utils/src/kdl/mod.rs')
-rw-r--r-- | zellij-utils/src/kdl/mod.rs | 1626 |
1 files changed, 1626 insertions, 0 deletions
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs new file mode 100644 index 000000000..7092653c6 --- /dev/null +++ b/zellij-utils/src/kdl/mod.rs @@ -0,0 +1,1626 @@ +mod kdl_layout_parser; +use crate::data::{InputMode, Key, Palette, PaletteColor}; +use crate::envs::EnvironmentVariables; +use crate::input::command::RunCommand; +use crate::input::config::{Config, ConfigError, KdlError}; +use crate::input::keybinds::Keybinds; +use crate::input::layout::{Layout, RunPlugin, RunPluginLocation}; +use crate::input::options::{Clipboard, OnForceClose, Options}; +use crate::input::plugins::{PluginConfig, PluginTag, PluginType, PluginsConfig}; +use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig}; +use kdl_layout_parser::KdlLayoutParser; +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; +use strum::IntoEnumIterator; +use url::Url; + +use miette::NamedSource; + +use kdl::{KdlDocument, KdlEntry, KdlNode}; + +use std::path::PathBuf; +use std::str::FromStr; + +use crate::input::actions::{Action, Direction, ResizeDirection, SearchDirection, SearchOption}; +use crate::input::command::RunCommandAction; + +#[macro_export] +macro_rules! parse_kdl_action_arguments { + ( $action_name:expr, $action_arguments:expr, $action_node:expr ) => {{ + if !$action_arguments.is_empty() { + Err(ConfigError::new_kdl_error( + format!("Action '{}' must have arguments", $action_name), + $action_node.span().offset(), + $action_node.span().len(), + )) + } else { + match $action_name { + "Quit" => Ok(Action::Quit), + "FocusNextPane" => Ok(Action::FocusNextPane), + "FocusPreviousPane" => Ok(Action::FocusPreviousPane), + "SwitchFocus" => Ok(Action::SwitchFocus), + "EditScrollback" => Ok(Action::EditScrollback), + "ScrollUp" => Ok(Action::ScrollUp), + "ScrollDown" => Ok(Action::ScrollDown), + "ScrollToBottom" => Ok(Action::ScrollToBottom), + "PageScrollUp" => Ok(Action::PageScrollUp), + "PageScrollDown" => Ok(Action::PageScrollDown), + "HalfPageScrollUp" => Ok(Action::HalfPageScrollUp), + "HalfPageScrollDown" => Ok(Action::HalfPageScrollDown), + "ToggleFocusFullscreen" => Ok(Action::ToggleFocusFullscreen), + "TogglePaneFrames" => Ok(Action::TogglePaneFrames), + "ToggleActiveSyncTab" => Ok(Action::ToggleActiveSyncTab), + "TogglePaneEmbedOrFloating" => Ok(Action::TogglePaneEmbedOrFloating), + "ToggleFloatingPanes" => Ok(Action::ToggleFloatingPanes), + "CloseFocus" => Ok(Action::CloseFocus), + "UndoRenamePane" => Ok(Action::UndoRenamePane), + "NoOp" => Ok(Action::NoOp), + "GoToNextTab" => Ok(Action::GoToNextTab), + "GoToPreviousTab" => Ok(Action::GoToPreviousTab), + "CloseTab" => Ok(Action::CloseTab), + "ToggleTab" => Ok(Action::ToggleTab), + "UndoRenameTab" => Ok(Action::UndoRenameTab), + "Detach" => Ok(Action::Detach), + "Copy" => Ok(Action::Copy), + "Confirm" => Ok(Action::Confirm), + "Deny" => Ok(Action::Deny), + _ => Err(ConfigError::new_kdl_error( + format!("Unsupported action: {:?}", $action_name), + $action_node.span().offset(), + $action_node.span().len(), + )), + } + } + }}; +} + +#[macro_export] +macro_rules! parse_kdl_action_u8_arguments { + ( $action_name:expr, $action_arguments:expr, $action_node:expr ) => {{ + let mut bytes = vec![]; + for kdl_entry in $action_arguments.iter() { + match kdl_entry.value().as_i64() { + Some(int_value) => bytes.push(int_value as u8), + None => { + return Err(ConfigError::new_kdl_error( + format!("Arguments for '{}' must be integers", $action_name), + kdl_entry.span().offset(), + kdl_entry.span().len(), + )); + }, + } + } + Action::new_from_bytes($action_name, bytes, $action_node) + }}; +} + +#[macro_export] +macro_rules! kdl_parsing_error { + ( $message:expr, $entry:expr ) => { + ConfigError::new_kdl_error($message, $entry.span().offset(), $entry.span().len()) + }; +} + +#[macro_export] +macro_rules! kdl_entries_as_i64 { + ( $node:expr ) => { + $node + .entries() + .iter() + .map(|kdl_node| kdl_node.value().as_i64()) + }; +} + +#[macro_export] +macro_rules! kdl_first_entry_as_string { + ( $node:expr ) => { + $node + .entries() + .iter() + .next() + .and_then(|s| s.value().as_string()) + }; +} + +#[macro_export] +macro_rules! kdl_first_entry_as_i64 { + ( $node:expr ) => { + $node + .entries() + .iter() + .next() + .and_then(|i| i.value().as_i64()) + }; +} + +#[macro_export] +macro_rules! entry_count { + ( $node:expr ) => {{ + $node.entries().iter().len() + }}; +} + +#[macro_export] +macro_rules! parse_kdl_action_char_or_string_arguments { + ( $action_name:expr, $action_arguments:expr, $action_node:expr ) => {{ + let mut chars_to_write = String::new(); + for kdl_entry in $action_arguments.iter() { + match kdl_entry.value().as_string() { + Some(string_value) => chars_to_write.push_str(string_value), + None => { + return Err(ConfigError::new_kdl_error( + format!("All entries for action '{}' must be strings", $action_name), + kdl_entry.span().offset(), + kdl_entry.span().len(), + )) + }, + } + } + Action::new_from_string($action_name, chars_to_write, $action_node) + }}; +} + +#[macro_export] +macro_rules! kdl_arg_is_truthy { + ( $kdl_node:expr, $arg_name:expr ) => { + match $kdl_node.get($arg_name) { + Some(arg) => match arg.value().as_bool() { + Some(value) => value, + None => { + return Err(ConfigError::new_kdl_error( + format!("Argument must be true or false, found: {}", arg.value()), + arg.span().offset(), + arg.span().len(), + )) + }, + }, + None => false, + } + }; +} + +#[macro_export] +macro_rules! kdl_children_nodes_or_error { + ( $kdl_node:expr, $error:expr ) => { + $kdl_node + .children() + .ok_or(ConfigError::new_kdl_error( + $error.into(), + $kdl_node.span().offset(), + $kdl_node.span().len(), + ))? + .nodes() + }; +} + +#[macro_export] +macro_rules! kdl_children_nodes { + ( $kdl_node:expr ) => { + $kdl_node.children().map(|c| c.nodes()) + }; +} + +#[macro_export] +macro_rules! kdl_property_nodes { + ( $kdl_node:expr ) => {{ + $kdl_node + .entries() + .iter() + .filter_map(|e| e.name()) + .map(|e| e.value()) + }}; +} + +#[macro_export] +macro_rules! kdl_children_or_error { + ( $kdl_node:expr, $error:expr ) => { + $kdl_node.children().ok_or(ConfigError::new_kdl_error( + $error.into(), + $kdl_node.span().offset(), + $kdl_node.span().len(), + ))? + }; +} + +#[macro_export] +macro_rules! kdl_children { + ( $kdl_node:expr ) => { + $kdl_node.children().iter().copied().collect() + }; +} + +#[macro_export] +macro_rules! kdl_string_arguments { + ( $kdl_node:expr ) => {{ + let res: Result<Vec<_>, _> = $kdl_node + .entries() + .iter() + .map(|e| { + e.value().as_string().ok_or(ConfigError::new_kdl_error( + "Not a string".into(), + e.span().offset(), + e.span().len(), + )) + }) + .collect(); + res? + }}; +} + +#[macro_export] +macro_rules! kdl_property_names { + ( $kdl_node:expr ) => {{ + $kdl_node + .entries() + .iter() + .filter_map(|e| e.name()) + .map(|e| e.value()) + }}; +} + +#[macro_export] +macro_rules! kdl_argument_values { + ( $kdl_node:expr ) => { + $kdl_node.entries().iter().collect() + }; +} + +#[macro_export] +macro_rules! kdl_name { + ( $kdl_node:expr ) => { + $kdl_node.name().value() + }; +} + +#[macro_export] +macro_rules! kdl_document_name { + ( $kdl_node:expr ) => { + $kdl_node.node().name().value() + }; +} + +#[macro_export] +macro_rules! keys_from_kdl { + ( $kdl_node:expr ) => { + kdl_string_arguments!($kdl_node) + .iter() + .map(|k| { + Key::from_str(k).map_err(|_| { + ConfigError::new_kdl_error( + format!("Invalid key: '{}'", k), + $kdl_node.span().offset(), + $kdl_node.span().len(), + ) + }) + }) + .collect::<Result<_, _>>()? + }; +} + +#[macro_export] +macro_rules! actions_from_kdl { + ( $kdl_node:expr ) => { + kdl_children_nodes_or_error!($kdl_node, "no actions found for key_block") + .iter() + .map(|kdl_action| Action::try_from(kdl_action)) + .collect::<Result<_, _>>()? + }; +} + +pub fn kdl_arguments_that_are_strings<'a>( + arguments: impl Iterator<Item = &'a KdlEntry>, +) -> Result<Vec<String>, ConfigError> { + // pub fn kdl_arguments_that_are_strings <'a>(arguments: impl Iterator<Item=&'a KdlValue>) -> Result<Vec<String>, ConfigError> { + let mut args: Vec<String> = vec![]; + for kdl_entry in arguments { + match kdl_entry.value().as_string() { + Some(string_value) => args.push(string_value.to_string()), + None => { + return Err(ConfigError::new_kdl_error( + format!("Argument must be a string"), + kdl_entry.span().offset(), + kdl_entry.span().len(), + )); + }, + } + } + Ok(args) +} + +pub fn kdl_child_string_value_for_entry<'a>( + command_metadata: &'a KdlDocument, + entry_name: &'a str, +) -> Option<&'a str> { + command_metadata + .get(entry_name) + .and_then(|cwd| cwd.entries().iter().next()) + .and_then(|cwd_value| cwd_value.value().as_string()) +} + +impl Action { + pub fn new_from_bytes( + action_name: &str, + bytes: Vec<u8>, + action_node: &KdlNode, + ) -> Result<Self, ConfigError> { + match action_name { + "Write" => Ok(Action::Write(bytes)), + "PaneNameInput" => Ok(Action::PaneNameInput(bytes)), + "TabNameInput" => Ok(Action::TabNameInput(bytes)), + "SearchInput" => Ok(Action::SearchInput(bytes)), + "GoToTab" => { + let tab_index = *bytes.get(0).ok_or_else(|| { + ConfigError::new_kdl_error( + format!("Missing tab index"), + action_node.span().offset(), + action_node.span().len(), + ) + })? as u32; + Ok(Action::GoToTab(tab_index)) + }, + _ => Err(ConfigError::new_kdl_error( + "Failed to parse action".into(), + action_node.span().offset(), + action_node.span().len(), + )), + } + } + pub fn new_from_string( + action_name: &str, + string: String, + action_node: &KdlNode, + ) -> Result<Self, ConfigError> { + match action_name { + "WriteChars" => Ok(Action::WriteChars(string)), + "SwitchToMode" => match InputMode::from_str(string.as_str()) { + Ok(input_mode) => Ok(Action::SwitchToMode(input_mode)), + Err(_e) => { + return Err(ConfigError::new_kdl_error( + format!("Unknown InputMode '{}'", string), + action_node.span().offset(), + action_node.span().len(), + )) + }, + }, + "Resize" => { + let direction = ResizeDirection::from_str(string.as_str()).map_err(|_| { + ConfigError::new_kdl_error( + format!("Invalid direction: '{}'", string), + action_node.span().offset(), + action_node.span().len(), + ) + })?; + Ok(Action::Resize(direction)) + }, + "MoveFocus" => { + let direction = Direction::from_str(string.as_str()).map_err(|_| { + ConfigError::new_kdl_error( + format!("Invalid direction: '{}'", string), + action_node.span().offset(), + action_node.span().len(), + ) + })?; + Ok(Action::MoveFocus(direction)) + }, + "MoveFocusOrTab" => { + let direction = Direction::from_str(string.as_str()).map_err(|_| { + ConfigError::new_kdl_error( + format!("Invalid direction: '{}'", string), + action_node.span().offset(), + action_node.span().len(), + ) + })?; + Ok(Action::MoveFocusOrTab(direction)) + }, + "MovePane" => { + if string.is_empty() { + return Ok(Action::MovePane(None)); + } else { + let direction = Direction::from_str(string.as_str()).map_err(|_| { + ConfigError::new_kdl_error( + format!("Invalid direction: '{}'", string), + action_node.span().offset(), + action_node.span().len(), + ) + })?; + Ok(Action::MovePane(Some(direction))) + } + }, + "DumpScreen" => Ok(Action::DumpScreen(string)), + "NewPane" => { + if string.is_empty() { + return Ok(Action::NewPane(None)); + } else { + let direction = Direction::from_str(string.as_str()).map_err(|_| { + ConfigError::new_kdl_error( + format!("Invalid direction: '{}'", string), + action_node.span().offset(), + action_node.span().len(), + ) + })?; + Ok(Action::NewPane(Some(direction))) + } + }, + "SearchToggleOption" => { + let toggle_option = SearchOption::from_str(string.as_str()).map_err(|_| { + ConfigError::new_kdl_error( + format!("Invalid direction: '{}'", string), + action_node.span().offset(), + action_node.span().len(), + ) + })?; + Ok(Action::SearchToggleOption(toggle_option)) + }, + "Search" => { + let search_direction = + SearchDirection::from_str(string.as_str()).map_err(|_| { + ConfigError::new_kdl_error( + format!("Invalid direction: '{}'", string), + action_node.span().offset(), + action_node.span().len(), + ) + })?; + Ok(Action::Search(search_direction)) + }, + _ => Err(ConfigError::new_kdl_error( + format!("Unsupported action: {}", action_name), + action_node.span().offset(), + action_node.span().len(), + )), + } + } +} + +impl TryFrom<(&str, &KdlDocument)> for PaletteColor { + type Error = ConfigError; + + fn try_from( + (color_name, theme_colors): (&str, &KdlDocument), + ) -> Result<PaletteColor, Self::Error> { + let color = theme_colors + .get(color_name) + .ok_or(ConfigError::new_kdl_error( + format!("Missing theme color: {}", color_name), + theme_colors.span().offset(), + theme_colors.span().len(), + ))?; + let entry_count = entry_count!(color); + let is_rgb = || entry_count == 3; + let is_three_digit_hex = || { + match kdl_first_entry_as_string!(color) { + // 4 including the '#' character + Some(s) => entry_count == 1 && s.starts_with('#') && s.len() == 4, + None => false, + } + }; + let is_six_digit_hex = || { + match kdl_first_entry_as_string!(color) { + // 7 including the '#' character + Some(s) => entry_count == 1 && s.starts_with('#') && s.len() == 7, + None => false, + } + }; + let is_eight_bit = || kdl_first_entry_as_i64!(color).is_some() && entry_count == 1; + if is_rgb() { + let mut channels = kdl_entries_as_i64!(color); + let r = channels.next().unwrap().ok_or(ConfigError::new_kdl_error( + format!("invalid rgb color"), + color.span().offset(), + color.span().len(), + ))? as u8; + let g = channels.next().unwrap().ok_or(ConfigError::new_kdl_error( + format!("invalid rgb color"), + color.span().offset(), + color.span().len(), + ))? as u8; + let b = channels.next().unwrap().ok_or(ConfigError::new_kdl_error( + format!("invalid rgb color"), + color.span().offset(), + color.span().len(), + ))? as u8; + Ok(PaletteColor::Rgb((r, g, b))) + } else if is_three_digit_hex() { + // eg. #fff (hex, will be converted to rgb) + let mut s = String::from(kdl_first_entry_as_string!(color).unwrap()); + s.remove(0); + let r = u8::from_str_radix(&s[0..1], 16).map_err(|_| { + ConfigError::new_kdl_error( + "Failed to parse hex color".into(), + color.span().offset(), + color.span().len(), + ) + })? * 0x11; + let g = u8::from_str_radix(&s[1..2], 16).map_err(|_| { + ConfigError::new_kdl_error( + "Failed to parse hex color".into(), + color.span().offset(), + color.span().len(), + ) + })? * 0x11; + let b = u8::from_str_radix(&s[2..3], 16).map_err(|_| { + ConfigError::new_kdl_error( + "Failed to parse hex color".into(), + color.span().offset(), + color.span().len(), + ) + })? * 0x11; + Ok(PaletteColor::Rgb((r, g, b))) + } else if is_six_digit_hex() { + // eg. #ffffff (hex, will be converted to rgb) + let mut s = String::from(kdl_first_entry_as_string!(color).unwrap()); + s.remove(0); + let r = u8::from_str_radix(&s[0..2], 16).map_err(|_| { + ConfigError::new_kdl_error( + "Failed to parse hex color".into(), + color.span().offset(), + color.span().len(), + ) + })?; + let g = u8::from_str_radix(&s[2..4], 16).map_err(|_| { + ConfigError::new_kdl_error( + "Failed to parse hex color".into(), + color.span().offset(), + color.span().len(), + ) + })?; + let b = u8::from_str_radix(&s[4..6], 16).map_err(|_| { + ConfigError::new_kdl_error( + "Failed to parse hex color".into(), + color.span().offset(), + color.span().len(), + ) + })?; + Ok(PaletteColor::Rgb((r, g, b))) + } else if is_eight_bit() { + let n = kdl_first_entry_as_i64!(color).ok_or(ConfigError::new_kdl_error( + "Failed to parse color".into(), + color.span().offset(), + color.span().len(), + ))?; + Ok(PaletteColor::EightBit(n as u8)) + } else { + Err(ConfigError::new_kdl_error( + "Failed to parse color".into(), + color.span().offset(), + color.span().len(), + )) + } + } +} + +impl TryFrom<&KdlNode> for Action { + // type Error = Box<dyn std::error::Error>; + type Error = ConfigError; + fn try_from(kdl_action: &KdlNode) -> Result<Self, Self::Error> { + let action_name = kdl_name!(kdl_action); + let action_arguments: Vec<&KdlEntry> = kdl_argument_values!(kdl_action); + // let action_arguments: Vec<&KdlValue> = kdl_argument_values!(kdl_action); + let action_children: Vec<&KdlDocument> = kdl_children!(kdl_action); + match action_name { + "Quit" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "FocusNextPane" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "FocusPreviousPane" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "SwitchFocus" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "EditScrollback" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "ScrollUp" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "ScrollDown" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "ScrollToBottom" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "PageScrollUp" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "PageScrollDown" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "HalfPageScrollUp" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "HalfPageScrollDown" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "ToggleFocusFullscreen" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "TogglePaneFrames" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "ToggleActiveSyncTab" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "TogglePaneEmbedOrFloating" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "ToggleFloatingPanes" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "CloseFocus" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "UndoRenamePane" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "NoOp" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "GoToNextTab" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "GoToPreviousTab" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "CloseTab" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "ToggleTab" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "UndoRenameTab" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, + "Detach" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "Copy" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "Confirm" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "Deny" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), + "Write" => parse_kdl_action_u8_arguments!(action_name, action_arguments, kdl_action), + "WriteChars" => parse_kdl_action_char_or_string_arguments!( + action_name, + action_arguments, + kdl_action + ), + "SwitchToMode" => parse_kdl_action_char_or_string_arguments!( + action_name, + action_arguments, + kdl_action + ), + "Search" => parse_kdl_action_char_or_string_arguments!( + action_name, + action_arguments, + kdl_action + ), + "Resize" => parse_kdl_action_char_or_string_arguments!( + action_name, + action_arguments, + kdl_action + ) |