marten/README.md

359 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# marten
**A modern, accuracy-first image viewer for Linux.**
Version 0.4.0 · GPL-2.0-or-later · Built with Rust, iced, wgpu, and winit.
Marten is a desktop image viewer for Linux, written from scratch in Rust.
Named after the marten (genus *Martes*) — a small agile mustelid native to
forests across the Northern Hemisphere. Like its cousin the ferret (marten's
sibling app for video playback), the marten is quick, curious, and nimble.
Fitting energy for a photo viewer designed to move fast through large libraries.
---
## What marten does
Marten is a **single-window image viewer** focused on fast folder-based
browsing. It is not a photo manager, not an editor, not a library tool. It
opens a folder, shows you the images in it, and gets out of your way.
### Core features (v0.4.0)
- **Ristretto-style scroll-wheel navigation** — scroll the wheel anywhere in
the image area to shuffle to the next/previous photo in the folder.
Natural-scroll direction (scroll up = previous, scroll down = next).
- **gpicview-style bottom toolbar** — all controls live in a thin bottom bar,
leaving the top free for the thumbnail strip. SVG icons (Lucide) throughout.
- **Top thumbnail strip** — lazy-loaded horizontal strip at the top of the
window. Current image highlighted with an accent border. Click any
thumbnail to jump to it. Thumbnails decode on a tokio `spawn_blocking`
thread pool at 72px and are cached in an LRU window of ±15 around the
current image.
- **Tier 2 format support** — JPEG XL (`.jxl`), TIFF (`.tif`/`.tiff`),
SVG (`.svg`/`.svgz`), and OpenEXR (`.exr`) join the Tier 1 roster via
dedicated codec modules. Thumbnails and EXIF parsing apply to every
supported format.
- **Step-aware random navigation** — `z` jumps to a random image in the
current folder; `Shift+Z` walks the parent folder tree recursively,
collects every supported image across all subfolders, and picks one
at random. If the chosen image lives in a different subfolder, marten
switches to that folder automatically. Both actions are step-aware:
subsequent arrow/scroll navigation continues sequentially from the
new position rather than from where you left off.
- **Timeline-style thumbnail auto-scroll** — the top thumbnail strip
auto-scrolls to keep the current image centered, like a video
editor's playhead on a timeline. Triggered on every navigation event
(scroll, arrow keys, random jump, thumbnail click).
- **Folder-as-video export** — right-click → Export folder as video…
opens a modal that turns the current folder into a `.webm` slideshow
with a user-selected audio track. ffmpeg runs in a background thread
via `tokio::task::spawn_blocking`; VP9 (`libvpx-vp9`) and AV1
(`libaom-av1`) codecs are selectable. Requires ffmpeg installed.
- **Togglable folder tree sidebar** — press `Tab` to show a 240px left
panel listing sibling folders (children of the current parent that
contain images), each with an image count. Click a folder to switch to
it. The sidebar hides in fullscreen.
- **Slideshow mode** — press `s` to auto-advance to the next photo every
three seconds via an `iced::time::every` subscription. Press `s` again
or `Escape` to stop. A toast confirms the state change.
- **EXIF properties panel** — press `i` or right-click → Properties to
open a modal showing filename, path, dimensions, format, file size, and
EXIF metadata (camera make/model, lens, ISO, aperture, shutter speed,
focal length, timestamp, GPS coordinates, orientation) parsed via
`kamadak-exif`.
- **Fit-to-window rendering** — iced's native `ContentFit::Contain` handles
centering and scaling. No manual offset math, no overflow, no scrollbars
in fit mode. Image always perfectly centered in the viewport.
- **Zoom modes** — `0` for fit-to-window, `1` for 100%, `+`/`-` or
`Ctrl+scroll` for custom zoom. Each zoom step multiplies the factor by
1.1 (or divides by 1.1) for smooth, graceful changes. In zoom modes
the image is the direct child of a bidirectional `scrollable` with
`ContentFit::Contain` and `Length::Fixed(dw/dh)` — the previous
`Length::Fill` wrapper collapsed inside `scrollable` and hid the image.
Pan by dragging the scrollbars or scrolling.
- **Visual rotation** — `R` rotates 90° clockwise, `Shift+R` counter-
clockwise. Rotation is applied by pre-rotating the RGBA pixel buffer
(iced 0.13 lacks native image rotation; the buffer-rotation approach
is the chosen implementation). The original image is preserved so
rotation is non-destructive and reversible.
- **gpicview-style right-click context menu** — 12 actions, all functional:
- Open With… (xdg-open)
- Copy Path (to clipboard)
- Copy Image (pixel data to clipboard)
- Copy to Pictures (Shift+Home — copies file to `~/Pictures/`)
- Rotate 90° CW / CCW
- Set as Wallpaper (gsettings, with feh as alternate backend)
- Move to Trash (trash crate)
- Delete Permanently (Shift+Delete — `std::fs::remove_file`)
- Properties (opens the EXIF properties panel)
- Export folder as video… (opens the video export dialog)
- About marten (shows the About dialog)
- **Anti-list error modal** — opening a file on the project's anti-list
(HEIC, CR3, NEF, ARW, PSD, DNG, Apple Live Photos, etc.) shows a
full-screen modal explaining the project's stance and suggesting open
alternatives. The viewer does NOT silently skip these files; it tells
you *why* it refuses to open them.
- **Configurable keymap** — every keybinding is overridable via
`~/.config/marten/keymap.toml`. Ship-the-defaults works out of the box;
power users can remap everything.
- **Fullscreen mode** — `F11` or `Shift+F` collapses all chrome; image
fills the window. A small floating hint at the top-center of the
screen ("Press F11 to exit fullscreen") with a semi-transparent dark
background reminds you how to leave. Press again to exit.
- **About dialog** — press `a` or click the info button in the toolbar.
Matches the ferret app's About style: orange accent border, dark card,
author/website/license rows, build info, copyright.
---
## Format support
Marten takes a deliberate stance on format support. There are three tiers
plus an explicit anti-list.
### Tier 1 — supported (via the `image` crate)
| Format | Extensions | Notes |
|---|---|---|
| PNG | `.png` `.apng` | Including animated PNG |
| JPEG | `.jpg` `.jpeg` `.jfif` | Baseline + progressive |
| GIF | `.gif` | Including animation |
| WebP | `.webp` | Lossy + lossless + animated |
| AVIF | `.avif` | HDR, animation, alpha — our modern baseline |
| BMP | `.bmp` | Legacy compatibility |
| ICO / CUR | `.ico` `.cur` | Windows icon / cursor |
### Tier 2 — supported (dedicated codec modules)
| Format | Extensions | Decoder crate |
|---|---|---|
| JPEG XL | `.jxl` | `jxl-oxide` |
| TIFF | `.tif` `.tiff` | `tiff` |
| SVG | `.svg` `.svgz` | `resvg` (with `usvg` + `tiny-skia`) |
| OpenEXR | `.exr` | `exr` |
### Tier 3 — niche
QOI, JPEG 2000, JPEG XS.
### 🚫 Anti-list — will NEVER be supported
Marten explicitly rejects proprietary or patent-encumbered formats:
- **HEIF / HEIC** — HEVC patent-licensing baggage. AVIF covers the same use case royalty-free.
- **Canon CR3, Nikon NEF, Sony ARW** — proprietary camera RAW specs.
- **Adobe PSD** — proprietary Photoshop format.
- **Adobe DNG** — "partially open"; Adobe-controlled.
- **Apple Live Photos** — proprietary paired image+video container.
If you open one of these files, marten shows a modal explaining the stance
and suggesting an open alternative. See `DECISION.md` (decisions D002 and
D007) for the full rationale.
---
## Project layout
The codebase is a single Cargo binary crate with internal modules. Total
source size: 6,008 lines of Rust across 25 files, with 35 unit tests.
```
marten/
├── Cargo.toml # package metadata, deps, release profile
├── Cargo.lock # pinned dependency versions
├── LICENSE # GPL-2.0 full text
├── README.md # this file
├── QUICKSTART.md # 5-minute getting-started guide
├── BLOG.md # v0.4.0 release announcement
├── DECISION.md # architectural decision records (D001D016)
├── config/
│ ├── keymap.toml # default keymap, user-overridable
│ └── settings.toml # default settings, user-overridable
├── bin/
│ └── marten # prebuilt release binary (x86-64 Linux, 24.5 MB)
├── src/
│ ├── main.rs # 26 lines — entry point, iced bootstrap
│ ├── app.rs # 1,317 lines — Viewer state, Message dispatch, subscriptions
│ ├── config.rs # 367 lines — keymap.toml parser + loader
│ ├── settings.rs # 187 lines — settings.toml parser + loader
│ ├── codec/
│ │ ├── mod.rs # 339 lines — FormatRegistry, Codec trait, rotate_rgba()
│ │ ├── anti_list.rs # 126 lines — 8 rejected formats with reasons
│ │ ├── image_crate.rs # 110 lines — Tier 1 decoder (image crate)
│ │ ├── jxl.rs # 141 lines — JPEG XL decoder (jxl-oxide)
│ │ ├── tiff.rs # 189 lines — TIFF decoder (tiff crate)
│ │ ├── svg.rs # 123 lines — SVG decoder (resvg + tiny-skia)
│ │ └── exr.rs # 120 lines — OpenEXR decoder (exr crate)
│ ├── nav/
│ │ ├── mod.rs # 210 lines — Navigator (index, wrap-around, random)
│ │ └── folder.rs # 134 lines — scan_folder() + walk_folder_tree()
│ └── ui/
│ ├── mod.rs # 43 lines — refined dark palette
│ ├── icons.rs # 79 lines — 20 Lucide SVG icons
│ ├── image_view.rs # 244 lines — fit/zoom/pan/rotation rendering
│ ├── toolbar.rs # 167 lines — bottom toolbar with shuffle button
│ ├── status_bar.rs # 81 lines — filename · n/total · dims · zoom%
│ ├── thumbnail_bar.rs # 200 lines — lazy-loading top strip, scrollable::Id
│ ├── context_menu.rs # 208 lines — gpicview-style right-click menu
│ ├── error_modal.rs # 207 lines — anti-list / decode error overlay
│ ├── exif_panel.rs # 338 lines — EXIF properties modal
│ ├── sidebar.rs # 228 lines — togglable folder tree panel
│ ├── about_dialog.rs # 225 lines — ferret-style About dialog
│ └── export_dialog.rs # 555 lines — folder-as-video export modal
└── prototypes/ # original iced/egui spikes (archived)
├── SPIKE_COMPARISON.md
├── iced-viewer/
└── egui-viewer-archived/
```
### Module responsibilities
- **`main.rs`** — boots `env_logger`, calls `iced::application()` with the
`Viewer::update` / `Viewer::view` pair, sets dark theme and initial
window size (1200×800).
- **`app.rs`** — the central `Viewer` struct owns all sub-state (codec
registry wrapped in `Arc<FormatRegistry>` for sharing across async
boundaries, navigator, image view, toolbar, status bar, thumbnail bar,
sidebar, EXIF panel, context menu, error modal, about dialog, export
dialog, slideshow timer). The `Message` enum is the union of all
sub-component messages. `update()` dispatches; `view()` composes the
layout (thumbnail bar top, optional sidebar left, image middle, status
+ toolbar bottom) and layers overlays via `iced::widget::stack`;
`subscription()` drives the slideshow tick.
- **`config.rs`** — `Keymap` struct with serde, `parse_binding()` for
`"Ctrl+Shift+R"` style strings, `load_keymap()` that merges user toml
over defaults. Unknown keys warn but do not crash.
- **`settings.rs`** — `Settings` struct with serde, `load()` that merges
user `settings.toml` over defaults. Controls slideshow interval,
thumbnail size, default zoom mode, thumbnail cache window,
smooth-scroll toggle, and fullscreen hint toggle.
- **`codec/`** — `FormatRegistry` owns the codec list and the anti-list.
`decode(path)` checks the anti-list first (fail fast, no I/O), then
dispatches to the matching codec. `rotate_rgba()` rotates pixel buffers
by 90/180/270°. The Tier 2 decoders (`jxl.rs`, `tiff.rs`, `svg.rs`,
`exr.rs`) live alongside `image_crate.rs`.
- **`nav/`** — `Navigator` manages the sorted image list + current index
with wrap-around. `scan_folder()` walks a directory, filters by
supported extensions, skips anti-listed and hidden files.
`walk_folder_tree()` recursively collects all supported images in a
folder tree for random tree-wide navigation. `random_same_folder()`
and `random_from_tree()` use `SystemTime` nanos as the entropy source.
- **`ui/`** — one module per UI piece. Each module exports its state
struct, message enum, and `view()` function. The `theme` module owns
the palette constants.
---
## Build & install
### Prerequisites (Arch Linux)
```bash
sudo pacman -S --needed rust gtk3 wayland-protocols libx11 libxcb fontconfig
```
### Build from source
```bash
git clone http://git.dcos.net/dcosnet/marten.git
cd marten
cargo build --release
# binary is at target/release/marten
```
### Use the prebuilt binary
The `bin/marten` file in this distribution is a prebuilt release binary
(x86-64 Linux ELF, dynamically linked, stripped, 24.5 MB). Copy it anywhere
in your `$PATH`:
```bash
cp bin/marten ~/.local/bin/
marten
```
### Run without installing
```bash
cd marten
cargo run --release
```
---
## Configuration
Marten loads two configuration files from `$XDG_CONFIG_HOME/marten/` (falling
back to `~/.config/marten/` if XDG is not set):
### Keymap — `keymap.toml`
Defines which key triggers which action. All fields optional; missing fields
use defaults. See `config/keymap.toml` for the full schema.
### Settings — `settings.toml`
Defines scalar runtime preferences (slideshow interval, thumbnail size,
default zoom mode, thumbnail cache window, smooth-scroll toggle, fullscreen
hint toggle). All fields optional; missing fields use defaults. See
`config/settings.toml` for the full schema.
```
~/.config/marten/
├── keymap.toml # key bindings
└── settings.toml # runtime preferences
```
If either file does not exist, marten uses built-in defaults. If a file
exists but is malformed, marten falls back to defaults and logs a warning.
Partial overrides merge over defaults.
---
## Inspiration (no code reuse)
Marten draws inspiration from four existing Linux image viewers. **No code
was reused from any of them** — the entire codebase is fresh Rust, written
from scratch.
- **Ristretto** (XFCE) — the scroll-wheel-to-shuffle-photos paradigm, the
thin-toolbar-plus-thumbnail-strip layout (which we inverted in v0.2).
- **gPhoto** (GNOME) — the togglable sidebar that lets you hop between
sibling folders without reopening the file picker (added in v0.3).
- **Viewnior** — the minimal, distraction-free fullscreen mode with the
exit hint.
- **gpicview** (LXDE) — the right-click context menu structure, which we
cloned almost one-to-one (with three additions: Copy to Pictures,
Export folder as video…, and About marten).
---
## License
marten is licensed under the GNU General Public License v2.0 or later.
See `LICENSE` for the full text.
```
marten — a modern, accuracy-first image viewer for Linux.
Copyright (C) 2026 Jeremy Anderson
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
```
The full text of the GPL-2.0 is in `LICENSE`. The "or later" clause means
you may also choose to apply GPL-3.0+ terms if you prefer.
---
## Author
**Jeremy Anderson** — http://git.dcos.net/dcosnet/marten
Marten is a sibling project to [ferret](http://git.dcos.net/dcosnet/ferret),
a modern accuracy-first video player for Linux. Both apps share the same
design language: dark themes, orange accent, ferret-style About dialog,
GPL-2.0-or-later license, and a small-mustelid naming convention.