Skip to content

feat: Inline image rendering using ratatui-image #1

@linuxmobile

Description

@linuxmobile

Problem Statement

Oxicord currently renders image attachments as text links (📎 filename.jpg). For a TUI client targeting power users, this is a significant UX gap compared to modern Discord clients that display images inline.

The roadmap already identifies this as a goal:

  • Image previews (Ratatui-image integration) (Monitoring for performance impact)
  • Image modal viewer ('o' binding)

Proposed Solution

Integrate ratatui-image to render images inline within the message pane. The library handles terminal protocol detection (Sixel, Kitty, iTerm2) and provides StatefulImage widgets that adapt to available render area.

User Experience Vision

  1. Inline thumbnails: Small previews (configurable max height, e.g., 6-10 rows) render directly in the message flow
  2. Modal viewer: Pressing o on a message with attachments opens a fullscreen modal for the selected image
  3. Graceful degradation: Terminals without graphics protocol support fall back to halfblocks or text placeholders

Technical Considerations

Performance Pitfalls (Critical)

The ratatui-image docs and examples highlight several issues that will cause problems if not addressed:

Problem Symptom Solution
Synchronous encoding in render loop UI freezes during image load/resize Offload resize+encode to worker thread (see examples/thread.rs)
Unbounded image cache RAM bloat (30MB to 600MB observed in similar projects) LRU cache with configurable max entries
Re-encoding on every frame CPU spikes, scroll jank Cache StatefulProtocol per attachment URL
Pixel/cell mismatch Scroll artifacts, image tearing Use Picker::from_query_stdio() for accurate font-size detection

Architecture Requirements

Following Oxicord's Clean Architecture:

domain/
  entities/attachment.rs    # Already has is_image() - no changes needed

application/
  services/image_service.rs # NEW: Manages image fetching, caching, protocol state

infrastructure/
  image/                    # NEW: HTTP fetching, disk cache (optional)

presentation/
  widgets/image_widget.rs   # NEW: Wrapper around StatefulImage
  ui/image_modal.rs         # NEW: Fullscreen image viewer

Threading Model

Oxicord already uses tokio. The examples/tokio.rs pattern fits well:

// Simplified concept - actual implementation will differ
let (tx, rx) = unbounded_channel();
let protocol = ThreadProtocol::new(tx, Some(picker.new_resize_protocol(dyn_img)));

// In event loop:
select! {
    Some(request) = rx.recv() => {
        protocol.update_resized_protocol(request.resize_encode()?);
    }
}

The key insight: never call resize_encode() in the render path. Always receive completed protocols from a background task.

Implementation Checklist

Phase 1: Foundation

  • Add ratatui-image dependency with crossterm and tokio features
  • Create ImageService in application layer for cache management
  • Implement Picker initialization in app startup (query terminal capabilities once)
  • Add LRU cache for StatefulProtocol keyed by attachment URL

Phase 2: Inline Rendering

  • Modify message_pane.rs to detect image attachments via Attachment::is_image()
  • Calculate inline thumbnail dimensions (respect terminal width, configurable max height)
  • Render StatefulImage widget within message scroll view
  • Handle loading state (show placeholder while fetching/encoding)

Phase 3: Modal Viewer

  • Add image_modal.rs widget for fullscreen display
  • Bind o key in message pane to open modal (keybinding already defined: Action::OpenAttachments)
  • Implement arrow key navigation for messages with multiple attachments
  • Add Esc to close modal

Phase 4: Polish

  • Add configuration options (enable/disable, max cache size, thumbnail height)
  • Implement fallback rendering for unsupported terminals
  • Add loading/error indicators
  • Test on Sixel (xterm, foot), Kitty, and iTerm2 terminals

Resources

Acceptance Criteria

  • Images from message attachments render inline without blocking the UI thread
  • Scrolling through messages with images does not cause lag or artifacts
  • Memory usage remains stable when scrolling through channels with many images
  • o key opens a fullscreen modal for the selected attachment
  • Terminals without graphics protocol support show a text fallback (e.g., [Image: filename.jpg])
  • Works on at least: Kitty, Foot (Sixel), and WezTerm (iTerm2)

Open Questions

  1. Thumbnail sizing: Should thumbnails have a fixed max height (e.g., 8 rows), or scale based on terminal size?
  2. Caching strategy: In-memory only, or persist decoded images to disk (~/.cache/oxicord/images/)?
  3. Animated GIFs: Attempt animation (CPU-intensive) or render first frame only?
  4. Proxy/CDN images: Discord uses cdn.discordapp.com URLs. Any concerns about rate limiting when fetching many images?

This is a non-trivial feature due to the threading requirements and cache management. However, the existing codebase structure (Clean Architecture, tokio runtime) and well-documented examples from ratatui-image make it approachable.

Suggested starting point: Study examples/tokio.rs thoroughly, then implement a minimal proof-of-concept that renders a single hardcoded image in the message pane before tackling the full integration.

Note on current status:
I've attached a video of my initial POC below. It works, but it's not quite perfect yet—you'll notice some of the jank/artifacts we need to solve before this is ready for prime time.

recording_20260116_224610.mp4

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions