Created
January 14, 2026 16:35
-
-
Save llimllib/45a99a5640c5577563f0becf51f77d71 to your computer and use it in GitHub Desktop.
Claude Code session: HTML img tag support and kitty image sizing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ▐▛███▜▌ Claude Code v2.1.7 | |
| ▝▜█████▛▘ Opus 4.5 · Claude Pro | |
| ▘▘ ▝▝ ~/code/mdriver/display-img-html-tag | |
| ❯ your task is in task.md | |
| ⏺ Read(task.md) | |
| ⎿ Read 15 lines | |
| ⏺ Let me explore the codebase to understand how images are currently handled. | |
| ⏺ Explore(Explore image handling code) | |
| ⎿ Done (19 tool uses · 59.8k tokens · 1m 9s) | |
| ⏺ Read(src/lib.rs) | |
| ⎿ Read 100 lines | |
| ⏺ Now I understand the sizing issue. The code resizes images to term_width character cells (typically 80-100), treating it as pixels. This results in tiny images. Let me look at the HTML parsing code to understand how to add <img> tag support. | |
| ⏺ Read(src/lib.rs) | |
| ⎿ Read 160 lines | |
| ⏺ Read(src/lib.rs) | |
| ⎿ Read 100 lines | |
| ⏺ Now I understand both issues. Let me look at the kitty protocol documentation to understand the proper parameters for sizing. | |
| ⏺ Web Search("kitty graphics protocol terminal image display size columns rows | |
| parameters") | |
| ⎿ Did 2 searches in 33s | |
| ⏺ Now I understand. The issue is that the code doesn't specify c (columns) parameter in the kitty escape sequence, so the image displays at 1:1 pixel scale. Let me check how to get terminal dimensions in both cells and pixels. | |
| ⏺ Search(pattern: "term_size", path: "~/code/mdriver/display-img-html-tag", | |
| output_mode: "content") | |
| ⎿ Found 4 lines (ctrl+o to expand) | |
| ⏺ Read(src/lib.rs) | |
| ⎿ Read 120 lines | |
| ⏺ Now I understand the issues clearly. Let me implement both fixes: | |
| 1. Image sizing: Add c=<columns> parameter to kitty escape sequence | |
| 2. <img> tag support: Handle <img> in HTML parsing | |
| ⏺ Update(src/lib.rs) | |
| ⎿ Added 14 lines, removed 7 lines | |
| 1601 .with_guessed_format()? | |
| 1602 .decode()?; | |
| 1603 | |
| 1604 - // Get terminal width (assume 80 if can't determine) | |
| 1605 - let term_width | |
| - = term_size::dimensions().map(|(w, _)| w).unwrap_or(80); | |
| 1604 + // Get terminal width in columns | |
| +(assume 80 if can't determine) | |
| 1605 + let term_cols | |
| + = term_size::dimensions().map(|(w, _)| w).unwrap_or(80); | |
| 1606 | |
| 1607 - // Resize to terminal width, preserve aspect ratio | |
| 1608 - let resized = if img.width() > term_width as u32 { | |
| 1607 + // Calculate aspect ratio (height/width) to | |
| +determine rows | |
| 1608 + // Assume terminal cells are roughly 2:1 | |
| +(height:width in pixels) | |
| 1609 + let aspect_ratio = img.height() as f64 / | |
| +img.width() as f64; | |
| 1610 + let display_rows = ((term_cols as f64) * | |
| +aspect_ratio / 2.0).ceil() as usize; | |
| 1611 + | |
| 1612 + // Resize large images to reduce transfer size (cap | |
| + at 2000px width) | |
| 1613 + // but let kitty handle the display scaling | |
| 1614 + let max_transfer_width = 2000u32; | |
| 1615 + let resized = if img.width() > max_transfer_width { | |
| 1616 img.resize( | |
| 1617 - term_width as u32, | |
| 1617 + max_transfer_width, | |
| 1618 u32::MAX, | |
| 1619 image::imageops::FilterType::Lanczos3, | |
| 1620 ) | |
| ... | |
| 1619 let mut png_data = Vec::new(); | |
| 1620 resized.write_to(&mut Cursor::new(&mut png_data), | |
| image::ImageFormat::Png)?; | |
| 1621 | |
| 1622 - // Render using kitty protocol | |
| 1623 - Ok(self.render_kitty_image(&png_data)) | |
| 1622 + // Render using kitty protocol | |
| + with display size in terminal cells | |
| 1623 + Ok(self.render_kitty_image(&png_data | |
| +, term_cols, display_rows)) | |
| 1624 } | |
| 1625 | |
| 1626 fn render_kitty_image(&self, png_data: &[u8]) -> String | |
| { | |
| ⏺ Update(src/lib.rs) | |
| ⎿ Added 7 lines, removed 3 lines | |
| 1630 Ok(self.render_kitty_image(&png_data, term_cols, | |
| display_rows)) | |
| 1631 } | |
| 1632 | |
| 1633 - fn render_kitty_image(&self, png_data: &[u8]) -> String { | |
| 1633 + fn render_kitty_image(&self, png_data: &[u8] | |
| +, columns: usize, rows: usize) -> String { | |
| 1634 use base64::{engine::general_purpose::STANDARD, | |
| Engine as _}; | |
| 1635 | |
| 1636 let encoded = STANDARD.encode(png_data); | |
| ... | |
| 1648 let m = if is_last { 0 } else { 1 }; | |
| 1649 | |
| 1650 if i == 0 { | |
| 1651 - // First chunk: include format and | |
| -transmission parameters | |
| 1652 - | |
| -output.push_str(&format!("\x1b_Gf=100,a=T,m={};{}\x1b\\", m, | |
| - chunk)); | |
| 1651 + // First chunk: include format, | |
| +transmission parameters, and display size | |
| 1652 + // c=columns, r=rows tells kitty to scale | |
| +the image to fit in that many cells | |
| 1653 + output.push_str(&format!( | |
| 1654 + | |
| +"\x1b_Gf=100,a=T,c={},r={},m={};{}\x1b\\", | |
| 1655 + columns, rows, m, chunk | |
| 1656 + )); | |
| 1657 } else { | |
| 1658 // Continuation chunks | |
| 1659 | |
| output.push_str(&format!("\x1b_Gm={};{}\x1b\\", m, chunk)); | |
| ⏺ Let me verify the build works before moving on. | |
| ⏺ Bash(cargo build 2>&1) | |
| ⎿ Updating crates.io index | |
| Locking 202 packages to latest compatible versions | |
| Adding toml v0.8.23 (available: v0.9.11+spec-1.1.0) | |
| … +164 lines (ctrl+o to expand) | |
| ⏺ Build successful. Now let me update the todo and implement <img> tag support. | |
| ⏺ Let me fix the HTML parsing to properly handle <img> tags. I need to: | |
| 1. Fix self-closing tag name extraction (only first word) | |
| 2. Handle <img> as a void element | |
| 3. Extract src attribute and render the image | |
| ⏺ Read(src/lib.rs) | |
| ⎿ Read 40 lines | |
| ⏺ Update(src/lib.rs) | |
| ⎿ Added 52 lines, removed 2 lines | |
| 1734 let tag_content: String = chars[start + | |
| 1..tag_end].iter().collect(); | |
| 1735 let tag_content = tag_content.trim(); | |
| 1736 | |
| 1737 - // Check for self-closing tags like <br/> or <hr/> | |
| 1737 + // Check for self-closing tags like <br/> or <hr/> | |
| + or <img src="..."/> | |
| 1738 if tag_content.ends_with('/') { | |
| 1739 - let tag_name = | |
| -tag_content.trim_end_matches('/').trim().to_lowercase(); | |
| 1739 + // Extract tag name (first word only) | |
| 1740 + let tag_trimmed = | |
| +tag_content.trim_end_matches('/').trim(); | |
| 1741 + let tag_name: String = tag_trimmed | |
| 1742 + .chars() | |
| 1743 + .take_while(|c| c.is_alphanumeric()) | |
| 1744 + .collect::<String>() | |
| 1745 + .to_lowercase(); | |
| 1746 + | |
| 1747 if tag_name == "br" { | |
| 1748 return Some(HtmlTagResult { | |
| 1749 formatted: "\n".to_string(), | |
| 1750 end_pos: tag_end + 1, | |
| 1751 }); | |
| 1752 } | |
| 1753 + if tag_name == "img" { | |
| 1754 + // Extract src attribute and render image | |
| 1755 + if let Some(src) = | |
| +self.extract_attr(tag_trimmed, "src") { | |
| 1756 + let alt = | |
| +self.extract_attr(tag_trimmed, "alt").unwrap_or_default(); | |
| 1757 + return Some(HtmlTagResult { | |
| 1758 + formatted: self.render_image(&alt, | |
| +&src), | |
| 1759 + end_pos: tag_end + 1, | |
| 1760 + }); | |
| 1761 + } | |
| 1762 + } | |
| 1763 // Skip other self-closing tags | |
| 1764 return Some(HtmlTagResult { | |
| 1765 formatted: String::new(), | |
| ... | |
| 1750 }); | |
| 1751 } | |
| 1752 | |
| 1753 + // Check for void elements (like <img>, <br>, <hr> | |
| +without trailing /) | |
| 1754 + // Extract tag name first to check | |
| 1755 + let tag_name_check: String = tag_content | |
| 1756 + .chars() | |
| 1757 + .take_while(|c| c.is_alphanumeric()) | |
| 1758 + .collect::<String>() | |
| 1759 + .to_lowercase(); | |
| 1760 + | |
| 1761 + // Handle void elements that don't need closing | |
| +tags | |
| 1762 + if matches!(tag_name_check.as_str(), "img" | "br" | | |
| + "hr" | "meta" | "link" | "input") { | |
| 1763 + if tag_name_check == "br" { | |
| 1764 + return Some(HtmlTagResult { | |
| 1765 + formatted: "\n".to_string(), | |
| 1766 + end_pos: tag_end + 1, | |
| 1767 + }); | |
| 1768 + } | |
| 1769 + if tag_name_check == "img" { | |
| 1770 + // Extract src attribute and render image | |
| 1771 + if let Some(src) = | |
| +self.extract_attr(tag_content, "src") { | |
| 1772 + let alt = | |
| +self.extract_attr(tag_content, "alt").unwrap_or_default(); | |
| 1773 + return Some(HtmlTagResult { | |
| 1774 + formatted: self.render_image(&alt, | |
| +&src), | |
| 1775 + end_pos: tag_end + 1, | |
| 1776 + }); | |
| 1777 + } | |
| 1778 + } | |
| 1779 + // Skip other void elements | |
| 1780 + return Some(HtmlTagResult { | |
| 1781 + formatted: String::new(), | |
| 1782 + end_pos: tag_end + 1, | |
| 1783 + }); | |
| 1784 + } | |
| 1785 + | |
| 1786 // Extract tag name (first word, lowercased) | |
| 1787 let tag_name: String = tag_content | |
| 1788 .chars() | |
| ⏺ Now I need to add the extract_attr helper method. Let me find where to add it and create a generalized attribute extraction function. | |
| ⏺ Read(src/lib.rs) | |
| ⎿ Read 50 lines | |
| ⏺ Update(src/lib.rs) | |
| ⎿ Added 10 lines, removed 5 lines | |
| 1913 Some(HtmlTagResult { formatted, end_pos }) | |
| 1914 } | |
| 1915 | |
| 1916 - /// Extract href attribute value from tag content like 'a | |
| - href="url"' | |
| 1917 - pub fn extract_href(&self, tag_content: &str | |
| -) -> Option<String> { | |
| 1916 + /// Extract an attribute value from tag content like 'img | |
| + src="url"' | |
| 1917 + fn extract_attr(&self, tag_content: &str, attr_name: &str | |
| +) -> Option<String> { | |
| 1918 let lower = tag_content.to_lowercase(); | |
| 1919 - let href_pos = lower.find("href")?; | |
| 1920 - let after_href = &tag_content[href_pos + 4..]; | |
| 1921 - let trimmed = after_href.trim_start(); | |
| 1919 + let attr_pos = | |
| +lower.find(&attr_name.to_lowercase())?; | |
| 1920 + let after_attr = &tag_content[attr_pos + | |
| +attr_name.len()..]; | |
| 1921 + let trimmed = after_attr.trim_start(); | |
| 1922 | |
| 1923 // Expect '=' | |
| 1924 if !trimmed.starts_with('=') { | |
| ... | |
| 1944 } | |
| 1945 } | |
| 1946 | |
| 1947 + /// Extract href attribute value from tag content like | |
| +'a href="url"' | |
| 1948 + pub fn extract_href(&self, tag_content: &str) -> | |
| +Option<String> { | |
| 1949 + self.extract_attr(tag_content, "href") | |
| 1950 + } | |
| 1951 + | |
| 1952 fn parse_image(&self, chars: &[char], start: usize) -> | |
| Option<ImageData> { | |
| 1953 // Looking for  or  | |
| 1954 // start points to '!' | |
| ⏺ Let me build and run tests to verify the changes work. | |
| ⏺ Bash(cargo build 2>&1) | |
| ⎿ Compiling mdriver v0.8.0 (/Users/llimllib/code/mdriver/display-img-h | |
| tml-tag) | |
| Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.79s | |
| ⏺ Bash(cargo clippy --all-targets --all-features -- -D warnings 2>&1) | |
| ⎿ Checking stable_deref_trait v1.2.1 | |
| Checking cfg-if v1.0.4 | |
| Checking memchr v2.7.6 | |
| … +138 lines (ctrl+o to expand) | |
| ⏺ Bash(cargo test 2>&1 | head -100) | |
| ⎿ Compiling serde v1.0.228 | |
| Compiling toml_write v0.1.2 | |
| Compiling winnow v0.7.14 | |
| … +105 lines (ctrl+o to expand) | |
| ⏺ Bash(cargo test 2>&1 | tail -20) | |
| ⎿ test strip_ansi_tests::test_strip_mixed_content ... ok | |
| test strip_ansi_tests::test_strip_long_url ... ok | |
| test strip_ansi_tests::test_strip_osc8_with_styling ... ok | |
| … +18 lines (ctrl+o to expand) | |
| ⏺ Bash(cargo fmt) | |
| ⎿ (No content) | |
| ⏺ Let me build and test manually with the example from the task. | |
| ⏺ Bash(cargo build --release 2>&1 | tail -5) | |
| ⎿ Compiling ravif v0.12.0 | |
| Compiling two-face v0.5.1 | |
| Compiling ureq v2.12.1 | |
| … +2 lines (ctrl+o to expand) | |
| ⏺ Bash(echo '' | | |
| ./target/release/mdriver --images kitty 2>&1 | head -c 500) | |
| ⎿ | |
| ⏺ The markdown image now includes c=80,r=19 parameters. Now let me test the HTML <img> tag. | |
| ⏺ Bash(echo '<img src="http://taskforce.sh/assets/logo_square.png" alt="Test | |
| Image"/>' | ./target/release/mdriver --images kitty 2>&1 | head -c 500) | |
| ⎿ | |
| ⏺ Bash(echo '<img src="http://taskforce.sh/assets/logo_square.png" alt="Test">' | |
| | ./target/release/mdriver --images kitty 2>&1 | head -c 500) | |
| ⎿ | |
| ⏺ Bash(echo '<div><img src="http://taskforce.sh/assets/logo_square.png" | |
| width="200" alt="Taskforce.sh, Inc" style="padding: 200px"/></div>' | | |
| ./target/release/mdriver…) | |
| ⎿ | |
| ⏺ Both issues have been fixed. Here's a summary: | |
| Issue 1: Images displaying tiny | |
| Root cause: The code was resizing images to terminal width in character cells (e.g., 80) but treating that as pixels. Then it wasn't telling kitty how to scale the image for display. | |
| Fix (src/lib.rs:1595-1665): | |
| 1. Changed process_image to: | |
| - Calculate display rows based on aspect ratio | |
| - Cap resize at 2000px (for transfer efficiency only, not display) | |
| - Pass column/row counts to render_kitty_image | |
| 2. Changed render_kitty_image to add c=<columns>,r=<rows> parameters to the kitty escape sequence, telling kitty to scale the image to fit the terminal width. | |
| Issue 2: <img> HTML tag support | |
| Fix (src/lib.rs:1737-1801): | |
| 1. Fixed self-closing tag name extraction to only take the first word (was incorrectly taking entire content like img src="...") | |
| 2. Added img handling in self-closing tags with src attribute extraction | |
| 3. Added void element detection for <img> tags without trailing / | |
| 4. Created generalized extract_attr helper function | |
| Both markdown images  and HTML <img src="url"> tags now display at terminal width with proper aspect ratio when --images kitty is passed. | |
| Sources: | |
| - https://sw.kovidgoyal.net/kitty/graphics-protocol/ | |
| ✻ Cogitated for 7m 2s | |
| ❯ add approprite tests | |
| ⏺ Search(pattern: "tests/**/*.rs") | |
| ⎿ Found 4 files (ctrl+o to expand) | |
| ⏺ Read(tests/unit.rs) | |
| ⎿ Read 626 lines | |
| ⏺ Update(tests/unit.rs) | |
| ⎿ Added 110 lines | |
| 407 } | |
| 408 } | |
| 409 | |
| 410 + mod img_tag { | |
| 411 + use super::*; | |
| 412 + | |
| 413 + // When ImageProtocol::None, img tags should output | |
| +markdown-style  | |
| 414 + | |
| 415 + #[test] | |
| 416 + fn test_img_self_closing_with_space() { | |
| 417 + let p = parser(); | |
| 418 + let result = p.format_inline(r#"<img | |
| +src="https://example.com/image.png" />"#); | |
| 419 + assert_eq!(result, | |
| +""); | |
| 420 + } | |
| 421 + | |
| 422 + #[test] | |
| 423 + fn test_img_self_closing_no_space() { | |
| 424 + let p = parser(); | |
| 425 + let result = p.format_inline(r#"<img | |
| +src="https://example.com/image.png"/>"#); | |
| 426 + assert_eq!(result, | |
| +""); | |
| 427 + } | |
| 428 + | |
| 429 + #[test] | |
| 430 + fn test_img_void_element() { | |
| 431 + // HTML5 void element - no closing tag or trailing | |
| +slash | |
| 432 + let p = parser(); | |
| 433 + let result = p.format_inline(r#"<img | |
| +src="https://example.com/image.png">"#); | |
| 434 + assert_eq!(result, | |
| +""); | |
| 435 + } | |
| 436 + | |
| 437 + #[test] | |
| 438 + fn test_img_with_alt() { | |
| 439 + let p = parser(); | |
| 440 + let result = p.format_inline(r#"<img | |
| +src="https://example.com/image.png" alt="My Image"/>"#); | |
| 441 + assert_eq!(result, ""); | |
| 442 + } | |
| 443 + | |
| 444 + #[test] | |
| 445 + fn test_img_with_alt_void_element() { | |
| 446 + let p = parser(); | |
| 447 + let result = p.format_inline(r#"<img | |
| +src="https://example.com/image.png" alt="My Image">"#); | |
| 448 + assert_eq!(result, ""); | |
| 449 + } | |
| 450 + | |
| 451 + #[test] | |
| 452 + fn test_img_single_quoted_attrs() { | |
| 453 + let p = parser(); | |
| 454 + let result = p.format_inline(r#"<img | |
| +src='https://example.com/image.png' alt='Alt Text'/>"#); | |
| 455 + assert_eq!(result, ""); | |
| 456 + } | |
| 457 + | |
| 458 + #[test] | |
| 459 + fn test_img_case_insensitive_tag() { | |
| 460 + let p = parser(); | |
| 461 + let result = p.format_inline(r#"<IMG | |
| +src="https://example.com/image.png"/>"#); | |
| 462 + assert_eq!(result, | |
| +""); | |
| 463 + } | |
| 464 + | |
| 465 + #[test] | |
| 466 + fn test_img_case_insensitive_attrs() { | |
| 467 + let p = parser(); | |
| 468 + let result = p.format_inline(r#"<img | |
| +SRC="https://example.com/image.png" ALT="Test"/>"#); | |
| 469 + assert_eq!(result, | |
| +""); | |
| 470 + } | |
| 471 + | |
| 472 + #[test] | |
| 473 + fn test_img_with_other_attrs() { | |
| 474 + let p = parser(); | |
| 475 + let result = p.format_inline( | |
| 476 + r#"<img src="https://example.com/image.png" | |
| +width="200" alt="Logo" style="padding: 10px"/>"#, | |
| 477 + ); | |
| 478 + assert_eq!(result, | |
| +""); | |
| 479 + } | |
| 480 + | |
| 481 + #[test] | |
| 482 + fn test_img_inside_div() { | |
| 483 + let p = parser(); | |
| 484 + let result = p.format_inline(r#"<div><img | |
| +src="https://example.com/image.png" alt="Test"/></div>"#); | |
| 485 + assert_eq!(result, | |
| +""); | |
| 486 + } | |
| 487 + | |
| 488 + #[test] | |
| 489 + fn test_img_no_src_returns_empty() { | |
| 490 + let p = parser(); | |
| 491 + let result = p.format_inline(r#"<img alt="No | |
| +Source"/>"#); | |
| 492 + // When no src attribute, should output empty (tag | |
| +is skipped) | |
| 493 + assert_eq!(result, ""); | |
| 494 + } | |
| 495 + | |
| 496 + #[test] | |
| 497 + fn test_img_inline_with_text() { | |
| 498 + let p = parser(); | |
| 499 + let result = p.format_inline(r#"Here is an image: | |
| +<img src="https://example.com/image.png" alt="pic"/> and more | |
| + text"#); | |
| 500 + assert_eq!(result, "Here is an image: | |
| + and more text"); | |
| 501 + } | |
| 502 + | |
| 503 + #[test] | |
| 504 + fn test_multiple_img_tags() { | |
| 505 + let p = parser(); | |
| 506 + let result = p.format_inline( | |
| 507 + r#"<img src="https://example.com/a.png" | |
| +alt="A"/> and <img src="https://example.com/b.png" | |
| +alt="B"/>"#, | |
| 508 + ); | |
| 509 + assert_eq!(result, " | |
| +and "); | |
| 510 + } | |
| 511 + | |
| 512 + #[test] | |
| 513 + fn test_img_with_local_path() { | |
| 514 + let p = parser(); | |
| 515 + let result = p.format_inline(r#"<img | |
| +src="./images/logo.png" alt="Logo"/>"#); | |
| 516 + assert_eq!(result, ""); | |
| 517 + } | |
| 518 + } | |
| 519 + | |
| 520 mod html_entities { | |
| 521 use super::*; | |
| 522 | |
| ⏺ Let me run the tests to verify they pass. | |
| ⏺ Bash(cargo test img_tag 2>&1) | |
| ⎿ Compiling mdriver v0.8.0 (/Users/llimllib/code/mdriver/display-img-h | |
| tml-tag) | |
| Finished `test` profile [unoptimized + debuginfo] target(s) in 1.03 | |
| … +45 lines (ctrl+o to expand) | |
| ⏺ All 14 new tests pass. Let me run all tests and clippy to ensure everything works. | |
| ⏺ Bash(cargo test 2>&1 | tail -30) | |
| ⎿ test img_tag::test_img_self_closing_no_space ... ok | |
| test img_tag::test_img_void_element ... ok | |
| test img_tag::test_img_single_quoted_attrs ... ok | |
| … +28 lines (ctrl+o to expand) | |
| ⏺ Bash(cargo fmt && cargo clippy --all-targets --all-features -- -D warnings | |
| 2>&1 | tail -10) | |
| ⎿ Checking mdriver v0.8.0 (/Users/llimllib/code/mdriver/display-img-h | |
| tml-tag) | |
| Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.64s | |
| ⏺ Added 14 tests for <img> HTML tag handling in tests/unit.rs:410-524. The tests cover: | |
| Self-closing forms: | |
| - <img src="..." /> (with space before slash) | |
| - <img src="..."/> (no space before slash) | |
| Void element form: | |
| - <img src="..."> (no closing tag or slash) | |
| Attribute handling: | |
| - With alt attribute | |
| - Single-quoted attributes | |
| - Case-insensitive tag name (<IMG>) | |
| - Case-insensitive attribute names (SRC, ALT) | |
| - Extra attributes (width, style) are ignored | |
| Integration scenarios: | |
| - <img> nested inside <div> | |
| - Multiple <img> tags in one string | |
| - <img> inline with surrounding text | |
| - Missing src attribute (returns empty) | |
| - Local file paths | |
| All 80 tests pass (66 original + 14 new), and clippy reports no warnings. | |
| ✻ Baked for 1m 17s | |
| ❯ is mdriver now stretching the image to full width? | |
| ⏺ Yes, currently the implementation stretches images to full terminal width. In src/lib.rs:1604-1610: | |
| let term_cols = term_size::dimensions().map(|(w, _)| w).unwrap_or(80); | |
| let aspect_ratio = img.height() as f64 / img.width() as f64; | |
| let display_rows = ((term_cols as f64) * aspect_ratio / 2.0).ceil() as usize; | |
| This always uses term_cols as the display width. | |
| Would you prefer a different behavior? Some options: | |
| 1. Respect image's natural size - Display at natural pixel dimensions (but cap at terminal width) | |
| 2. Respect HTML width attribute - For <img width="200">, display at ~200 pixels worth of columns | |
| 3. Use a max width percentage - e.g., cap at 50% or 75% of terminal width | |
| 4. Keep full width - Current behavior (good for large images, but small images get stretched) | |
| What sizing behavior would you like? | |
| ❯ we should use the image's natural size, unless it's larger than the | |
| --width setting, in which case we should display it no wider than that, | |
| and displayed in the correct aspect ratio | |
| ⏺ I'll update the image sizing logic to use the image's natural size, capped at the configured width. | |
| ⏺ Read(src/lib.rs) | |
| ⎿ Read 40 lines | |
| ⏺ Update(src/lib.rs) | |
| ⎿ Added 11 lines, removed 6 lines | |
| 1601 .with_guessed_format()? | |
| 1602 .decode()?; | |
| 1603 | |
| 1604 - // Get terminal width in columns (assume 80 if | |
| -can't determine) | |
| 1605 - let term_cols = term_size::dimensions().map(|(w, | |
| -_)| w).unwrap_or(80); | |
| 1604 + // Convert image pixel width to terminal columns | |
| 1605 + // Assume ~9 pixels per terminal column (typical | |
| +for monospace fonts) | |
| 1606 + const PIXELS_PER_COLUMN: f64 = 9.0; | |
| 1607 + let natural_cols = (img.width() as f64 / | |
| +PIXELS_PER_COLUMN).ceil() as usize; | |
| 1608 | |
| 1609 - // Calculate aspect ratio (height/width) to | |
| -determine rows | |
| 1610 - // Assume terminal cells are roughly 2:1 | |
| -(height:width in pixels) | |
| 1609 + // Use the smaller of natural size or configured | |
| +max width | |
| 1610 + let display_cols = natural_cols.min(self.width); | |
| 1611 + | |
| 1612 + // Calculate rows to maintain aspect ratio | |
| 1613 + // Terminal cells are roughly 2:1 (height:width in | |
| +pixels) | |
| 1614 let aspect_ratio = img.height() as f64 / | |
| img.width() as f64; | |
| 1615 - let display_rows = ((term_cols | |
| - as f64) * aspect_ratio / 2.0).ceil() as usize; | |
| 1615 + let display_rows = ((display_cols | |
| + as f64) * aspect_ratio / 2.0).ceil() as usize; | |
| 1616 | |
| 1617 // Resize large images to reduce transfer size (cap | |
| at 2000px width) | |
| 1618 // but let kitty handle the display scaling | |
| ... | |
| 1627 resized.write_to(&mut Cursor::new(&mut png_data), | |
| image::ImageFormat::Png)?; | |
| 1628 | |
| 1629 // Render using kitty protocol with display size in | |
| terminal cells | |
| 1630 - Ok(self.render_kitty_image(&png_data, term_cols | |
| -, display_rows)) | |
| 1630 + Ok(self.render_kitty_image(&png_data, display_cols | |
| +, display_rows)) | |
| 1631 } | |
| 1632 | |
| 1633 fn render_kitty_image(&self, png_data: &[u8], columns: | |
| usize, rows: usize) -> String { | |
| ⏺ Bash(cargo build --release 2>&1 | tail -5) | |
| ⎿ Compiling mdriver v0.8.0 (/Users/llimllib/code/mdriver/display-img-h | |
| tml-tag) | |
| Finished `release` profile [optimized] target(s) in 6.77s | |
| ⏺ Bash(echo '' | | |
| ./target/release/mdriver --images kitty 2>&1 | head -c 200) | |
| ⎿ | |
| ⏺ Now the image displays at c=66,r=16 (592px ÷ 9 ≈ 66 columns) instead of being stretched to full width. Let me run tests and clippy. | |
| ⏺ Bash(cargo test 2>&1 | tail -10 && cargo clippy --all-targets --all-features | |
| -- -D warnings 2>&1 | tail -5) | |
| ⎿ test wrap_text_tests::test_wrap_with_hyperlink_visible_width ... ok | |
| test result: ok. 80 passed; 0 failed; 0 ignored; 0 measured; 0 filtered | |
| … +12 lines (ctrl+o to expand) | |
| ⏺ Bash(cargo fmt) | |
| ⎿ (No content) | |
| ⏺ Done. The image sizing logic now: | |
| 1. Calculates natural size: Converts image pixel width to terminal columns using ~9 pixels/column | |
| 2. Caps at --width: Uses min(natural_cols, self.width) so images won't exceed the configured width | |
| 3. Maintains aspect ratio: Rows are calculated from the display columns and image aspect ratio | |
| For the 592×281 pixel image: | |
| - Natural size: 592 ÷ 9 ≈ 66 columns | |
| - With default --width 80, displays at 66 columns (its natural size) | |
| - If you used --width 50, it would cap at 50 columns | |
| ✻ Sautéed for 1m 28s | |
| ❯ great, that's definitely an improvement. commit it | |
| ⏺ Bash(git status) | |
| ⎿ On branch display-img-html-tag | |
| Changes not staged for commit: | |
| (use "git add <file>..." to update what will be committed) | |
| … +9 lines (ctrl+o to expand) | |
| ⏺ Bash(git diff) | |
| ⎿ diff --git i/src/lib.rs w/src/lib.rs | |
| index dc9c6d9..9110ad9 100644 | |
| --- i/src/lib.rs | |
| … +346 lines (ctrl+o to expand) | |
| ⏺ Bash(git log --oneline -5) | |
| ⎿ 3b91858 feat: add HTML entity support (#10) | |
| 656905e feat: add red foreground color to inline code for better visibi | |
| lity (#9) | |
| … +3 lines (ctrl+o to expand) | |
| ⏺ Bash(git add src/lib.rs tests/unit.rs && git commit -m "feat: add HTML <img> | |
| tag support and fix kitty image sizing…) | |
| ⎿ [display-img-html-tag c5ce560] feat: add HTML <img> tag support and fix | |
| kitty image sizing | |
| 2 files changed, 210 insertions(+), 17 deletions(-) | |
| ⏺ Bash(git status && git log --oneline -3) | |
| ⎿ On branch display-img-html-tag | |
| Untracked files: | |
| (use "git add <file>..." to include in what will be committed) | |
| … +8 lines (ctrl+o to expand) | |
| ⏺ Done. Committed to branch display-img-html-tag: | |
| c5ce560 feat: add HTML <img> tag support and fix kitty image sizing | |
| ✻ Worked for 34s | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment