diff --git a/core/src/inner.rs b/core/src/inner.rs index e965a372..f936e3b2 100644 --- a/core/src/inner.rs +++ b/core/src/inner.rs @@ -21,16 +21,29 @@ pub fn get_tabs(validate: bool) -> Vec { }); let tabs: Vec = tabs - .map(|(TabEntry { name, data }, directory)| { - let mut tree = Tree::new(ListNode { - name: "root".to_string(), - description: "".to_string(), - command: Command::None, - }); - let mut root = tree.root_mut(); - create_directory(data, &mut root, &directory); - Tab { name, tree } - }) + .map( + |( + TabEntry { + name, + data, + multi_selectable, + }, + directory, + )| { + let mut tree = Tree::new(ListNode { + name: "root".to_string(), + description: String::new(), + command: Command::None, + }); + let mut root = tree.root_mut(); + create_directory(data, &mut root, &directory); + Tab { + name, + tree, + multi_selectable, + } + }, + ) .collect(); if tabs.is_empty() { @@ -48,6 +61,12 @@ struct TabList { struct TabEntry { name: String, data: Vec, + #[serde(default = "default_multi_selectable")] + multi_selectable: bool, +} + +fn default_multi_selectable() -> bool { + true } #[derive(Deserialize)] diff --git a/core/src/lib.rs b/core/src/lib.rs index 164e4e42..1d52116b 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -16,6 +16,7 @@ pub enum Command { pub struct Tab { pub name: String, pub tree: Tree, + pub multi_selectable: bool, } #[derive(Clone, Hash, Eq, PartialEq)] diff --git a/tabs/utils/tab_data.toml b/tabs/utils/tab_data.toml index 3843b89e..39072521 100644 --- a/tabs/utils/tab_data.toml +++ b/tabs/utils/tab_data.toml @@ -1,4 +1,5 @@ name = "Utilities" +multi_selectable = false [[data]] name = "Auto Login" diff --git a/tui/src/floating_text.rs b/tui/src/floating_text.rs index ad77c36f..878066f7 100644 --- a/tui/src/floating_text.rs +++ b/tui/src/floating_text.rs @@ -84,7 +84,7 @@ impl FloatContent for FloatingText { let inner_area = block.inner(area); // Create the list of lines to be displayed - let mut lines: Vec = self + let lines: Vec = self .text .iter() .skip(self.scroll) diff --git a/tui/src/hint.rs b/tui/src/hint.rs index 93ec8312..31bbef40 100644 --- a/tui/src/hint.rs +++ b/tui/src/hint.rs @@ -144,6 +144,10 @@ pub fn draw_shortcuts(state: &AppState, frame: &mut Frame, area: Rect) { hints.push(Shortcut::new(vec!["j", "Down"], "Select item below")); hints.push(Shortcut::new(vec!["t"], "Next theme")); hints.push(Shortcut::new(vec!["T"], "Previous theme")); + if state.is_current_tab_multi_selectable() { + hints.push(Shortcut::new(vec!["v"], "Toggle multi-selection mode")); + hints.push(Shortcut::new(vec!["Space"], "Select multiple commands")); + } hints.push(Shortcut::new(vec!["Tab"], "Next tab")); hints.push(Shortcut::new(vec!["Shift-Tab"], "Previous tab")); ShortcutList { diff --git a/tui/src/running_command.rs b/tui/src/running_command.rs index d8df3402..c605b204 100644 --- a/tui/src/running_command.rs +++ b/tui/src/running_command.rs @@ -136,25 +136,30 @@ impl FloatContent for RunningCommand { } impl RunningCommand { - pub fn new(command: Command) -> Self { + pub fn new(commands: Vec) -> Self { let pty_system = NativePtySystem::default(); // Build the command based on the provided Command enum variant - let mut cmd = CommandBuilder::new("sh"); - match command { - Command::Raw(prompt) => { - cmd.arg("-c"); - cmd.arg(prompt); - } - Command::LocalFile(file) => { - cmd.arg(&file); - if let Some(parent) = file.parent() { - cmd.cwd(parent); + let mut cmd: CommandBuilder = CommandBuilder::new("sh"); + cmd.arg("-c"); + + // All the merged commands are passed as a single argument to reduce the overhead of rebuilding the command arguments for each and every command + let mut script = String::new(); + for command in commands { + match command { + Command::Raw(prompt) => script.push_str(&format!("{}\n", prompt)), + Command::LocalFile(file) => { + if let Some(parent) = file.parent() { + script.push_str(&format!("cd {}\n", parent.display())); + } + script.push_str(&format!("sh {}\n", file.display())); } + Command::None => panic!("Command::None was treated as a command"), } - Command::None => panic!("Command::None was treated as a command"), } + cmd.arg(script); + // Open a pseudo-terminal with initial size let pair = pty_system .openpty(PtySize { diff --git a/tui/src/state.rs b/tui/src/state.rs index 5b7c9849..856a614a 100644 --- a/tui/src/state.rs +++ b/tui/src/state.rs @@ -33,6 +33,8 @@ pub struct AppState { /// widget selection: ListState, filter: Filter, + multi_select: bool, + selected_commands: Vec, drawable: bool, } @@ -61,6 +63,8 @@ impl AppState { visit_stack: vec![root_id], selection: ListState::default().with_selected(Some(0)), filter: Filter::new(), + multi_select: false, + selected_commands: Vec::new(), drawable: false, }; state.update_items(); @@ -199,12 +203,29 @@ impl AppState { |ListEntry { node, has_children, .. }| { - if *has_children { - Line::from(format!("{} {}", self.theme.dir_icon(), node.name)) - .style(self.theme.dir_color()) + let is_selected = self.selected_commands.contains(&node.command); + let (indicator, style) = if is_selected { + (self.theme.multi_select_icon(), Style::default().bold()) } else { - Line::from(format!("{} {}", self.theme.cmd_icon(), node.name)) - .style(self.theme.cmd_color()) + ("", Style::new()) + }; + if *has_children { + Line::from(format!( + "{} {} {}", + self.theme.dir_icon(), + node.name, + indicator + )) + .style(self.theme.dir_color()) + } else { + Line::from(format!( + "{} {} {}", + self.theme.cmd_icon(), + node.name, + indicator + )) + .style(self.theme.cmd_color()) + .patch_style(style) } }, )); @@ -216,11 +237,15 @@ impl AppState { } else { Style::new() }) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("Linux Toolbox - {}", env!("BUILD_DATE"))), - ) + .block(Block::default().borders(Borders::ALL).title(format!( + "Linux Toolbox - {} {}", + env!("BUILD_DATE"), + if self.multi_select { + "[Multi-Select]" + } else { + "" + } + ))) .scroll_padding(1); frame.render_stateful_widget(list, chunks[1], &mut self.selection); @@ -254,7 +279,7 @@ impl AppState { match key.code { KeyCode::Tab => { if self.current_tab.selected().unwrap() == self.tabs.len() - 1 { - self.current_tab.select_first(); // Select first tab when it is at last + self.current_tab.select_first(); } else { self.current_tab.select_next(); } @@ -262,7 +287,7 @@ impl AppState { } KeyCode::BackTab => { if self.current_tab.selected().unwrap() == 0 { - self.current_tab.select(Some(self.tabs.len() - 1)); // Select last tab when it is at first + self.current_tab.select(Some(self.tabs.len() - 1)); } else { self.current_tab.select_previous(); } @@ -329,6 +354,8 @@ impl AppState { KeyCode::Char('/') => self.enter_search(), KeyCode::Char('t') => self.theme.next(), KeyCode::Char('T') => self.theme.prev(), + KeyCode::Char('v') | KeyCode::Char('V') => self.toggle_multi_select(), + KeyCode::Char(' ') if self.multi_select => self.toggle_selection(), _ => {} }, @@ -336,13 +363,39 @@ impl AppState { }; true } - + fn toggle_multi_select(&mut self) { + if self.is_current_tab_multi_selectable() { + self.multi_select = !self.multi_select; + if !self.multi_select { + self.selected_commands.clear(); + } + } + } + fn toggle_selection(&mut self) { + if let Some(command) = self.get_selected_command() { + if self.selected_commands.contains(&command) { + self.selected_commands.retain(|c| c != &command); + } else { + self.selected_commands.push(command); + } + } + } + pub fn is_current_tab_multi_selectable(&self) -> bool { + let index = self.current_tab.selected().unwrap_or(0); + self.tabs + .get(index) + .map_or(false, |tab| tab.multi_selectable) + } fn update_items(&mut self) { self.filter.update_items( &self.tabs, self.current_tab.selected().unwrap(), *self.visit_stack.last().unwrap(), ); + if !self.is_current_tab_multi_selectable() { + self.multi_select = false; + self.selected_commands.clear(); + } } /// Checks either the current tree node is the root node (can we go up the tree or no) @@ -471,9 +524,15 @@ impl AppState { } fn handle_enter(&mut self) { - if let Some(cmd) = self.get_selected_command() { - let command = RunningCommand::new(cmd); + if self.selected_item_is_cmd() { + if self.selected_commands.is_empty() { + if let Some(cmd) = self.get_selected_command() { + self.selected_commands.push(cmd); + } + } + let command = RunningCommand::new(self.selected_commands.clone()); self.spawn_float(command, 80, 80); + self.selected_commands.clear(); } else { self.go_to_selected_dir(); } diff --git a/tui/src/theme.rs b/tui/src/theme.rs index 84fa15b4..8337645a 100644 --- a/tui/src/theme.rs +++ b/tui/src/theme.rs @@ -56,6 +56,13 @@ impl Theme { } } + pub fn multi_select_icon(&self) -> &'static str { + match self { + Theme::Default => "", + Theme::Compatible => "*", + } + } + pub fn success_color(&self) -> Color { match self { Theme::Default => Color::Rgb(199, 55, 44),