Skip to content

Show images and play video in the terminal client - #16

Merged
IGN-Styly merged 5 commits into
t3/build-tui-prfrom
t3/qmsg-media
Oct 14, 2026
Merged

IGN-Styly merged 5 commits into
t3/build-tui-prfrom
t3/qmsg-media

Conversation

@IGN-Styly

@IGN-Styly IGN-Styly commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Stacked on #15 (t3/build-tui-pr). It adds tuicord-style images and video to the terminal client.

Behavior

  • Inline images. In terminals with Kitty graphics (kitty, Ghostty, WezTerm), images on screen show inline, up to 64×12 cells, using fxcode Image. Rows are reserved from the media's width and height, and images are never enlarged. Images that are partly scrolled off, or covered by an overlay, show their name instead.
  • Viewer. o opens the selected message's image or video full screen; p plays its video. Without a selected message, both use the newest message with media. In the viewer, Space pauses, ←/→ seek 5 s (or move between a message's images), r replays, and q or Esc closes. Video plays through fxcode Video (libmpv) with fxcode's control bar.
  • Other terminals show [image: name] and download nothing. o explains that kitty, Ghostty or WezTerm is needed. Spoilers show as [spoiler image] until opened.

Media handling

  • Loading. Media loads only when on screen or opened, at most four at a time. That matches the backend's per-device read quota, and the loads run outside the input loop. An image the cache dropped is not fetched again automatically, so a full cache cannot loop.
  • URLs are fetched without the device token or cookies. Only HTTPS is allowed, plus HTTP on loopback; links with a username or password are refused. Redirects are limited to three and must pass the same checks. Error text never includes the URL, so signed query strings stay out of the UI.
  • Expiry. Expired links are skipped until a history refresh brings a renewed one.
  • Blobs are read with ReadBlob in 256 KiB chunks and always released afterwards. The UI releases handles for messages a refresh replaced or dropped, for discarded or stale history pages, and for snapshot icons and avatars, which it does not show. A handle is never released while a read is using it, and never released twice.
  • Limits. Downloads stop at 25 MiB for images and 64 MiB for videos, checked against Content-Length and while streaming. Images are capped at 16384 px a side and 40 MP before full decoding. Only PNG, JPEG, GIF (first frame) and WebP are decoded, and the format comes from the bytes. Decoded images are downscaled to 1600 px and share a 96 MiB LRU. HTML is never decoded.
  • Videos are written to a fresh 0700 directory with a 0600 file, under $XDG_RUNTIME_DIR or the temp directory. The file name comes only from the MIME type. The directory is deleted when the viewer closes.
  • Player errors. If the player fails, the viewer closes and the error shows as the status line; the app keeps running.

fxcode

Pins fxcode PR #2 (27431fc, stacked on fxcode #1). Without it, seeking past the end of a video made fxcode return a fatal mpv: error running command, which closed qmsg. CI already installs libmpv-dev.

Validation

  • cargo test -p qmsg: 50 tests pass, including these media tests:
    • Decoding with downscaling and bounds checks, and the HTTPS, size and redirect rules against a local HTTP server, including a fourth redirect being refused.
    • Video file permissions and cleanup, and cache eviction.
    • Four-at-a-time loading that never loops, and no loads without graphics.
    • Blob release rules: in-flight reads are kept, finished reads are not released twice, and snapshot avatars and icons are released.
    • Expired links waiting for a refresh, the video viewer lifecycle (play, pause, close deletes the file), and row reservation.
    • Two mutations of the release and refetch logic were each caught by a failing test.
  • cargo clippy --workspace --all-targets with -D warnings, cargo fmt --all --check, and cargo test -p qmsg-orchestrator pass. Downloads use hyper-rustls rather than reqwest. Enabling reqwest's TLS feature here would also enable it for the orchestrator, whose reqwest::Client::new() then panics; that broke the first CI run.
  • A real HTTPS download, through rustls with ring and the webpki roots, was checked once with a scratch test that is not committed.
  • Isolated Wayland sandbox, Ghostty. Ran a local fake App API serving an HTTP JPEG, a Blob PNG through ReadBlob, and an MP4. Both images showed inline. The blob was read once and released, with no duplicate releases across refreshes. The video played, paused, seeked past its end twice, and replayed. Closing removed its temp directory.
  • Alacritty, without Kitty graphics, against the same fake: names only, and zero downloads.
  • Read-only Discord was not used for media screenshots, and no message was sent.

Limits

GIFs show their first frame. Videos have no inline poster frame: they show ▶ [video: name] until played. The viewer shows the image downscaled to at most 1600 px. Seeking past the end restarts the video from the beginning, which is mpv's behavior with the fxcode fix. Images are not shown inside reply previews.

Screenshots

These links use Claudio's S9 file server and require Tailscale, so GitHub cannot show them inline. They show only demo data and a local fixture.

Before (536d5d1) After
Demo Demo with an inline image
Local fixture: names only URL image and Blob image inline
— Image viewer (o)
— Video player with controls (p)
— Terminal without graphics (Alacritty)

Ghostty, except the last row in Alacritty. 1280×800, font size 11.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 59b7721a-67ae-4e6f-bc78-7d63f72b5fde

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@IGN-Styly

Copy link
Copy Markdown
Member Author

@coderabbitai review

Rebased onto #15 (12ec205). This stacked PR has not had a bot review yet.

@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

IGN-Styly and others added 5 commits October 14, 2026 12:38
…iles

Public URLs are fetched over HTTPS only, without credentials, within size,
redirect and time limits; errors never include the URL. Images are decoded
with dimension and allocation limits and downscaled; videos go to an
owner-only temporary file that is removed on drop. Decoded images share an
LRU cache with a 64 MiB budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Request::Media loads in a spawned task gated by the four-read device
quota: Url sources use the public download client, Blob sources are read in
256 KiB chunks and always released. Results arrive as Update::Media.
Request::Release frees blob handles the UI no longer needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fxcode PR #2 keeps a finished video open and treats a failed seek as a
no-op, so seeking past the end no longer stops the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On terminals with Kitty graphics, images on screen load four at a time
and show inline. o opens a message's image or video full screen and p
plays its video, with pause, seek, and replay keys. The app releases
blob handles it does not show or no longer needs, skips expired links
until a refresh renews them, and never loads an image again after the
cache drops it. A player error closes the viewer instead of the app.
The demo includes a generated image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enabling reqwest's TLS feature for qmsg also enabled it for the
orchestrator's reqwest, whose Client::new() then panicked without a
crypto provider. hyper-rustls with ring keeps the same rules: HTTPS
or loopback HTTP only, no credentials in the URL, at most three
checked redirects, size limits, and errors without the URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IGN-Styly
IGN-Styly merged commit d7521c7 into t3/build-tui-pr Oct 14, 2026
5 checks passed
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