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
50 changes: 50 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Deploy mdBook Docs

on:
push:
branches:
- docs/mdbook
paths:
- "book/**"
- "book.toml"
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install mdBook
run: |
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.40/mdbook-v0.4.40-x86_64-unknown-linux-gnu.tar.gz \
| tar -xz --directory /usr/local/bin
Comment on lines +30 to +31

- name: Build docs
run: mdbook build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: book/html

deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ target
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
/target
book/html
20 changes: 20 additions & 0 deletions book.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[book]
title = "TRX — Package Manager TUI"
authors = ["pie-314"]
description = "Documentation for TRX, a fast keyboard-driven TUI package manager written in Rust."
language = "en"
src = "book/src"

[build]
build-dir = "book/html"

[output.html]
default-theme = "navy"
preferred-dark-theme = "ayu"
git-repository-url = "https://github.com/pie-314/trx"
edit-url-template = "https://github.com/pie-314/trx/edit/docs/mdbook/book/src/{path}"
site-url = "/trx/"

[output.html.fold]
enable = true
level = 1
49 changes: 49 additions & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Summary

[Introduction](./introduction.md)

---

# Getting Started

- [Installation](./installation.md)
- [Usage](./usage.md)

---

# Architecture

- [Overview](./architecture/overview.md)
- [Concurrency Model](./architecture/concurrency.md)
- [UI & Rendering](./architecture/ui-rendering.md)
- [Fuzzy Search Engine](./architecture/fuzzy-search.md)

---

# Package Manager Backends

- [Overview](./backends/index.md)
- [Arch Linux — Pacman](./backends/pacman.md)
- [Arch Linux — AUR (yay)](./backends/aur-yay.md)
- [Debian / Ubuntu — APT](./backends/apt.md)
- [macOS — Homebrew](./backends/homebrew.md)
- [Adding a New Backend](./backends/new-backend.md)

---

# Configuration

- [Config File](./configuration.md)

---

# Contributing

- [Contributing Guide](./contributing/index.md)
- [Coding Guidelines](./contributing/coding-guidelines.md)

---

# Roadmap

- [Roadmap](./roadmap.md)
65 changes: 65 additions & 0 deletions book/src/architecture/concurrency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Concurrency Model

TRX deliberately avoids an async runtime. All background work is done with **OS threads** and **`std::sync::mpsc` channels**. This keeps the dependency tree small, makes the code easy to reason about, and avoids the overhead of an executor in a single-user TUI.

---

## Channel Architecture

Two channels flow into the main event loop:

| Channel | Producer | Consumer | Payload |
|---------|----------|----------|---------|
| `result_rx` | Search / list-load threads | `App::run` | `(String, Vec<Package>)` — a tag plus a list of packages |
| `details_rx` | Details-fetch threads | `App::run` | `DetailsState` |

The tag in `result_rx` lets the event loop distinguish between results for **Search**, **Installed** (`"__INSTALLED__"`), and **Updates** (`"__UPDATES__"`). Stale results (where the tag no longer matches the current UI state) are discarded.

---

## Search Flow

```
User types a character
(50 ms debounce)
App spawns OS thread
PackageManager::search(&query) ← runs system command, parses output, scores results
result_tx.send((query, packages))
Main loop: result_rx.try_recv()
App updates packages list + triggers details fetch
```

The **50 ms debounce** is implemented in `input.rs` / `app.rs`: `last_input_time` is refreshed on every keystroke. `check_and_execute_search` is called each frame and only fires a thread when `Instant::now() - last_input_time >= 50ms` and the query has changed.

Comment on lines +23 to +40
---

## Details Fetch Flow

Whenever the selected row changes (navigation or new search results), `trigger_details_fetch` spawns a thread that calls `PackageManager::get_details`. Results arrive on `details_rx` and update the sidebar.

A global `DETAILS_CACHE` (`Arc<Mutex<HashMap<String, HashMap<String, String>>>>`) prevents redundant system calls for packages that have been inspected before.

---

## External Command Execution

When the user triggers an install (`i`), remove (`x`), upgrade (`U`), or refresh (`R`), TRX must hand control of the terminal to the package manager's interactive output. This is handled by `execute_external_command` in `main.rs`:

1. **Disable raw mode** — so the child process receives normal terminal I/O.
2. **Leave alternate screen** — the TUI disappears; the package manager's output is printed normally.
3. **Run the command** — via `std::process::Command`.
4. **Wait for Enter** — the user can review the output.
5. **Re-enter alternate screen and raw mode** — TRX redraws the TUI.

---

## Thread Safety

`manager` is wrapped in `Arc<Box<dyn PackageManager>>` (where `PackageManager: Send + Sync`). Cloning the `Arc` into a spawned thread is the only synchronisation needed for backend calls.
54 changes: 54 additions & 0 deletions book/src/architecture/fuzzy-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Fuzzy Search Engine

The fuzzy search engine lives in `src/fuzzy/mod.rs`. It is intentionally self-contained — no external crates — and is optimised for the substring-heavy patterns typical in package names.

---

## Public API

