Skip to content
This repository was archived by the owner on Jul 28, 2026. It is now read-only.

Commit 6beb2fe

Browse files
JessyTsuiclaude
andcommitted
Fix version comparison, implement inline image display
- Version check now uses semver comparison (is_newer), not just inequality. Prevents "downgrade" prompts from stale cache. - Inline images: when `images = true` in config, download keyframe or thumbnail and render using iTerm2 Inline Images Protocol. Falls back gracefully on unsupported terminals. - Add base64 + reqwest blocking dependencies for image handling. - Bump to 0.0.9. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 33d873b commit 6beb2fe

3 files changed

Lines changed: 59 additions & 6 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cerul"
3-
version = "0.0.8"
3+
version = "0.0.9"
44
edition = "2021"
55
description = "Official Cerul CLI - search video knowledge from your terminal"
66
license = "MIT"
@@ -12,11 +12,12 @@ categories = ["command-line-utilities"]
1212

1313
[dependencies]
1414
anyhow = "1"
15+
base64 = "0.22"
1516
clap = { version = "4", features = ["derive"] }
1617
colored = "3"
1718
dialoguer = { version = "0.11", features = ["fuzzy-select"] }
1819
flate2 = "1"
19-
reqwest = { version = "0.12", features = ["json", "rustls-tls", "socks"], default-features = false }
20+
reqwest = { version = "0.12", features = ["json", "rustls-tls", "socks", "blocking"], default-features = false }
2021
serde = { version = "1", features = ["derive"] }
2122
serde_json = "1"
2223
tar = "0.4"

src/commands/upgrade.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ pub async fn check_update_background() -> Option<String> {
8181
if modified.elapsed().unwrap_or_default().as_secs() < 86400 {
8282
let cached = std::fs::read_to_string(&cache_path).ok()?;
8383
let latest = cached.trim().to_string();
84-
if !latest.is_empty() && latest != CURRENT_VERSION {
84+
if !latest.is_empty() && is_newer(&latest, CURRENT_VERSION) {
8585
return Some(latest);
8686
}
8787
return None;
@@ -97,7 +97,7 @@ pub async fn check_update_background() -> Option<String> {
9797
std::fs::write(dir.join("last_update_check"), &latest).ok();
9898
}
9999

100-
if latest != CURRENT_VERSION {
100+
if is_newer(&latest, CURRENT_VERSION) {
101101
Some(latest)
102102
} else {
103103
None
@@ -202,6 +202,19 @@ fn extract_tar_entry(tar_data: &[u8], entry_name: &str) -> Result<Vec<u8>> {
202202
bail!("Binary '{entry_name}' not found in archive");
203203
}
204204

205+
/// Compare semver strings: is `latest` strictly newer than `current`?
206+
fn is_newer(latest: &str, current: &str) -> bool {
207+
let parse = |s: &str| -> (u64, u64, u64) {
208+
let parts: Vec<u64> = s.split('.').filter_map(|p| p.parse().ok()).collect();
209+
(
210+
parts.first().copied().unwrap_or(0),
211+
parts.get(1).copied().unwrap_or(0),
212+
parts.get(2).copied().unwrap_or(0),
213+
)
214+
};
215+
parse(latest) > parse(current)
216+
}
217+
205218
fn cache_dir() -> Result<std::path::PathBuf> {
206219
let base = if cfg!(target_os = "macos") {
207220
env::var("HOME")

src/output.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
use std::io::IsTerminal;
2+
13
use anyhow::{Context, Result};
24
use colored::Colorize;
35
use serde::Serialize;
46

57
use crate::types::{SearchResponse, UsageResponse};
68

7-
pub fn print_search_human(response: &SearchResponse, _show_images: bool) {
9+
pub fn print_search_human(response: &SearchResponse, show_images: bool) {
810
let credit_note = if response.credits_used == 0 {
911
"free daily search".green().to_string()
1012
} else {
@@ -49,6 +51,16 @@ pub fn print_search_human(response: &SearchResponse, _show_images: bool) {
4951
}
5052
eprintln!(" │ {}", meta.join(" · ").dimmed());
5153

54+
// Inline image (iTerm2 / Kitty protocol)
55+
if show_images {
56+
if let Some(url) = result.keyframe_url.as_deref().or(result.thumbnail_url.as_deref()) {
57+
eprintln!(" │");
58+
if let Some(img_data) = fetch_image_bytes(url) {
59+
print_inline_image(&img_data);
60+
}
61+
}
62+
}
63+
5264
// Transcript / snippet
5365
let text = result
5466
.transcript
@@ -211,6 +223,33 @@ fn osc8_link(url: &str, label: &str) -> String {
211223
}
212224

213225
fn is_terminal_interactive() -> bool {
214-
use std::io::IsTerminal;
215226
std::io::stderr().is_terminal()
216227
}
228+
229+
/// Download image bytes. Returns None on any failure (non-blocking to UX).
230+
fn fetch_image_bytes(url: &str) -> Option<Vec<u8>> {
231+
// Use a blocking reqwest call since we're already in an output function.
232+
// Short timeout to avoid slowing down results.
233+
let client = reqwest::blocking::Client::builder()
234+
.timeout(std::time::Duration::from_secs(5))
235+
.build()
236+
.ok()?;
237+
let resp = client.get(url).send().ok()?;
238+
if !resp.status().is_success() {
239+
return None;
240+
}
241+
resp.bytes().ok().map(|b| b.to_vec())
242+
}
243+
244+
/// Print an inline image using the iTerm2 Inline Images Protocol.
245+
/// Falls back to nothing on unsupported terminals (the escape is simply ignored).
246+
fn print_inline_image(data: &[u8]) {
247+
use base64::{engine::general_purpose::STANDARD, Engine};
248+
let encoded = STANDARD.encode(data);
249+
// iTerm2 protocol: \x1b]1337;File=inline=1;width=40;preserveAspectRatio=1:<base64>\x07
250+
eprint!(
251+
" │ \x1b]1337;File=inline=1;width=40;preserveAspectRatio=1:{}\x07",
252+
encoded
253+
);
254+
eprintln!();
255+
}

0 commit comments

Comments
 (0)