Merge branch 'ChrisTitusTech:main' into main

This commit is contained in:
JEEVITHA KANNAN K S 2024-08-16 11:38:22 +05:30 committed by GitHub
commit e1ccd4f9a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 785 additions and 633 deletions

56
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,56 @@
# Contributing Guidelines for Linutil
Thank you for considering contributing to Linutil! We appreciate your effort in helping improve this project. To ensure that your contributions align with the goals and quality standards of Linutil, please follow these guidelines:
## 1. Setting Up Your Development Environment
1. **Clone the repo**: Start by cloning the Linutil repository to your local machine.
```bash
git clone https://github.com/christitustech/linutil.git
cd linutil
```
2. **Install Rust**: Make sure you have Rust installed on your machine. If you don't, you can install it by following the instructions at [rust-lang.org](https://www.rust-lang.org/tools/install).
3. **Edit the files you want to change**: Make your changes to the relevant files.
4. **Test your changes**: Run `cargo run` to test your modifications in a local environment and ensure everything works as expected.
## 2. Make Meaningful Changes
- **Have a clear reason**: Dont change the way things are done without a valid reason. If you propose an alteration, be prepared to explain why its necessary and how it improves the project.
- **Respect existing conventions**: Changes should align with the existing code style, design patterns, and overall project philosophy. If you want to introduce a new way of doing things, justify it clearly.
## 3. Learn from Past Pull Requests (PRs)
- **Check merged PRs**: Reviewing merged pull requests can give you an idea of what kind of contributions are accepted and how they are implemented.
- **Study rejected PRs**: This is especially important as it helps you avoid making similar mistakes or proposing changes that have already been considered and declined.
## 4. Write Clean, Descriptive Commit Messages
- **Be descriptive**: Your commit messages should clearly describe what the change does and why it was made.
- **Use the imperative mood**: For example, "Add feature X" or "Fix bug in Y", rather than "Added feature X" or "Fixed bug in Y".
- **Keep commits clean**: Avoid committing a change and then immediately following it with a fix for that change. Instead, amend your commit or squash it if needed.
## 5. Keep Your Pull Requests (PRs) Small and Focused
- **Make small, targeted PRs**: Focus on one feature or fix per pull request. This makes it easier to review and increases the likelihood of acceptance.
- **Avoid combining unrelated changes**: PRs that tackle multiple unrelated issues are harder to review and might be rejected because of a single problem.
## 6. Code Review and Feedback
- **Expect feedback**: PRs will undergo code review. Be open to feedback and willing to make adjustments as needed.
- **Participate in reviews**: If you feel comfortable, review other contributors' PRs as well. Peer review is a great way to learn and ensure high-quality contributions.
## 7. Contributing Is More Than Just Code
- **Test the tool**: Running tests and providing feedback on how the tool works in different environments is a valuable contribution.
- **Write well-formed issues**: Clearly describe bugs or problems you encounter, providing as much detail as possible, including steps to reproduce the issue.
- **Propose reasonable feature requests**: When suggesting new features, ensure they fit within the scope, style, and design of the project. Provide clear reasoning and use cases.
## 8. Documentation
- **Update the documentation**: If your change affects the functionality, please update the relevant documentation files to reflect this.
## 9. License
- **Agree to the license**: By contributing to Linutil, you agree that your contributions will be licensed under the project's MIT license.
We appreciate your contributions and look forward to collaborating with you to make Linutil better!

View File

