mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2024-11-05 21:28:48 +00:00
feat: Multi selection and installation (#338)
* Fix conflicts * Fix cmd running when selected is directory * Clean comments
This commit is contained in:
parent
bd9c5a1ad7
commit
a747f80c85
|
@ -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(
|
||||||
let mut tree = Tree::new(ListNode {
|
|(
|
||||||
name: "root".to_string(),
|
TabEntry {
|
||||||
description: "".to_string(),
|
name,
|
||||||
command: Command::None,
|
data,
|
||||||
});
|
multi_selectable,
|
||||||
let mut root = tree.root_mut();
|
},
|
||||||
create_directory(data, &mut root, &directory);
|
directory,
|
||||||
Tab { name, tree }
|
)| {
|
||||||
})
|
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();
|
.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)]
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
name = "Utilities"
|
name = "Utilities"
|
||||||
|
multi_selectable = false
|
||||||
|
|
||||||
[[data]]
|
[[data]]
|
||||||
name = "Auto Login"
|
name = "Auto Login"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -136,25 +136,30 @@ 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 {
|
cmd.arg("-c");
|
||||||
Command::Raw(prompt) => {
|
|
||||||
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
|
||||||
cmd.arg(prompt);
|
let mut script = String::new();
|
||||||
}
|
for command in commands {
|
||||||
Command::LocalFile(file) => {
|
match command {
|
||||||
cmd.arg(&file);
|
Command::Raw(prompt) => script.push_str(&format!("{}\n", prompt)),
|
||||||
if let Some(parent) = file.parent() {
|
Command::LocalFile(file) => {
|
||||||
cmd.cwd(parent);
|
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
|
// Open a pseudo-terminal with initial size
|
||||||
let pair = pty_system
|
let pair = pty_system
|
||||||
.openpty(PtySize {
|
.openpty(PtySize {
|
||||||
|
|
|
@ -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, ..
|
||||||
}| {
|
}| {
|
||||||
if *has_children {
|
let is_selected = self.selected_commands.contains(&node.command);
|
||||||
Line::from(format!("{} {}", self.theme.dir_icon(), node.name))
|
let (indicator, style) = if is_selected {
|
||||||
.style(self.theme.dir_color())
|
(self.theme.multi_select_icon(), Style::default().bold())
|
||||||
} else {
|
} else {
|
||||||
Line::from(format!("{} {}", self.theme.cmd_icon(), node.name))
|
("", Style::new())
|
||||||
.style(self.theme.cmd_color())
|
};
|
||||||
|
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 {
|
} 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 let Some(cmd) = self.get_selected_command() {
|
if self.selected_item_is_cmd() {
|
||||||
let command = RunningCommand::new(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.spawn_float(command, 80, 80);
|
||||||
|
self.selected_commands.clear();
|
||||||
} else {
|
} else {
|
||||||
self.go_to_selected_dir();
|
self.go_to_selected_dir();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user