update zsh install

This commit is contained in:
Chris Titus 2024-08-08 15:54:37 -05:00
commit e58c54ad04
37 changed files with 874 additions and 441 deletions

View File

@ -6,7 +6,7 @@
## Type of Change ## Type of Change
- [ ] New feature - [ ] New feature
- [ ] Bug fix - [ ] Bug fix
- [ ] Documentation Update (Due to be added!) - [ ] Documentation Update
- [ ] Refactoring - [ ] Refactoring
- [ ] Hotfix - [ ] Hotfix
- [ ] Security patch - [ ] Security patch

View File

@ -8,3 +8,8 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
ignore:
- dependency-name: "actions/stale"
versions: '>= 9'
- dependency-name: "actions/setup-python"
versions: '> 4'

22
.github/workflows/github-pages.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: GitHub Pages Deploy
on:
release:
types: [published, prereleased]
workflow_dispatch:
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 3.x
- uses: actions/cache@v4
with:
key: ${{ github.ref }}
path: .cache
- run: pip install mkdocs-material
- run: pip install pillow cairosvg
- run: mkdocs gh-deploy --force

View File

@ -0,0 +1,45 @@
name: Close issue on /close
on:
issue_comment:
types: [created, edited]
jobs:
closeIssueOnClose:
# Skip this job if the comment was created/edited on a PR
if: ${{ !github.event.issue.pull_request }}
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: none
contents: read
steps:
- name: Check for /close comment
id: check_comment
run: |
if [[ "${{ contains(github.event.comment.body, '/close') }}" == "true" ]]; then
echo "comment=true" >> $GITHUB_ENV
else
echo "comment=false" >> $GITHUB_ENV
fi
- name: Check if the user is allowed
id: check_user
if: env.comment == 'true'
run: |
ALLOWED_USERS=("ChrisTitusTech" "afonsofrancof" "Marterich" "MyDrift-user" "Real-MullaC")
if [[ " ${ALLOWED_USERS[@]} " =~ " ${{ github.event.comment.user.login }} " ]]; then
echo "user=true" >> $GITHUB_ENV
else
echo "user=false" >> $GITHUB_ENV
fi
- name: Close issue if conditions are met
if: env.comment == 'true' && env.user == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
echo Closing the issue...
gh issue close $ISSUE_NUMBER --repo ${{ github.repository }}

View File

@ -2,7 +2,7 @@ name: LinUtil Release
on: on:
push: push:
branches: [ "main" ] branches: ["main"]
permissions: permissions:
contents: write contents: write
@ -16,25 +16,40 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Cache Cargo registry - name: Cache Cargo registry
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cargo/registry path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-registry- restore-keys: ${{ runner.os }}-cargo-registry-
- name: Cache Cargo index - name: Cache Cargo index
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cargo/git path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-index- restore-keys: ${{ runner.os }}-cargo-index-
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Build with:
run: cargo build --target-dir=build --release --verbose targets: x86_64-unknown-linux-musl
- uses: stefanzweifel/git-auto-commit-action@v5 - name: Install cross-rs for cross-compilation
with: run: cargo install cross
commit_message: Commit Linutil - name: Build x86_64 binary
file_pattern: 'build/release/linutil' run: cargo build --target-dir=build --release --verbose --target=x86_64-unknown-linux-musl
if: success() - name: Build aarch64 binary
run: cross build --target-dir=build --release --verbose --target=aarch64-unknown-linux-musl
- name: Move binaries to build directory
run: |
mv build/x86_64-unknown-linux-musl/release/linutil build/linutil
mv build/aarch64-unknown-linux-musl/release/linutil build/linutil-aarch64
- name: Pull latest changes
run: |
git config --global user.email "github-actions@github.com"
git config --global user.name "GitHub Actions"
git pull origin main
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Commit Linutil
file_pattern: "build/linutil build/linutil-aarch64"
if: success()

View File

@ -1,8 +1,8 @@
name: Pre-Release LinUtil name: Pre-Release LinUtil
permissions: permissions:
contents: write # Grant write permissions to contents contents: write # Grant write permissions to contents
packages: write # Grant write permissions to packages packages: write # Grant write permissions to packages
on: on:
workflow_dispatch: # Manual trigger added workflow_dispatch: # Manual trigger added
@ -30,7 +30,8 @@ jobs:
body: "![GitHub Downloads (specific asset, specific tag)](https://img.shields.io/github/downloads/ChrisTitusTech/linutil/${{ env.version }}/linutil)" body: "![GitHub Downloads (specific asset, specific tag)](https://img.shields.io/github/downloads/ChrisTitusTech/linutil/${{ env.version }}/linutil)"
append_body: false append_body: false
files: | files: |
./build/release/linutil ./build/linutil
./build/linutil-aarch64
prerelease: true prerelease: true
generate_release_notes: true generate_release_notes: true
env: env:

View File

@ -2,53 +2,39 @@ name: Rust Checks
on: on:
pull_request: pull_request:
branches: [ "main" ] branches: ["main"]
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
jobs: jobs:
cargo_check:
name: Cargo Check
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-registry-
- name: Cache Cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-index-
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Build
run: cargo check
lints: lints:
name: Lints name: Lints
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Run cargo fmt - name: Cache Cargo registry
run: cargo fmt --all --check uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-registry-
- name: Run cargo clippy - name: Cache Cargo index
run: cargo clippy uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-index-
- name: Run cargo clippy
run: cargo clippy
- name: Run cargo fmt
run: cargo fmt --all --check

95
Cargo.lock generated
View File