@ -5,27 +5,34 @@
![Preview](docs/assets/preview.png)
A distro-agnostic* toolbox which helps with everyday Linux tasks. It can help you set up applications and your system for specific use cases! Written with Rust 🦀
**Linutil** is a distro-agnostic toolbox designed to simplify everyday Linux tasks. It helps you set up applications and optimize your system for specific use cases. The utility is actively developed in Rust 🦀, providing performance and reliability.
\* — The project is in active development, so there could be some issues. Please consider [submitting feedback](https://github.com/ChrisTitusTech/linutil/issues).
*Note:* Since the project is still in active development, you may encounter some issues. Please consider [submitting feedback](https://github.com/ChrisTitusTech/linutil/issues) if you do.
## 💡 Usage
Open your terminal and paste this command:
To get started, open your terminal and run the following command:
```bash
curl -fsSL https://christitus.com/linux | sh
```
## 💖 Support
If you find Linutil helpful, please consider giving it a ⭐️ to show your support!
## 🎓 Documentation
### [LinUtil Official Documentation](https://christitustech.github.io/linutil/)
## 💖 Support
To morally and mentally support the project, make sure to leave a ⭐️!
For comprehensive information on how to use Linutil, visit the [Linutil Official Documentation](https://christitustech.github.io/linutil/).
## 🏅 Thanks to all Contributors
Thanks a lot for spending your time helping Linutil grow. Keep rocking 🍻.
## 🛠 Contributing
We welcome contributions from the community! Before you start, please review our [Contributing Guidelines](CONTRIBUTING.md) to understand how to make the most effective and efficient contributions.
## 🏅 Thanks to All Contributors
Thank you to everyone who has contributed to the development of Linutil. Your efforts are greatly appreciated, and youre helping make this tool better for everyone!
[![Contributors](https://contrib.rocks/image?repo=ChrisTitusTech/linutil)](https://github.com/ChrisTitusTech/linutil/graphs/contributors)
## Credits
Rust Shell written by [@JustLinuxUser](https://github.com/JustLinuxUser)
## 📜 Credits
Linutils Rust shell was developed by [@JustLinuxUser](https://github.com/JustLinuxUser).

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -1,3 +0,0 @@
# Update Log
#

View File

@ -5,7 +5,6 @@ nav:
- Introduction: 'index.md'
- User Guide: 'userguide.md'
- Contribute: 'contribute.md'
- Updates: 'updates.md'
- Known Issues: 'KnownIssues.md'
- FAQ: 'faq.md'

View File

@ -7,7 +7,7 @@ setupAlacritty() {
if ! command_exists alacritty; then
case ${PACKAGER} in
pacman)
sudo ${PACKAGER} -S --noconfirm alacritty
sudo ${PACKAGER} -S --needed --noconfirm alacritty
;;
*)
sudo ${PACKAGER} install -y alacritty

View File

@ -13,7 +13,7 @@ setupDWM() {
echo "Installing DWM-Titus if not already installed"
case "$PACKAGER" in # Install pre-Requisites
pacman)
sudo "$PACKAGER" -S --noconfirm --needed base-devel libx11 libxinerama libxft imlib2
sudo "$PACKAGER" -S --needed --noconfirm base-devel libx11 libxinerama libxft imlib2
;;
*)
sudo "$PACKAGER" install -y build-essential libx11-dev libxinerama-dev libxft-dev libimlib2-dev

View File

@ -7,7 +7,7 @@ setupKitty() {
if ! command_exists kitty; then
case ${PACKAGER} in
pacman)
sudo "${PACKAGER}" -S --noconfirm kitty
sudo "${PACKAGER}" -S --needed --noconfirm kitty
;;
*)
sudo "${PACKAGER}" install -y kitty

View File

@ -7,7 +7,7 @@ setupRofi() {
if ! command_exists rofi; then
case "$PACKAGER" in
pacman)
sudo "$PACKAGER" -S --noconfirm rofi
sudo "$PACKAGER" -S --needed --noconfirm rofi
;;
*)
sudo "$PACKAGER" install -y rofi

View File

@ -8,7 +8,7 @@ install_zsh() {
if ! command_exists zsh; then
case "$PACKAGER" in
pacman)
sudo "$PACKAGER" -Sy --noconfirm zsh
sudo "$PACKAGER" -S --needed --noconfirm zsh
;;
*)
sudo "$PACKAGER" install -y zsh

View File

@ -7,7 +7,7 @@ installPkg() {
if ! command_exists ufw; then
case ${PACKAGER} in
pacman)
sudo "${PACKAGER}" -Sy --noconfirm ufw
sudo "${PACKAGER}" -S --needed --noconfirm ufw
;;
*)
sudo "${PACKAGER}" install -y ufw

View File

@ -33,13 +33,13 @@ installDepend() {
if ! grep -q "^\s*\[multilib\]" /etc/pacman.conf; then
echo "[multilib]" | sudo tee -a /etc/pacman.conf
echo "Include = /etc/pacman.d/mirrorlist" | sudo tee -a /etc/pacman.conf
sudo "$PACKAGER" -Sy
sudo "$PACKAGER" -Syu
else
echo "Multilib is already enabled."
fi
if ! command_exists yay && ! command_exists paru; then
echo "Installing yay as AUR helper..."
sudo "$PACKAGER" --noconfirm -S base-devel
sudo "$PACKAGER" -S --needed --noconfirm base-devel
cd /opt && sudo git clone https://aur.archlinux.org/yay-git.git && sudo chown -R "$USER":"$USER" ./yay-git
cd yay-git && makepkg --noconfirm -si
else
@ -53,7 +53,7 @@ installDepend() {
echo "No AUR helper found. Please install yay or paru."
exit 1
fi
"$AUR_HELPER" --noconfirm -S "$DEPENDENCIES"
"$AUR_HELPER" -S --needed --noconfirm "$DEPENDENCIES"
;;
apt-get|nala)
COMPILEDEPS='build-essential'

View File

@ -9,13 +9,13 @@ installDepend() {
if ! grep -q "^\s*\[multilib\]" /etc/pacman.conf; then
echo "[multilib]" | sudo tee -a /etc/pacman.conf
echo "Include = /etc/pacman.d/mirrorlist" | sudo tee -a /etc/pacman.conf
sudo ${PACKAGER} -Sy
sudo ${PACKAGER} -Syu
else
echo "Multilib is already enabled."
fi
if ! command_exists yay && ! command_exists paru; then
echo "Installing yay as AUR helper..."
sudo ${PACKAGER} --noconfirm -S base-devel
sudo ${PACKAGER} -S --needed --noconfirm base-devel
cd /opt && sudo git clone https://aur.archlinux.org/yay-git.git && sudo chown -R ${USER}:${USER} ./yay-git
cd yay-git && makepkg --noconfirm -si
else
@ -29,7 +29,7 @@ installDepend() {
echo "No AUR helper found. Please install yay or paru."
exit 1
fi
${AUR_HELPER} --noconfirm -S wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls \
${AUR_HELPER} -S --needed --noconfirm wine giflib lib32-giflib libpng lib32-libpng libldap lib32-libldap gnutls lib32-gnutls \
mpg123 lib32-mpg123 openal lib32-openal v4l-utils lib32-v4l-utils libpulse lib32-libpulse libgpg-error \
lib32-libgpg-error alsa-plugins lib32-alsa-plugins alsa-lib lib32-alsa-lib libjpeg-turbo lib32-libjpeg-turbo \
sqlite lib32-sqlite libxcomposite lib32-libxcomposite libxinerama lib32-libgcrypt libgcrypt lib32-libxinerama \

View File

@ -29,8 +29,7 @@ install_theme_tools() {
sudo dnf install -y qt6ct kvantum
;;
pacman)
sudo pacman -Sy
sudo pacman --noconfirm -S qt6ct kvantum
sudo pacman -S --needed --noconfirm qt6ct kvantum
;;
*)
printf "${RED}Unsupported package manager. Please install qt6ct and kvantum manually.${RC}\n"

View 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" -S --needed --noconfirm 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

View 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" -S --needed --noconfirm 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

View File

@ -7,7 +7,7 @@ fastUpdate() {
pacman)
if ! command_exists yay && ! command_exists paru; then
echo "Installing yay as AUR helper..."
sudo ${PACKAGER} --noconfirm -S base-devel || { echo -e "${RED}Failed to install base-devel${RC}"; exit 1; }
sudo ${PACKAGER} -S --needed --noconfirm base-devel || { echo -e "${RED}Failed to install base-devel${RC}"; exit 1; }
cd /opt && sudo git clone https://aur.archlinux.org/yay-git.git && sudo chown -R ${USER}:${USER} ./yay-git
cd yay-git && makepkg --noconfirm -si || { echo -e "${RED}Failed to install yay${RC}"; exit 1; }
else
@ -21,7 +21,7 @@ fastUpdate() {
echo "No AUR helper found. Please install yay or paru."
exit 1
fi
${AUR_HELPER} --noconfirm -S rate-mirrors-bin
${AUR_HELPER} -S --needed --noconfirm rate-mirrors-bin
if [ -s /etc/pacman.d/mirrorlist ]; then
sudo cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
fi

View File

@ -75,7 +75,7 @@ prompt_for_mac() {
fi
# Display devices with numbers
IFS=$'\n' read -rd '' -a device_list <<<"$devices"
IFS=$'\n' read -r -a device_list <<<"$devices"
for i in "${!device_list[@]}"; do
echo "$((i+1)). ${device_list[$i]}"
done

View File

@ -101,7 +101,7 @@ prompt_for_network() {
fi
# Display networks with numbers
IFS=$'\n' read -rd '' -a network_list <<<"$networks"
IFS=$'\n' read -r -a network_list <<<"$networks"
for i in "${!network_list[@]}"; do
ssid=$(echo "${network_list[$i]}" | awk -F: '{print $1}')
echo "$((i+1)). SSID: $ssid"

View File

@ -10,16 +10,16 @@ pub trait FloatContent {
fn is_finished(&self) -> bool;
}
pub struct Float<T: FloatContent> {
content: Option<T>,
pub struct Float {
content: Box<dyn FloatContent>,
width_percent: u16,
height_percent: u16,
}
impl<T: FloatContent> Float<T> {
pub fn new(width_percent: u16, height_percent: u16) -> Self {
impl Float {
pub fn new(content: Box<dyn FloatContent>, width_percent: u16, height_percent: u16) -> Self {
Self {
content: None,
content,
width_percent,
height_percent,
}
@ -48,7 +48,6 @@ impl<T: FloatContent> Float<T> {
pub fn draw(&mut self, frame: &mut Frame, parent_area: Rect) {
let popup_area = self.floating_window(parent_area);
if let Some(content) = &mut self.content {
let content_area = Rect {
x: popup_area.x,
y: popup_area.y,
@ -56,36 +55,18 @@ impl<T: FloatContent> Float<T> {
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 {
if let Some(content) = &mut self.content {
match key.code {
KeyCode::Enter | KeyCode::Char('p') | KeyCode::Esc | KeyCode::Char('q') => {
if content.is_finished() {
self.content = None;
} else {
content.handle_key_event(key);
}
}
_ => {
content.handle_key_event(key);
}
}
KeyCode::Enter | KeyCode::Char('p') | KeyCode::Esc | KeyCode::Char('q')
if self.content.is_finished() =>
{
true
} else {
false
}
_ => self.content.handle_key_event(key),
}
}
pub fn get_content(&self) -> &Option<T> {
&self.content
}
pub fn set_content(&mut self, content: Option<T>) {
self.content = content;
}
}

View File

@ -1,4 +1,4 @@
use crate::float::FloatContent;
use crate::{float::FloatContent, running_command::Command};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
layout::Rect,
@ -7,6 +7,7 @@ use ratatui::{
widgets::{Block, Borders, List},
Frame,
};
use std::path::PathBuf;
pub struct FloatingText {
text: Vec<String>,
@ -18,6 +19,26 @@ impl FloatingText {
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) {
if self.scroll + 1 < self.text.len() {
self.scroll += 1;
@ -64,16 +85,11 @@ impl FloatContent for FloatingText {
fn handle_key_event(&mut self, key: &KeyEvent) -> bool {
match key.code {
KeyCode::Down | KeyCode::Char('j') => {
self.scroll_down();
true
}
KeyCode::Up | KeyCode::Char('k') => {
self.scroll_up();
true
}
_ => false,
KeyCode::Down | KeyCode::Char('j') => self.scroll_down(),
KeyCode::Up | KeyCode::Char('k') => self.scroll_up(),
_ => {}
}
false
}
fn is_finished(&self) -> bool {

View File

@ -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
}
}

View File

@ -1,8 +1,8 @@
mod float;
mod floating_text;
mod list;
mod running_command;
pub mod state;
mod tabs;
mod theme;
use std::{
@ -13,23 +13,16 @@ use std::{
use clap::Parser;
use crossterm::{
cursor::RestorePosition,
event::{self, DisableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, Event, KeyEventKind},
style::ResetColor,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use float::Float;
use include_dir::include_dir;
use list::CustomList;
use ratatui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout},
style::{Color, Style},
text::Span,
widgets::{Block, Borders, Paragraph},
Terminal,
};
use running_command::RunningCommand;
use state::AppState;
use tempdir::TempDir;
use theme::THEMES;
@ -56,17 +49,14 @@ fn main() -> std::io::Result<()> {
.extract(temp_dir.path())
.expect("Failed to extract the saved directory");
let state = AppState {
theme,
temp_path: temp_dir.path().to_owned(),
};
let mut state = AppState::new(theme, temp_dir.path().to_owned());
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
run(&mut terminal, &state)?;
run(&mut terminal, &mut state)?;
// restore terminal
disable_raw_mode()?;
@ -78,55 +68,9 @@ fn main() -> std::io::Result<()> {
Ok(())
}
fn run<B: Backend>(terminal: &mut Terminal<B>, state: &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;
fn run<B: Backend>(terminal: &mut Terminal<B>, state: &mut AppState) -> io::Result<()> {
loop {
// Always redraw
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();
terminal.draw(|frame| state.draw(frame)).unwrap();
// Wait for an event
if !event::poll(Duration::from_millis(10))? {
@ -141,47 +85,8 @@ fn run<B: Backend>(terminal: &mut Terminal<B>, state: &AppState) -> io::Result<(
continue;
}
//Send the key to the float
//If we receive true, then the float processed the input
//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;
}
_ => {}
}
}
if !state.handle_key(&key) {
return Ok(());
}
}
}

View File

@ -1,4 +1,4 @@
use crate::{float::FloatContent, state::AppState};
use crate::float::FloatContent;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use oneshot::{channel, Receiver};
use portable_pty::{
@ -13,6 +13,7 @@ use ratatui::{
};
use std::{
io::Write,
path::Path,
sync::{Arc, Mutex},
thread::JoinHandle,
};
@ -125,7 +126,7 @@ impl FloatContent for 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();
// 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"),
}
cmd.cwd(&state.temp_path);
cmd.cwd(temp_path);
// Open a pseudo-terminal with initial size
let pair = pty_system

81
src/search.rs Normal file
View 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()
}
}

View File

@ -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;
pub struct AppState {
/// Selected theme
pub theme: Theme,
/// 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
View 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"),
}
},
}),
},
]
});

View File

@ -4,27 +4,39 @@ use ratatui::style::Color;
pub struct Theme {
pub dir_color: Color,
pub cmd_color: Color,
pub tab_color: Color,
pub dir_icon: &'static str,
pub cmd_icon: &'static str,
pub tab_icon: &'static str,
pub success_color: Color,
pub fail_color: Color,
pub focused_color: Color,
pub unfocused_color: Color,
}
pub const THEMES: [Theme; 2] = [
Theme {
dir_color: Color::Blue,
cmd_color: Color::LightGreen,
tab_color: Color::Yellow,
dir_icon: "[DIR]",
cmd_icon: "[CMD]",
tab_icon: ">> ",
success_color: Color::Green,
fail_color: Color::Red,
focused_color: Color::LightBlue,
unfocused_color: Color::Gray,
},
Theme {
dir_color: Color::Blue,
cmd_color: Color::Rgb(204, 224, 208),
tab_color: Color::Rgb(255, 255, 85),
dir_icon: "",
cmd_icon: "",
tab_icon: "",
fail_color: Color::Rgb(199, 55, 44),
success_color: Color::Rgb(5, 255, 55),
focused_color: Color::LightBlue,
unfocused_color: Color::Gray,
},
];