```rust
/// Returns a score in [0.0, ∞). Returns 0.0 when there is no fuzzy match.
pub fn fuzzy_match(query: &str, target: &str) -> f64;

/// Returns the character indices in `target` that match `query` in order,
/// or `None` if no such sequence exists.
pub fn fuzzy_get_indexes(query: &[char], target: &[char]) -> Option<Vec<usize>>;

/// Computes the final score given the matched positions.
pub fn calculate_score(query: &[char], target: &[char], indices: &[usize]) -> f64;
```

---

## Matching Algorithm

`fuzzy_get_indexes` performs a greedy left-to-right scan: for each character in `query` it finds the first remaining position in `target` that matches (case-insensitively). If any query character cannot be matched, `None` is returned and the package is excluded from results.

---

## Scoring

`calculate_score` is inspired by the [VS Code fuzzy finder algorithm](https://github.com/microsoft/vscode/blob/main/src/vs/base/common/fuzzyScorer.ts). The score rewards:

| Condition | Bonus |
|-----------|-------|
| Every matched character | +1.0 |
| Consecutive run of matches | +1.0 + 0.3 × run length |
| Match at position 0 (start of name) | +4.0 |
| Match after a separator (`-`, `_`, `/`, `.`, ` `) | +2.5 |

And penalises gaps between matched characters:

| Condition | Penalty |
|-----------|---------|
| Gap of *n* characters between consecutive matches | −0.15 × n |

The raw score is then normalised by `target_length * 0.15 + 1.0` to prevent long package names from dominating.

---

## Integration

`fuzzy_match` is called from `parse_alternating_lines` in `src/managers/mod.rs` for every package returned by a backend. Packages with a score ≤ 0.01 are dropped, and the remainder are sorted descending by score before being sent to the UI.

Each backend's `search` method may also pass the query directly to the underlying tool (e.g. `pacman -Ss <query>`), so the fuzzy layer acts as a **re-ranking** step on top of the backend's own filtering, not a replacement for it.
90 changes: 90 additions & 0 deletions book/src/architecture/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Architecture Overview

TRX is split into a small set of focused modules. Each module has a single responsibility and communicates with the others through well-defined interfaces.

```
src/
├── main.rs # Entry point, terminal setup, execute_external_command helper
├── config.rs # TOML configuration loading
├── updater.rs # GitHub release polling and binary self-replacement
├── ui/
│ ├── mod.rs
│ ├── app.rs # App state, event loop, channel polling
│ ├── draw.rs # ratatui rendering logic
│ └── input.rs # InputMode enum, character-level editing, debounce state
├── managers/
│ ├── mod.rs # PackageManager trait, Package struct, parse_alternating_lines, DETAILS_CACHE
│ ├── arch.rs # ArchManager — delegates to pacman.rs and yay.rs
│ ├── pacman.rs # Pacman system-call wrapper
│ ├── yay.rs # yay/AUR system-call wrapper
│ ├── apt.rs # AptManager
│ └── brew.rs # BrewManager
└── fuzzy/
└── mod.rs # Scoring-based fuzzy match engine
```

---

## Key Data Structures

### `Package`

Defined in `src/managers/mod.rs`, this is the universal package representation passed through all layers of the application:

```rust
pub struct Package {
pub provider: String, // e.g. "pacman", "aur", "apt", "brew"
pub name: String, // full name, possibly prefixed: "core/ripgrep"
pub version: String,
pub description: String,
pub score: f64, // fuzzy match score used for ranking
}
```

### `App`

Defined in `src/ui/app.rs`. Holds all runtime state:

- `input` — current search string
- `current_tab` — which of the three tabs is active
- `packages` — the currently displayed list
- `checked` / `selected_names` — multi-selection state
- `installed_packages` — `HashSet<String>` fetched once on startup
- `details_state` — sidebar content (`Empty | Loading | Success | Error`)
- `loading` — drives the spinner in the header
- `manager` — `Arc<Box<dyn PackageManager>>` shared across spawned threads

---

## Module Interactions

```
keyboard event
App::run() ──── spawns thread ──► PackageManager::search()
│ │
│◄── result_rx (mpsc) ◄───────────────┘
draw_ui() (ratatui frame render)
```

The event loop in `App::run` does three things every iteration:

1. **Poll keyboard** — via `crossterm::event::poll` with a short timeout so the loop never blocks long.
2. **Drain channels** — `try_recv` on `result_rx` and `details_rx` (non-blocking).
3. **Render** — call `draw_ui` to produce the next terminal frame.

---

## Startup Sequence

1. Parse CLI flags (`--version`, `--help`).
2. Call `updater::check_for_updates()` — if a newer release exists, self-update and exit.
3. Initialise the ratatui terminal (`ratatui::init`).
4. Load `Config` from the TOML file (or write defaults).
5. Call `managers::get_system_manager(&config)` to select the correct backend.
6. Create the `mpsc` channels and construct `App`.
Comment on lines +84 to +88
7. Enter `App::run()` — the main event loop.
8. On exit, restore the terminal (`ratatui::restore`).
Loading
Loading