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
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ jobs:
- name: Test
run: cargo test --locked

- name: Clippy (lite)
run: cargo clippy --locked --all-targets --no-default-features -p wiki-reader -p wiki-reader-render -p wiki-reader-media -- -D warnings

- name: Test (lite)
run: cargo test --locked --no-default-features -p wiki-reader -p wiki-reader-render -p wiki-reader-media

- name: Lite tree has no image deps (ADR-0023)
run: |
tree=$(cargo tree --locked --no-default-features -p wiki-reader -e normal)
if echo "$tree" | grep -E '(^|[^A-Za-z0-9_-])(image|resvg|mermaid-rs-renderer|ratatui-image) v'; then
echo "lite build must not pull image, resvg, mermaid-rs-renderer or ratatui-image (ADR-0023)" >&2
exit 1
fi

- name: Link check
run: cargo run --locked --quiet -p wiki-reader-tools --bin link-check

Expand Down
14 changes: 11 additions & 3 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ mermaid-rs-renderer = { version = "0.3.1", default-features = false }
resvg = { version = "0.48.1", default-features = false, features = ["text"] }
signal-hook = "0.3.18"
wiki-reader-core = { path = "crates/wiki-reader-core", version = "0.1.4" }
wiki-reader-render = { path = "crates/wiki-reader-render", version = "0.1.4" }
# Heavy media deps are opt-in per crate: the lite build drops them (ADR-0023).
wiki-reader-media = { path = "crates/wiki-reader-media", version = "0.1.4", default-features = false }
wiki-reader-render = { path = "crates/wiki-reader-render", version = "0.1.4", default-features = false }

[workspace.lints.rust]
unsafe_code = "forbid"
Expand Down
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ A terminal wiki reader for markdown collections. It browses like a documentation
│ ││ │
│ ││ Start at Architecture Overview, follow Token │
│ ││ Projection, then return via back when history │
│ ││ exists. Enter a picture for Tab carousel / a fit. │
│ │├────────────┬─────────────┬─────────────────────────┤
│ ││ ‹ Overview │ │ Architecture Overview › │
└────────────────────────┘└────────────┴─────────────┴─────────────────────────┘
Expand All @@ -29,7 +30,7 @@ A terminal wiki reader for markdown collections. It browses like a documentation

Help (`?`), search (`/`) and options (`,` or `c`) are popups over the same layout. Click `?` / ⚙ at the full-width layout footer's bottom right, below both panes, for Help / Options; the header retains ◫ / ✕ for nav / quit.

Nav rows show page titles by default (`nav.labels = "filename"` switches to filenames); folders always show the folder name. Press `?` in the app for every key.
Nav rows show page titles by default (`nav.labels = "filename"` switches to filenames); folders always show the folder name. Press `?` in the app for every key. In the image / diagram viewer: `Tab` / `Shift+Tab` step through pictures on the page, `a` toggles fit / actual size, `v` cycles a diagram's image / text / source view.

## Why not an existing reader?

