Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: CI

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

env:
CARGO_TERM_COLOR: always

jobs:
check-and-test:
name: Check, Test & Build
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt

- name: Rust Cache
uses: Swatinem/rust-cache@v2

- name: Cargo Format Check
run: cargo fmt --check

- name: Clippy Linter
run: cargo clippy -- -D warnings

- name: Run Tests
run: cargo test --verbose

- name: Build Release
run: cargo build --release --verbose
56 changes: 56 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
build-and-release:
name: Build (${{ matrix.target }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
asset_name: glare-x86_64-linux
use_cross: false
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
asset_name: glare-aarch64-linux
use_cross: true

steps:
- uses: actions/checkout@v4

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- name: Install cross
if: matrix.use_cross
run: cargo install cross --git https://github.com/cross-rs/cross

- name: Build binary
run: |
if [ "${{ matrix.use_cross }}" = "true" ]; then
cross build --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi

- name: Package asset
run: |
cp "target/${{ matrix.target }}/release/glare" "${{ matrix.asset_name }}"

- name: Upload binary to release
uses: softprops/action-gh-release@v2
with:
files: ${{ matrix.asset_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,43 @@ Think of it as a **file manager, but for your system state**.
- **Dashboard** -- Overview with package stats, disk usage breakdown, and top 10 largest packages with bar chart
- **Blazing Fast** -- Pure Rust, single binary, queries pacman live with zero persistent storage
- **Adaptive Theme** -- Respects your terminal colors with a clean, minimal aesthetic
## Installation

Install Glare via the one-liner script (fetches the prebuilt binary directly to `~/.local/bin`, no `sudo` required):

```sh
curl -sSL https://raw.githubusercontent.com/TattvaOrg/Glare/main/install.sh | bash
```

## Installation
## Updating

### One-Liner
Update Glare to the latest release at any time:

```bash
curl -sL https://raw.githubusercontent.com/TattvaOrg/Glare/main/install.sh | bash
```sh
glare update
```

### From Source
Or re-run the installer:

```bash
```sh
curl -sSL https://raw.githubusercontent.com/TattvaOrg/Glare/main/install.sh | bash
```

## Build from Source

```sh
git clone https://github.com/TattvaOrg/Glare.git
cd Glare
cargo build --release
sudo install -Dm755 target/release/glare /usr/local/bin/glare
install -Dm755 target/release/glare ~/.local/bin/glare
```

### Requirements

- Arch Linux (or Arch-based: CachyOS, EndeavourOS, Manjaro, etc.)
- `pacman` (comes with Arch)
- Rust toolchain (for building from source)
- `pacman` (standard on Arch Linux)
- `curl` (for prebuilt installer)
- Rust toolchain (only if compiling from source)

## Usage

Expand Down
87 changes: 76 additions & 11 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,19 +1,84 @@
#!/bin/bash
set -e

REPO="https://github.com/TattvaOrg/Glare.git"
INSTALL_DIR="/usr/local/bin"
TMP_DIR=$(mktemp -d)
ARCH=$(uname -m)
case $ARCH in
x86_64)
TARGET_ARCH="x86_64"
;;
aarch64|arm64)
TARGET_ARCH="aarch64"
;;
*)
echo "Error: Unsupported architecture: $ARCH"
exit 1
;;
esac

trap 'rm -rf "$TMP_DIR"' EXIT
BIN_DIR="${CARGO_HOME:-$HOME/.local}/bin"
[ -d "$HOME/.local/bin" ] && BIN_DIR="$HOME/.local/bin"
mkdir -p "$BIN_DIR"

echo ":: Cloning Glare..."
git clone --depth 1 "$REPO" "$TMP_DIR/glare" 2>/dev/null
IS_UPDATE=false
if [ -f "$BIN_DIR/glare" ]; then
IS_UPDATE=true
CURRENT_VERSION=$("$BIN_DIR/glare" --version 2>/dev/null || echo "existing")
echo "==> Existing Glare installation detected ($CURRENT_VERSION)."
echo "==> Updating Glare at $BIN_DIR/glare..."
else
echo "==> Installing Glare to $BIN_DIR/glare..."
fi

echo ":: Building (release)..."
cargo build --release --manifest-path "$TMP_DIR/glare/Cargo.toml" 2>/dev/null
URL="https://github.com/TattvaOrg/Glare/releases/latest/download/glare-$TARGET_ARCH-linux"
TMP_BIN=$(mktemp)
trap 'rm -f "$TMP_BIN"' EXIT

echo ":: Installing to $INSTALL_DIR..."
sudo install -Dm755 "$TMP_DIR/glare/target/release/glare" "$INSTALL_DIR/glare"
echo "==> Fetching prebuilt release binary..."
if curl -sSfL "$URL" -o "$TMP_BIN" 2>/dev/null; then
# Verify file is an executable binary and not an error page
if file "$TMP_BIN" | grep -qE "ELF|executable"; then
# Unlink existing binary first to avoid ETXTBSY if currently running
rm -f "$BIN_DIR/glare"
mv -f "$TMP_BIN" "$BIN_DIR/glare"
chmod +x "$BIN_DIR/glare"
NEW_VER=$("$BIN_DIR/glare" --version 2>/dev/null || echo "")
if [ "$IS_UPDATE" = true ]; then
echo "✅ Glare updated successfully! ($NEW_VER)"
else
echo "✅ Glare installed successfully! ($NEW_VER)"
fi
else
echo "Error: Downloaded file is not a valid binary."
exit 1
fi
else
echo "Notice: Prebuilt release binary not found at $URL"
if command -v cargo >/dev/null 2>&1; then
echo "==> Compiling and updating from source using cargo..."
cargo install --force --git https://github.com/TattvaOrg/Glare.git --root "${BIN_DIR%/bin}"
NEW_VER=$("$BIN_DIR/glare" --version 2>/dev/null || echo "")
if [ "$IS_UPDATE" = true ]; then
echo "✅ Glare updated successfully via cargo! ($NEW_VER)"
else
echo "✅ Glare installed successfully via cargo! ($NEW_VER)"
fi
else
echo "Error: Could not download prebuilt binary and 'cargo' is not installed."
echo "Please install Rust (https://rustup.rs) or check https://github.com/TattvaOrg/Glare/releases"
exit 1
fi
fi

echo ":: Done. Run 'glare' to start."
# PATH guidance
case ":$PATH:" in
*":$BIN_DIR:"*) ;;
*)
echo ""
echo "⚠️ Note: $BIN_DIR is not in your PATH."
echo "Add it to your shell configuration (e.g. ~/.bashrc or ~/.zshrc):"
echo " export PATH=\"\$PATH:$BIN_DIR\""
;;
esac

