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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ members = [

[package]
name = "retch-cli"
version = "0.6.17"
version = "0.6.18"
edition = "2021"
authors = ["Ken Tobias"]
description = "A fast, feature-rich system information fetcher written in Rust (similar to fastfetch or neofetch)"
Expand All @@ -21,7 +21,7 @@ name = "retch"
path = "src/main.rs"

[dependencies]
retch-sysinfo = { path = "crates/sysinfo", version = "=0.1.52" }
retch-sysinfo = { path = "crates/sysinfo", version = "=0.1.53" }
clap = { version = "4.6", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
toml = "1.1"
Expand Down
95 changes: 94 additions & 1 deletion NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,68 @@ The `retch-sysinfo` crate can be used independently as a library for cross-platf

---

## Current State (v0.6.17)
## Current State (v0.6.18)
- **v0.6.18 — `Packages` without root, Rio detection under `sudo`, and aspect-correct logo
scaling** (`crates/sysinfo/src/packages.rs`, `src/logo.rs`, `src/display.rs`). Three
user-reported defects, all found by diffing a `sudo retch --full` run against a plain one
on corrino (i7-1360P, Fedora 44, Rio).
- **`Packages` appeared only under `sudo`** (bugfix). `detect_packages` opened
`/var/lib/rpm/rpmdb.sqlite` with `rusqlite::Connection::open`, i.e. **read-write**. The
rpmdb is `root:root 0644` inside a root-owned directory, so SQLite cannot create the
journal sidecars it wants and **every query** fails with `attempt to write a readonly
database` — note *query*, not `open()`, which is why the existing `eprintln!` (guarding
only the open) never fired and the field vanished with no diagnostic at all. Plain
`mode=ro` does not help for the same reason; it still needs to touch the directory. Fixed
with `open_with_flags(rpm_db_uri(path), SQLITE_OPEN_READ_ONLY | SQLITE_OPEN_URI |
SQLITE_OPEN_NO_MUTEX)` over a `file:…?immutable=1` URI, which lets SQLite skip locking and
sidecars entirely. Verified end to end: `2509` as an unprivileged user, byte-identical to
what the `sudo` run reported. The query error is now surfaced rather than swallowed, so
the next failure in this path says why. Pure `rpm_db_uri` helper, 2 unit tests.
- **Rio lost graphics support under `sudo`** (bugfix). `supports_kitty`/`supports_iterm2`/
`supports_sixel` identified Rio **only** by `TERM_PROGRAM`, which is not in sudo's default
`env_keep`, so `sudo retch` silently fell all the way through to Chafa while the same
command as the user used the Kitty protocol. New `is_rio_terminal()` also accepts
`TERM=rio`/`xterm-rio` — `TERM` *is* preserved by sudo — and all three checks route through
it. 3 unit tests, including a negative case pinning that `rioja` is not matched.
- **Test-isolation defect fixed in the same change, same class as #155/v0.6.2:**
`test_supports_iterm2_heuristics` guarded only `TERM_PROGRAM`, so once `supports_iterm2`
began reading `TERM` the *host's* value leaked in and its negative assertions failed on a
Rio box while passing on CI and everywhere else. It now guards and clears `TERM` too.
- **The Kitty logo was stretched ~3× vertically** (bugfix). `print_graphical_logo` emitted a
hardcoded `c=26,r=10`, and Kitty **forces** an image into the `c`×`r` rectangle — it does
not preserve aspect ratio when both are given. Five of the assets are wide horizontal
lockups (`fedora.png` is 384×108, i.e. 3.56:1; also arch/nixos/ubuntu/tux), so they were
squashed into a roughly 1:1 cell box. Compounding it, `display.rs` separately assumed a
fixed **40**-column width for layout while `graphical_logo_height_lines` derived the row
count a third way — three inconsistent answers for one footprint. Fixed with a single pure
`fit_logo_cells(img_w, img_h, cell_w, cell_h, max_cols, max_rows) -> LogoFit` that fits the
image in the cell box preserving aspect (in pixels, so non-square cells are handled), now
used by **all three** protocol emitters *and* by `plan_layout`. iTerm2 additionally passes
an explicit cell `width` alongside `preserveAspectRatio=1`; Sixel resizes to the same box
rather than a fixed 240×200. 6 unit tests.
- **Computing `c` and `r` correctly is not sufficient — Kitty must be given only *one* of
them.** Cells are indivisible, so the rounded rectangle is never exactly the image's
aspect, and Kitty scales each axis independently to fill whatever rectangle it is given.
Measured in a PTY with real pixel dimensions (169×47 cells, 22×51 px): passing both
correct values still left a **9%** vertical stretch. `LogoFit::width_limited` records
which dimension the image touches first and `kitty_placement_spec` emits just that one
(`c=45` for Fedora), letting Kitty derive the other — **0.0% aspect error**, verified the
same way. The layout's `div_ceil` reservation (6 rows for a 5.46-row draw) still covers
it, which is the safe direction.
- **Chafa logo box widened 28→45 columns** (`LOGO_MAX_COLS`), height cap unchanged at 10
(`LOGO_MAX_ROWS`). Chafa fits *within* the box preserving aspect, so a narrow box caps a
wide image's height long before the row cap does: at 28 columns the Fedora logo collapsed
to **4 rows** of symbols and was unreadable; at 45 it renders **7**. Both chafa call sites
now share `chafa_size_arg()`. **The side-by-side threshold is unaffected** — the text column floors
at 45 and 45 + 45 = 90 ≤ 95, so a full-width logo still sits beside the text at the 95-col
cutoff; pinned by a new `plan_layout` test at both 95 and 169 columns.
- Assets deliberately **not** changed: cropping the wide lockups to their square icon halves
would render larger still, but it is a content decision and was declined in favour of the
layout-only fix.
- New §6b documents the privilege-dependent fields in both directions (root-only
`phys-mem`/btrfs snapshots; user-only `editor`/`desktop`/`wm`), mirrored in `README.md`
and a new `PRIVILEGES` section in `docs/retch.1.md`.
- `retch-sysinfo` → `0.1.53` (library behaviour change); `retch-cli` → `0.6.18`. Patch bump.
- **v0.6.17 — Disabled Claude Code Review on GitHub Actions CI** (`.github/workflows/claude-code-review.yml`).
Disabled the `pull_request` trigger and set `if: false` on the `claude-review` job in `claude-code-review.yml`. `retch-cli` → `0.6.17`. Patch bump.
- **v0.6.16 — Graphic logo size reduction and controlled info line wrapping** (`src/display.rs`, `src/logo.rs`, `crates/sysinfo/src/audio.rs`).
Expand Down Expand Up @@ -889,6 +950,38 @@ load-average equivalent), `editor` (env-only `$VISUAL`/`$EDITOR`), conhost `term

---

## 6b. Privilege-dependent fields (Linux) — what `sudo retch` changes, both directions

Running retch under `sudo` does not simply add fields: it adds some and **removes others**,
because `sudo`'s default `env_reset` strips most of the environment. Diffing a `sudo --full`
run against a plain one is therefore not a fair before/after, and the differences below are
expected behaviour, not bugs. (`Packages` used to be on this list and no longer is — see the
v0.6.18 entry.)

**Only available as root**
- **`phys-mem`** — reads `/sys/firmware/dmi/tables/DMI`, mode `0400 root`, via `dmidecode`.
There is no unprivileged source for per-DIMM type/capacity/speed on Linux, so the field is
omitted rather than guessed. (Windows reads SMBIOS natively and needs no elevation.)
- **`btrfs` snapshot count** — `btrfs subvolume list -s` requires root. Deliberately
**omitted rather than shown as `0`** when it cannot be read, so "couldn't check" is never
mistaken for "no snapshots"; the label/subvolume/space part of the field still renders.

**Only available as the logged-in user** (lost under `sudo`, since `env_reset` drops them)
- **`editor`** — `$VISUAL` / `$EDITOR`.
- **`desktop`**, **`wm`** — `XDG_CURRENT_DESKTOP` / `XDG_SESSION_DESKTOP` / `GDMSESSION`.
- **Logo protocol selection** — `TERM_PROGRAM` is not in sudo's `env_keep`. This used to
silently downgrade Rio from the Kitty graphics protocol to Chafa; since v0.6.18 the Rio
check also consults `TERM` (`xterm-rio`), which sudo *does* preserve. Any other terminal
identified solely by `TERM_PROGRAM` still degrades under sudo by design — it is a heuristic
over an environment sudo is entitled to clear.

**Rule of thumb for this class of bug:** if a field is missing only for the unprivileged user,
check whether the underlying source is genuinely root-only before adding an elevation note —
`Packages` looked exactly like a permissions limit for a long time and was in fact a fixable
SQLite open-mode defect.

---

## 7. Major Achievements

### v0.6.1 - Fix Windows Camera (scanners) + Users (=0) bugs (July 13, 2026)
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ Show help:
retch --help
```

### Running under `sudo`

`sudo retch` is not simply "retch with more fields" — it trades one set for another, because
`sudo`'s default `env_reset` clears most of the environment:

| | Field |
|---|---|
| **Root only** | `phys-mem` (reads the DMI tables, mode `0400 root`); the snapshot count in `btrfs` (`btrfs subvolume list -s`) |
| **User only** | `editor` (`$VISUAL`/`$EDITOR`), `desktop` and `wm` (`XDG_CURRENT_DESKTOP` and friends) |

Everything else is identical either way, so run retch normally unless you specifically want
the DIMM breakdown or btrfs snapshot counts.

## Shell Completions

Generate completion scripts for your shell:
Expand Down Expand Up @@ -235,6 +248,8 @@ separator_color = "bright_black"

# Ordered list of system information fields to display
# Note: "phys-mem" requires root (sudo) on Linux to read DMI memory tables. On Windows, reads the SMBIOS table natively (no PowerShell).
# Note: "btrfs" snapshot counts require root on Linux; the count is omitted (not shown as 0) when it can't be read.
# Note: "editor", "desktop" and "wm" read environment variables, so they are absent under `sudo` (env_reset).
# Note: "phys-disk" on Windows uses native storage IOCTLs (no PowerShell, no admin).
# Note: "weather" requires network access; shown in full mode only by default.
# Note: "domain-search" queries resolvectl; shown in full mode only by default.
Expand Down
2 changes: 1 addition & 1 deletion crates/sysinfo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "retch-sysinfo"
version = "0.1.52"
version = "0.1.53"
edition = "2021"
authors = ["Ken Tobias"]
description = "System information gathering library for retch"
Expand Down
58 changes: 53 additions & 5 deletions crates/sysinfo/src/packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@
//! Supports Pacman (Arch), Dpkg (Debian), XBPS (Void), RPM (Fedora/RHEL) on Linux,
//! Homebrew (Formulae and Casks) and MacPorts on macOS, and Scoop/Chocolatey on Windows.

/// Builds the SQLite URI used to read the RPM database without write access.
///
/// `/var/lib/rpm/rpmdb.sqlite` is owned by root (mode 0644) inside a root-owned directory,
/// so an unprivileged process cannot create the journal sidecar files SQLite wants — and
/// SQLite reports that as `attempt to write a readonly database` on the **query**, not on
/// `open()`. Plain `mode=ro` is not enough for the same reason: it still needs to touch the
/// directory. `immutable=1` promises SQLite the file will not change while it is open, which
/// lets it skip locking and sidecars entirely, so the count succeeds as a normal user.
///
/// This is why `Packages` previously appeared only under `sudo`.
#[cfg(any(not(any(target_os = "macos", target_os = "windows")), test))]
fn rpm_db_uri(path: &str) -> String {
format!("file:{path}?immutable=1")
}

pub(crate) fn detect_packages() -> Option<usize> {
#[cfg(target_os = "macos")]
{
Expand Down Expand Up @@ -105,22 +120,55 @@ pub(crate) fn detect_packages() -> Option<usize> {

let rpm_db = "/var/lib/rpm/rpmdb.sqlite";
if std::path::Path::new(rpm_db).exists() {
match rusqlite::Connection::open(rpm_db) {
use rusqlite::OpenFlags;
let flags = OpenFlags::SQLITE_OPEN_READ_ONLY
| OpenFlags::SQLITE_OPEN_URI
| OpenFlags::SQLITE_OPEN_NO_MUTEX;
match rusqlite::Connection::open_with_flags(rpm_db_uri(rpm_db), flags) {
Ok(conn) => {
if let Ok(count) = conn.query_row("SELECT COUNT(*) FROM Packages", [], |row| {
match conn.query_row("SELECT COUNT(*) FROM Packages", [], |row| {
row.get::<_, i64>(0)
}) {
if count > 0 {
return Some(count as usize);
Ok(count) if count > 0 => return Some(count as usize),
Ok(_) => {}
// Surfaced rather than swallowed: the read-only-database failure this
// URI exists to prevent used to land here and vanish silently, so the
// field simply disappeared with no clue why.
Err(e) => {
eprintln!("warning: failed to query RPM database at {rpm_db}: {e}");
}
}
}
Err(e) => {
eprintln!("warning: failed to open RPM database at {}: {}", rpm_db, e);
eprintln!("warning: failed to open RPM database at {rpm_db}: {e}");
}
}
}

None
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_rpm_db_uri_requests_immutable() {
// `immutable=1` is the load-bearing part: without it an unprivileged read of the
// root-owned rpmdb fails with "attempt to write a readonly database".
assert_eq!(
rpm_db_uri("/var/lib/rpm/rpmdb.sqlite"),
"file:/var/lib/rpm/rpmdb.sqlite?immutable=1"
);
}

#[test]
fn test_rpm_db_uri_is_a_file_uri() {
// The `file:` scheme is what makes SQLITE_OPEN_URI parse the query string at all;
// a bare path would silently ignore `immutable=1`.
let uri = rpm_db_uri("/tmp/some.sqlite");
assert!(uri.starts_with("file:"));
assert!(uri.contains("?immutable=1"));
}
}
33 changes: 32 additions & 1 deletion docs/retch.1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.TH "RETCH" "1" "August 2026" "retch 0.6.17" "System Information Fetcher"
.TH "RETCH" "1" "August 2026" "retch 0.6.18" "System Information Fetcher"

.SH "NAME"
.PP
Expand Down Expand Up @@ -441,6 +441,37 @@ Logos are automatically suppressed when stdout is not a terminal (e\.g\. when pi
.PP
Use `\f[CR]\-\-ascii\-only\fP` to force text\-only output\. Use `\f[CR]\-\-no\-logo\fP` to suppress the logo unconditionally\.

.SH "PRIVILEGES"
.PP
Running \fBretch\fP under \fBsudo\fP(8) exchanges one set of fields for another rather than
simply adding to them, because sudo's default `\f[CR]env_reset\fP` clears most of the environment\.
.PP
Available only as root:
.RS
.Bl
.IP \(bu 4
`\f[CR]phys\-mem\fP` \-\- the per\-DIMM breakdown reads the DMI tables (`\f[CR]/sys/firmware/dmi/tables/DMI\fP`, mode 0400)\.
.El
.Bl
.IP \(bu 4
`\f[CR]btrfs\fP` \-\- the snapshot count requires `\f[CR]btrfs subvolume list \-s\fP`\. It is omitted, never shown as zero, when it cannot be read\.
.El
.RE
.PP
Available only as the logged\-in user:
.RS
.Bl
.IP \(bu 4
`\f[CR]editor\fP` \-\- read from `\f[CR]$VISUAL\fP` / `\f[CR]$EDITOR\fP`\.
.El
.Bl
.IP \(bu 4
`\f[CR]desktop\fP`, `\f[CR]wm\fP` \-\- read from `\f[CR]XDG_CURRENT_DESKTOP\fP`, `\f[CR]XDG_SESSION_DESKTOP\fP` and `\f[CR]GDMSESSION\fP`\.
.El
.RE
.PP
All other fields are identical either way\.

.SH "EXIT STATUS"
.PP
\fBretch\fP exits with status 0 on success, and non\-zero on error\.
Expand Down
17 changes: 17 additions & 0 deletions docs/retch.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,23 @@ retch supports both ASCII and graphical logos.

Use `--ascii-only` to force text-only output. Use `--no-logo` to suppress the logo unconditionally.

# PRIVILEGES

Running **retch** under **sudo**(8) exchanges one set of fields for another rather than
simply adding to them, because sudo's default `env_reset` clears most of the environment.

Available only as root:

- `phys-mem` — the per-DIMM breakdown reads the DMI tables (`/sys/firmware/dmi/tables/DMI`, mode 0400).
- `btrfs` — the snapshot count requires `btrfs subvolume list -s`. It is omitted, never shown as zero, when it cannot be read.

Available only as the logged-in user:

- `editor` — read from `$VISUAL` / `$EDITOR`.
- `desktop`, `wm` — read from `XDG_CURRENT_DESKTOP`, `XDG_SESSION_DESKTOP` and `GDMSESSION`.

All other fields are identical either way.

# EXIT STATUS

**retch** exits with status 0 on success, and non-zero on error.
Expand Down
Loading