mirror of
https://github.com/zellij-org/zellij.git
synced 2026-09-22 08:02:38 +00:00
* allow sending a stringified layout with --layout-string * add --tab-id to all the things * fix windows tests * add pr
740 lines
28 KiB
Rust
740 lines
28 KiB
Rust
use crate::action_types::ActionType;
|
|
use std::collections::HashSet;
|
|
use zellij_tile::prelude::actions::Action;
|
|
use zellij_tile::prelude::*;
|
|
|
|
pub struct KeybindProcessor;
|
|
|
|
impl KeybindProcessor {
|
|
/// Find predetermined actions based on predicates while maintaining order
|
|
pub fn find_predetermined_actions<F>(
|
|
mode_info: &ModeInfo,
|
|
mode: InputMode,
|
|
predicates: Vec<F>,
|
|
) -> Vec<(String, String)>
|
|
where
|
|
F: Fn(&Action) -> bool,
|
|
{
|
|
let mut result = Vec::new();
|
|
let keybinds = mode_info.get_keybinds_for_mode(mode);
|
|
let mut processed_action_types = HashSet::new();
|
|
|
|
// Iterate through predicates in order to maintain the desired sequence
|
|
for predicate in predicates {
|
|
// Find the first matching action for this predicate
|
|
let mut found_match = false;
|
|
for (_key, actions) in &keybinds {
|
|
if let Some(first_action) = actions.first() {
|
|
if predicate(first_action) {
|
|
let action_type = ActionType::from_action(first_action);
|
|
|
|
// Skip if we've already processed this action type
|
|
if processed_action_types.contains(&action_type) {
|
|
found_match = true;
|
|
break;
|
|
}
|
|
|
|
let mut matching_keys = Vec::new();
|
|
|
|
// Find all keys that match this action type (including different directions)
|
|
for (inner_key, inner_actions) in &keybinds {
|
|
if let Some(inner_first_action) = inner_actions.first() {
|
|
if ActionType::from_action(inner_first_action) == action_type {
|
|
matching_keys.push(format!("{}", inner_key));
|
|
}
|
|
}
|
|
}
|
|
|
|
if !matching_keys.is_empty() {
|
|
let description = action_type.description();
|
|
let should_add_brackets_to_keys = mode != InputMode::Normal;
|
|
|
|
// Check if this is switching to normal mode
|
|
let is_switching_to_locked = matches!(
|
|
first_action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::Locked
|
|
}
|
|
);
|
|
|
|
let grouped_keys = Self::group_key_sets(
|
|
&matching_keys,
|
|
should_add_brackets_to_keys,
|
|
is_switching_to_locked,
|
|
);
|
|
result.push((grouped_keys, description));
|
|
processed_action_types.insert(action_type);
|
|
}
|
|
|
|
found_match = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we found a match for this predicate, we've processed it
|
|
if found_match {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Group keys into sets and separate different key types with '|'
|
|
fn group_key_sets(
|
|
keys: &[String],
|
|
should_add_brackets_to_keys: bool,
|
|
is_switching_to_locked: bool,
|
|
) -> String {
|
|
if keys.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
// Filter out Esc and Enter keys when switching to normal mode, but only if other keys exist
|
|
let filtered_keys: Vec<String> = if is_switching_to_locked {
|
|
let non_esc_enter_keys: Vec<String> = keys
|
|
.iter()
|
|
.filter(|k| k.as_str() != "ESC" && k.as_str() != "ENTER")
|
|
.cloned()
|
|
.collect();
|
|
|
|
if non_esc_enter_keys.is_empty() {
|
|
// If no other keys exist, keep the original keys
|
|
keys.to_vec()
|
|
} else {
|
|
// Use filtered keys (without Esc/Enter)
|
|
non_esc_enter_keys
|
|
}
|
|
} else {
|
|
keys.to_vec()
|
|
};
|
|
|
|
if filtered_keys.len() == 1 {
|
|
return if should_add_brackets_to_keys {
|
|
format!("<{}>", filtered_keys[0])
|
|
} else {
|
|
filtered_keys[0].clone()
|
|
};
|
|
}
|
|
|
|
// Group keys by type
|
|
let mut arrow_keys = Vec::new();
|
|
let mut hjkl_lower = Vec::new();
|
|
let mut hjkl_upper = Vec::new();
|
|
let mut square_bracket_keys = Vec::new();
|
|
let mut plus_minus_keys = Vec::new();
|
|
let mut pgup_pgdown = Vec::new();
|
|
let mut other_keys = Vec::new();
|
|
|
|
for key in &filtered_keys {
|
|
match key.as_str() {
|
|
"Left" | "←" => arrow_keys.push("←"),
|
|
"Down" | "↓" => arrow_keys.push("↓"),
|
|
"Up" | "↑" => arrow_keys.push("↑"),
|
|
"Right" | "→" => arrow_keys.push("→"),
|
|
"h" => hjkl_lower.push("h"),
|
|
"j" => hjkl_lower.push("j"),
|
|
"k" => hjkl_lower.push("k"),
|
|
"l" => hjkl_lower.push("l"),
|
|
"H" => hjkl_upper.push("H"),
|
|
"J" => hjkl_upper.push("J"),
|
|
"K" => hjkl_upper.push("K"),
|
|
"L" => hjkl_upper.push("L"),
|
|
"[" => square_bracket_keys.push("["),
|
|
"]" => square_bracket_keys.push("]"),
|
|
"+" => plus_minus_keys.push("+"),
|
|
"-" => plus_minus_keys.push("-"),
|
|
"=" => plus_minus_keys.push("="),
|
|
"PgUp" => pgup_pgdown.push("PgUp"),
|
|
"PgDn" => pgup_pgdown.push("PgDn"),
|
|
_ => {
|
|
if should_add_brackets_to_keys {
|
|
other_keys.push(format!("<{}>", key));
|
|
} else {
|
|
other_keys.push(key.clone());
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
let mut groups = Vec::new();
|
|
|
|
// Add hjkl group if present (prioritize hjkl over arrows)
|
|
if !hjkl_lower.is_empty() {
|
|
Self::sort_hjkl(&mut hjkl_lower);
|
|
groups.push(Self::format_key_group(
|
|
&hjkl_lower,
|
|
should_add_brackets_to_keys,
|
|
false,
|
|
));
|
|
}
|
|
|
|
// Add HJKL group if present
|
|
if !hjkl_upper.is_empty() {
|
|
Self::sort_hjkl_upper(&mut hjkl_upper);
|
|
groups.push(Self::format_key_group(
|
|
&hjkl_upper,
|
|
should_add_brackets_to_keys,
|
|
false,
|
|
));
|
|
}
|
|
|
|
// Add arrow keys group if present
|
|
if !arrow_keys.is_empty() {
|
|
Self::sort_arrows(&mut arrow_keys);
|
|
groups.push(Self::format_key_group(
|
|
&arrow_keys,
|
|
should_add_brackets_to_keys,
|
|
false,
|
|
));
|
|
}
|
|
|
|
if !square_bracket_keys.is_empty() {
|
|
Self::sort_square_brackets(&mut square_bracket_keys);
|
|
groups.push(Self::format_key_group(
|
|
&square_bracket_keys,
|
|
should_add_brackets_to_keys,
|
|
false,
|
|
));
|
|
}
|
|
|
|
if !plus_minus_keys.is_empty() {
|
|
Self::sort_plus_minus(&mut plus_minus_keys);
|
|
groups.push(Self::format_key_group(
|
|
&plus_minus_keys,
|
|
should_add_brackets_to_keys,
|
|
false,
|
|
));
|
|
}
|
|
|
|
if !pgup_pgdown.is_empty() {
|
|
Self::sort_pgup_pgdown(&mut pgup_pgdown);
|
|
groups.push(Self::format_key_group(
|
|
&pgup_pgdown,
|
|
should_add_brackets_to_keys,
|
|
true,
|
|
));
|
|
}
|
|
|
|
// Add other keys with / separator
|
|
if !other_keys.is_empty() {
|
|
groups.push(other_keys.join("/"));
|
|
}
|
|
|
|
groups.join("/")
|
|
}
|
|
|
|
fn sort_hjkl(keys: &mut Vec<&str>) {
|
|
keys.sort_by(|a, b| {
|
|
let order = ["h", "j", "k", "l"];
|
|
let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
|
|
let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
|
|
pos_a.cmp(&pos_b)
|
|
});
|
|
}
|
|
|
|
fn sort_hjkl_upper(keys: &mut Vec<&str>) {
|
|
keys.sort_by(|a, b| {
|
|
let order = ["H", "J", "K", "L"];
|
|
let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
|
|
let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
|
|
pos_a.cmp(&pos_b)
|
|
});
|
|
}
|
|
|
|
fn sort_arrows(keys: &mut Vec<&str>) {
|
|
keys.sort();
|
|
keys.dedup();
|
|
keys.sort_by(|a, b| {
|
|
let order = ["←", "↓", "↑", "→"];
|
|
let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
|
|
let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
|
|
pos_a.cmp(&pos_b)
|
|
});
|
|
}
|
|
|
|
fn sort_square_brackets(keys: &mut Vec<&str>) {
|
|
keys.sort_by(|a, b| {
|
|
let order = ["[", "]"];
|
|
let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
|
|
let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
|
|
pos_a.cmp(&pos_b)
|
|
});
|
|
}
|
|
|
|
fn sort_plus_minus(keys: &mut Vec<&str>) {
|
|
keys.sort_by(|a, b| {
|
|
let order = ["+", "-"];
|
|
let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
|
|
let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
|
|
pos_a.cmp(&pos_b)
|
|
});
|
|
// Remove "=" if both "+" and "=" are present
|
|
if keys.contains(&"+") && keys.contains(&"=") {
|
|
keys.retain(|k| k != &"=");
|
|
}
|
|
}
|
|
|
|
fn sort_pgup_pgdown(keys: &mut Vec<&str>) {
|
|
keys.sort_by(|a, b| {
|
|
let order = ["PgUp", "PgDn"];
|
|
let pos_a = order.iter().position(|&x| &x == a).unwrap_or(usize::MAX);
|
|
let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
|
|
pos_a.cmp(&pos_b)
|
|
});
|
|
}
|
|
|
|
fn format_key_group(
|
|
keys: &[&str],
|
|
should_add_brackets: bool,
|
|
use_pipe_separator: bool,
|
|
) -> String {
|
|
let separator = if use_pipe_separator { "|" } else { "" };
|
|
let joined = keys.join(separator);
|
|
|
|
if should_add_brackets {
|
|
format!("<{}>", joined)
|
|
} else {
|
|
joined
|
|
}
|
|
}
|
|
|
|
/// Get predetermined actions for a specific mode
|
|
pub fn get_predetermined_actions(
|
|
mode_info: &ModeInfo,
|
|
mode: InputMode,
|
|
) -> Vec<(String, String)> {
|
|
match mode {
|
|
InputMode::Locked => {
|
|
let ordered_predicates = vec![|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::Normal
|
|
}
|
|
)
|
|
}];
|
|
Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
|
|
},
|
|
InputMode::Normal => {
|
|
let ordered_predicates = vec![
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::Locked
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::Pane
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::Tab
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::Resize
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::Move
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::Scroll
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::Session
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| matches!(action, Action::Quit),
|
|
];
|
|
Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
|
|
},
|
|
InputMode::Pane => {
|
|
let ordered_predicates = vec![
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::NewPane {
|
|
direction: None,
|
|
pane_name: None,
|
|
start_suppressed: false
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::MoveFocus {
|
|
direction: Direction::Left
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::MoveFocus {
|
|
direction: Direction::Down
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::MoveFocus {
|
|
direction: Direction::Up
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::MoveFocus {
|
|
direction: Direction::Right
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| matches!(action, Action::CloseFocus),
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::RenamePane
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| matches!(action, Action::ToggleFocusFullscreen),
|
|
|action: &Action| matches!(action, Action::ToggleFloatingPanes),
|
|
|action: &Action| matches!(action, Action::TogglePaneEmbedOrFloating),
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::NewStackedPane {
|
|
command: None,
|
|
pane_name: None,
|
|
near_current_pane: false,
|
|
..
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::NewPane {
|
|
direction: Some(Direction::Right),
|
|
pane_name: None,
|
|
start_suppressed: false
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::NewPane {
|
|
direction: Some(Direction::Down),
|
|
pane_name: None,
|
|
start_suppressed: false
|
|
}
|
|
)
|
|
},
|
|
];
|
|
Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
|
|
},
|
|
InputMode::Tab => {
|
|
let ordered_predicates = vec![
|
|
|action: &Action| matches!(action, Action::GoToPreviousTab),
|
|
|action: &Action| matches!(action, Action::GoToNextTab),
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::NewTab {
|
|
tiled_layout: None,
|
|
floating_layouts: _,
|
|
swap_tiled_layouts: None,
|
|
swap_floating_layouts: None,
|
|
tab_name: None,
|
|
should_change_focus_to_new_tab: true,
|
|
cwd: None,
|
|
initial_panes: _,
|
|
first_pane_unblock_condition: _,
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| matches!(action, Action::CloseTab),
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::RenameTab
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| matches!(action, Action::TabNameInput { .. }),
|
|
|action: &Action| matches!(action, Action::ToggleActiveSyncTab),
|
|
|action: &Action| matches!(action, Action::BreakPane),
|
|
|action: &Action| matches!(action, Action::BreakPaneLeft),
|
|
|action: &Action| matches!(action, Action::BreakPaneRight),
|
|
|action: &Action| matches!(action, Action::ToggleTab),
|
|
];
|
|
Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
|
|
},
|
|
InputMode::Resize => {
|
|
let ordered_predicates = vec![
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Resize {
|
|
resize: Resize::Increase,
|
|
direction: None
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Resize {
|
|
resize: Resize::Decrease,
|
|
direction: None
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Resize {
|
|
resize: Resize::Increase,
|
|
direction: Some(Direction::Left)
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Resize {
|
|
resize: Resize::Increase,
|
|
direction: Some(Direction::Down)
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Resize {
|
|
resize: Resize::Increase,
|
|
direction: Some(Direction::Up)
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Resize {
|
|
resize: Resize::Increase,
|
|
direction: Some(Direction::Right)
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Resize {
|
|
resize: Resize::Decrease,
|
|
direction: Some(Direction::Left)
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Resize {
|
|
resize: Resize::Decrease,
|
|
direction: Some(Direction::Down)
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Resize {
|
|
resize: Resize::Decrease,
|
|
direction: Some(Direction::Up)
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Resize {
|
|
resize: Resize::Decrease,
|
|
direction: Some(Direction::Right)
|
|
}
|
|
)
|
|
},
|
|
];
|
|
Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
|
|
},
|
|
InputMode::Move => {
|
|
let ordered_predicates = vec![
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::MovePane {
|
|
direction: Some(Direction::Left)
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::MovePane {
|
|
direction: Some(Direction::Down)
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::MovePane {
|
|
direction: Some(Direction::Up)
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::MovePane {
|
|
direction: Some(Direction::Right)
|
|
}
|
|
)
|
|
},
|
|
];
|
|
Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
|
|
},
|
|
InputMode::Scroll => {
|
|
let ordered_predicates = vec![
|
|
|action: &Action| matches!(action, Action::ScrollDown),
|
|
|action: &Action| matches!(action, Action::ScrollUp),
|
|
|action: &Action| matches!(action, Action::HalfPageScrollDown),
|
|
|action: &Action| matches!(action, Action::HalfPageScrollUp),
|
|
|action: &Action| matches!(action, Action::PageScrollDown),
|
|
|action: &Action| matches!(action, Action::PageScrollUp),
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::EnterSearch
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| matches!(action, Action::EditScrollback { .. }),
|
|
];
|
|
Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
|
|
},
|
|
InputMode::Search => {
|
|
let ordered_predicates = vec![
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SwitchToMode {
|
|
input_mode: InputMode::EnterSearch
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| matches!(action, Action::SearchInput { .. }),
|
|
|action: &Action| matches!(action, Action::ScrollDown),
|
|
|action: &Action| matches!(action, Action::ScrollUp),
|
|
|action: &Action| matches!(action, Action::PageScrollDown),
|
|
|action: &Action| matches!(action, Action::PageScrollUp),
|
|
|action: &Action| matches!(action, Action::HalfPageScrollDown),
|
|
|action: &Action| matches!(action, Action::HalfPageScrollUp),
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Search {
|
|
direction: actions::SearchDirection::Down
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::Search {
|
|
direction: actions::SearchDirection::Up
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SearchToggleOption {
|
|
option: actions::SearchOption::CaseSensitivity
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SearchToggleOption {
|
|
option: actions::SearchOption::Wrap
|
|
}
|
|
)
|
|
},
|
|
|action: &Action| {
|
|
matches!(
|
|
action,
|
|
Action::SearchToggleOption {
|
|
option: actions::SearchOption::WholeWord
|
|
}
|
|
)
|
|
},
|
|
];
|
|
Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
|
|
},
|
|
InputMode::Session => {
|
|
let ordered_predicates = vec![
|
|
|action: &Action| matches!(action, Action::Detach),
|
|
|action: &Action| action.launches_plugin("session-manager"),
|
|
|action: &Action| action.launches_plugin("plugin-manager"),
|
|
|action: &Action| action.launches_plugin("configuration"),
|
|
|action: &Action| action.launches_plugin("zellij:about"),
|
|
];
|
|
Self::find_predetermined_actions(mode_info, mode, ordered_predicates)
|
|
},
|
|
InputMode::EnterSearch
|
|
| InputMode::RenameTab
|
|
| InputMode::RenamePane
|
|
| InputMode::Prompt
|
|
| InputMode::Tmux => Vec::new(),
|
|
}
|
|
}
|
|
}
|