echo ""
echo "Run 'glare' to start the application."
37 changes: 28 additions & 9 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::config::Config;
use crate::package::{FileGroup, Package, PackageSource, InstallReason, group_files};
use crate::package::{group_files, FileGroup, InstallReason, Package, PackageSource};
use crate::pacman;
use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
Expand All @@ -8,6 +8,7 @@ use std::collections::{HashMap, HashSet};
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::upper_case_acronyms)]
pub enum FilterCategory {
All,
Explicit,
Expand Down Expand Up @@ -123,6 +124,7 @@ pub struct App {
pub aur_names: HashSet<String>,
pub cached_files: HashMap<String, Vec<FileGroup>>,
pub total_installed_size: u64,
#[allow(dead_code)]
pub config: Config,
}

Expand All @@ -144,7 +146,7 @@ impl App {
}

// Sort by name initially
packages.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
packages.sort_by_key(|a| a.name.to_lowercase());

let total_installed_size: u64 = packages.iter().map(|p| p.installed_size_bytes).sum();
let filtered_indices: Vec<usize> = (0..packages.len()).collect();
Expand Down Expand Up @@ -354,10 +356,12 @@ impl App {
pub fn load_files_for_selected(&mut self) {
if let Some(pkg) = self.selected_package() {
let name = pkg.name.clone();
if !self.cached_files.contains_key(&name) {
if let std::collections::hash_map::Entry::Vacant(e) =
self.cached_files.entry(name.clone())
{
if let Ok(files) = pacman::load_package_files(&name) {
let grouped = group_files(files);
self.cached_files.insert(name, grouped);
e.insert(grouped);
}
}
}
Expand Down Expand Up @@ -393,7 +397,8 @@ impl App {

fn move_to_end(&mut self) {
if !self.filtered_indices.is_empty() {
self.list_state.select(Some(self.filtered_indices.len() - 1));
self.list_state
.select(Some(self.filtered_indices.len() - 1));
self.detail_scroll = 0;
}
}
Expand All @@ -404,11 +409,17 @@ impl App {
}

pub fn explicit_count(&self) -> usize {
self.packages.iter().filter(|p| p.install_reason == InstallReason::Explicit).count()
self.packages
.iter()
.filter(|p| p.install_reason == InstallReason::Explicit)
.count()
}

pub fn dependency_count(&self) -> usize {
self.packages.iter().filter(|p| p.install_reason == InstallReason::Dependency).count()
self.packages
.iter()
.filter(|p| p.install_reason == InstallReason::Dependency)
.count()
}

pub fn aur_count(&self) -> usize {
Expand All @@ -421,7 +432,15 @@ impl App {

pub fn top_largest(&self, n: usize) -> Vec<&Package> {
let mut indices: Vec<usize> = (0..self.packages.len()).collect();
indices.sort_by(|&a, &b| self.packages[b].installed_size_bytes.cmp(&self.packages[a].installed_size_bytes));
indices.into_iter().take(n).map(|i| &self.packages[i]).collect()
indices.sort_by(|&a, &b| {
self.packages[b]
.installed_size_bytes
.cmp(&self.packages[a].installed_size_bytes)
});
indices
.into_iter()
.take(n)
.map(|i| &self.packages[i])
.collect()
}
}
14 changes: 8 additions & 6 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use serde::Deserialize;
use std::path::PathBuf;

#[derive(Debug, Deserialize, Clone)]
#[allow(dead_code)]
pub struct Config {
#[serde(default = "default_aur_helper")]
pub aur_helper: String,
Expand All @@ -11,14 +12,17 @@ pub struct Config {
pub default_sort: String,
}

#[allow(dead_code)]
fn default_aur_helper() -> String {
"paru".to_string()
}

#[allow(dead_code)]
fn default_filter() -> String {
"all".to_string()
}

#[allow(dead_code)]
fn default_sort() -> String {
"name".to_string()
}
Expand All @@ -38,12 +42,10 @@ impl Default for Config {
pub fn load_config() -> Config {
let config_path = get_config_path();
if config_path.exists() {
match std::fs::read_to_string(&config_path) {
Ok(content) => match toml::from_str(&content) {
Ok(config) => return config,
Err(_) => {}
},
Err(_) => {}
if let Ok(content) = std::fs::read_to_string(&config_path) {
if let Ok(config) = toml::from_str(&content) {
return config;
}
}
}
Config::default()
Expand Down
4 changes: 1 addition & 3 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ impl EventHandler {
pub fn next(&self) -> Result<AppEvent> {
if event::poll(self.tick_rate)? {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => {
Ok(AppEvent::Key(key))
}
Event::Key(key) if key.kind == KeyEventKind::Press => Ok(AppEvent::Key(key)),
_ => Ok(AppEvent::Tick),
}
} else {
Expand Down
Loading
Loading