mirror of
https://github.com/ChrisTitusTech/linutil.git
synced 2024-11-05 21:28:48 +00:00
Merge branch 'ChrisTitusTech:main' into main
This commit is contained in:
commit
63efe076e4
BIN
build/linutil
BIN
build/linutil
Binary file not shown.
Binary file not shown.
25
src/commands/system-setup/arch/paru-setup.sh
Normal file
25
src/commands/system-setup/arch/paru-setup.sh
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#!/bin/sh -e
|
||||||
|
|
||||||
|
. "$(dirname "$0")/../../common-script.sh"
|
||||||
|
|
||||||
|
installDepend() {
|
||||||
|
case $PACKAGER in
|
||||||
|
pacman)
|
||||||
|
if ! command_exists paru; then
|
||||||
|
echo "Installing paru as AUR helper..."
|
||||||
|
sudo "$PACKAGER" --noconfirm -S base-devel
|
||||||
|
cd /opt && sudo git clone https://aur.archlinux.org/paru.git && sudo chown -R "$USER": ./paru
|
||||||
|
cd paru && makepkg --noconfirm -si
|
||||||
|
echo "Paru installed"
|
||||||
|
else
|
||||||
|
echo "Paru already installed"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported package manager: $PACKAGER"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEnv
|
||||||
|
installDepend
|
25
src/commands/system-setup/arch/yay-setup.sh
Normal file
25
src/commands/system-setup/arch/yay-setup.sh
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#!/bin/sh -e
|
||||||
|
|
||||||
|
. "$(dirname "$0")/../../common-script.sh"
|
||||||
|
|
||||||
|
installDepend() {
|
||||||
|
case $PACKAGER in
|
||||||
|
pacman)
|
||||||
|
if ! command_exists yay; then
|
||||||
|
echo "Installing yay as AUR helper..."
|
||||||
|
sudo "$PACKAGER" --noconfirm -S base-devel
|
||||||
|
cd /opt && sudo git clone https://aur.archlinux.org/yay-git.git && sudo chown -R "$USER": ./yay-git
|
||||||
|
cd yay-git && makepkg --noconfirm -si
|
||||||
|
echo "Yay installed"
|
||||||
|
else
|
||||||
|
echo "Aur helper already installed"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported package manager: $PACKAGER"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
checkEnv
|
||||||
|
installDepend
|
57
src/float.rs
57
src/float.rs
|
@ -10,16 +10,16 @@ pub trait FloatContent {
|
||||||
fn is_finished(&self) -> bool;
|
fn is_finished(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Float<T: FloatContent> {
|
pub struct Float {
|
||||||
content: Option<T>,
|
content: Box<dyn FloatContent>,
|
||||||
width_percent: u16,
|
width_percent: u16,
|
||||||
height_percent: u16,
|
height_percent: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FloatContent> Float<T> {
|
impl Float {
|
||||||
pub fn new(width_percent: u16, height_percent: u16) -> Self {
|
pub fn new(content: Box<dyn FloatContent>, width_percent: u16, height_percent: u16) -> Self {
|
||||||
Self {
|
Self {
|
||||||
content: None,
|
content,
|
||||||
width_percent,
|
width_percent,
|
||||||
height_percent,
|
height_percent,
|
||||||
}
|
}
|
||||||
|
@ -48,44 +48,25 @@ impl<T: FloatContent> Float<T> {
|
||||||
pub fn draw(&mut self, frame: &mut Frame, parent_area: Rect) {
|
pub fn draw(&mut self, frame: &mut Frame, parent_area: Rect) {
|
||||||
let popup_area = self.floating_window(parent_area);
|
let popup_area = self.floating_window(parent_area);
|
||||||
|
|
||||||
if let Some(content) = &mut self.content {
|
let content_area = Rect {
|
||||||
let content_area = Rect {
|
x: popup_area.x,
|
||||||
x: popup_area.x,
|
y: popup_area.y,
|
||||||
y: popup_area.y,
|
width: popup_area.width,
|
||||||
width: popup_area.width,
|
height: popup_area.height,
|
||||||
height: popup_area.height,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
content.draw(frame, content_area);
|
self.content.draw(frame, content_area);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if the key was processed by this Float.
|
// Returns true if the floating window is finished.
|
||||||
pub fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
pub fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
||||||
if let Some(content) = &mut self.content {
|
match key.code {
|
||||||
match key.code {
|
KeyCode::Enter | KeyCode::Char('p') | KeyCode::Esc | KeyCode::Char('q')
|
||||||
KeyCode::Enter | KeyCode::Char('p') | KeyCode::Esc | KeyCode::Char('q') => {
|
if self.content.is_finished() =>
|
||||||
if content.is_finished() {
|
{
|
||||||
self.content = None;
|
true
|
||||||
} else {
|
|
||||||
content.handle_key_event(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
content.handle_key_event(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
true
|
_ => self.content.handle_key_event(key),
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_content(&self) -> &Option<T> {
|
|
||||||
&self.content
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_content(&mut self, content: Option<T>) {
|
|
||||||
self.content = content;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::float::FloatContent;
|
use crate::{float::FloatContent, running_command::Command};
|
||||||
use crossterm::event::{KeyCode, KeyEvent};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
|
@ -7,6 +7,7 @@ use ratatui::{
|
||||||
widgets::{Block, Borders, List},
|
widgets::{Block, Borders, List},
|
||||||
Frame,
|
Frame,
|
||||||
};
|
};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub struct FloatingText {
|
pub struct FloatingText {
|
||||||
text: Vec<String>,
|
text: Vec<String>,
|
||||||
|
@ -18,6 +19,26 @@ impl FloatingText {
|
||||||
Self { text, scroll: 0 }
|
Self { text, scroll: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_command(command: &Command, mut full_path: PathBuf) -> Option<Self> {
|
||||||
|
let lines = match command {
|
||||||
|
Command::Raw(cmd) => {
|
||||||
|
// Reconstruct the line breaks and file formatting after the
|
||||||
|
// 'include_str!()' call in the node
|
||||||
|
cmd.lines().map(|line| line.to_string()).collect()
|
||||||
|
}
|
||||||
|
Command::LocalFile(file_path) => {
|
||||||
|
full_path.push(file_path);
|
||||||
|
let file_contents = std::fs::read_to_string(&full_path)
|
||||||
|
.map_err(|_| format!("File not found: {:?}", &full_path))
|
||||||
|
.unwrap();
|
||||||
|
file_contents.lines().map(|line| line.to_string()).collect()
|
||||||
|
}
|
||||||
|
// If command is a folder, we don't display a preview
|
||||||
|
Command::None => return None,
|
||||||
|
};
|
||||||
|
Some(Self::new(lines))
|
||||||
|
}
|
||||||
|
|
||||||
fn scroll_down(&mut self) {
|
fn scroll_down(&mut self) {
|
||||||
if self.scroll + 1 < self.text.len() {
|
if self.scroll + 1 < self.text.len() {
|
||||||
self.scroll += 1;
|
self.scroll += 1;
|
||||||
|
@ -64,16 +85,11 @@ impl FloatContent for FloatingText {
|
||||||
|
|
||||||
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Down | KeyCode::Char('j') => {
|
KeyCode::Down | KeyCode::Char('j') => self.scroll_down(),
|
||||||
self.scroll_down();
|
KeyCode::Up | KeyCode::Char('k') => self.scroll_up(),
|
||||||
true
|
_ => {}
|
||||||
}
|
|
||||||
KeyCode::Up | KeyCode::Char('k') => {
|
|
||||||
self.scroll_up();
|
|
||||||
true
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_finished(&self) -> bool {
|
fn is_finished(&self) -> bool {
|
||||||
|
|
444
src/list.rs
444
src/list.rs
|
@ -1,444 +0,0 @@
|
||||||
use crate::{float::Float, floating_text::FloatingText, running_command::Command, state::AppState};
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
|
||||||
use ego_tree::{tree, NodeId};
|
|
||||||
use ratatui::{
|
|
||||||
layout::Rect,
|
|
||||||
style::{Style, Stylize},
|
|
||||||
text::Line,
|
|
||||||
widgets::{Block, Borders, List, ListState},
|
|
||||||
Frame,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct ListNode {
|
|
||||||
name: &'static str,
|
|
||||||
command: Command,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is a data structure that has everything necessary to draw and manage a menu of commands
|
|
||||||
pub struct CustomList {
|
|
||||||
/// The tree data structure, to represent regular items
|
|
||||||
/// and "directories"
|
|
||||||
inner_tree: ego_tree::Tree<ListNode>,
|
|
||||||
/// This stack keeps track of our "current dirrectory". You can think of it as `pwd`. but not
|
|
||||||
/// just the current directory, all paths that took us here, so we can "cd .."
|
|
||||||
visit_stack: Vec<NodeId>,
|
|
||||||
/// This is the state asociated with the list widget, used to display the selection in the
|
|
||||||
/// widget
|
|
||||||
list_state: ListState,
|
|
||||||
// This stores the current search query
|
|
||||||
filter_query: String,
|
|
||||||
// This stores the filtered tree
|
|
||||||
filtered_items: Vec<ListNode>,
|
|
||||||
// This is the preview window for the commands
|
|
||||||
preview_float: Float<FloatingText>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CustomList {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
// When a function call ends with an exclamation mark, it means it's a macro, like in this
|
|
||||||
// case the tree! macro expands to `ego-tree::tree` data structure
|
|
||||||
let tree = tree!(ListNode {
|
|
||||||
name: "root",
|
|
||||||
command: Command::None,
|
|
||||||
} => {
|
|
||||||
ListNode {
|
|
||||||
name: "Applications Setup",
|
|
||||||
command: Command::None
|
|
||||||
} => {
|
|
||||||
ListNode {
|
|
||||||
name: "Alacritty",
|
|
||||||
command: Command::LocalFile("applications-setup/alacritty-setup.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Bash Prompt",
|
|
||||||
command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\""),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "DWM-Titus",
|
|
||||||
command: Command::LocalFile("applications-setup/dwmtitus-setup.sh")
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Kitty",
|
|
||||||
command: Command::LocalFile("applications-setup/kitty-setup.sh")
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Neovim",
|
|
||||||
command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/neovim/main/setup.sh)\""),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Rofi",
|
|
||||||
command: Command::LocalFile("applications-setup/rofi-setup.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "ZSH Prompt",
|
|
||||||
command: Command::LocalFile("applications-setup/zsh-setup.sh"),
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Security",
|
|
||||||
command: Command::None
|
|
||||||
} => {
|
|
||||||
ListNode {
|
|
||||||
name: "Firewall Baselines (CTT)",
|
|
||||||
command: Command::LocalFile("security/firewall-baselines.sh"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "System Setup",
|
|
||||||
command: Command::None,
|
|
||||||
} => {
|
|
||||||
ListNode {
|
|
||||||
name: "Build Prerequisites",
|
|
||||||
command: Command::LocalFile("system-setup/1-compile-setup.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Gaming Dependencies",
|
|
||||||
command: Command::LocalFile("system-setup/2-gaming-setup.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Global Theme",
|
|
||||||
command: Command::LocalFile("system-setup/3-global-theme.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Remove Snaps",
|
|
||||||
command: Command::LocalFile("system-setup/4-remove-snaps.sh"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Utilities",
|
|
||||||
command: Command::None
|
|
||||||
} => {
|
|
||||||
ListNode {
|
|
||||||
name: "Wifi Manager",
|
|
||||||
command: Command::LocalFile("utils/wifi-control.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Bluetooth Manager",
|
|
||||||
command: Command::LocalFile("utils/bluetooth-control.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "MonitorControl(xorg)",
|
|
||||||
command: Command::None,
|
|
||||||
} => {
|
|
||||||
ListNode {
|
|
||||||
name: "Set Resolution",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/set_resolutions.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Duplicate Displays",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/duplicate_displays.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Extend Displays",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/extend_displays.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Auto Detect Displays",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/auto_detect_displays.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Enable Monitor",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/enable_monitor.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Disable Monitor",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/disable_monitor.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Set Primary Monitor",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/set_primary_monitor.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Change Orientation",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/change_orientation.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Manage Arrangement",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/manage_arrangement.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Scale Monitors",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/scale_monitor.sh"),
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Reset Scaling",
|
|
||||||
command: Command::LocalFile("utils/monitor-control/reset_scaling.sh"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ListNode {
|
|
||||||
name: "Full System Update",
|
|
||||||
command: Command::LocalFile("system-update.sh"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// We don't get a reference, but rather an id, because references are siginficantly more
|
|
||||||
// paintfull to manage
|
|
||||||
let root_id = tree.root().id();
|
|
||||||
Self {
|
|
||||||
inner_tree: tree,
|
|
||||||
visit_stack: vec![root_id],
|
|
||||||
list_state: ListState::default().with_selected(Some(0)),
|
|
||||||
filter_query: String::new(),
|
|
||||||
filtered_items: vec![],
|
|
||||||
preview_float: Float::new(80, 80),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw our custom widget to the frame
|
|
||||||
pub fn draw(&mut self, frame: &mut Frame, area: Rect, state: &AppState) {
|
|
||||||
let item_list: Vec<Line> = if self.filter_query.is_empty() {
|
|
||||||
let mut items: Vec<Line> = vec![];
|
|
||||||
// If we are not at the root of our filesystem tree, we need to add `..` path, to be able
|
|
||||||
// to go up the tree
|
|
||||||
// icons:
|
|
||||||
if !self.at_root() {
|
|
||||||
items.push(
|
|
||||||
Line::from(format!("{} ..", state.theme.dir_icon))
|
|
||||||
.style(state.theme.dir_color),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the last element in the `visit_stack` vec
|
|
||||||
let curr = self
|
|
||||||
.inner_tree
|
|
||||||
.get(*self.visit_stack.last().unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Iterate through all the children
|
|
||||||
for node in curr.children() {
|
|
||||||
// The difference between a "directory" and a "command" is simple: if it has children,
|
|
||||||
// it's a directory and will be handled as such
|
|
||||||
if node.has_children() {
|
|
||||||
items.push(
|
|
||||||
Line::from(format!("{} {}", state.theme.dir_icon, node.value().name))
|
|
||||||
.style(state.theme.dir_color),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
items.push(
|
|
||||||
Line::from(format!("{} {}", state.theme.cmd_icon, node.value().name))
|
|
||||||
.style(state.theme.cmd_color),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items
|
|
||||||
} else {
|
|
||||||
self.filtered_items
|
|
||||||
.iter()
|
|
||||||
.map(|node| {
|
|
||||||
Line::from(format!("{} {}", state.theme.cmd_icon, node.name))
|
|
||||||
.style(state.theme.cmd_color)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
// create the normal list widget containing only item in our "working directory" / tree
|
|
||||||
// node
|
|
||||||
let list = List::new(item_list)
|
|
||||||
.highlight_style(Style::default().reversed())
|
|
||||||
.block(Block::default().borders(Borders::ALL).title(format!(
|
|
||||||
"Linux Toolbox - {}",
|
|
||||||
chrono::Local::now().format("%Y-%m-%d")
|
|
||||||
)))
|
|
||||||
.scroll_padding(1);
|
|
||||||
|
|
||||||
// Render it
|
|
||||||
frame.render_stateful_widget(list, area, &mut self.list_state);
|
|
||||||
|
|
||||||
//Render the preview window
|
|
||||||
self.preview_float.draw(frame, area);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn filter(&mut self, query: String) {
|
|
||||||
self.filter_query.clone_from(&query);
|
|
||||||
self.filtered_items.clear();
|
|
||||||
|
|
||||||
let query_lower = query.to_lowercase();
|
|
||||||
|
|
||||||
let mut stack = vec![self.inner_tree.root().id()];
|
|
||||||
|
|
||||||
while let Some(node_id) = stack.pop() {
|
|
||||||
let node = self.inner_tree.get(node_id).unwrap();
|
|
||||||
|
|
||||||
if node.value().name.to_lowercase().contains(&query_lower) && !node.has_children() {
|
|
||||||
self.filtered_items.push(node.value().clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
for child in node.children() {
|
|
||||||
stack.push(child.id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.filtered_items.sort_by(|a, b| a.name.cmp(b.name));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the selection to the first item
|
|
||||||
pub fn reset_selection(&mut self) {
|
|
||||||
if !self.filtered_items.is_empty() {
|
|
||||||
self.list_state.select(Some(0));
|
|
||||||
} else {
|
|
||||||
self.list_state.select(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle key events, we are only interested in `Press` and `Repeat` events
|
|
||||||
pub fn handle_key(&mut self, event: KeyEvent, state: &AppState) -> Option<Command> {
|
|
||||||
if event.kind == KeyEventKind::Release {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.preview_float.handle_key_event(&event) {
|
|
||||||
return None; // If the key event was handled by the preview, don't propagate it further
|
|
||||||
}
|
|
||||||
|
|
||||||
match event.code {
|
|
||||||
// Damm you Up arrow, use vim lol
|
|
||||||
KeyCode::Char('j') | KeyCode::Down => {
|
|
||||||
self.list_state.select_next();
|
|
||||||
None
|
|
||||||
}
|
|
||||||
KeyCode::Char('k') | KeyCode::Up => {
|
|
||||||
self.list_state.select_previous();
|
|
||||||
None
|
|
||||||
}
|
|
||||||
KeyCode::Char('p') => {
|
|
||||||
self.toggle_preview_window(state);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyCode::Enter | KeyCode::Right | KeyCode::Char('l') => {
|
|
||||||
if self.preview_float.get_content().is_none() {
|
|
||||||
self.handle_enter()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Left | KeyCode::Char('h') if !self.at_root() => self.enter_parent_directory(),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_selected_command(&self) -> Option<Command> {
|
|
||||||
let selected_index = self.list_state.selected().unwrap_or(0);
|
|
||||||
|
|
||||||
if self.filter_query.is_empty() {
|
|
||||||
// No filter query, use the regular tree navigation
|
|
||||||
let curr = self
|
|
||||||
.inner_tree
|
|
||||||
.get(*self.visit_stack.last().unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if !self.at_root() && selected_index == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut actual_index = selected_index;
|
|
||||||
if !self.at_root() {
|
|
||||||
actual_index -= 1; // Adjust for the ".." item if not at root
|
|
||||||
}
|
|
||||||
|
|
||||||
for (idx, node) in curr.children().enumerate() {
|
|
||||||
if idx == actual_index {
|
|
||||||
return Some(node.value().command.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Filter query is active, use the filtered items
|
|
||||||
if let Some(filtered_node) = self.filtered_items.get(selected_index) {
|
|
||||||
return Some(filtered_node.command.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enter_parent_directory(&mut self) -> Option<Command> {
|
|
||||||
self.visit_stack.pop();
|
|
||||||
self.list_state.select(Some(0));
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles the <Enter> key. This key can do 3 things:
|
|
||||||
/// - Run a command, if it is the currently selected item,
|
|
||||||
/// - Go up a directory
|
|
||||||
/// - Go down into a directory
|
|
||||||
///
|
|
||||||
/// Returns `Some(command)` when command is selected, othervise we returns `None`
|
|
||||||
fn handle_enter(&mut self) -> Option<Command> {
|
|
||||||
let selected_index = self.list_state.selected().unwrap_or(0);
|
|
||||||
|
|
||||||
if self.filter_query.is_empty() {
|
|
||||||
// No filter query, use the regular tree navigation
|
|
||||||
let curr = self
|
|
||||||
.inner_tree
|
|
||||||
.get(*self.visit_stack.last().unwrap())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if !self.at_root() && selected_index == 0 {
|
|
||||||
return self.enter_parent_directory();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut actual_index = selected_index;
|
|
||||||
if !self.at_root() {
|
|
||||||
actual_index -= 1; // Adjust for the ".." item if not at root
|
|
||||||
}
|
|
||||||
|
|
||||||
for (idx, node) in curr.children().enumerate() {
|
|
||||||
if idx == actual_index {
|
|
||||||
if node.has_children() {
|
|
||||||
self.visit_stack.push(node.id());
|
|
||||||
self.list_state.select(Some(0));
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
return Some(node.value().command.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Filter query is active, use the filtered items
|
|
||||||
if let Some(filtered_node) = self.filtered_items.get(selected_index) {
|
|
||||||
return Some(filtered_node.command.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_preview_window(&mut self, state: &AppState) {
|
|
||||||
if self.preview_float.get_content().is_some() {
|
|
||||||
// If the preview window is active, disable it
|
|
||||||
self.preview_float.set_content(None);
|
|
||||||
} else {
|
|
||||||
// If the preview window is not active, show it
|
|
||||||
|
|
||||||
// Get the selected command
|
|
||||||
if let Some(selected_command) = self.get_selected_command() {
|
|
||||||
let lines = match selected_command {
|
|
||||||
Command::Raw(cmd) => cmd.lines().map(|line| line.to_string()).collect(),
|
|
||||||
Command::LocalFile(file_path) => {
|
|
||||||
if file_path.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut full_path = state.temp_path.clone();
|
|
||||||
full_path.push(file_path);
|
|
||||||
let file_contents = std::fs::read_to_string(&full_path)
|
|
||||||
.map_err(|_| format!("File not found: {:?}", &full_path))
|
|
||||||
.unwrap();
|
|
||||||
file_contents.lines().map(|line| line.to_string()).collect()
|
|
||||||
}
|
|
||||||
Command::None => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.preview_float
|
|
||||||
.set_content(Some(FloatingText::new(lines)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks weather the current tree node is the root node (can we go up the tree or no)
|
|
||||||
/// Returns `true` if we can't go up the tree (we are at the tree root)
|
|
||||||
/// else returns `false`
|
|
||||||
fn at_root(&self) -> bool {
|
|
||||||
self.visit_stack.len() == 1
|
|
||||||
}
|
|
||||||
}
|
|
111
src/main.rs
111
src/main.rs
|
@ -1,8 +1,8 @@
|
||||||
mod float;
|
mod float;
|
||||||
mod floating_text;
|
mod floating_text;
|
||||||
mod list;
|
|
||||||
mod running_command;
|
mod running_command;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
mod tabs;
|
||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -13,23 +13,16 @@ use std::{
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor::RestorePosition,
|
cursor::RestorePosition,
|
||||||
event::{self, DisableMouseCapture, Event, KeyCode, KeyEventKind},
|
event::{self, DisableMouseCapture, Event, KeyEventKind},
|
||||||
style::ResetColor,
|
style::ResetColor,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
ExecutableCommand,
|
ExecutableCommand,
|
||||||
};
|
};
|
||||||
use float::Float;
|
|
||||||
use include_dir::include_dir;
|
use include_dir::include_dir;
|
||||||
use list::CustomList;
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
backend::{Backend, CrosstermBackend},
|
||||||
layout::{Constraint, Direction, Layout},
|
|
||||||
style::{Color, Style},
|
|
||||||
text::Span,
|
|
||||||
widgets::{Block, Borders, Paragraph},
|
|
||||||
Terminal,
|
Terminal,
|
||||||
};
|
};
|
||||||
use running_command::RunningCommand;
|
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
use theme::THEMES;
|
use theme::THEMES;
|
||||||
|
@ -56,17 +49,14 @@ fn main() -> std::io::Result<()> {
|
||||||
.extract(temp_dir.path())
|
.extract(temp_dir.path())
|
||||||
.expect("Failed to extract the saved directory");
|
.expect("Failed to extract the saved directory");
|
||||||
|
|
||||||
let state = AppState {
|
let mut state = AppState::new(theme, temp_dir.path().to_owned());
|
||||||
theme,
|
|
||||||
temp_path: temp_dir.path().to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
stdout().execute(EnterAlternateScreen)?;
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||||
terminal.clear()?;
|
terminal.clear()?;
|
||||||
|
|
||||||
run(&mut terminal, &state)?;
|
run(&mut terminal, &mut state)?;
|
||||||
|
|
||||||
// restore terminal
|
// restore terminal
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
|
@ -78,55 +68,9 @@ fn main() -> std::io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run<B: Backend>(terminal: &mut Terminal<B>, state: &AppState) -> io::Result<()> {
|
fn run<B: Backend>(terminal: &mut Terminal<B>, state: &mut AppState) -> io::Result<()> {
|
||||||
//Create the search field
|
|
||||||
let mut search_input = String::new();
|
|
||||||
//Create the command list
|
|
||||||
let mut custom_list = CustomList::new();
|
|
||||||
//Create the float to hold command output
|
|
||||||
let mut command_float = Float::new(60, 60);
|
|
||||||
let mut in_search_mode = false;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Always redraw
|
terminal.draw(|frame| state.draw(frame)).unwrap();
|
||||||
terminal
|
|
||||||
.draw(|frame| {
|
|
||||||
//Split the terminal into 2 vertical chunks
|
|
||||||
//One for the search bar and one for the command list
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
|
|
||||||
.split(frame.size());
|
|
||||||
|
|
||||||
//Set the search bar text (If empty use the placeholder)
|
|
||||||
let display_text = if search_input.is_empty() {
|
|
||||||
if in_search_mode {
|
|
||||||
Span::raw("")
|
|
||||||
} else {
|
|
||||||
Span::raw("Press / to search")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Span::raw(&search_input)
|
|
||||||
};
|
|
||||||
|
|
||||||
//Create the search bar widget
|
|
||||||
let mut search_bar = Paragraph::new(display_text)
|
|
||||||
.block(Block::default().borders(Borders::ALL).title("Search"))
|
|
||||||
.style(Style::default().fg(Color::DarkGray));
|
|
||||||
|
|
||||||
//Change the color if in search mode
|
|
||||||
if in_search_mode {
|
|
||||||
search_bar = search_bar.clone().style(Style::default().fg(Color::Blue));
|
|
||||||
}
|
|
||||||
|
|
||||||
//Render the search bar (First chunk of the screen)
|
|
||||||
frame.render_widget(search_bar, chunks[0]);
|
|
||||||
//Render the command list (Second chunk of the screen)
|
|
||||||
custom_list.draw(frame, chunks[1], state);
|
|
||||||
//Render the command float in the custom_list chunk
|
|
||||||
command_float.draw(frame, chunks[1]);
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Wait for an event
|
// Wait for an event
|
||||||
if !event::poll(Duration::from_millis(10))? {
|
if !event::poll(Duration::from_millis(10))? {
|
||||||
|
@ -141,47 +85,8 @@ fn run<B: Backend>(terminal: &mut Terminal<B>, state: &AppState) -> io::Result<(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Send the key to the float
|
if !state.handle_key(&key) {
|
||||||
//If we receive true, then the float processed the input
|
return Ok(());
|
||||||
//If that's the case, don't propagate input to other widgets
|
|
||||||
if !command_float.handle_key_event(&key) {
|
|
||||||
//Insert user input into the search bar
|
|
||||||
if in_search_mode {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
search_input.push(c);
|
|
||||||
custom_list.filter(search_input.clone());
|
|
||||||
}
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
search_input.pop();
|
|
||||||
custom_list.filter(search_input.clone());
|
|
||||||
}
|
|
||||||
KeyCode::Esc => {
|
|
||||||
search_input = String::new();
|
|
||||||
custom_list.filter(search_input.clone());
|
|
||||||
in_search_mode = false
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
in_search_mode = false;
|
|
||||||
custom_list.reset_selection();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else if let Some(cmd) = custom_list.handle_key(key, state) {
|
|
||||||
command_float.set_content(Some(RunningCommand::new(cmd, state)));
|
|
||||||
} else {
|
|
||||||
// Handle keys while not in search mode
|
|
||||||
match key.code {
|
|
||||||
// Exit the program
|
|
||||||
KeyCode::Char('q') => return Ok(()),
|
|
||||||
//Activate search mode if the forward slash key gets pressed
|
|
||||||
KeyCode::Char('/') => {
|
|
||||||
in_search_mode = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{float::FloatContent, state::AppState};
|
use crate::float::FloatContent;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use oneshot::{channel, Receiver};
|
use oneshot::{channel, Receiver};
|
||||||
use portable_pty::{
|
use portable_pty::{
|
||||||
|
@ -13,6 +13,7 @@ use ratatui::{
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
io::Write,
|
io::Write,
|
||||||
|
path::Path,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
};
|
};
|
||||||
|
@ -125,7 +126,7 @@ impl FloatContent for RunningCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunningCommand {
|
impl RunningCommand {
|
||||||
pub fn new(command: Command, state: &AppState) -> Self {
|
pub fn new(command: Command, temp_path: &Path) -> 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
|
||||||
|
@ -141,7 +142,7 @@ impl RunningCommand {
|
||||||
Command::None => panic!("Command::None was treated as a command"),
|
Command::None => panic!("Command::None was treated as a command"),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.cwd(&state.temp_path);
|
cmd.cwd(temp_path);
|
||||||
|
|
||||||
// Open a pseudo-terminal with initial size
|
// Open a pseudo-terminal with initial size
|
||||||
let pair = pty_system
|
let pair = pty_system
|
||||||
|
|
81
src/search.rs
Normal file
81
src/search.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use ratatui::{
|
||||||
|
layout::Rect,
|
||||||
|
style::Style,
|
||||||
|
text::Span,
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
pub struct SearchBar {
|
||||||
|
search_input: String,
|
||||||
|
in_search_mode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchBar {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
SearchBar {
|
||||||
|
search_input: String::new(),
|
||||||
|
in_search_mode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_search(&mut self) {
|
||||||
|
self.in_search_mode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deactivate_search(&mut self) {
|
||||||
|
self.in_search_mode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_search_active(&self) -> bool {
|
||||||
|
self.in_search_mode
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&self, frame: &mut Frame, area: Rect, state: &AppState) {
|
||||||
|
//Set the search bar text (If empty use the placeholder)
|
||||||
|
let display_text = if !self.in_search_mode && self.search_input.is_empty() {
|
||||||
|
Span::raw("Press / to search")
|
||||||
|
} else {
|
||||||
|
Span::raw(&self.search_input)
|
||||||
|
};
|
||||||
|
|
||||||
|
//Create the search bar widget
|
||||||
|
let mut search_bar = Paragraph::new(display_text)
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("Search"))
|
||||||
|
.style(Style::default().fg(state.theme.unfocused_color));
|
||||||
|
|
||||||
|
//Change the color if in search mode
|
||||||
|
if self.in_search_mode {
|
||||||
|
search_bar = search_bar
|
||||||
|
.clone()
|
||||||
|
.style(Style::default().fg(state.theme.focused_color));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Render the search bar (First chunk of the screen)
|
||||||
|
frame.render_widget(search_bar, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_key(&mut self, event: KeyEvent) -> String {
|
||||||
|
//Insert user input into the search bar
|
||||||
|
match event.code {
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
self.search_input.push(c);
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
self.search_input.pop();
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.search_input = String::new();
|
||||||
|
self.in_search_mode = false;
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
self.in_search_mode = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.search_input.clone()
|
||||||
|
}
|
||||||
|
}
|
321
src/state.rs
321
src/state.rs
|
@ -1,9 +1,326 @@
|
||||||
use crate::theme::Theme;
|
use crate::{
|
||||||
|
float::{Float, FloatContent},
|
||||||
|
floating_text::FloatingText,
|
||||||
|
running_command::{Command, RunningCommand},
|
||||||
|
tabs::{ListNode, TABS},
|
||||||
|
theme::Theme,
|
||||||
|
};
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||||
|
use ego_tree::NodeId;
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Constraint, Direction, Layout},
|
||||||
|
style::{Color, Style, Stylize},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Borders, List, ListState, Paragraph},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
/// Selected theme
|
/// Selected theme
|
||||||
pub theme: Theme,
|
pub theme: Theme,
|
||||||
/// Path to the root of the unpacked files in /tmp
|
/// Path to the root of the unpacked files in /tmp
|
||||||
pub temp_path: PathBuf,
|
temp_path: PathBuf,
|
||||||
|
/// Currently focused area
|
||||||
|
focus: Focus,
|
||||||
|
/// Current tab
|
||||||
|
current_tab: ListState,
|
||||||
|
/// Current search query
|
||||||
|
search_query: String,
|
||||||
|
/// Current items
|
||||||
|
items: Vec<ListEntry>,
|
||||||
|
/// This stack keeps track of our "current dirrectory". You can think of it as `pwd`. but not
|
||||||
|
/// just the current directory, all paths that took us here, so we can "cd .."
|
||||||
|
visit_stack: Vec<NodeId>,
|
||||||
|
/// This is the state asociated with the list widget, used to display the selection in the
|
||||||
|
/// widget
|
||||||
|
selection: ListState,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Focus {
|
||||||
|
Search,
|
||||||
|
TabList,
|
||||||
|
List,
|
||||||
|
FloatingWindow(Float),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ListEntry {
|
||||||
|
node: ListNode,
|
||||||
|
id: NodeId,
|
||||||
|
has_children: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(theme: Theme, temp_path: PathBuf) -> Self {
|
||||||
|
let root_id = TABS[0].tree.root().id();
|
||||||
|
let mut state = Self {
|
||||||
|
theme,
|
||||||
|
temp_path,
|
||||||
|
focus: Focus::List,
|
||||||
|
current_tab: ListState::default().with_selected(Some(0)),
|
||||||
|
search_query: String::new(),
|
||||||
|
items: vec![],
|
||||||
|
visit_stack: vec![root_id],
|
||||||
|
selection: ListState::default().with_selected(Some(0)),
|
||||||
|
};
|
||||||
|
state.update_items();
|
||||||
|
state
|
||||||
|
}
|
||||||
|
pub fn draw(&mut self, frame: &mut Frame) {
|
||||||
|
let longest_tab_display_len = TABS
|
||||||
|
.iter()
|
||||||
|
.map(|tab| tab.name.len() + self.theme.tab_icon.len())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let horizontal = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Min(longest_tab_display_len as u16 + 5),
|
||||||
|
Constraint::Percentage(100),
|
||||||
|
])
|
||||||
|
.split(frame.size());
|
||||||
|
let left_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(1)])
|
||||||
|
.split(horizontal[0]);
|
||||||
|
|
||||||
|
let tabs = TABS.iter().map(|tab| tab.name).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let tab_hl_style = if let Focus::TabList = self.focus {
|
||||||
|
Style::default().reversed().fg(self.theme.tab_color)
|
||||||
|
} else {
|
||||||
|
Style::new().fg(self.theme.tab_color)
|
||||||
|
};
|
||||||
|
|
||||||
|
let list = List::new(tabs)
|
||||||
|
.block(Block::default().borders(Borders::ALL))
|
||||||
|
.highlight_style(tab_hl_style)
|
||||||
|
.highlight_symbol(self.theme.tab_icon);
|
||||||
|
frame.render_stateful_widget(list, left_chunks[1], &mut self.current_tab);
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
|
||||||
|
.split(horizontal[1]);
|
||||||
|
|
||||||
|
// Render search bar
|
||||||
|
let search_text = match self.focus {
|
||||||
|
Focus::Search => Span::raw(&self.search_query),
|
||||||
|
_ if !self.search_query.is_empty() => Span::raw(&self.search_query),
|
||||||
|
_ => Span::raw("Press / to search"),
|
||||||
|
};
|
||||||
|
let search_bar = Paragraph::new(search_text)
|
||||||
|
.block(Block::default().borders(Borders::ALL))
|
||||||
|
.style(Style::default().fg(if let Focus::Search = self.focus {
|
||||||
|
Color::Blue
|
||||||
|
} else {
|
||||||
|
Color::DarkGray
|
||||||
|
}));
|
||||||
|
frame.render_widget(search_bar, chunks[0]);
|
||||||
|
|
||||||
|
let mut items: Vec<Line> = Vec::new();
|
||||||
|
if !self.at_root() {
|
||||||
|
items.push(
|
||||||
|
Line::from(format!("{} ..", self.theme.dir_icon)).style(self.theme.dir_color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.extend(self.items.iter().map(
|
||||||
|
|ListEntry {
|
||||||
|
node, has_children, ..
|
||||||
|
}| {
|
||||||
|
if *has_children {
|
||||||
|
Line::from(format!("{} {}", self.theme.dir_icon, node.name))
|
||||||
|
.style(self.theme.dir_color)
|
||||||
|
} else {
|
||||||
|
Line::from(format!("{} {}", self.theme.cmd_icon, node.name))
|
||||||
|
.style(self.theme.cmd_color)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
// Create the list widget with items
|
||||||
|
let list = List::new(items)
|
||||||
|
.highlight_style(if let Focus::List = self.focus {
|
||||||
|
Style::default().reversed()
|
||||||
|
} else {
|
||||||
|
Style::new()
|
||||||
|
})
|
||||||
|
.block(Block::default().borders(Borders::ALL).title(format!(
|
||||||
|
"Linux Toolbox - {}",
|
||||||
|
chrono::Local::now().format("%Y-%m-%d")
|
||||||
|
)))
|
||||||
|
.scroll_padding(1);
|
||||||
|
frame.render_stateful_widget(list, chunks[1], &mut self.selection);
|
||||||
|
|
||||||
|
if let Focus::FloatingWindow(float) = &mut self.focus {
|
||||||
|
float.draw(frame, chunks[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn handle_key(&mut self, key: &KeyEvent) -> bool {
|
||||||
|
match &mut self.focus {
|
||||||
|
Focus::FloatingWindow(command) => {
|
||||||
|
if command.handle_key_event(key) {
|
||||||
|
self.focus = Focus::List;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Focus::Search => {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char(c) => self.search_query.push(c),
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
self.search_query.pop();
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.search_query = String::new();
|
||||||
|
self.exit_search();
|
||||||
|
}
|
||||||
|
KeyCode::Enter => self.exit_search(),
|
||||||
|
_ => return true,
|
||||||
|
}
|
||||||
|
self.update_items();
|
||||||
|
}
|
||||||
|
_ if key.code == KeyCode::Char('q') => return false,
|
||||||
|
Focus::TabList => match key.code {
|
||||||
|
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => {
|
||||||
|
self.focus = Focus::List
|
||||||
|
}
|
||||||
|
KeyCode::Char('j') | KeyCode::Down
|
||||||
|
if self.current_tab.selected().unwrap() + 1 < TABS.len() =>
|
||||||
|
{
|
||||||
|
self.current_tab.select_next();
|
||||||
|
self.refresh_tab();
|
||||||
|
}
|
||||||
|
KeyCode::Char('k') | KeyCode::Up => {
|
||||||
|
self.current_tab.select_previous();
|
||||||
|
self.refresh_tab();
|
||||||
|
}
|
||||||
|
KeyCode::Char('/') => self.enter_search(),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Focus::List if key.kind != KeyEventKind::Release => match key.code {
|
||||||
|
KeyCode::Char('j') | KeyCode::Down => self.selection.select_next(),
|
||||||
|
KeyCode::Char('k') | KeyCode::Up => self.selection.select_previous(),
|
||||||
|
KeyCode::Char('p') => self.enable_preview(),
|
||||||
|
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right => self.handle_enter(),
|
||||||
|
KeyCode::Char('h') | KeyCode::Left => {
|
||||||
|
if self.at_root() {
|
||||||
|
self.focus = Focus::TabList;
|
||||||
|
} else {
|
||||||
|
self.enter_parent_directory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Char('/') => self.enter_search(),
|
||||||
|
KeyCode::Tab => self.focus = Focus::TabList,
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
true
|
||||||
|
}
|
||||||
|
pub fn update_items(&mut self) {
|
||||||
|
if self.search_query.is_empty() {
|
||||||
|
let curr = TABS[self.current_tab.selected().unwrap()]
|
||||||
|
.tree
|
||||||
|
.get(*self.visit_stack.last().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.items = curr
|
||||||
|
.children()
|
||||||
|
.map(|node| ListEntry {
|
||||||
|
node: node.value().clone(),
|
||||||
|
id: node.id(),
|
||||||
|
has_children: node.has_children(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
} else {
|
||||||
|
self.items.clear();
|
||||||
|
|
||||||
|
let query_lower = self.search_query.to_lowercase();
|
||||||
|
for tab in TABS.iter() {
|
||||||
|
let mut stack = vec![tab.tree.root().id()];
|
||||||
|
while let Some(node_id) = stack.pop() {
|
||||||
|
let node = tab.tree.get(node_id).unwrap();
|
||||||
|
|
||||||
|
if node.value().name.to_lowercase().contains(&query_lower)
|
||||||
|
&& !node.has_children()
|
||||||
|
{
|
||||||
|
self.items.push(ListEntry {
|
||||||
|
node: node.value().clone(),
|
||||||
|
id: node.id(),
|
||||||
|
has_children: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.extend(node.children().map(|child| child.id()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.items.sort_by(|a, b| a.node.name.cmp(b.node.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Checks ehther the current tree node is the root node (can we go up the tree or no)
|
||||||
|
/// Returns `true` if we can't go up the tree (we are at the tree root)
|
||||||
|
/// else returns `false`
|
||||||
|
fn at_root(&self) -> bool {
|
||||||
|
self.visit_stack.len() == 1
|
||||||
|
}
|
||||||
|
fn enter_parent_directory(&mut self) {
|
||||||
|
self.visit_stack.pop();
|
||||||
|
self.selection.select(Some(0));
|
||||||
|
self.update_items();
|
||||||
|
}
|
||||||
|
fn get_selected_command(&mut self, change_directory: bool) -> Option<Command> {
|
||||||
|
let mut selected_index = self.selection.selected().unwrap_or(0);
|
||||||
|
|
||||||
|
if !self.at_root() && selected_index == 0 {
|
||||||
|
if change_directory {
|
||||||
|
self.enter_parent_directory();
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !self.at_root() {
|
||||||
|
selected_index = selected_index.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(item) = self.items.get(selected_index) {
|
||||||
|
if !item.has_children {
|
||||||
|
return Some(item.node.command.clone());
|
||||||
|
} else if change_directory {
|
||||||
|
self.visit_stack.push(item.id);
|
||||||
|
self.selection.select(Some(0));
|
||||||
|
self.update_items();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn enable_preview(&mut self) {
|
||||||
|
if let Some(command) = self.get_selected_command(false) {
|
||||||
|
if let Some(preview) = FloatingText::from_command(&command, self.temp_path.clone()) {
|
||||||
|
self.spawn_float(preview, 80, 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn handle_enter(&mut self) {
|
||||||
|
if let Some(cmd) = self.get_selected_command(true) {
|
||||||
|
let command = RunningCommand::new(cmd, &self.temp_path);
|
||||||
|
self.spawn_float(command, 80, 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn spawn_float<T: FloatContent + 'static>(&mut self, float: T, width: u16, height: u16) {
|
||||||
|
self.focus = Focus::FloatingWindow(Float::new(Box::new(float), width, height));
|
||||||
|
}
|
||||||
|
fn enter_search(&mut self) {
|
||||||
|
self.focus = Focus::Search;
|
||||||
|
self.selection.select(None);
|
||||||
|
}
|
||||||
|
fn exit_search(&mut self) {
|
||||||
|
self.selection.select(Some(0));
|
||||||
|
self.focus = Focus::List;
|
||||||
|
self.update_items();
|
||||||
|
}
|
||||||
|
fn refresh_tab(&mut self) {
|
||||||
|
self.visit_stack = vec![TABS[self.current_tab.selected().unwrap()].tree.root().id()];
|
||||||
|
self.selection.select(Some(0));
|
||||||
|
self.update_items();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
175
src/tabs.rs
Normal file
175
src/tabs.rs
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use ego_tree::{tree, Tree};
|
||||||
|
|
||||||
|
use crate::running_command::Command;
|
||||||
|
|
||||||
|
pub struct Tab {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub tree: Tree<ListNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ListNode {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static TABS: LazyLock<Vec<Tab>> = LazyLock::new(|| {
|
||||||
|
vec![
|
||||||
|
Tab {
|
||||||
|
name: "System Setup",
|
||||||
|
tree: tree!(ListNode {
|
||||||
|
name: "root",
|
||||||
|
command: Command::None,
|
||||||
|
} => {
|
||||||
|
ListNode {
|
||||||
|
name: "Arch Linux",
|
||||||
|
command: Command::None,
|
||||||
|
} => {
|
||||||
|
ListNode {
|
||||||
|
name: "Yay AUR Helper",
|
||||||
|
command: Command::LocalFile("system-setup/arch/yay-setup.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Paru AUR Helper",
|
||||||
|
command: Command::LocalFile("system-setup/arch/paru-setup.sh"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Full System Update",
|
||||||
|
command: Command::LocalFile("system-update.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Build Prerequisites",
|
||||||
|
command: Command::LocalFile("system-setup/1-compile-setup.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Gaming Dependencies",
|
||||||
|
command: Command::LocalFile("system-setup/2-gaming-setup.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Global Theme",
|
||||||
|
command: Command::LocalFile("system-setup/3-global-theme.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Remove Snaps",
|
||||||
|
command: Command::LocalFile("system-setup/4-remove-snaps.sh"),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Tab {
|
||||||
|
name: "Applications Setup",
|
||||||
|
tree: tree!(ListNode {
|
||||||
|
name: "root",
|
||||||
|
command: Command::None,
|
||||||
|
} => {
|
||||||
|
ListNode {
|
||||||
|
name: "Alacritty",
|
||||||
|
command: Command::LocalFile("applications-setup/alacritty-setup.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Bash Prompt",
|
||||||
|
command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\""),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "DWM-Titus",
|
||||||
|
command: Command::LocalFile("applications-setup/dwmtitus-setup.sh")
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Kitty",
|
||||||
|
command: Command::LocalFile("applications-setup/kitty-setup.sh")
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Neovim",
|
||||||
|
command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/neovim/main/setup.sh)\""),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Rofi",
|
||||||
|
command: Command::LocalFile("applications-setup/rofi-setup.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "ZSH Prompt",
|
||||||
|
command: Command::LocalFile("applications-setup/zsh-setup.sh"),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Tab {
|
||||||
|
name: "Security",
|
||||||
|
tree: tree!(ListNode {
|
||||||
|
name: "root",
|
||||||
|
command: Command::None,
|
||||||
|
} => {
|
||||||
|
ListNode {
|
||||||
|
name: "Firewall Baselines (CTT)",
|
||||||
|
command: Command::LocalFile("security/firewall-baselines.sh"),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
Tab {
|
||||||
|
name: "Utilities",
|
||||||
|
tree: tree!(ListNode {
|
||||||
|
name: "root",
|
||||||
|
command: Command::None,
|
||||||
|
} => {
|
||||||
|
ListNode {
|
||||||
|
name: "Wifi Manager",
|
||||||
|
command: Command::LocalFile("utils/wifi-control.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Bluetooth Manager",
|
||||||
|
command: Command::LocalFile("utils/bluetooth-control.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "MonitorControl(xorg)",
|
||||||
|
command: Command::None,
|
||||||
|
} => {
|
||||||
|
ListNode {
|
||||||
|
name: "Set Resolution",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/set_resolutions.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Duplicate Displays",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/duplicate_displays.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Extend Displays",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/extend_displays.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Auto Detect Displays",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/auto_detect_displays.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Enable Monitor",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/enable_monitor.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Disable Monitor",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/disable_monitor.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Set Primary Monitor",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/set_primary_monitor.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Change Orientation",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/change_orientation.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Manage Arrangement",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/manage_arrangement.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Scale Monitors",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/scale_monitor.sh"),
|
||||||
|
},
|
||||||
|
ListNode {
|
||||||
|
name: "Reset Scaling",
|
||||||
|
command: Command::LocalFile("utils/monitor-control/reset_scaling.sh"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
12
src/theme.rs
12
src/theme.rs
|
@ -4,27 +4,39 @@ use ratatui::style::Color;
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub dir_color: Color,
|
pub dir_color: Color,
|
||||||
pub cmd_color: Color,
|
pub cmd_color: Color,
|
||||||
|
pub tab_color: Color,
|
||||||
pub dir_icon: &'static str,
|
pub dir_icon: &'static str,
|
||||||
pub cmd_icon: &'static str,
|
pub cmd_icon: &'static str,
|
||||||
|
pub tab_icon: &'static str,
|
||||||
pub success_color: Color,
|
pub success_color: Color,
|
||||||
pub fail_color: Color,
|
pub fail_color: Color,
|
||||||
|
pub focused_color: Color,
|
||||||
|
pub unfocused_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const THEMES: [Theme; 2] = [
|
pub const THEMES: [Theme; 2] = [
|
||||||
Theme {
|
Theme {
|
||||||
dir_color: Color::Blue,
|
dir_color: Color::Blue,
|
||||||
cmd_color: Color::LightGreen,
|
cmd_color: Color::LightGreen,
|
||||||
|
tab_color: Color::Yellow,
|
||||||
dir_icon: "[DIR]",
|
dir_icon: "[DIR]",
|
||||||
cmd_icon: "[CMD]",
|
cmd_icon: "[CMD]",
|
||||||
|
tab_icon: ">> ",
|
||||||
success_color: Color::Green,
|
success_color: Color::Green,
|
||||||
fail_color: Color::Red,
|
fail_color: Color::Red,
|
||||||
|
focused_color: Color::LightBlue,
|
||||||
|
unfocused_color: Color::Gray,
|
||||||
},
|
},
|
||||||
Theme {
|
Theme {
|
||||||
dir_color: Color::Blue,
|
dir_color: Color::Blue,
|
||||||
cmd_color: Color::Rgb(204, 224, 208),
|
cmd_color: Color::Rgb(204, 224, 208),
|
||||||
|
tab_color: Color::Rgb(255, 255, 85),
|
||||||
dir_icon: " ",
|
dir_icon: " ",
|
||||||
cmd_icon: " ",
|
cmd_icon: " ",
|
||||||
|
tab_icon: " ",
|
||||||
fail_color: Color::Rgb(199, 55, 44),
|
fail_color: Color::Rgb(199, 55, 44),
|
||||||
success_color: Color::Rgb(5, 255, 55),
|
success_color: Color::Rgb(5, 255, 55),
|
||||||
|
focused_color: Color::LightBlue,
|
||||||
|
unfocused_color: Color::Gray,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
Loading…
Reference in New Issue
Block a user