@ -163,9 +163,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.9" version = "4.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -173,9 +173,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.9" version = "4.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -185,9 +185,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.8" version = "4.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -280,6 +280,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.14.5"
@ -319,6 +325,25 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "include_dir"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
dependencies = [
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "ioctl-rs" name = "ioctl-rs"
version = "0.1.6" version = "0.1.6"
@ -525,6 +550,34 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "rand"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
dependencies = [
"fuchsia-cprng",
"libc",
"rand_core 0.3.1",
"rdrand",
"winapi",
]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
dependencies = [
"rand_core 0.4.2",
]
[[package]]
name = "rand_core"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]] [[package]]
name = "ratatui" name = "ratatui"
version = "0.27.0" version = "0.27.0"
@ -546,6 +599,15 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
dependencies = [
"rand_core 0.3.1",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.1" version = "0.5.1"
@ -555,6 +617,15 @@ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
] ]
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.17" version = "1.0.17"
@ -722,6 +793,16 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "tempdir"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
dependencies = [
"rand",
"remove_dir_all",
]
[[package]] [[package]]
name = "termios" name = "termios"
version = "0.2.2" version = "0.2.2"
@ -759,9 +840,11 @@ dependencies = [
"clap", "clap",
"crossterm", "crossterm",
"ego-tree", "ego-tree",
"include_dir",
"oneshot", "oneshot",
"portable-pty", "portable-pty",
"ratatui", "ratatui",
"tempdir",
"tui-term", "tui-term",
] ]

View File