Expand All @@ -45,7 +46,7 @@ Other tools that shaped it, and what each contributed, are listed in [Prior art

## Status

Phase 2 is feature-complete on a dogfood hold (clock 2026-09-29 → ≈ 2026-10-13); Phase 3 alpha polish is active. See the [roadmap](wiki/roadmap/README.md) for what shipped and what's next, and the [dogfood log](wiki/roadmap/dogfood-log.md) for dated bites. Prereleases and binaries are on [GitHub Releases](https://github.com/luckgrid/wiki-reader/releases).
Phase 2 is feature-complete on a dogfood hold (clock 2026-09-29 → ≈ 2026-10-13). Phase 3 alpha polish is implemented on a branch for v0.1.5 (lite build, viewer sizing, carousel, diagram views); the phase stays active until review, the screenshot retake and installed-binary dogfood. See the [roadmap](wiki/roadmap/README.md) and the [dogfood log](wiki/roadmap/dogfood-log.md). Prereleases and binaries are on [GitHub Releases](https://github.com/luckgrid/wiki-reader/releases).

## Install

Expand All @@ -55,6 +56,12 @@ From git (supported today):
cargo install --locked --git https://github.com/luckgrid/wiki-reader wiki-reader
```

Lite build (no images, SVG or Mermaid pictures; diagrams stay text, source or a placeholder; smaller binary, faster build; see [ADR-0023](wiki/decisions/0023-lite-build-is-a-cargo-feature.md)). It is a cargo feature only; there is no `-lite` release download:

```bash
cargo install --locked --no-default-features --git https://github.com/luckgrid/wiki-reader wiki-reader
```

Release binaries (macOS arm64 / x86_64, Linux x86_64) ship on `v*` tags under [GitHub Releases](https://github.com/luckgrid/wiki-reader/releases). Download the matching `.tar.gz`, verify the checksum, and put `wiki-reader` on your `PATH`:

```bash
Expand Down Expand Up @@ -104,17 +111,14 @@ such as overlays and popups show text only.)
## Checks

```bash
cargo fmt --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked
cargo run --locked --quiet -p wiki-reader-tools --bin link-check
rumdl fmt --check .
rumdl check .
./scripts/check.sh # default and lite builds; `default` or `lite` runs one half
```

That runs `cargo fmt --check`, clippy and tests for both feature sets, link-check, `rumdl` and the dependency-tree guards.

## Workspace

Cargo workspace with `wiki-reader-core` (terminal-free), `wiki-reader-render`, and the `wiki-reader` TUI binary. Docs live in `wiki/`. See [guides/development.md](wiki/guides/development.md).
Cargo workspace with `wiki-reader-core` (terminal-free), `wiki-reader-render`, `wiki-reader-media` (image decode and Mermaid/SVG rasterisation), and the `wiki-reader` TUI binary. Docs live in `wiki/`. See [guides/development.md](wiki/guides/development.md).

## License

Expand Down
4 changes: 3 additions & 1 deletion assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

The documentation screenshot of the app browsing its own wiki, used by the root [README](../README.md). They are not test fixtures: they are large and change with the UI. Fixtures for image tests live under `fixtures/` and stay tiny.

`wiki/assets/` holds an identical copy of it so wiki pages can show it from inside the collection root (images outside it are not rendered, [ADR-0017](../wiki/decisions/0017-static-local-images-only.md)), plus the help and search screenshots, which only the wiki shows. Update both copies of `wiki-reader.png` when retaking it. A full retake (new theme, footer icons, tab/footer borders) waits for [P3-23](../wiki/roadmap/phase-3-alpha.md) so screenshots are not taken twice.
`wiki/assets/` holds an identical copy of it so wiki pages can show it from inside the collection root (images outside it are not rendered, [ADR-0017](../wiki/decisions/0017-static-local-images-only.md)), plus the help and search screenshots, which only the wiki shows. Update both copies of `wiki-reader.png` when retaking it.

P3-23 refreshed the README / ui-spec ASCII diagram and documented the carousel keys. The PNG retake (`assets/wiki-reader.png`, `wiki/assets/wiki-reader.png`, and ideally help/search) still needs an operator pass in Ghostty with Screen Recording permission — the agent session could not write a display capture.
27 changes: 27 additions & 0 deletions crates/wiki-reader-media/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "wiki-reader-media"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "Image decode and Mermaid/SVG rasterisation for wiki-reader"
rust-version.workspace = true
repository.workspace = true
homepage.workspace = true
readme = "../../README.md"

[features]
default = ["raster"]
# Heavy deps (ADR-0023): off in the lite build, leaving only the palette and fit maths.
raster = ["dep:image", "dep:resvg", "dep:mermaid-rs-renderer"]

[dependencies]
image = { workspace = true, optional = true }
mermaid-rs-renderer = { workspace = true, optional = true }
resvg = { workspace = true, optional = true }
wiki-reader-core = { workspace = true }

[lints]
workspace = true

[lib]
path = "src/lib.rs"
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
- SHA-256: `b85c38ecea8a7cfb39c24e395a4007474fa5a4fc864f6ee33309eb4948d232d5`
- License: SIL Open Font License 1.1; see `OFL.txt`.

Used by `mermaid_raster` so Mermaid and SVG rasterisation never depends on system fonts.
Used by the `raster` module of `wiki-reader-media` so Mermaid and SVG rasterisation never depends on system fonts.
58 changes: 58 additions & 0 deletions crates/wiki-reader-media/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! Media helpers (P3-19): the diagram palette and fit maths are always built; image decode and
//! Mermaid/SVG rasterisation sit behind the default `raster` feature (the lite build drops it).
//! This crate must not depend on `wiki-reader-render`.

mod palette;

pub use palette::DiagramPalette;

#[cfg(feature = "raster")]
mod raster;
#[cfg(feature = "raster")]
pub use raster::{
RasterError, RasterImage, decode_file, image_dimensions, mermaid_svg_bytes, mermaid_to_svg,
rasterise_svg, rasterise_svg_scaled, render_mermaid, render_mermaid_for_pane, svg_natural_size,
};

/// Minimum scale of natural diagram pixels into the slot before the text tier is preferred.
pub const MIN_LEGIBLE_SCALE: f64 = 0.55;

/// Fit scale for `px_w × px_h` into `max_cols` × `max_rows` at `cell_px`. Never upscales.
#[must_use]
pub fn fit_scale(px_w: u32, px_h: u32, cell_px: (u16, u16), max_cols: u16, max_rows: u16) -> f64 {
let cell_w = f64::from(cell_px.0.max(1));
let cell_h = f64::from(cell_px.1.max(1));
let w = f64::from(px_w.max(1));
let h = f64::from(px_h.max(1));
let max_cols = max_cols.max(1);
(f64::from(max_cols) * cell_w / w)
.min(f64::from(max_rows.max(1)) * cell_h / h)
.min(1.0)
}

/// True when fitting `px_w`×`px_h` into `max_cols` at `cell_px` keeps scale ≥ [`MIN_LEGIBLE_SCALE`].
#[must_use]
pub fn is_legible(px_w: u32, px_h: u32, cell_px: (u16, u16), max_cols: u16, max_rows: u16) -> bool {
fit_scale(px_w, px_h, cell_px, max_cols, max_rows) >= MIN_LEGIBLE_SCALE
}

/// True for a `.svg` path (case-insensitive).
#[must_use]
pub fn is_svg_path(path: &std::path::Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("svg"))
}

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

#[test]
fn legibility_gate_rejects_wide_natural_size() {
// 2000 px into 80×8 = 640 px pane → scale 0.32 < 0.55.
assert!(!is_legible(2000, 400, (8, 17), 80, 30));
// Compact diagram stays legible.
assert!(is_legible(400, 200, (8, 17), 80, 30));
}
}
44 changes: 44 additions & 0 deletions crates/wiki-reader-media/src/palette.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! Diagram colours: plain RGB, no heavy deps. The raster module adds the theme overlay.

/// Colours a Mermaid diagram is drawn with, so the picture matches the active theme.
///
/// Plain RGB so the renderer stays terminal-free; the TUI fills it from its theme tokens. It is
/// part of every diagram cache key, so changing preset re-measures and re-rasterises.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DiagramPalette {
/// Card background, also used to fill the slot's padding so the picture is opaque.
pub bg: (u8, u8, u8),
/// Labels and titles.
pub text: (u8, u8, u8),
/// Edges and axes.
pub line: (u8, u8, u8),
/// Node fill.
pub node_fill: (u8, u8, u8),
/// Node outline.
pub node_border: (u8, u8, u8),
/// Subgraph / cluster fill.
pub cluster_fill: (u8, u8, u8),
/// Subgraph / cluster outline.
pub cluster_border: (u8, u8, u8),
/// Sequence / state note fill (its text uses [`text`](Self::text)).
pub note_fill: (u8, u8, u8),
/// Sequence / state note outline.
pub note_border: (u8, u8, u8),
}

impl Default for DiagramPalette {
/// Dark card, matching the default (dark) theme's code background.
fn default() -> Self {
Self {
bg: (30, 32, 36),
text: (226, 229, 236),
line: (140, 148, 164),
node_fill: (44, 48, 58),
node_border: (110, 120, 140),
cluster_fill: (38, 41, 50),
cluster_border: (80, 88, 104),
note_fill: (58, 54, 40),
note_border: (150, 130, 80),
}
}
}
Loading
Loading