feat: Multi selection and installation (#338)

* Fix conflicts

* Fix cmd running when selected is directory

* Clean comments
This commit is contained in:
JEEVITHA KANNAN K S 2024-09-19 23:45:30 +05:30 committed by GitHub
parent bd9c5a1ad7
commit a747f80c85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 134 additions and 38 deletions

View File

@ -21,16 +21,29 @@ pub fn get_tabs(validate: bool) -> Vec<Tab> {
}); });
let tabs: Vec<Tab> = tabs let tabs: Vec<Tab> = tabs
.map(|(TabEntry { name, data }, directory)| { .map(
|(
TabEntry {
name,
data,
multi_selectable,
},
directory,
)| {
let mut tree = Tree::new(ListNode { let mut tree = Tree::new(ListNode {
name: "root".to_string(), name: "root".to_string(),
description: "".to_string(), description: String::new(),
command: Command::None, command: Command::None,
}); });
let mut root = tree.root_mut(); let mut root = tree.root_mut();
create_directory(data, &mut root, &directory); create_directory(data, &mut root, &directory);
Tab { name, tree } Tab {
}) name,
tree,
multi_selectable,
}
},
)
.collect(); .collect();
if tabs.is_empty() { if tabs.is_empty() {
@ -48,6 +61,12 @@ struct TabList {
struct TabEntry { struct TabEntry {
name: String, name: String,
data: Vec<Entry>, data: Vec<Entry>,
#[serde(default = "default_multi_selectable")]
multi_selectable: bool,
}
fn default_multi_selectable() -> bool {
true
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View File

@ -16,6 +16,7 @@ pub enum Command {
pub struct Tab { pub struct Tab {
pub name: String, pub name: String,
pub tree: Tree<ListNode>, pub tree: Tree<ListNode>,
pub multi_selectable: bool,
} }
#[derive(Clone, Hash, Eq, PartialEq)] #[derive(Clone, Hash, Eq, PartialEq)]

View File

@ -1,4 +1,5 @@
name = "Utilities" name = "Utilities"
multi_selectable = false
[[data]] [[data]]
name = "Auto Login" name = "Auto Login"

View File

@ -84,7 +84,7 @@ impl FloatContent for FloatingText {
let inner_area = block.inner(area); let inner_area = block.inner(area);
// Create the list of lines to be displayed // Create the list of lines to be displayed
let mut lines: Vec<Line> = self let lines: Vec<Line> = self
.text .text
.iter() .iter()
.skip(self.scroll) .skip(self.scroll)

View File

@ -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!["j", "Down"], "Select item below"));
hints.push(Shortcut::new(vec!["t"], "Next theme")); hints.push(Shortcut::new(vec!["t"], "Next theme"));
hints.push(Shortcut::new(vec!["T"], "Previous 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!["Tab"], "Next tab"));
hints.push(Shortcut::new(vec!["Shift-Tab"], "Previous tab")); hints.push(Shortcut::new(vec!["Shift-Tab"], "Previous tab"));
ShortcutList { ShortcutList {

View File

@ -136,24 +136,29 @@ impl FloatContent for RunningCommand {
} }
impl RunningCommand { impl RunningCommand {
pub fn new(command: Command) -> Self { pub fn new(commands: Vec<Command>) -> Self {
let pty_system = NativePtySystem::default(); let pty_system = NativePtySystem::default();
// Build the command based on the provided Command enum variant // Build the command based on the provided Command enum variant
let mut cmd = CommandBuilder::new("sh"); let mut cmd: CommandBuilder = CommandBuilder::new("sh");
match command {
Command::Raw(prompt) => {
cmd.arg("-c"); cmd.arg("-c");
cmd.arg(prompt);
} // 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) => { Command::LocalFile(file) => {
cmd.arg(&file);
if let Some(parent) = file.parent() { if let Some(parent) = file.parent() {
cmd.cwd(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 // Open a pseudo-terminal with initial size
let pair = pty_system let pair = pty_system

View File

@ -33,6 +33,8 @@ pub struct AppState {
/// widget /// widget
selection: ListState, selection: ListState,
filter: Filter, filter: Filter,
multi_select: bool,
selected_commands: Vec<Command>,
drawable: bool, drawable: bool,
} }
@ -61,6 +63,8 @@ impl AppState {
visit_stack: vec![root_id], visit_stack: vec![root_id],
selection: ListState::default().with_selected(Some(0)), selection: ListState::default().with_selected(Some(0)),
filter: Filter::new(), filter: Filter::new(),
multi_select: false,
selected_commands: Vec::new(),
drawable: false, drawable: false,
}; };
state.update_items(); state.update_items();
@ -199,12 +203,29 @@ impl AppState {
|ListEntry { |ListEntry {
node, has_children, .. node, has_children, ..
}| { }| {
let is_selected = self.selected_commands.contains(&node.command);
let (indicator, style) = if is_selected {
(self.theme.multi_select_icon(), Style::default().bold())
} else {
("", Style::new())
};
if *has_children { if *has_children {
Line::from(format!("{} {}", self.theme.dir_icon(), node.name)) Line::from(format!(
"{} {} {}",
self.theme.dir_icon(),
node.name,
indicator
))
.style(self.theme.dir_color()) .style(self.theme.dir_color())
} else { } else {
Line::from(format!("{} {}", self.theme.cmd_icon(), node.name)) Line::from(format!(
"{} {} {}",
self.theme.cmd_icon(),
node.name,
indicator
))
.style(self.theme.cmd_color()) .style(self.theme.cmd_color())
.patch_style(style)
} }
}, },
)); ));
@ -216,11 +237,15 @@ impl AppState {
} else { } else {
Style::new() Style::new()
}) })
.block( .block(Block::default().borders(Borders::ALL).title(format!(
Block::default() "Linux Toolbox - {} {}",
.borders(Borders::ALL) env!("BUILD_DATE"),
.title(format!("Linux Toolbox - {}", env!("BUILD_DATE"))), if self.multi_select {
) "[Multi-Select]"
} else {
""
}
)))
.scroll_padding(1); .scroll_padding(1);
frame.render_stateful_widget(list, chunks[1], &mut self.selection); frame.render_stateful_widget(list, chunks[1], &mut self.selection);
@ -254,7 +279,7 @@ impl AppState {
match key.code { match key.code {
KeyCode::Tab => { KeyCode::Tab => {
if self.current_tab.selected().unwrap() == self.tabs.len() - 1 { 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 { } else {
self.current_tab.select_next(); self.current_tab.select_next();
} }
@ -262,7 +287,7 @@ impl AppState {
} }
KeyCode::BackTab => { KeyCode::BackTab => {
if self.current_tab.selected().unwrap() == 0 { 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 { } else {
self.current_tab.select_previous(); self.current_tab.select_previous();
} }
@ -329,6 +354,8 @@ impl AppState {
KeyCode::Char('/') => self.enter_search(), KeyCode::Char('/') => self.enter_search(),
KeyCode::Char('t') => self.theme.next(), KeyCode::Char('t') => self.theme.next(),
KeyCode::Char('T') => self.theme.prev(), 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 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) { fn update_items(&mut self) {
self.filter.update_items( self.filter.update_items(
&self.tabs, &self.tabs,
self.current_tab.selected().unwrap(), self.current_tab.selected().unwrap(),
*self.visit_stack.last().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) /// 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) { fn handle_enter(&mut self) {
if self.selected_item_is_cmd() {
if self.selected_commands.is_empty() {
if let Some(cmd) = self.get_selected_command() { if let Some(cmd) = self.get_selected_command() {
let command = RunningCommand::new(cmd); self.selected_commands.push(cmd);
}
}
let command = RunningCommand::new(self.selected_commands.clone());
self.spawn_float(command, 80, 80); self.spawn_float(command, 80, 80);
self.selected_commands.clear();
} else { } else {
self.go_to_selected_dir(); self.go_to_selected_dir();
} }

View File

@ -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 { pub fn success_color(&self) -> Color {
match self { match self {
Theme::Default => Color::Rgb(199, 55, 44), Theme::Default => Color::Rgb(199, 55, 44),