@ -5,13 +5,15 @@ edition = "2021"
[dependencies] [dependencies]
chrono = "0.4.33" chrono = "0.4.33"
clap = { version = "4.5.9", features = ["derive"] } clap = { version = "4.5.11", features = ["derive"] }
crossterm = "0.27.0" crossterm = "0.27.0"
ego-tree = "0.6.2" ego-tree = "0.6.2"
oneshot = "0.1.8" oneshot = "0.1.8"
portable-pty = "0.8.1" portable-pty = "0.8.1"
ratatui = "0.27.0" ratatui = "0.27.0"
tui-term = "0.1.12" tui-term = "0.1.12"
include_dir = "0.7.4"
tempdir = "0.3.7"
[[bin]] [[bin]]
name = "linutil" name = "linutil"

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Chris Titus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,21 +1,29 @@
# Chris Titus Tech's Linux Utility # Chris Titus Tech's Linux Utility
[![Version](https://img.shields.io/github/v/release/ChrisTitusTech/linutil?color=%230567ff&label=Latest%20Release&style=for-the-badge)](https://github.com/ChrisTitusTech/linutil/releases/latest) [![Version](https://img.shields.io/github/v/release/ChrisTitusTech/linutil?color=%230567ff&label=Latest%20Release&style=for-the-badge)](https://github.com/ChrisTitusTech/linutil/releases/latest)
![GitHub Downloads (specific asset, all releases)](https://img.shields.io/github/downloads/ChrisTitusTech/linutil/linutil?label=Total%20Downloads&style=for-the-badge) ![GitHub Downloads (specific asset, all releases)](https://img.shields.io/github/downloads/ChrisTitusTech/linutil/linutil?label=Total%20Downloads&style=for-the-badge)
![Preview](docs/assets/preview.png) ![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 🦀
\* — The project is in active development, so there could be some issues. Please consider [submitting feedback](https://github.com/ChrisTitusTech/linutil/issues).
## 💡 Usage ## 💡 Usage
Open your terminal and paste this command Open your terminal and paste this command:
```bash ```bash
curl -fsSL https://christitus.com/linux | sh curl -fsSL https://christitus.com/linux | sh
``` ```
## 🎓 Documentation
### [LinUtil Official Documentation](https://christitustech.github.io/linutil/)
## 💖 Support ## 💖 Support
- To morally and mentally support the project, make sure to leave a ⭐️! To morally and mentally support the project, make sure to leave a ⭐️!
## 🏅 Thanks to all Contributors ## 🏅 Thanks to all Contributors
Thanks a lot for spending your time helping Winutil grow. Thanks a lot! Keep rocking 🍻. Thanks a lot for spending your time helping Linutil grow. Keep rocking 🍻.
[![Contributors](https://contrib.rocks/image?repo=ChrisTitusTech/linutil)](https://github.com/ChrisTitusTech/linutil/graphs/contributors) [![Contributors](https://contrib.rocks/image?repo=ChrisTitusTech/linutil)](https://github.com/ChrisTitusTech/linutil/graphs/contributors)

Binary file not shown.

BIN
build/linutil Executable file

Binary file not shown.

BIN
build/linutil-aarch64 Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,11 +1,6 @@
#!/bin/sh -e #!/bin/sh -e
checkEnv() { . ./common-script.sh
checkCommandRequirements 'curl groups sudo'
checkPackageManager 'apt-get dnf pacman zypper'
checkSuperUser
checkDistro
}
setupAlacritty() { setupAlacritty() {
echo "Install Alacritty if not already installed..." echo "Install Alacritty if not already installed..."

View File

@ -0,0 +1,27 @@
#!/bin/sh -e
. ./common-script.sh
makeDWM(){
cd $HOME && git clone https://github.com/ChrisTitusTech/dwm-titus.git # CD to Home directory to install dwm-titus
# This path can be changed (e.g. to linux-toolbox directory)
cd dwm-titus/ # Hardcoded path, maybe not the best.
sudo ./setup.sh # Run setup
sudo make clean install # Run make clean install
}
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" install -y build-essential libx11-dev libxinerama-dev libxft-dev libimlib2-dev
;;
esac
}
checkEnv
setupDWM
makeDWM

View File

@ -1,11 +1,6 @@
#!/bin/sh -e #!/bin/sh -e
checkEnv() { . ./common-script.sh
checkCommandRequirements 'curl groups sudo'
checkPackageManager 'apt-get dnf pacman zypper'
checkSuperUser
checkDistro
}
setupKitty() { setupKitty() {
echo "Install Kitty if not already installed..." echo "Install Kitty if not already installed..."

View File

@ -1,11 +1,6 @@
#!/bin/sh -e #!/bin/sh -e
checkEnv() { . ./common-script.sh
checkCommandRequirements 'curl groups sudo'
checkPackageManager 'apt-get dnf pacman zypper'
checkSuperUser
checkDistro
}
setupRofi() { setupRofi() {
echo "Install Rofi if not already installed..." echo "Install Rofi if not already installed..."

View File

@ -0,0 +1,51 @@
#!/bin/sh
. ./common-script.sh
# Function to install zsh
install_zsh() {
echo "Install ZSH if not already installed..."
if ! command_exists zsh; then
case "$PACKAGER" in
pacman)
sudo "$PACKAGER" -S --noconfirm zsh
;;
*)
sudo "$PACKAGER" install -y zsh
;;
esac
else
echo "ZSH is already installed."
fi
}
# Function to setup zsh configuration
setup_zsh_config() {
CONFIG_DIR="$HOME/.config/zsh"
ZSHRC_FILE="$CONFIG_DIR/.zshrc"
if [ ! -d "$CONFIG_DIR" ]; then
mkdir -p "$CONFIG_DIR"
fi
# Write the configuration to .zshrc
cat <<EOL >"$ZSHRC_FILE"
HISTFILE=~/.config/zsh/.histfile
HISTSIZE=5000
SAVEHIST=100000
setopt autocd extendedglob
unsetopt beep
bindkey -v
# Configure the prompt with embedded Solarized color codes
PROMPT='%F{32}%n%f%F{166}@%f%F{64}%m:%F{166}%~%f%F{15}$%f '
RPROMPT='%F{15}(%F{166}%D{%H:%M}%F{15})%f'
EOL
# Ensure /etc/zsh/zshenv sets ZDOTDIR to the user's config directory
echo 'export ZDOTDIR="$HOME/.config/zsh"' | sudo tee -a /etc/zsh/zshenv
}
checkEnv
setup_zsh_config
install_zsh

View File

@ -78,7 +78,8 @@ checkDistro() {
checkEnv() { checkEnv() {
checkCommandRequirements 'curl groups sudo' checkCommandRequirements 'curl groups sudo'
checkPackageManager 'apt-get dnf pacman zypper' checkPackageManager 'apt-get nala dnf pacman zypper yum xbps-install nix-env'
checkCurrentDirectoryWritable
checkSuperUser checkSuperUser
checkDistro checkDistro
} }

View File

@ -1,59 +0,0 @@
#!/bin/sh
# Function to install zsh
install_zsh() {
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y zsh
elif command -v yum >/dev/null 2>&1; then
sudo yum install -y zsh
elif command -v dnf >/dev/null 2>&1; then
sudo dnf install -y zsh
elif command -v pacman >/dev/null 2>&1; then
sudo pacman -Syu zsh
elif command -v zypper >/dev/null 2>&1; then
sudo zypper install -y zsh
else
echo "No compatible package manager found. Please install zsh manually." >&2
exit 1
fi
}
# Function to setup zsh configuration
setup_zsh_config() {
CONFIG_DIR="$HOME/.config/zsh"
ZSHRC_FILE="$CONFIG_DIR/.zshrc"
# Create config directory if it doesn't exist
mkdir -p "$CONFIG_DIR"
# Write the configuration to .zshrc
cat <<EOL >"$ZSHRC_FILE"
# Lines configured by zsh-newuser-install
HISTFILE=~/.config/zsh/.histfile
HISTSIZE=5000
SAVEHIST=100000
setopt autocd extendedglob
unsetopt beep
bindkey -v
# End of lines configured by zsh-newuser-install
# Configure the prompt with embedded Solarized color codes
PROMPT='%F{32}%n%f%F{166}@%f%F{64}%m:%F{166}%~%f%F{15}$%f '
RPROMPT='%F{15}(%F{166}%D{%H:%M}%F{15})%f'
EOL
# Ensure /etc/zsh/zshenv sets ZDOTDIR to the user's config directory
echo 'export ZDOTDIR="$HOME/.config/zsh"' | sudo tee -a /etc/zsh/zshenv
}
# Execute the installation and setup
install_zsh
if [ $? -eq 0 ]; then
echo "zsh installed successfully."
setup_zsh_config
else
echo "zsh installation failed."
exit 1
fi

View File

@ -0,0 +1,39 @@
#!/bin/sh -e
installPkg() {
echo "Install UFW if not already installed..."
if ! command_exists ufw; then
case ${PACKAGER} in
pacman)
sudo "${PACKAGER}" -S --noconfirm ufw
;;
*)
sudo "${PACKAGER}" install -y ufw
;;
esac
else
echo "UFW is already installed."
fi
echo -e "${GREEN}Using Chris Titus Recommended Firewall Rules${RC}"
sudo ufw limit 22/tcp
echo "Limiting port 22/tcp (UFW)"
sudo ufw allow 80/tcp
echo "Allowing port 80/tcp (UFW)"
sudo ufw allow 443/tcp
echo "Allowing port 443/tcp (UFW)"
sudo ufw default deny incoming
echo "Denying Incoming Packets by Default(UFW)"
sudo ufw default allow outgoing
echo "Allowing Outcoming Packets by Default(UFW)"
sudo ufw enable
echo -e "${GREEN}Enabled Firewall with Baselines!${RC}"
}
checkEnv
installPkg

View File

@ -1,5 +1,7 @@
#!/bin/sh -e #!/bin/sh -e
. ./common-script.sh
# Check if the home directory and linuxtoolbox folder exist, create them if they don't # Check if the home directory and linuxtoolbox folder exist, create them if they don't
LINUXTOOLBOXDIR="$HOME/linuxtoolbox" LINUXTOOLBOXDIR="$HOME/linuxtoolbox"
@ -22,13 +24,6 @@ fi
cd "$LINUXTOOLBOXDIR/linutil" || exit cd "$LINUXTOOLBOXDIR/linutil" || exit
checkEnv() {
checkCommandRequirements 'curl groups sudo'
checkPackageHandler 'apt yum dnf pacman zypper'
checkCurrentDirectoryWritable
checkSuperUser
}
installDepend() { installDepend() {
## Check for dependencies. ## Check for dependencies.
DEPENDENCIES='tar tree multitail tldr trash-cli unzip cmake make jq' DEPENDENCIES='tar tree multitail tldr trash-cli unzip cmake make jq'
@ -60,12 +55,12 @@ installDepend() {
fi fi
"$AUR_HELPER" --noconfirm -S "$DEPENDENCIES" "$AUR_HELPER" --noconfirm -S "$DEPENDENCIES"
;; ;;
apt) apt-get|nala)
COMPILEDEPS='build-essential' COMPILEDEPS='build-essential'
sudo "$PACKAGER" update sudo "$PACKAGER" update
sudo dpkg --add-architecture i386 sudo dpkg --add-architecture i386
sudo "$PACKAGER" update sudo "$PACKAGER" update
sudo "$PACKAGER" install -y "$DEPENDENCIES" $COMPILEDEPS sudo "$PACKAGER" install -y $DEPENDENCIES $COMPILEDEPS
;; ;;
dnf) dnf)
COMPILEDEPS='@development-tools' COMPILEDEPS='@development-tools'
@ -81,7 +76,7 @@ installDepend() {
sudo "$PACKAGER" --non-interactive install libgcc_s1-gcc7-32bit glibc-devel-32bit sudo "$PACKAGER" --non-interactive install libgcc_s1-gcc7-32bit glibc-devel-32bit
;; ;;
*) *)
sudo "$PACKAGER" install -y "$DEPENDENCIES" sudo "$PACKAGER" install -y $DEPENDENCIES # Fixed bug where no packages found on debian-based
;; ;;
esac esac
} }

View File

@ -1,11 +1,6 @@
#!/bin/sh -e #!/bin/sh -e
checkEnv() { . ./common-script.sh
checkCommandRequirements 'curl groups sudo'
checkPackageManager 'apt-get yum dnf pacman zypper'
checkCurrentDirectoryWritable
checkSuperUser
}
installDepend() { installDepend() {
## Check for dependencies. ## Check for dependencies.

View File

@ -1,18 +1,20 @@
#!/bin/sh -e #!/bin/sh -e
. ./common-script.sh
# Check if the home directory and linuxtoolbox folder exist, create them if they don't # Check if the home directory and linuxtoolbox folder exist, create them if they don't
LINUXTOOLBOXDIR="$HOME/linuxtoolbox" LINUXTOOLBOXDIR="$HOME/linuxtoolbox"
if [ ! -d "$LINUXTOOLBOXDIR" ]; then if [ ! -d "$LINUXTOOLBOXDIR" ]; then
echo -e "${YELLOW}Creating linuxtoolbox directory: $LINUXTOOLBOXDIR${RC}" printf "${YELLOW}Creating linuxtoolbox directory: %s${RC}\n" "$LINUXTOOLBOXDIR"
mkdir -p "$LINUXTOOLBOXDIR" mkdir -p "$LINUXTOOLBOXDIR"
echo -e "${GREEN}linuxtoolbox directory created: $LINUXTOOLBOXDIR${RC}" printf "${GREEN}linuxtoolbox directory created: %s${RC}\n" "$LINUXTOOLBOXDIR"
fi fi
cd "$LINUXTOOLBOXDIR" || exit cd "$LINUXTOOLBOXDIR" || exit
install_theme_tools() { install_theme_tools() {
echo -e "${YELLOW}Installing theme tools (qt6ct and kvantum)...${RC}" printf "${YELLOW}Installing theme tools (qt6ct and kvantum)...${RC}\n"
case $PACKAGER in case $PACKAGER in
apt-get) apt-get)
sudo apt-get update sudo apt-get update
@ -31,14 +33,14 @@ install_theme_tools() {
sudo pacman --noconfirm -S qt6ct kvantum sudo pacman --noconfirm -S qt6ct kvantum
;; ;;
*) *)
echo -e "${RED}Unsupported package manager. Please install qt6ct and kvantum manually.${RC}" printf "${RED}Unsupported package manager. Please install qt6ct and kvantum manually.${RC}\n"
exit 1 exit 1
;; ;;
esac esac
} }
configure_qt6ct() { configure_qt6ct() {
echo -e "${YELLOW}Configuring qt6ct...${RC}" printf "${YELLOW}Configuring qt6ct...${RC}\n"
mkdir -p "$HOME/.config/qt6ct" mkdir -p "$HOME/.config/qt6ct"
cat <<EOF > "$HOME/.config/qt6ct/qt6ct.conf" cat <<EOF > "$HOME/.config/qt6ct/qt6ct.conf"
[Appearance] [Appearance]
@ -46,17 +48,26 @@ style=kvantum
color_scheme=default color_scheme=default
icon_theme=breeze icon_theme=breeze
EOF EOF
echo -e "${GREEN}qt6ct configured successfully.${RC}" printf "${GREEN}qt6ct configured successfully.${RC}\n"
# Add QT_QPA_PLATFORMTHEME to /etc/environment
if ! grep -q "QT_QPA_PLATFORMTHEME=qt6ct" /etc/environment; then
printf "${YELLOW}Adding QT_QPA_PLATFORMTHEME to /etc/environment...${RC}\n"
echo "QT_QPA_PLATFORMTHEME=qt6ct" | sudo tee -a /etc/environment > /dev/null
printf "${GREEN}QT_QPA_PLATFORMTHEME added to /etc/environment.${RC}\n"
else
printf "${GREEN}QT_QPA_PLATFORMTHEME already set in /etc/environment.${RC}\n"
fi
} }
configure_kvantum() { configure_kvantum() {
echo -e "${YELLOW}Configuring Kvantum...${RC}" printf "${YELLOW}Configuring Kvantum...${RC}\n"
mkdir -p "$HOME/.config/Kvantum" mkdir -p "$HOME/.config/Kvantum"
cat <<EOF > "$HOME/.config/Kvantum/kvantum.kvconfig" cat <<EOF > "$HOME/.config/Kvantum/kvantum.kvconfig"
[General] [General]
theme=Breeze theme=Breeze
EOF EOF
echo -e "${GREEN}Kvantum configured successfully.${RC}" printf "${GREEN}Kvantum configured successfully.${RC}\n"
} }
checkEnv checkEnv

View File

@ -1,11 +1,6 @@
#!/bin/sh -e #!/bin/sh -e
checkEnv() { . ./common-script.sh
checkCommandRequirements 'curl groups sudo'
checkPackageManager 'apt-get nala dnf pacman zypper yum xbps-install'
checkSuperUser
checkDistro
}
fastUpdate() { fastUpdate() {
case ${PACKAGER} in case ${PACKAGER} in
@ -83,7 +78,8 @@ updateSystem() {
sudo "${PACKAGER}" upgrade -y sudo "${PACKAGER}" upgrade -y
;; ;;
pacman) pacman)
sudo "${PACKAGER}" -Syu --noconfirm sudo "${PACKAGER}" -Sy --noconfirm --needed archlinux-keyring
sudo "${PACKAGER}" -Su --noconfirm
;; ;;
zypper) zypper)
sudo ${PACKAGER} ref sudo ${PACKAGER} ref
@ -99,6 +95,13 @@ updateSystem() {
esac esac
} }
updateFlatpaks() {
if command_exists flatpak; then
flatpak update -y
fi
}
checkEnv checkEnv
fastUpdate fastUpdate
updateSystem updateSystem
updateFlatpaks

5
src/commands/test/lib.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
say_hello () {
echo Hi
}

6
src/commands/test/main.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
# The current working directory will always be inside "commands"
. test/lib.sh
say_hello

View File

@ -1,4 +1,4 @@
use crate::{float::floating_window, theme::*}; use crate::{float::floating_window, running_command::Command, state::AppState};
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
use ego_tree::{tree, NodeId}; use ego_tree::{tree, NodeId};
use ratatui::{ use ratatui::{
@ -9,18 +9,10 @@ use ratatui::{
Frame, Frame,
}; };
macro_rules! with_common_script { #[derive(Clone)]
($command:expr) => {
concat!(
include_str!("commands/common-script.sh"),
include_str!($command)
)
};
}
struct ListNode { struct ListNode {
name: &'static str, name: &'static str,
command: &'static str, command: Command,
} }
/// This is a data structure that has everything necessary to draw and manage a menu of commands /// This is a data structure that has everything necessary to draw and manage a menu of commands
@ -37,6 +29,10 @@ pub struct CustomList {
/// This stores the preview windows state. If it is None, it will not be displayed. /// This stores the preview windows state. If it is None, it will not be displayed.
/// If it is Some, we show it with the content of the selected item /// If it is Some, we show it with the content of the selected item
preview_window_state: Option<PreviewWindowState>, preview_window_state: Option<PreviewWindowState>,
// This stores the current search query
filter_query: String,
// This stores the filtered tree
filtered_items: Vec<ListNode>,
} }
/// This struct stores the preview window state /// This struct stores the preview window state
@ -60,66 +56,72 @@ impl CustomList {
// case the tree! macro expands to `ego-tree::tree` data structure // case the tree! macro expands to `ego-tree::tree` data structure
let tree = tree!(ListNode { let tree = tree!(ListNode {
name: "root", name: "root",
command: "" command: Command::None,
} => { } => {
ListNode {
name: "Full System Update",
command: with_common_script!("commands/system-update.sh"),
},
ListNode {
name: "Setup Bash Prompt",
command: "bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\""
},
ListNode {
name: "Setup Neovim",
command: "bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/neovim/main/setup.sh)\""
},
ListNode {
name: "Setup Zsh and Prompt",
command: with_common_script!("commands/install-zsh.sh"),
},
// ListNode {
// name: "Just ls, nothing special, trust me",
// command: include_str!("commands/special_ls.sh"),
// },
ListNode { ListNode {
name: "System Setup", name: "System Setup",
command: "" command: Command::None,
} => { } => {
ListNode { ListNode {
name: "Build Prerequisites", name: "Build Prerequisites",
command: with_common_script!("commands/system-setup/1-compile-setup.sh"), command: Command::LocalFile("system-setup/1-compile-setup.sh"),
}, },
ListNode { ListNode {
name: "Gaming Dependencies", name: "Gaming Dependencies",
command: with_common_script!("commands/system-setup/2-gaming-setup.sh"), command: Command::LocalFile("system-setup/2-gaming-setup.sh"),
}, },
ListNode { ListNode {
name: "Global Theme", name: "Global Theme",
command: with_common_script!("commands/system-setup/3-global-theme.sh"), command: Command::LocalFile("system-setup/3-global-theme.sh"),
}, },
},
ListNode {
name: "Security",
command: Command::None
} => {
ListNode { ListNode {
name: "Recursion?", name: "Firewall Baselines (CTT)",
command: "cargo run" command: Command::LocalFile("security/firewall-baselines.sh"),
} }
}, },
ListNode { ListNode {
name: "Titus Dotfiles", name: "Applications Setup",
command: "" command: Command::None
} => { } => {
ListNode { ListNode {
name: "Alacritty Setup", name: "Alacritty",
command: with_common_script!("commands/dotfiles/alacritty-setup.sh"), command: Command::LocalFile("applications-setup/alacritty-setup.sh"),
}, },
ListNode { ListNode {
name: "Kitty Setup", name: "Bash Prompt",
command: with_common_script!("commands/dotfiles/kitty-setup.sh"), command: Command::Raw("bash -c \"$(curl -s https://raw.githubusercontent.com/ChrisTitusTech/mybash/main/setup.sh)\""),
}, },
ListNode { ListNode {
name: "Rofi Setup", name: "DWM-Titus",
command: with_common_script!("commands/dotfiles/rofi-setup.sh"), 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: "Full System Update",
command: Command::LocalFile("system-update.sh"),
},
}); });
// We don't get a reference, but rather an id, because references are siginficantly more // We don't get a reference, but rather an id, because references are siginficantly more
// paintfull to manage // paintfull to manage
@ -130,46 +132,61 @@ impl CustomList {
list_state: ListState::default().with_selected(Some(0)), list_state: ListState::default().with_selected(Some(0)),
// By default the PreviewWindowState is set to None, so it is not being shown // By default the PreviewWindowState is set to None, so it is not being shown
preview_window_state: None, preview_window_state: None,
filter_query: String::new(),
filtered_items: vec![],
} }
} }
/// Draw our custom widget to the frame /// Draw our custom widget to the frame
pub fn draw(&mut self, frame: &mut Frame, area: Rect) { pub fn draw(&mut self, frame: &mut Frame, area: Rect, state: &AppState) {
// Get the last element in the `visit_stack` vec let item_list: Vec<Line> = if self.filter_query.is_empty() {
let theme = get_theme(); let mut items: Vec<Line> = vec![];
let curr = self // If we are not at the root of our filesystem tree, we need to add `..` path, to be able
.inner_tree // to go up the tree
.get(*self.visit_stack.last().unwrap()) // icons:  
.unwrap(); if !self.at_root() {
let mut items = 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!("{} ..", theme.dir_icon)).style(theme.dir_color));
}
// 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( items.push(
Line::from(format!("{} {}", theme.dir_icon, node.value().name)) Line::from(format!("{} ..", state.theme.dir_icon))
.style(theme.dir_color), .style(state.theme.dir_color),
);
} else {
items.push(
Line::from(format!("{} {}", theme.cmd_icon, node.value().name))
.style(theme.cmd_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 // create the normal list widget containing only item in our "working directory" / tree
// node // node
let list = List::new(items) let list = List::new(item_list)
.highlight_style(Style::default().reversed()) .highlight_style(Style::default().reversed())
.block(Block::default().borders(Borders::ALL).title(format!( .block(Block::default().borders(Borders::ALL).title(format!(
"Linux Toolbox - {}", "Linux Toolbox - {}",
@ -208,8 +225,39 @@ impl CustomList {
} }
} }
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 /// Handle key events, we are only interested in `Press` and `Repeat` events
pub fn handle_key(&mut self, event: KeyEvent) -> Option<&'static str> { pub fn handle_key(&mut self, event: KeyEvent, state: &AppState) -> Option<Command> {
if event.kind == KeyEventKind::Release { if event.kind == KeyEventKind::Release {
return None; return None;
} }
@ -223,7 +271,7 @@ impl CustomList {
return None; return None;
} }
self.try_scroll_down(); self.list_state.select_next();
None None
} }
KeyCode::Char('k') | KeyCode::Up => { KeyCode::Char('k') | KeyCode::Up => {
@ -234,19 +282,26 @@ impl CustomList {
return None; return None;
} }
self.try_scroll_up(); self.list_state.select_previous();
None None
} }
// The 'p' key toggles the preview on and off // The 'p' key toggles the preview on and off
KeyCode::Char('p') => { KeyCode::Char('p') => {
self.toggle_preview_window(); self.toggle_preview_window(state);
None None
} }
KeyCode::Enter => self.handle_enter(),
KeyCode::Enter => {
if self.preview_window_state.is_none() {
self.handle_enter()
} else {
None
}
}
_ => None, _ => None,
} }
} }
fn toggle_preview_window(&mut self) { fn toggle_preview_window(&mut self, state: &AppState) {
// If the preview window is active, disable it // If the preview window is active, disable it
if self.preview_window_state.is_some() { if self.preview_window_state.is_some() {
self.preview_window_state = None; self.preview_window_state = None;
@ -255,46 +310,29 @@ impl CustomList {
// Get the selected command // Get the selected command
if let Some(selected_command) = self.get_selected_command() { if let Some(selected_command) = self.get_selected_command() {
// If command is a folder, we don't display a preview let lines = match selected_command {
if selected_command.is_empty() { Command::Raw(cmd) => {
return; // Reconstruct the line breaks and file formatting after the
} // 'include_str!()' call in the node
cmd.lines().map(|line| line.to_string()).collect()
// Reconstruct the line breaks and file formatting after the }
// 'include_str!()' call in the node Command::LocalFile(file_path) => {
let lines: Vec<String> = selected_command let mut full_path = state.temp_path.clone();
.lines() full_path.push(file_path);
.map(|line| line.to_string()) let file_contents = std::fs::read_to_string(&full_path)
.collect(); .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,
};
// Show the preview window with the text lines // Show the preview window with the text lines
self.preview_window_state = Some(PreviewWindowState::new(lines)); self.preview_window_state = Some(PreviewWindowState::new(lines));
} }
} }
} }
fn try_scroll_up(&mut self) {
self.list_state
.select(Some(self.list_state.selected().unwrap().saturating_sub(1)));
}
fn try_scroll_down(&mut self) {
let curr = self
.inner_tree
.get(*self.visit_stack.last().unwrap())
.unwrap();
let count = curr.children().count();
let curr_selection = self.list_state.selected().unwrap();
if self.at_root() {
self.list_state
.select(Some((curr_selection + 1).min(count - 1)));
} else {
// When we are not at the root, we have to account for 1 more "virtual" node, `..`. So
// the count is 1 bigger (select is 0 based, because it's an index)
self.list_state
.select(Some((curr_selection + 1).min(count)));
}
}
/// Scroll the preview window down /// Scroll the preview window down
fn scroll_preview_window_down(&mut self) { fn scroll_preview_window_down(&mut self) {
@ -314,31 +352,44 @@ impl CustomList {
} }
} }
/// This method return the currently selected command, or None if no command is selected. /// This method returns the currently selected command, or None if no command is selected.
/// It was extracted from the 'handle_enter()' /// It was extracted from the 'handle_enter()'
/// ///
/// This could probably be integrated into the 'handle_enter()' method as to avoid code /// This could probably be integrated into the 'handle_enter()' method to avoid code
/// duplication, but I don't want to make too major changes to the codebase. /// duplication, but I don't want to make too major changes to the codebase.
fn get_selected_command(&self) -> Option<&'static str> { fn get_selected_command(&self) -> Option<Command> {
let curr = self let selected_index = self.list_state.selected().unwrap_or(0);
.inner_tree println!("Selected Index: {}", selected_index);
.get(*self.visit_stack.last().unwrap())
.unwrap();
let selected = self.list_state.selected().unwrap();
// If we are not at the root and the first item is selected, it's the `..` item if self.filter_query.is_empty() {
if !self.at_root() && selected == 0 { // No filter query, use the regular tree navigation
return None; let curr = self
} .inner_tree
.get(*self.visit_stack.last().unwrap())
.unwrap();
for (mut idx, node) in curr.children().enumerate() { if !self.at_root() && selected_index == 0 {
return None;
}
let mut actual_index = selected_index;
if !self.at_root() { if !self.at_root() {
idx += 1; actual_index -= 1; // Adjust for the ".." item if not at root
} }
if idx == selected {
return Some(node.value().command); 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) {
println!("Filtered Node Name: {}", filtered_node.name);
return Some(filtered_node.command.clone());
} }
} }
None None
} }
@ -346,39 +397,47 @@ impl CustomList {
/// - Run a command, if it is the currently selected item, /// - Run a command, if it is the currently selected item,
/// - Go up a directory /// - Go up a directory
/// - Go down into a directory /// - Go down into a directory
///
/// Returns `Some(command)` when command is selected, othervise we returns `None` /// Returns `Some(command)` when command is selected, othervise we returns `None`
fn handle_enter(&mut self) -> Option<&'static str> { fn handle_enter(&mut self) -> Option<Command> {
// Get the current node (current directory) let selected_index = self.list_state.selected().unwrap_or(0);
let curr = self
.inner_tree
.get(*self.visit_stack.last().unwrap())
.unwrap();
let selected = self.list_state.selected().unwrap();
// if we are not at the root, and the first element is selected, if self.filter_query.is_empty() {
// we can be sure it's '..', so we go up the directory // No filter query, use the regular tree navigation
if !self.at_root() && selected == 0 { let curr = self
self.visit_stack.pop(); .inner_tree
self.list_state.select(Some(0)); .get(*self.visit_stack.last().unwrap())
return None; .unwrap();
}
for (mut idx, node) in curr.children().enumerate() { if !self.at_root() && selected_index == 0 {
// at this point, we know that we are not on the .. item, and our indexes of the items never had .. self.visit_stack.pop();
// item. so to balance it out, in case the selection index contains .., se add 1 to our node index self.list_state.select(Some(0));
if !self.at_root() { return None;
idx += 1;
} }
if idx == selected {
if node.has_children() { let mut actual_index = selected_index;
self.visit_stack.push(node.id()); if !self.at_root() {
self.list_state.select(Some(0)); actual_index -= 1; // Adjust for the ".." item if not at root
return None; }
} else {
return Some(node.value().command); 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 None
} }

View File

@ -1,6 +1,7 @@
mod float; mod float;
mod list; mod list;
mod running_command; mod running_command;
pub mod state;
mod theme; mod theme;
use std::{ use std::{
@ -16,13 +17,20 @@ use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand, ExecutableCommand,
}; };
use include_dir::include_dir;
use list::CustomList; 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 running_command::RunningCommand;
use theme::set_theme; use state::AppState;
use tempdir::TempDir;
use theme::THEMES;
/// This is a binary :), Chris, change this to update the documentation on -h /// This is a binary :), Chris, change this to update the documentation on -h
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
@ -34,16 +42,29 @@ struct Args {
fn main() -> std::io::Result<()> { fn main() -> std::io::Result<()> {
let args = Args::parse(); let args = Args::parse();
if args.compat {
set_theme(0); let theme = if args.compat {
} THEMES[0].clone()
} else {
THEMES[1].clone()
};
let commands_dir = include_dir!("src/commands");
let temp_dir: TempDir = TempDir::new("linutil_scripts").unwrap();
commands_dir
.extract(temp_dir.path())
.expect("Failed to extract the saved directory");
let state = AppState {
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)?; run(&mut terminal, &state)?;
// restore terminal // restore terminal
disable_raw_mode()?; disable_raw_mode()?;
@ -55,17 +76,51 @@ fn main() -> std::io::Result<()> {
Ok(()) Ok(())
} }
fn run<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> { fn run<B: Backend>(terminal: &mut Terminal<B>, state: &AppState) -> io::Result<()> {
let mut command_opt: Option<RunningCommand> = None; let mut command_opt: Option<RunningCommand> = None;
let mut custom_list = CustomList::new(); let mut custom_list = CustomList::new();
let mut search_input = String::new();
let mut in_search_mode = false;
loop { loop {
// Always redraw // Always redraw
terminal terminal
.draw(|frame| { .draw(|frame| {
custom_list.draw(frame, frame.size()); //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);
if let Some(ref mut command) = &mut command_opt { if let Some(ref mut command) = &mut command_opt {
command.draw(frame); command.draw(frame, state);
} }
}) })
.unwrap(); .unwrap();
@ -90,8 +145,36 @@ fn run<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
if key.code == KeyCode::Char('q') { if key.code == KeyCode::Char('q') {
return Ok(()); return Ok(());
} }
if let Some(cmd) = custom_list.handle_key(key) { //Activate search mode if the forward slash key gets pressed
command_opt = Some(RunningCommand::new(cmd)); if key.code == KeyCode::Char('/') {
// Enter search mode
in_search_mode = true;
continue;
}
//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_opt = Some(RunningCommand::new(cmd, state));
} }
} }
} }

View File

@ -11,7 +11,7 @@ use portable_pty::{
}; };
use ratatui::{ use ratatui::{
layout::Size, layout::Size,
style::{Color, Style, Stylize}, style::{Style, Stylize},
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Borders}, widgets::{Block, Borders},
Frame, Frame,
@ -21,7 +21,14 @@ use tui_term::{
widget::PseudoTerminal, widget::PseudoTerminal,
}; };
use crate::{float::floating_window, theme::get_theme}; use crate::{float::floating_window, state::AppState};
#[derive(Clone)]
pub enum Command {
Raw(&'static str),
LocalFile(&'static str),
None, // Directory
}
/// This is a struct for storing everything connected to a running command /// This is a struct for storing everything connected to a running command
// Create a new instance on every new command you want to run // Create a new instance on every new command you want to run
@ -50,14 +57,22 @@ pub struct RunningCommand {
} }
impl RunningCommand { impl RunningCommand {
pub fn new(command: &str) -> Self { pub fn new(command: Command, state: &AppState) -> Self {
let pty_system = NativePtySystem::default(); let pty_system = NativePtySystem::default();
let mut cmd = CommandBuilder::new("sh");
cmd.arg("-c");
cmd.arg(command);
let cwd = std::env::current_dir().unwrap(); let mut cmd = CommandBuilder::new("sh");
cmd.cwd(cwd); match command {
Command::Raw(prompt) => {
cmd.arg("-c");
cmd.arg(prompt);
}
Command::LocalFile(file) => {
cmd.arg(file);
}
Command::None => panic!("Command::None was treated as a command"),
}
cmd.cwd(&state.temp_path);
let pair = pty_system let pair = pty_system
.openpty(PtySize { .openpty(PtySize {
@ -153,59 +168,57 @@ impl RunningCommand {
} }
} }
pub fn draw(&mut self, frame: &mut Frame) { pub fn draw(&mut self, frame: &mut Frame, state: &AppState) {
{ // Funny name
let theme = get_theme(); let floater = floating_window(frame.size());
// Funny name
let floater = floating_window(frame.size());
let inner_size = Size { let inner_size = Size {
width: floater.width - 2, // Because we add a `Block` with a border width: floater.width - 2, // Because we add a `Block` with a border
height: floater.height - 2, height: floater.height - 2,
}; };
// When the command is running // When the command is running
let term_border = if !self.is_finished() { let term_border = if !self.is_finished() {
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
.title_top(Line::from("Running the command....").centered()) .title_top(Line::from("Running the command....").centered())
.title_style(Style::default().reversed()) .title_style(Style::default().reversed())
.title_bottom(Line::from("Press Ctrl-C to KILL the command")) .title_bottom(Line::from("Press Ctrl-C to KILL the command"))
} else { } else {
// This portion is just for pretty colors. // This portion is just for pretty colors.
// You can use multiple `Span`s with different styles each, to construct a line, // You can use multiple `Span`s with different styles each, to construct a line,
// which can be used as a list item, or in this case a `Block` title // which can be used as a list item, or in this case a `Block` title
let mut title_line = if self.get_exit_status().success() { let mut title_line = if self.get_exit_status().success() {
Line::from( Line::from(
Span::default()
.content("SUCCESS!")
.style(Style::default().fg(theme.success_color).reversed()),
)
} else {
Line::from(
Span::default()
.content("FAILED!")
.style(Style::default().fg(theme.fail_color).reversed()),
)
};
title_line.push_span(
Span::default() Span::default()
.content(" press <ENTER> to close this window ") .content("SUCCESS!")
.style(Style::default()), .style(Style::default().fg(state.theme.success_color).reversed()),
); )
} else {
Block::default() Line::from(
.borders(Borders::ALL) Span::default()
.title_top(title_line.centered()) .content("FAILED!")
.style(Style::default().fg(state.theme.fail_color).reversed()),
)
}; };
let screen = self.screen(inner_size); // when the terminal is changing a lot, there
// will be 1 frame of lag on resizing title_line.push_span(
let pseudo_term = PseudoTerminal::new(&screen).block(term_border); Span::default()
frame.render_widget(pseudo_term, floater); .content(" press <ENTER> to close this window ")
} .style(Style::default()),
);
Block::default()
.borders(Borders::ALL)
.title_top(title_line.centered())
};
let screen = self.screen(inner_size); // when the terminal is changing a lot, there
// will be 1 frame of lag on resizing
let pseudo_term = PseudoTerminal::new(&screen).block(term_border);
frame.render_widget(pseudo_term, floater);
} }
/// Send SIGHUB signal, *not* SIGKILL or SIGTERM, to the child process /// Send SIGHUB signal, *not* SIGKILL or SIGTERM, to the child process
pub fn kill_child(&mut self) { pub fn kill_child(&mut self) {
if !self.is_finished() { if !self.is_finished() {

9
src/state.rs Normal file
View File

@ -0,0 +1,9 @@
use crate::theme::Theme;
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,
}

View File

@ -1,7 +1,6 @@
use ratatui::style::Color; use ratatui::style::Color;
pub static mut THEME_IDX: usize = 1; #[derive(Clone)]
pub struct Theme { pub struct Theme {
pub dir_color: Color, pub dir_color: Color,
pub cmd_color: Color, pub cmd_color: Color,
@ -29,11 +28,3 @@ pub const THEMES: [Theme; 2] = [
success_color: Color::Rgb(5, 255, 55), success_color: Color::Rgb(5, 255, 55),
}, },
]; ];
pub fn get_theme() -> &'static Theme {
&THEMES[unsafe { THEME_IDX }]
}
pub fn set_theme(idx: usize) {
unsafe { THEME_IDX = idx };
}

View File

@ -3,8 +3,6 @@
rc='\033[0m' rc='\033[0m'
red='\033[0;31m' red='\033[0;31m'
binary_url="https://github.com/ChrisTitusTech/linutil/releases/latest/download/linutil"
check() { check() {
exit_code=$1 exit_code=$1
message=$2 message=$2
@ -18,10 +16,26 @@ check() {
unset message unset message
} }
findArch() {
case "$(uname -m)" in
x86_64|amd64) arch="x86_64" ;;
aarch64|arm64) arch="aarch64" ;;
*) check 1 "Unsupported architecture"
esac
}
getUrl() {
case "${arch}" in
x86_64) echo "https://github.com/ChrisTitusTech/linutil/releases/latest/download/linutil";;
*) echo "https://github.com/ChrisTitusTech/linutil/releases/latest/download/linutil-${arch}";;
esac
}
findArch
temp_file=$(mktemp) temp_file=$(mktemp)
check $? "Creating the temporary file" check $? "Creating the temporary file"
curl -fsL "$binary_url" -o "$temp_file" curl -fsL "$(getUrl)" -o "$temp_file"
check $? "Downloading linutil" check $? "Downloading linutil"
chmod +x "$temp_file" chmod +x "$temp_file"

View File

@ -24,6 +24,7 @@ redirect_to_latest_pre_release() {
echo "Using latest Full Release" echo "Using latest Full Release"
url="https://github.com/ChrisTitusTech/linutil/releases/latest/download/linutil" url="https://github.com/ChrisTitusTech/linutil/releases/latest/download/linutil"
fi fi
addArch
echo "Using URL: $url" # Log the URL being used echo "Using URL: $url" # Log the URL being used
} }
@ -37,6 +38,22 @@ check() {
fi fi
} }
addArch() {
case "${arch}" in
x86_64);;
*) url="${url}-${arch}";;
esac
}
findArch() {
case "$(uname -m)" in
x86_64|amd64) arch="x86_64" ;;
aarch64|arm64) arch="aarch64" ;;
*) check 1 "Unsupported architecture"
esac
}
findArch
redirect_to_latest_pre_release redirect_to_latest_pre_release
TMPFILE=$(mktemp) TMPFILE=$(mktemp)