Skip to content

feat: display profile avatar image with terminal graphics protocols#23

Merged
unhappychoice merged 2 commits intomainfrom
feat/display-images
Feb 13, 2026
Merged

feat: display profile avatar image with terminal graphics protocols#23
unhappychoice merged 2 commits intomainfrom
feat/display-images

Conversation

@unhappychoice
Copy link
Owner

@unhappychoice unhappychoice commented Feb 13, 2026

Summary

Add --image flag to display Steam profile avatar using terminal graphics protocols instead of the ASCII logo. Closes #6.

Features

  • --image flag replaces ASCII logo with profile avatar
  • --image-protocol to select protocol: auto (default), kitty, iterm, sixel
  • Auto-detection of terminal capabilities (Kitty, iTerm2, Windows Terminal, WezTerm, foot, etc.)
  • Block character fallback for terminals without graphics support
  • Image caching at ~/.cache/steamfetch/images/

Supported Protocols

Protocol Terminals
Sixel Windows Terminal, WezTerm, foot, mlterm, xterm
Kitty Kitty
iTerm2 iTerm2
Block (fallback) Any terminal with Unicode support

Usage

steamfetch --image                          # Auto-detect protocol
steamfetch --image --image-protocol sixel   # Force sixel
steamfetch --demo --image                   # Demo with image

