Skip to content

feat: async + sandboxed image loading via glycin with GDK fallback#3

Merged
green2grey merged 4 commits into
mainfrom
feat/async-image-loading
Jul 6, 2026
Merged

feat: async + sandboxed image loading via glycin with GDK fallback#3
green2grey merged 4 commits into
mainfrom
feat/async-image-loading

Conversation

@green2grey

Copy link
Copy Markdown
Member

Summary

Replaces the synchronous Texture::from_file in load_file with fully async decoding off the GTK main thread. Implements ADR-0004 and closes the NFR-001 (no UI freeze on decode) / NFR-002 (sandboxed decoding of untrusted images) gaps — the first Phase 7 task.

How it works

  • New decode module (crates/quickview-ui/src/decode.rs) with two backends:
    • glycin — decode runs in a sandboxed loader process, driven via glib::MainContext::spawn_local (no extra thread)
    • GDK — in-process Texture::from_filename on a worker thread + async-channel (same plumbing as OCR)
  • Runtime probe, no cargo feature: the glycin client crate is pure Rust and always compiled; Loader::supported_mime_types() is probed once per session and the chosen backend is logged at first decode (visible under RUST_LOG=info). One build recipe works across Arch / Ubuntu 24.04+ / Debian 13+ and degrades gracefully elsewhere.
  • Strict fallback policy (Loupe-style): GDK is used only when the session has no glycin loaders at all. A file glycin rejects is a failed load ("Could not load image") and is never re-fed to the unsandboxed GDK decoder — per-file fallback would let a crafted image reach the unsandboxed path by deliberately crashing the sandboxed loader.
  • Stale-decode guard: new decode_job_id mirrors the existing OCR job-id pattern; fast arrow-key navigation abandons superseded decodes. load_file also invalidates in-flight OCR up front so a late OCR result can't clear the busy spinner mid-decode. FileInfo emission and OCR kickoff moved into the finish_decode completion handler.
  • UX: the previous image stays visible under the busy spinner until the new decode completes; the spinner carries through into OCR.

