Skip to content

Commit a747f80

Browse files
feat: Multi selection and installation (#338)
* Fix conflicts * Fix cmd running when selected is directory * Clean comments
1 parent bd9c5a1 commit a747f80

File tree

8 files changed

+133
-37
lines changed

8 files changed

+133
-37
lines changed

core/src/inner.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,29 @@ pub fn get_tabs(validate: bool) -> Vec<Tab> {
2121
});
2222

2323
let tabs: Vec<Tab> = tabs
24-
.map(|(TabEntry { name, data }, directory)| {
25-
let mut tree = Tree::new(ListNode {
26-
name: "root".to_string(),
27-
description: "".to_string(),
28-
command: Command::None,
29-
});
30-
let mut root = tree.root_mut();
31-
create_directory(data, &mut root, &directory);
32-
Tab { name, tree }
33-
})
24+
.map(
25+
|(
26+
TabEntry {
27+
name,
28+
data,
29+
multi_selectable,
30+
},
31+
directory,
32+
)| {
33+
let mut tree = Tree::new(ListNode {
34+
name: "root".to_string(),
35+
description: String::new(),
36+
command: Command::None,
37+
});
38+
let mut root = tree.root_mut();
39+
create_directory(data, &mut root, &directory);
40+
Tab {
41+
name,
42+
tree,
43+
multi_selectable,
44+
}
45+
},
46+
)
3447
.collect();
3548

3649
if tabs.is_empty() {
@@ -48,6 +61,12 @@ struct TabList {
4861
struct TabEntry {
4962
name: String,
5063
data: Vec<Entry>,
64+
#[serde(default = "default_multi_selectable")]
65+
multi_selectable: bool,
66+
}
67+
68+
fn default_multi_selectable() -> bool {
69+
true
5170
}
5271

5372
#[derive(Deserialize)]

core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub enum Command {
1616
pub struct Tab {
1717
pub name: String,
1818
pub tree: Tree<ListNode>,
19+
pub multi_selectable: bool,
1920
}
2021

2122
#[derive(Clone, Hash, Eq, PartialEq)]

tabs/utils/tab_data.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
name = "Utilities"
2+
multi_selectable = false
23

34
[[data]]
45
name = "Auto Login"

tui/src/floating_text.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ impl FloatContent for FloatingText {
8484
let inner_area = block.inner(area);
8585

8686
// Create the list of lines to be displayed
87-
let mut lines: Vec<Line> = self
87+
let lines: Vec<Line> = self
8888
.text
8989
.iter()
9090
.skip(self.scroll)

tui/src/hint.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ pub fn draw_shortcuts(state: &AppState, frame: &mut Frame, area: Rect) {
144144
hints.push(Shortcut::new(vec!["j", "Down"], "Select item below"));
145145
hints.push(Shortcut::new(vec!["t"], "Next theme"));
146146
hints.push(Shortcut::new(vec!["T"], "Previous theme"));
147+
if state.is_current_tab_multi_selectable() {
148+
hints.push(Shortcut::new(vec!["v"], "Toggle multi-selection mode"));
149+
hints.push(Shortcut::new(vec!["Space"], "Select multiple commands"));
150+
}
147151
hints.push(Shortcut::new(vec!["Tab"], "Next tab"));
148152
hints.push(Shortcut::new(vec!["Shift-Tab"], "Previous tab"));
149153
ShortcutList {

tui/src/running_command.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -136,25 +136,30 @@ impl FloatContent for RunningCommand {
136136
}
137137

138138
impl RunningCommand {
139-
pub fn new(command: Command) -> Self {
139+
pub fn new(commands: Vec<Command>) -> Self {
140140
let pty_system = NativePtySystem::default();
141141

142142
// Build the command based on the provided Command enum variant
143-
let mut cmd = CommandBuilder::new("sh");
144-
match command {
145-
Command::Raw(prompt) => {
146-
cmd.arg("-c");
147-
cmd.arg(prompt);
148-
}
149-
Command::LocalFile(file) => {
150-
cmd.arg(&file);
151-
if let Some(parent) = file.parent() {
152-
cmd.cwd(parent);
143+
let mut cmd: CommandBuilder = CommandBuilder::new("sh");
144+
cmd.arg("-c");
145+
146+
// All the merged commands are passed as a single argument to reduce the overhead of rebuilding the command arguments for each and every command
147+
let mut script = String::new();
148+
for command in commands {
149+
match command {
150+
Command::Raw(prompt) => script.push_str(&format!("{}\n", prompt)),
151+
Command::LocalFile(file) => {
152+
if let Some(parent) = file.parent() {
153+
script.push_str(&format!("cd {}\n", parent.display()));
154+
}
155+
script.push_str(&format!("sh {}\n", file.display()));
153156
}
157+
Command::None => panic!("Command::None was treated as a command"),
154158
}
155-
Command::None => panic!("Command::None was treated as a command"),
156159
}
157160

161+
cmd.arg(script);
162+
158163
// Open a pseudo-terminal with initial size
159164
let pair = pty_system
160165
.openpty(PtySize {

tui/src/state.rs

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub struct AppState {
3333
/// widget
3434
selection: ListState,
3535
filter: Filter,
36+
multi_select: bool,
37+
selected_commands: Vec<Command>,
3638
drawable: bool,
3739
}
3840

@@ -61,6 +63,8 @@ impl AppState {
6163
visit_stack: vec![root_id],
6264
selection: ListState::default().with_selected(Some(0)),
6365
filter: Filter::new(),
66+
multi_select: false,
67+
selected_commands: Vec::new(),
6468
drawable: false,
6569
};
6670
state.update_items();
@@ -199,12 +203,29 @@ impl AppState {
199203
|ListEntry {
200204
node, has_children, ..
201205
}| {
206+
let is_selected = self.selected_commands.contains(&node.command);
207+
let (indicator, style) = if is_selected {
208+
(self.theme.multi_select_icon(), Style::default().bold())
209+
} else {
210+
("", Style::new())
211+
};
202212
if *has_children {
203-
Line::from(format!("{} {}", self.theme.dir_icon(), node.name))
204-
.style(self.theme.dir_color())
213+
Line::from(format!(
214+
"{} {} {}",
215+
self.theme.dir_icon(),
216+
node.name,
217+
indicator
218+
))
219+
.style(self.theme.dir_color())
205220
} else {
206-
Line::from(format!("{} {}", self.theme.cmd_icon(), node.name))
207-
.style(self.theme.cmd_color())
221+
Line::from(format!(
222+
"{} {} {}",
223+
self.theme.cmd_icon(),
224+
node.name,
225+
indicator
226+
))
227+
.style(self.theme.cmd_color())
228+
.patch_style(style)
208229
}
209230
},
210231
));
@@ -216,11 +237,15 @@ impl AppState {
216237
} else {
217238
Style::new()
218239
})
219-
.block(
220-
Block::default()
221-
.borders(Borders::ALL)
222-
.title(format!("Linux Toolbox - {}", env!("BUILD_DATE"))),
223-
)
240+
.block(Block::default().borders(Borders::ALL).title(format!(
241+
"Linux Toolbox - {} {}",
242+
env!("BUILD_DATE"),
243+
if self.multi_select {
244+
"[Multi-Select]"
245+
} else {
246+
""
247+
}
248+
)))
224249
.scroll_padding(1);
225250
frame.render_stateful_widget(list, chunks[1], &mut self.selection);
226251

@@ -254,15 +279,15 @@ impl AppState {
254279
match key.code {
255280
KeyCode::Tab => {
256281
if self.current_tab.selected().unwrap() == self.tabs.len() - 1 {
257-
self.current_tab.select_first(); // Select first tab when it is at last
282+
self.current_tab.select_first();
258283
} else {
259284
self.current_tab.select_next();
260285
}
261286
self.refresh_tab();
262287
}
263288
KeyCode::BackTab => {
264289
if self.current_tab.selected().unwrap() == 0 {
265-
self.current_tab.select(Some(self.tabs.len() - 1)); // Select last tab when it is at first
290+
self.current_tab.select(Some(self.tabs.len() - 1));
266291
} else {
267292
self.current_tab.select_previous();
268293
}
@@ -329,20 +354,48 @@ impl AppState {
329354
KeyCode::Char('/') => self.enter_search(),
330355
KeyCode::Char('t') => self.theme.next(),
331356
KeyCode::Char('T') => self.theme.prev(),
357+
KeyCode::Char('v') | KeyCode::Char('V') => self.toggle_multi_select(),
358+
KeyCode::Char(' ') if self.multi_select => self.toggle_selection(),
332359
_ => {}
333360
},
334361

335362
_ => (),
336363
};
337364
true
338365
}
339-
366+
fn toggle_multi_select(&mut self) {
367+
if self.is_current_tab_multi_selectable() {
368+
self.multi_select = !self.multi_select;
369+
if !self.multi_select {
370+
self.selected_commands.clear();
371+
}
372+
}
373+
}
374+
fn toggle_selection(&mut self) {
375+
if let Some(command) = self.get_selected_command() {
376+
if self.selected_commands.contains(&command) {
377+
self.selected_commands.retain(|c| c != &command);
378+
} else {
379+
self.selected_commands.push(command);
380+
}
381+
}
382+
}
383+
pub fn is_current_tab_multi_selectable(&self) -> bool {
384+
let index = self.current_tab.selected().unwrap_or(0);
385+
self.tabs
386+
.get(index)
387+
.map_or(false, |tab| tab.multi_selectable)
388+
}
340389
fn update_items(&mut self) {
341390
self.filter.update_items(
342391
&self.tabs,
343392
self.current_tab.selected().unwrap(),
344393
*self.visit_stack.last().unwrap(),
345394
);
395+
if !self.is_current_tab_multi_selectable() {
396+
self.multi_select = false;
397+
self.selected_commands.clear();
398+
}
346399
}
347400

348401
/// Checks either the current tree node is the root node (can we go up the tree or no)
@@ -471,9 +524,15 @@ impl AppState {
471524
}
472525

473526
fn handle_enter(&mut self) {
474-
if let Some(cmd) = self.get_selected_command() {
475-
let command = RunningCommand::new(cmd);
527+
if self.selected_item_is_cmd() {
528+
if self.selected_commands.is_empty() {
529+
if let Some(cmd) = self.get_selected_command() {
530+
self.selected_commands.push(cmd);
531+
}
532+
}
533+
let command = RunningCommand::new(self.selected_commands.clone());
476534
self.spawn_float(command, 80, 80);
535+
self.selected_commands.clear();
477536
} else {
478537
self.go_to_selected_dir();
479538
}

tui/src/theme.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ impl Theme {
5656
}
5757
}
5858

59+
pub fn multi_select_icon(&self) -> &'static str {
60+
match self {
61+
Theme::Default => "",
62+
Theme::Compatible => "*",
63+
}
64+
}
65+
5966
pub fn success_color(&self) -> Color {
6067
match self {
6168
Theme::Default => Color::Rgb(199, 55, 44),

0 commit comments

Comments
 (0)