Implementation

  • image_display module: Downloads, caches, and renders images via native protocol implementations (no viuer dependency)
    • Kitty: Chunked base64 RGBA via APC escape sequences
    • iTerm2: Inline PNG via OSC 1337
    • Sixel: Encoded via icy_sixel crate
    • Block: Half-block () character rendering with true color
  • Terminal cell size detection: ioctl(TIOCGWINSZ) with ESC[14t fallback (same approach as fastfetch) for accurate image-to-cell mapping
  • Cursor management: Image output followed by ESC[1G ESC[NA rewind, then ESC[NC per-line offset for side-by-side layout
  • SteamStats model extended with avatar_url field from Steam API player summary

Summary by CodeRabbit

  • New Features

    • Show Steam profile avatars and game artwork inline in terminal output with selectable protocols (Auto, Kitty, iTerm2, Sixel) and CLI flags to enable/choose image mode.
    • Local image caching to reduce repeated downloads; demo mode now includes avatar display.
  • Documentation

    • README updated with Image Display usage, supported protocols, override option, and cache location.
  • Chores

    • Added image and terminal-rendering dependencies.

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

Warning

Rate limit exceeded

@unhappychoice has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 45 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds terminal image support: new src/image_display.rs, image deps in Cargo.toml, avatar fields in Steam models, CLI flags for image mode and protocol, and an async display::render(stats, image_config) path that loads/caches and prints images with ASCII fallback.

Changes

Cohort / File(s) Summary
Manifest
Cargo.toml
Added four dependencies: image = "0.25", icy_sixel = "0.5", base64 = "0.22.1", libc = "0.2.182".
Image module
src/image_display.rs
New module implementing async image download, disk cache, protocol detection (Kitty/iTerm/Sixel/Block), scaling/printing functions (load_cached_or_download, print_image_and_rewind, cursor_right) and terminal size/cell detection helpers.
Display logic
src/display.rs
Render API changed to pub async fn render(stats: &SteamStats, image_config: &ImageConfig); added ImageConfig and new image-based render path (render_with_image) with fallback to existing ASCII rendering.
CLI & integration
src/main.rs
Added ImageProtocol enum, CLI flags (--image, --image-protocol), builds/passes ImageConfig to display::render (awaited), and demo stats include avatar_url.
Steam models & client
src/steam/models.rs, src/steam/client.rs
Added Player.avatarfull: Option<String> and SteamStats.avatar_url: Option<String>; client populates avatar_url when constructing SteamStats.
Docs
README.md
Documented new Image Display section: usage flags, supported protocols, auto-detect behavior, cache location and examples.

Sequence Diagram

sequenceDiagram
    participant User
    participant Main as Main (CLI)
    participant Steam as SteamClient
    participant Image as ImageDisplay
    participant Term as Terminal

    User->>Main: run with --image [--image-protocol]
    Main->>Steam: fetch_stats(user_id)
    Steam-->>Main: SteamStats { avatar_url, ... }
    Main->>Image: load_cached_or_download(avatar_url, cache_key)
    alt cache hit
        Image-->>Main: DynamicImage
    else cache miss
        Image->>Image: download_image(url)
        Image->>Image: save_to_cache
        Image-->>Main: DynamicImage
    end
    Main->>Image: print_image_and_rewind(img, protocol, cols, rows)
    Image->>Term: render via chosen protocol (Kitty/iTerm/Sixel/Block)
    Term-->>User: image displayed (or ASCII fallback if unsupported)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble bytes and chase the cache,

Avatars bloom where pixels splash.
Kitty, iTerm, Sixel play —
I hop the rows and print away.
Hooray for images on display!

🚥 Pre-merge checks | ✅ 4 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (8 files):

⚔️ Cargo.lock (content)
⚔️ Cargo.toml (content)
⚔️ README.md (content)
⚔️ src/display.rs (content)
⚔️ src/main.rs (content)
⚔️ src/steam/client.rs (content)
⚔️ src/steam/mod.rs (content)
⚔️ src/steam/models.rs (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature added: displaying profile avatar images using terminal graphics protocols, which aligns with the primary changeset.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #6: profile avatar display [#6], multiple protocol support (Kitty, iTerm2, Sixel) [#6], CLI flags for --image and --image-protocol [#6], image caching [#6], ASCII fallback [#6], and terminal capability detection [#6].
Out of Scope Changes check ✅ Passed All changes are directly related to implementing image display support as specified in issue #6; no unrelated modifications or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/display-images

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@Cargo.toml`:
- Around line 27-28: Update the viuer dependency in Cargo.toml from "0.9" to
"0.11.0" (replace the viuer = "0.9" line with viuer = "0.11.0"), then run cargo
update to refresh lockfile and rebuild to surface any API breakages; check any
uses of viuer API (e.g., functions like print_from_file or Config types) and
adapt callsites to match the 0.11.0 API if the compiler reports changes.

In `@src/image_display.rs`:
- Around line 9-12: download_image currently calls reqwest::get which uses a
default client with no timeout; create a dedicated reqwest::Client with a
timeout (e.g., Duration::from_secs(10)) and use client.get(url).send().await
(then .bytes().await) instead of reqwest::get so the request will fail fast on
slow/unresponsive servers; keep the existing Option return pattern by mapping
send/bytes/load_from_memory errors to None and reference the download_image
function when making the change.
- Around line 14-21: The cache key derived from user-controlled Steam
`personaname` can contain path separators and enable path traversal; modify the
code that builds and uses cache keys (e.g., where `format!("avatar_{}.png",
stats.username)` is produced and passed into `load_cached_or_download`,
`save_to_cache`, and `load_from_cache`) to produce a filesystem-safe key: either
sanitize the username by removing/replacing path-significant characters (like
`/`, `\`, `..`, null bytes) and restricting to a safe charset (alphanumeric,
dash, underscore) or compute a deterministic hash (e.g., SHA-256 or SHA-1 hex)
of the username and use that as the filename (e.g., `avatar_<hash>.png`); ensure
the same sanitized/hash logic is used wherever cache keys are generated and
consumed so existing lookup/save works correctly.
- Around line 23-38: Enable viuer's sixel feature in Cargo.toml (set viuer = {
version = "0.9", features = ["sixel"] }) and update print_image (function
print_image in src/image_display.rs) to set ViuerConfig.use_sixel based on the
ImageProtocol (check ImageProtocol::Sixel and ImageProtocol::Auto) so that when
protocol == ImageProtocol::Sixel viuer emits sixel output; keep existing
use_kitty/use_iterm logic and ensure use_sixel is included in the ViuerConfig
initialization.

In `@src/main.rs`:
- Around line 68-73: The current ImageConfig sets enabled and show_avatar from
the same expression, making show_avatar redundant; change show_avatar to use
cli.avatar only so that --image enables artwork (image_enabled = cli.image ||
cli.avatar) while show_avatar = cli.avatar controls whether to render the
avatar. Update the construction of display::ImageConfig (the ImageConfig
initializer in main.rs) to set enabled = image_enabled and show_avatar =
cli.avatar, and ensure display::render still branches on image_config.enabled &&
image_config.show_avatar for avatar vs non-avatar image rendering.
- Around line 172-176: The demo's top_game_ids vector does not match the titles
in top_games, causing incorrect artwork; locate the top_game_ids and top_games
variables (same scope as avatar_url) and replace the incorrect IDs (e.g.,
1514950 which points to Neodash) with the correct Steam app IDs that correspond
to the demo titles (e.g., Coin Push RPG and Deep Rock Galactic: Survivor as
listed in top_games); ensure each entry in top_game_ids maps one-to-one to the
names in top_games so --demo --image shows the right artwork.
🧹 Nitpick comments (3)
src/display.rs (2)

7-11: enabled and show_avatar are always set to the same value.

In main.rs, both fields are computed as cli.image || cli.avatar, so they're always identical. The show_avatar field is effectively redundant today. If the intent is to support a future mode where images are enabled without showing the avatar (e.g., game artwork only), consider adding a --no-avatar flag or documenting the planned distinction. Otherwise, a single enabled bool would simplify things.


62-80: Consider parallel image fetching for game artwork.

The three game artwork images are fetched sequentially. Since they're independent network requests, fetching them concurrently with futures::join_all or tokio::join! would reduce latency. This is a nice-to-have given only 3 images.

♻️ Sketch using concurrent fetching
 async fn fetch_game_artwork(
     game_ids: &[u32],
     config: &ImageConfig,
 ) -> Vec<(u32, image::DynamicImage)> {
     if !config.enabled || game_ids.is_empty() {
         return Vec::new();
     }
 
-    let mut images = Vec::new();
-    // Fetch artwork for top 3 games to keep output reasonable
-    for &appid in game_ids.iter().take(3) {
-        let url = image_display::game_header_image_url(appid);
-        let cache_key = format!("game_{}.jpg", appid);
-        if let Some(img) = image_display::load_cached_or_download(&url, &cache_key).await {
-            images.push((appid, img));
-        }
-    }
-    images
+    let futures: Vec<_> = game_ids
+        .iter()
+        .take(3)
+        .map(|&appid| async move {
+            let url = image_display::game_header_image_url(appid);
+            let cache_key = format!("game_{}.jpg", appid);
+            image_display::load_cached_or_download(&url, &cache_key)
+                .await
+                .map(|img| (appid, img))
+        })
+        .collect();
+    futures::future::join_all(futures)
+        .await
+        .into_iter()
+        .flatten()
+        .collect()
 }
src/image_display.rs (1)

57-63: save_to_cache always re-encodes as PNG regardless of source format.

JPEG game header images are re-encoded as PNG, which is lossless and typically much larger. This inflates cache size unnecessarily. Consider saving the original bytes instead, or inferring the format from the cache key extension.

@unhappychoice unhappychoice changed the title feat: display images (profile avatar, game artwork) feat: display profile avatar image with terminal graphics protocols Feb 13, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/image_display.rs`:
- Around line 30-38: The block branch double-resizes the image: remove the
pre-resize in the ResolvedProtocol::Block branch inside print_image_and_rewind
and instead pass the original image to print_block so print_block alone handles
sizing; specifically delete the term/pixel_w/pixel_h/resized lines and call
print_block(&img, cols) (or the existing image variable) and keep the rest
(term_rows handling and the rewind print) intact so print_block performs the
pixel-to-terminal resizing once.
- Around line 50-55: The iTerm branch is still using the original DynamicImage
(img) instead of the resized RGBA data, causing sizing mismatches with
print_kitty/print_sixel; update the call site to pass the resized image data
(the same rgba used for print_kitty/print_sixel) to print_iterm and adjust
print_iterm's signature/implementation to accept the resized image buffer or
resized DynamicImage (reuse symbols rgba, print_iterm, print_kitty, print_sixel)
so the iTerm OSC sequence renders the constrained size consistently with
rows/cols and the cursor rewind logic.
🧹 Nitpick comments (6)
src/image_display.rs (3)

89-106: MaybeUninit::zeroed().assume_init() is sound here but non-idiomatic.

For FFI structs, the idiomatic Rust pattern is to keep the value as MaybeUninit until after the ioctl call succeeds, then call assume_init(). Calling assume_init() before the ioctl is safe for libc::winsize (all-zeros is valid), but it hides the intent and won't generalize.


245-267: Kitty protocol sends raw RGBA — consider compressed PNG for large images.

print_kitty base64-encodes the raw RGBA pixel buffer (f=32). For a 340×360 image this is ~490 KB raw / ~654 KB base64. The Kitty protocol also supports f=100 (PNG), which would be significantly smaller and faster to transmit. This matters more for game artwork images which could be larger.


222-241: is_sixel_capable_terminal returns true for iTerm2 via LC_TERMINAL, but iTerm2 has its own protocol.

If LC_TERMINAL contains "iTerm2" and ITERM_SESSION_ID is not set (which is unlikely but possible), the auto-detection would resolve to Sixel instead of the native iTerm protocol. The iTerm-specific inline image protocol is more feature-rich. Consider checking the order or excluding iTerm2 from the sixel heuristic.

src/display.rs (2)

26-37: Avatar fetch failure silently falls back to ASCII — consider a brief diagnostic.

When image_display::load_cached_or_download returns None (line 35), the code silently falls back to ASCII. For users who explicitly passed --image, a brief stderr message (e.g., "Could not load avatar, falling back to ASCII") would help diagnose configuration or network issues.


42-47: Image print failure also falls back silently.

Same as above — if print_image_and_rewind returns None (line 45), the user gets ASCII without explanation. A short diagnostic on stderr would improve UX when --image is explicitly requested.

src/main.rs (1)

166-169: Hardcoded avatar URL in demo data is fine but will break if the CDN asset is removed.

Minor concern — the demo's avatar_url points to a specific Steam CDN asset. If that URL goes stale, --demo --image will silently fall back to ASCII. Consider noting this or using a more stable test image URL.

@unhappychoice unhappychoice force-pushed the feat/display-images branch 2 times, most recently from 3bbe096 to e0bfa66 Compare February 13, 2026 10:36
@unhappychoice unhappychoice merged commit 41ae1e2 into main Feb 13, 2026
8 checks passed
@unhappychoice unhappychoice deleted the feat/display-images branch February 13, 2026 10:45
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.

feat: display images (profile avatar, game artwork)

1 participant