Krolyxon
Commit f2bd31a

Branches

Tags

Refs →

remageCLAUDE.md

483 lines · 26.8 KB

remage

A keyboard-centric image viewer for Linux, written in Rust. It aims for nsxiv feature parity with vim-style control and the Nothing OS design language. It is GPU-rendered so it feels smooth, and it can be themed from the wallpaper (matugen or pywal). Scripting works through declarative commands plus an IPC socket. The binary and crate are both remage, and the Wayland app_id / X11 class is remage.

Principles (in priority order)

  1. Fast. Cold start to the first painted image takes < 200 ms. n/p to a preloaded image takes one frame. CPU use is 0% when idle: render on demand only, with no continuous loop.
  2. Smooth but light. Draw with wgpu and use short ease-out animations. No GUI toolkit and no embedded interpreter. Animation runs only while something moves.
  3. Keyboard first. Every feature is an action. Every action can be bound to a key and is reachable from the : palette and over IPC. The mouse works but is secondary.
  4. Everything configurable in TOML, with live reload.
  5. Never block the UI thread on I/O, decoding or scripts. A broken image or config never panics. The error shows in the status bar instead.

Tech stack

Use the latest stable crate versions at implementation time (cargo add) and never guess version numbers. Check APIs on docs.rs, especially winit, wgpu and glyphon, which break between releases and must stay mutually compatible.

Concern Choice
Window / input winit (ApplicationHandler API), Wayland + X11, no decorations by default
GPU wgpu: Vulkan + GL backends, PowerPreference::LowPower (an iGPU is plenty, and waking a sleeping laptop dGPU costs ~2 s; WGPU_POWER_PREF/WGPU_BACKEND override). PresentMode::Fifo, desired_maximum_frame_latency = 1, call pre_present_notify() before present
Text glyphon (cosmic-text) with embedded fonts
Decoding image (png/apng, jpeg, gif, webp, tiff, bmp, ico, tga, pnm, qoi, dds, hdr, exr), jxl-oxide, image/avif-native (dav1d), libheif-rs, resvg, rawler
Colour moxcms (ICC → sRGB, CICP); EXIF orientation from image / kamadak-exif
Thumbnails fast_image_resize (SIMD), png + md-5 for the freedesktop cache
Config serde + toml (spans give file:line:col errors)
Fuzzy matching nucleo-matcher
Watching / signals notify (config, includes, directories), signal-hook (SIGUSR1 = reload)
CLI clap derive + clap_complete + clap_mangen
Concurrency std threads + crossbeam-channel; workers wake the loop via EventLoopProxy
IPC std UnixListener + serde_json
Errors / logs anyhow, thiserror, log + env_logger (RUST_LOG=remage=debug)

Locked pairings (M0, 2026-11): glyphon 0.12 ↔ wgpu 30 (cosmic-text 0.19). winit stays on 0.30 until 0.31 leaves beta. Bump glyphon and wgpu together, never one alone. Crates still to add when their milestone needs them: jpeg-decoder (M3 scaled thumbnails) and clap_mangen (M7).

Cargo features are default = ["jxl", "svg", "avif", "heif", "raw"]. The only C dependencies (dav1d, libheif) sit behind avif and heif.

Build profiles:

Rust 2024 edition, stable toolchain.

Architecture

src/
  main.rs          CLI → App::run;  `remage msg …` client path
  cli.rs           clap definitions
  app.rs           winit ApplicationHandler: routes events, owns State + Renderer
  action.rs        Action enum + parser/Display (one grammar for keys, palette, IPC, scripts)
  scan.rs          CLI paths → file list (siblings, recursion, hidden files, unknown extensions to sniff)
  state/           PURE logic, no winit/wgpu types → unit-testable
    mod.rs         AppState: mode, pending input, status line
    queue.rs       file list, index, marks, sort, filter, refresh
    view.rs        zoom/pan/fit/rotate/flip maths, animation targets
    grid.rs        grid layout, cursor, scroll, visual range
  input/
    keys.rs        vim key-notation parser (<C-x>, <S-Tab>, gg, <Space>w, mouse)
    keymap.rs      per-mode trie, pending sequences, counts, which-key candidates
  config/
    mod.rs         schema, compiled-in defaults, include merge, validation, live reload
    theme.rs       tokens, presets, palette derivation, pywal import
  decode/
    mod.rs         sniff (magic bytes, then extension) → backend → DecodedImage{frames, icc, orientation, meta}
    *.rs           image_rs, jxl, svg, heif, raw, external
    color.rs       ICC/CICP → sRGB, orientation, HDR tone-map
  worker.rs        priority job pool with epoch cancellation
  cache.rs         decoded-image LRU (byte budget) + preload policy
  thumbs.rs        freedesktop thumbnail cache read/write + generation
  render/
    mod.rs         device/surface setup, frame orchestration
    image.rs       textures, tiling above max_texture_dimension_2d, mipmaps, filters
    prims.rs       one instanced SDF shader: rects, rounded rects, borders, dots
    text.rs        glyphon + embedded fonts
    shaders/*.wgsl image (gamma/brightness/contrast, alpha dots), prims
  ui/              statusbar, grid view, palette, which-key, help, info panel
  anim.rs          retargetable ease-out tweens; requests redraws only while active
  exec.rs          user commands: template expansion, spawn, confirm, output handling
  ipc.rs           socket server, JSON protocol, event subscriptions
assets/fonts/      SpaceGrotesk, SpaceMono, Doto + OFL licences
contrib/           config.example.toml, matugen/, pywal/, scripts/, remage.desktop, PKGBUILD
tests/fixtures/    tiny images per format, EXIF orientations 1–8, ICC-tagged, animated

Data flow. Input goes through the keymap, which produces an Action. State::apply turns the action into effects: jobs, spawns, redraw. Workers send UserEvents back through the proxy, and those update the state and renderer. The renderer reads state and never owns logic.

Threads.

Startup.

  1. Parse the CLI and config.
  2. Start decoding the first image right away.
  3. In parallel, create the window hidden and initialise wgpu.
  4. Show the window on the first finished frame, so it never flashes empty.

The directory listing continues in the background; only the first image is awaited.

Feature spec

Queue

Modes

Image view

Grid

Marks

Input

Actions

Actions share one grammar, name [args], used by keys, the palette, IPC and command stdout. Each action carries a one-line description, which feeds the palette, which-key and help.

Group Actions
Navigation next [n], prev [n], first, last, goto N, random, next-mark, prev-mark, next-frame, prev-frame
Marks mark [toggle|on|off], mark-all, unmark-all, invert-marks
View zoom in|out|N%, fit <mode>, pan <dir>, pan-edge <dir>, center, rotate cw|ccw|180, flip h|v, filter toggle, alpha-bg cycle, gamma|brightness|contrast +|-|reset
Playback play toggle, slideshow toggle, slideshow-delay ±N
UI mode image|grid|toggle, visual, fullscreen, bar toggle, info toggle, help, palette, search, grid-size +|-
Queue sort <key>, reverse, filter <pattern>, filter-clear, remove, reload, refresh, open <path>
App run <command>, reload-config, echo <msg>, quit, quit-no-output

Commands and scripting (replaces nsxiv's key-handler)

[commands.wallpaper]
desc = "Set as wallpaper"
run  = ["setwall", "-p", "{file}"]

[commands.copy]
desc = "Copy image"
run  = "wl-copy --type image/png < {file}"   # string = sh -c, placeholders shell-quoted

[commands.delete]
desc    = "Delete files"
run     = ["rm", "--", "{files}"]             # array = exec directly, {files} → N args
confirm = "Delete {count} file(s)?"

[commands.legacy]                             # an old nsxiv key-handler works unchanged
run   = ["~/.config/nsxiv/exec/key-handler", "w"]
stdin = "files"

[keys.global]
"<Space>w" = "run wallpaper"
"<Space>c" = "run copy"
"<Space>d" = "run delete"

Command fields:

IPC socket. It lives at $XDG_RUNTIME_DIR/remage/<pid>.sock and speaks newline-delimited JSON.

Config

[general]   open_siblings = true; recursive = false; sort = "natural"; start_mode = "image"; wrap = true; watch_dir = true
[window]    decorations = false; title = "{name} — remage"; class = "remage"; fullscreen = false
[image]     fit = "down"; zoom_step = 1.25; filter = "auto"; alpha_background = "dots"; auto_orient = true
            color_management = true; preload = 2; cache_mb = 512; slideshow_delay = 5.0
[grid]      sizes = [96, 128, 160, 224, 320]; size = 160; gap = 16; thumbnail_cache = true; names = false
[ui]        statusbar = true; which_key_delay_ms = 300; scale = 1.0
            statusbar_left = "{name}"; statusbar_right = "{format}  {width}×{height}  {zoom}%  {index}/{count}"
            info_command = ""   # optional script whose output fills a status segment (nsxiv image-info)
            font_ui = "Space Grotesk"; font_mono = "Space Mono"; font_display = "Doto"
[animation] enabled = true; duration_ms = 180; transition = "fade"
[theme]     preset = "nothing-dark"

Theming

UI design language (Nothing)

Load the nothing-design skill before any UI work. These are the remage-specific rules:

Formats

Formats Backend
JPEG, PNG/APNG, GIF, WebP (animated), BMP, ICO, TIFF (multi-page), TGA, PNM, QOI, DDS, HDR, OpenEXR image (pure Rust)
JPEG XL (incl. animation) jxl-oxide (pure Rust)
AVIF image avif-native (system dav1d)
HEIC / HEIF libheif-rs (system libheif)
SVG / SVGZ resvg, re-rasterised on zoom
Camera RAW (CR2/CR3/NEF/ARW/DNG/RAF/ORF/RW2…) rawler: the embedded preview first (fast), with a full develop on demand
Anything else (PSD, XCF, JP2, …) [[decoders]] entries { match = ["*.psd"], run = ["magick", "{file}[0]", "png:-"] }, which emit PNG/PPM/TIFF on stdout

Pipeline rules:

CLI

remage [OPTIONS] [PATHS]...
  -r --recursive   -i --stdin   -0 --null   -o --print-marked   -t --grid
  -n --start N     -f --fullscreen   -b --no-bar   -S --slideshow [SECS]
  -s --fit MODE    -z --zoom PCT     -g --geometry WxH   -c --config PATH
  -p --private     --class NAME      --sort KEY  --reverse   --no-siblings
  --dump-config    --check-config
remage msg [--pid PID] [--query Q | --subscribe EV] [ACTION...]
remage completions <SHELL>

Default keys

Mode Keys
global q quit · <CR> image⇄grid · n/p/<BS> next/prev · [/] ±10 · gg/G/[N]G · m/M/u mark/invert/unmark-all · N/P next/prev mark · f fullscreen · b bar · i info · r reload · D remove from queue · s slideshow · : palette · / search · ? help · <Space> leader (user commands, which-key) · <Esc> cancel
image hjkl/arrows pan · HJKL edges · z centre · +/-/= zoom in/out/100% · w/W/e/E fit/fill/width/height · </> rotate · |/_ flip · a filter · A alpha bg · {/} gamma · <C-Space> play/pause · <C-n>/<C-p> frame · wheel = zoom at cursor, drag = pan
grid hjkl move · <C-d>/<C-u> half page · <C-f>/<C-b> page · 0/$ row ends · +/- thumb size · v visual · click = select, double-click = open

Milestones

Each milestone should end with a usable binary.

Testing and verification

Conventions