Packaging / docs

  • Flatpak: added a glycin-loaders 2.1.5 meson module (loaders only) — the GNOME 49 runtime does not ship the loaders
  • PKGBUILD: updated the glycin optdepend wording; dropped glycin-gtk4 (the C bindings aren't used by the Rust crate)
  • ADR-0004 implementation notes, PHASED_PLAN Phase 7 status, CLAUDE.md current state, README/DEPENDENCIES/DECISIONS wording

Testing

  • cargo fmt / cargo clippy --all-targets --all-features -- -D warnings / cargo test --all all green (core crate untouched)
  • Smoke-tested: glycin backend logs and displays a normal image; corrupt file takes the load-failed path ("glycin load failed: Unknown image format"); forcing an empty loader config dir triggers the logged GDK fallback and still displays; quick-preview mode unaffected

Replace the synchronous Texture::from_file in load_file with async
decoding off the main thread (implements ADR-0004, NFR-001/NFR-002):

- new decode module: glycin sandboxed loader process when installed,
  in-process GDK decoding on a worker thread otherwise
- backend chosen once per session by probing installed glycin loaders;
  choice is logged at first decode
- fallback is strictly session-wide, never per-file: a file glycin
  rejects is a failed load and is never re-fed to the unsandboxed
  GDK decoder
- decode_job_id guard (same monotonic pattern as OCR) drops stale
  decodes during fast navigation; FileInfo emission and OCR kickoff
  move into the finish_decode completion handler
- previous image stays visible under the busy spinner until the new
  decode completes; spinner carries through into OCR
- remove the unused glycin cargo feature (crate is pure Rust and now
  always compiled)
- packaging: add glycin-loaders module to the Flatpak manifest (GNOME
  runtime does not ship it); update PKGBUILD optdepends
- docs: ADR-0004 implementation notes, PHASED_PLAN Phase 7, CLAUDE.md
  current state, README/DEPENDENCIES/DECISIONS wording
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: af87bf95-a7e1-4d42-af36-cfbe8107364d

📥 Commits

Reviewing files that changed from the base of the PR and between 3b16770 and 90806e7.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/ci.yml
📜 Recent review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: build-test

Summary by CodeRabbit

  • New Features
    • Image previews and the full viewer now decode asynchronously, using sandboxed decoding when available with automatic runtime fallback.
  • Bug Fixes
    • Prevented stale/late decode results from replacing the currently selected file during rapid switching.
    • Failed image loads are handled more robustly, and OCR starts only after a successful decode.
  • Chores
    • Updated Flatpak packaging to include the sandboxed image loader component.
    • Updated CI/dependency documentation for required imaging libraries and pinned cargo-deny checks.

Walkthrough

Makes glycin always compiled, adds a session-cached decode backend with a GDK fallback, wires async decoding into the viewer with job-id gating, and updates docs, packaging, CI, and advisory settings to match the new decode path.

Changes

Async decode backend and viewer flow

Layer / File(s) Summary
Decode backend selection
crates/quickview-ui/Cargo.toml, crates/quickview-ui/src/lib.rs, crates/quickview-ui/src/decode.rs
Makes glycin always available, adds the decode module, and routes decode_texture() through a session-cached glycin/GDK backend probe.
Viewer load and stale-result guard
crates/quickview-ui/src/windows/shared.rs
Adds decode_job_id, reworks load_file() to supersede in-flight decode work, and applies decode results only when the current job id still matches before starting OCR on success.
Docs, packaging, and CI updates
adrs/ADR-0004-Image-Decoding.md, docs/DECISIONS.md, docs/DEPENDENCIES.md, docs/PHASED_PLAN.md, README.md, packaging/arch/PKGBUILD, packaging/flatpak/com.example.QuickView.json, .claude/CLAUDE.md, .github/workflows/ci.yml
Updates the documented decode behavior, runtime fallback description, dependency notes, packaging metadata, status notes, and CI dependency/install steps.
Advisory ignore update
deny.toml
Adds RUSTSEC-2024-0436 to the ignored advisories list.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Poem

A rabbit hops through cached-backend light,
glycin or GDK, whichever feels right,
stale hops are silenced by job-ids in line,
the freshest decoded image is the one that will shine,
🐇✨ and the docs now tell the tale just fine.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: async sandboxed image loading with a GDK fallback.
Description check ✅ Passed The description covers the summary, implementation details, packaging/docs, and testing, though it doesn't use the exact checklist template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/async-image-loading

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 49243c29bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

glycin = { version = "3", optional = true }
# Sandboxed image decoding. Always compiled (pure Rust client, no system build
# deps); whether it is used is decided at runtime by probing installed loaders.
glycin = { version = "3", features = ["gdk4"] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Declare glycin's native build dependencies

Making glycin unconditional means clean source/package builds now need glycin's native libraries available at build time, before the runtime GDK fallback can ever run. In environments that have the existing GTK/Tesseract setup but not packages such as libseccomp/lcms2/fontconfig development files, cargo build can fail even though the docs and setup paths still present glycin as optional; please either add those native dependencies to the setup/package recipes or keep this behind a feature.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified and fixed in b8e81aacargo tree confirms the glycin client links libseccomp, lcms2, and fontconfig (via libseccomp-sys, lcms2-sys, yeslogic-fontconfig-sys), needed at build/link time regardless of which backend runs. They are now declared in the PKGBUILD depends, the README install line, and DEPENDENCIES.md (with the Debian/Ubuntu -dev package names), and the inaccurate "pure Rust, no system deps" wording was corrected in Cargo.toml and ADR-0004. Keeping the crate unconditional rather than feature-gated: these libraries are present on effectively every GTK-capable system, and a single build recipe avoids per-distro variants.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/quickview-ui/src/decode.rs (1)

77-93: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

Unbounded thread spawning per decode with no cancellation.

Each decode_gdk call spawns a fresh OS thread that keeps running to completion even if the caller's future is dropped (e.g. via the decode_job_id guard on a superseded load). Rapid navigation through many images could pile up several concurrent decode threads doing wasted work. Consider a bounded worker pool (or check the job id before/while decoding) to cap concurrent decode threads.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/quickview-ui/src/decode.rs` around lines 77 - 93, The decode_gdk
helper currently spawns a new OS thread for every decode and never cancels it,
so superseded loads can still consume work. Update decode_gdk to avoid per-call
thread spawning by using a bounded worker pool or another shared execution path,
and wire in cancellation/job validation so a dropped caller future (such as one
invalidated by decode_job_id) can stop or skip work. Keep the fix localized
around decode_gdk and its caller-side job guard so concurrent image decodes are
capped.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/quickview-ui/src/decode.rs`:
- Around line 77-93: The decode_gdk helper currently spawns a new OS thread for
every decode and never cancels it, so superseded loads can still consume work.
Update decode_gdk to avoid per-call thread spawning by using a bounded worker
pool or another shared execution path, and wire in cancellation/job validation
so a dropped caller future (such as one invalidated by decode_job_id) can stop
or skip work. Keep the fix localized around decode_gdk and its caller-side job
guard so concurrent image decodes are capped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b091e9cc-b57a-43a5-b06d-5e41a363c022

📥 Commits

Reviewing files that changed from the base of the PR and between dd33bc9 and 49243c2.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • .claude/CLAUDE.md
  • README.md
  • adrs/ADR-0004-Image-Decoding.md
  • crates/quickview-ui/Cargo.toml
  • crates/quickview-ui/src/decode.rs
  • crates/quickview-ui/src/lib.rs
  • crates/quickview-ui/src/windows/shared.rs
  • docs/DECISIONS.md
  • docs/DEPENDENCIES.md
  • docs/PHASED_PLAN.md
  • packaging/arch/PKGBUILD
  • packaging/flatpak/com.example.QuickView.json
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: build-test
  • GitHub Check: lint
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Use rustfmt and clippy for code formatting and linting, as tracked in rust-toolchain.toml
Run clippy with all targets and features enabled with -D warnings flag to treat warnings as errors

Files:

  • crates/quickview-ui/src/lib.rs
  • crates/quickview-ui/src/decode.rs
  • crates/quickview-ui/src/windows/shared.rs
🔇 Additional comments (12)
crates/quickview-ui/Cargo.toml (1)

7-8: LGTM!

Also applies to: 21-23

crates/quickview-ui/src/lib.rs (1)

9-9: LGTM!

crates/quickview-ui/src/windows/shared.rs (1)

44-45: LGTM!

Also applies to: 65-65, 106-177

.claude/CLAUDE.md (1)

47-61: LGTM!

packaging/flatpak/com.example.QuickView.json (1)

25-31: 🔒 Security & Privacy

Verify pinned glycin-loaders archive checksum.
The manifest pins a specific tarball and sha256; this needs a manual check against the official GNOME source release before merging.

crates/quickview-ui/src/decode.rs (1)

77-93: 🩺 Stability & Availability

Drop this warning: gdk4::Texture::from_filename is thread-safe in gdk4 0.10.0, and gdk4::Texture is Send + Sync. The worker-thread fallback does not have the compile/panic issue described here.

			> Likely an incorrect or invalid review comment.
adrs/ADR-0004-Image-Decoding.md (1)

23-36: LGTM!

docs/DECISIONS.md (1)

19-22: LGTM!

docs/DEPENDENCIES.md (1)

16-24: 📐 Maintainability & Code Quality

Disambiguate the installable package name.

The section uses glycin for both the optional system loader entry and the always-compiled Rust crate below, which makes the install path ambiguous. The provided context only confirms a Flatpak module named glycin-loaders; please verify the distro package name before publishing.

docs/PHASED_PLAN.md (1)

147-159: LGTM!

README.md (1)

56-56: LGTM!

packaging/arch/PKGBUILD (1)

18-22: 📐 Maintainability & Code Quality

Verify the Arch optdepend name.

The optdepend uses glycin, but the runtime dependency here is the loader package rather than the always-compiled Rust crate. Please confirm the Arch package name so users aren't told to install the wrong artifact.

Review follow-ups for PR #3:

- Codex: the glycin client crate links libseccomp, lcms2, and
  fontconfig (sandbox seccomp filter, ICC, font paths) — needed at
  build/link time regardless of the runtime backend. Declare them in
  PKGBUILD depends, the README install line, and DEPENDENCIES.md, and
  correct the inaccurate 'pure Rust, no system deps' wording in
  Cargo.toml and ADR-0004.
- CI: cargo-deny failed on RUSTSEC-2024-0436 (paste is unmaintained).
  paste is a build-time proc-macro pulled in transitively by glycin's
  deps with no fixed release; add a scoped, documented ignore.
@green2grey

Copy link
Copy Markdown
Member Author

@coderabbitai Re the decode_gdk unbounded-thread-spawning nitpick: acknowledged but intentionally skipped. It mirrors the existing per-job OCR thread pattern in shared.rs (short-lived thread per job, stale result dropped via monotonic job id), it only applies on the fallback path when no glycin loaders are installed, and concurrency is bounded in practice by navigation speed. A bounded worker pool is noted for the Phase 7 hardening item (memory usage limits) if it ever shows up as a real cost.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@green2grey Thanks for the context — that reasoning holds up. Mirroring the existing per-job OCR thread pattern in shared.rs keeps the concurrency model consistent, and since it's gated to the fallback-only path (no glycin loaders installed) with stale results already dropped via the monotonic job id, the unbounded-spawn risk is narrow in practice. Good call noting the bounded worker pool as a Phase 7 hardening follow-up if memory usage ever becomes a real concern rather than a theoretical one.

(=^・ω・^=) noted, makes sense


✏️ Learnings added
Learnt from: green2grey
URL: https://github.com/Green2Grey2/QuickView/pull/3

Timestamp: 2026-07-06T04:53:54.456Z
Learning: In quickview-ui (crates/quickview-ui/src/decode.rs), the GDK fallback decode path (`decode_gdk`) intentionally spawns an unbounded per-job OS thread for each decode request, mirroring the existing per-job OCR thread pattern in crates/quickview-ui/src/windows/shared.rs (short-lived thread per job, stale results dropped via a monotonic job id). This is accepted because: (1) it only applies on the fallback path when no glycin loaders are installed session-wide, (2) concurrency is bounded in practice by navigation speed. A bounded worker pool is deferred to the Phase 7 hardening item (memory usage limits) if it becomes a real cost.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8e81aab7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Sandboxed image decoding. Always compiled; whether it is used is decided at
# runtime by probing installed loaders. Links libseccomp, lcms2, and
# fontconfig (see docs/DEPENDENCIES.md).
glycin = { version = "3", features = ["gdk4"] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update CI deps for unconditional glycin

With glycin now always compiled, the clean Arch CI jobs need its native pkg-config dependencies before cargo clippy, cargo test, and cargo build run. I checked both install blocks in .github/workflows/ci.yml; they still install only GTK/Tesseract/layer-shell deps and omit the newly documented libseccomp, lcms2, and fontconfig, so the workflow can fail during dependency build/link before the runtime GDK fallback is ever reached. Fresh evidence beyond the earlier comment is that the CI workflow dependency lists were not updated in this commit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3b16770 — the three packages are now explicit in both install blocks. Worth noting the premise was partially off: CI passed on the reviewed commit because on Arch gtk4's dependency closure already pulls fontconfig, lcms2, and libseccomp (verified with pactree), and Arch ships headers/.pc files in the main packages. So the workflow could not actually fail as described — but depending on a transitive dep tree for directly-linked libraries is fragile, hence the explicit declaration.

- Add libseccomp/lcms2/fontconfig to both install blocks. They were
  already present transitively via gtk4's dependency closure (which is
  why CI passed), but the workflow shouldn't rely on another package's
  dep tree for libraries we link directly.
- Replace 'cargo install cargo-deny' with the pinned prebuilt musl
  binary: the source build cost ~100s per run whenever the cache
  missed or upstream released; the download takes ~2s. Drop the now
  unneeded ~/.cargo/bin cache path.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 94-101: The Install cargo-deny step currently pipes the downloaded
tarball straight into tar without verification. Update the workflow in the
Install cargo-deny job to fetch the matching cargo-deny-${DENY_VERSION} .sha256
checksum from the release, validate the tarball against it before extraction,
and only then unpack it; keep the change localized to this step so the existing
cargo-deny install flow remains the same but with checksum verification added.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87f75936-e95a-44e0-966f-048d627d17bc

📥 Commits

Reviewing files that changed from the base of the PR and between b8e81aa and 3b16770.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: build-test
🔇 Additional comments (2)
.github/workflows/ci.yml (2)

34-35: Native dependency additions align with packaging manifest.

gtk4-layer-shell, libseccomp, lcms2, and fontconfig match the runtime deps declared in packaging/arch/PKGBUILD, needed for glycin's sandbox and now-always-compiled build.

Also applies to: 78-80


83-93: Cache key rework is consistent with the new install method.

Dropping /root/.cargo/bin from the cache path is correct since cargo-deny is no longer installed via cargo install.

Comment thread .github/workflows/ci.yml Outdated
Checksum is hardcoded in the workflow rather than fetched from the
release, so a swapped artifact fails the build instead of running.
@green2grey green2grey merged commit a2c5e9f into main Jul 6, 2026
3 checks passed
@green2grey green2grey deleted the feat/async-image-loading branch July 6, 2026 05:11

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90806e75f9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Sandboxed image decoding. Always compiled; whether it is used is decided at
# runtime by probing installed loaders. Links libseccomp, lcms2, and
# fontconfig (see docs/DEPENDENCIES.md).
glycin = { version = "3", features = ["gdk4"] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the Arch bootstrap deps

Fresh evidence beyond the earlier dependency thread is that scripts/bootstrap_arch.sh, the Quickstart setup path, still installs only GTK/Tesseract/layer-shell deps and leaves glycin as an optional comment; with glycin now compiled unconditionally here, a clean Arch machine following that script can hit the native link/build requirements before any runtime GDK fallback is possible. Please add the same native packages documented elsewhere (at least the glycin client link deps such as libseccomp) to the bootstrap script too.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant