Skip to content

Commit

Permalink
Use window size from settings in kitty
Browse files Browse the repository at this point in the history
No longer run kitty icat --print-window-size to determine the window
size in pixels; instead rely on what we've got from the terminal
directly (kitty icat --print-window-size does nothing different).

This is simpler, faster and, above all, works over SSH connections.
  • Loading branch information
swsnr committed Oct 16, 2020
1 parent cf06ab1 commit 4335fc6
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 101 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ To publish a new release run `scripts/release` from the project directory.

### Changed
- `mdcat` now asks the controlling terminal for the terminal size and thus correctly detects the terminal size even if standard input, standard output and standard error are all redirected (see [GH-166]).
- `mdcat` no longer requires `kitty icat` to detect the size of kitty windows (see [GH-166]).
Consequently mdcat can now show images on Kitty terminals even over SSH.

[kitty-0.19]: https://sw.kovidgoyal.net/kitty/changelog.html#id2
[kitty GH-68]: https://github.com/kovidgoyal/kitty/issues/68
Expand Down
12 changes: 8 additions & 4 deletions src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use std::io::prelude::*;

use ansi_term::{Colour, Style};
use anyhow::anyhow;
use fehler::throws;
use pulldown_cmark::Event::*;
use pulldown_cmark::Tag::*;
Expand Down Expand Up @@ -588,10 +589,13 @@ pub fn write_event<'a, W: Write>(
})
.map(|_| RenderedImage)
.ok(),
(Some(Kitty(kitty)), Some(ref url)) => kitty
.read_and_render(url, settings.resource_access)
.and_then(|contents| {
kitty.write_inline_image(writer, contents)?;
(Some(Kitty(kitty)), Some(ref url)) => settings
.terminal_size
.pixels
.ok_or_else(|| anyhow!("Terminal pixel size not available"))
.and_then(|size| {
let image = kitty.read_and_render(url, settings.resource_access, size)?;
kitty.write_inline_image(writer, image)?;
Ok(RenderedImage)
})
.ok(),
Expand Down
121 changes: 26 additions & 95 deletions src/terminal/kitty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@

use crate::resources::read_url;
use crate::svg::render_svg;
use crate::terminal::size::PixelSize;
use crate::{magic, ResourceAccess};
use anyhow::{anyhow, Context, Error, Result};
use anyhow::{Context, Error};
use fehler::throws;
use image::imageops::FilterType;
use image::ColorType;
use image::{DynamicImage, GenericImageView};
use std::io::Write;
use std::process::{Command, Stdio};
use std::str;
use url::Url;

Expand All @@ -39,56 +39,6 @@ pub fn is_kitty() -> bool {
.unwrap_or(false)
}

/// Retrieve the terminal size in pixels by calling the command-line tool `kitty`.
///
/// ```console
/// $ kitty +kitten icat --print-window-size
/// ```
///
/// We cannot use the terminal size information from Context.output.size, because
/// the size information are in columns / rows instead of pixel.
fn get_terminal_size() -> Result<KittyDimension> {
let process = Command::new("kitty")
.arg("+kitten")
.arg("icat")
.arg("--print-window-size")
.stdout(Stdio::piped())
.spawn()
.with_context(|| "Failed to spawn kitty +kitten icat --print-window-size")?;

let output = process.wait_with_output()?;

if output.status.success() {
let terminal_size_str = std::str::from_utf8(&output.stdout).with_context(|| {
format!(
"kitty +kitten icat --print-window-size returned non-utf8: {:?}",
output.stdout
)
})?;
let terminal_size = terminal_size_str.split('x').collect::<Vec<&str>>();

terminal_size[0]
.parse::<u32>()
.and_then(|width| {
terminal_size[1]
.parse::<u32>()
.map(|height| KittyDimension { width, height })
})
.with_context(|| {
format!(
"Failed to parse kitty width and height from output: {}",
terminal_size_str
)
})
} else {
Err(anyhow!(
"kitty +kitten icat --print-window-size failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
))
}
}

/// Provides access to printing images for kitty.
#[derive(Debug, Copy, Clone)]
pub struct KittyImages;
Expand Down Expand Up @@ -134,9 +84,9 @@ impl KittyImages {
format!("f={}", image.format.control_data_value()),
];

if let Some(dimension) = image.dimension {
cmd_header.push(format!("s={}", dimension.width));
cmd_header.push(format!("v={}", dimension.height));
if let Some(size) = image.size {
cmd_header.push(format!("s={}", size.x));
cmd_header.push(format!("v={}", size.y));
}

let image_data = base64::encode(&image.contents);
Expand All @@ -163,9 +113,16 @@ impl KittyImages {
}

/// Read the image bytes from the given URL and wrap them in a `KittyImage`.
/// It scales the image down, if the image size exceeds the terminal window size.
///
/// If the image size exceeds `terminal_size` in either dimension scale the
/// image down to `terminal_size` (preserving aspect ratio).
#[throws]
pub fn read_and_render(self, url: &Url, access: ResourceAccess) -> KittyImage {
pub fn read_and_render(
self,
url: &Url,
access: ResourceAccess,
terminal_size: PixelSize,
) -> KittyImage {
let contents = read_url(url, access)?;
let mime = magic::detect_mime_type(&contents)
.with_context(|| format!("Failed to detect mime type for URL {}", url))?;
Expand All @@ -179,9 +136,8 @@ impl KittyImages {
image::load_from_memory(&contents)
.with_context(|| format!("Failed to load image from URL {}", url))?
};
let terminal_size = get_terminal_size()?;

if magic::is_png(&mime) && terminal_size.contains(&image.dimensions().into()) {
if magic::is_png(&mime) && PixelSize::from_xy(image.dimensions()) <= terminal_size {
self.render_as_png(contents)
} else {
self.render_as_rgb_or_rgba(image, terminal_size)
Expand All @@ -193,17 +149,15 @@ impl KittyImages {
KittyImage {
contents,
format: KittyFormat::PNG,
dimension: None,
size: None,
}
}

/// Render the image as RGB/RGBA format and wrap the image bytes in `KittyImage`.
/// It scales the image down if its size exceeds the terminal size.
fn render_as_rgb_or_rgba(
self,
image: DynamicImage,
terminal_size: KittyDimension,
) -> KittyImage {
///
/// If the image size exceeds `terminal_size` in either dimension scale the
/// image down to `terminal_size` (preserving aspect ratio).
fn render_as_rgb_or_rgba(self, image: DynamicImage, terminal_size: PixelSize) -> KittyImage {
let format = match image.color() {
ColorType::L8
| ColorType::Rgb8
Expand All @@ -217,25 +171,25 @@ impl KittyImages {
_ => KittyFormat::RGBA,
};

let image = if terminal_size.contains(&KittyDimension::from(image.dimensions())) {
let image = if PixelSize::from_xy(image.dimensions()) <= terminal_size {
image
} else {
image.resize(
terminal_size.width,
terminal_size.height,
terminal_size.x as u32,
terminal_size.y as u32,
FilterType::Nearest,
)
};

let image_dimension = image.dimensions().into();
let size = PixelSize::from_xy(image.dimensions());

KittyImage {
contents: match format {
KittyFormat::RGB => image.into_rgb().into_raw(),
_ => image.into_rgba().into_raw(),
},
format,
dimension: Some(image_dimension),
size: Some(size),
}
}
}
Expand All @@ -244,7 +198,7 @@ impl KittyImages {
pub struct KittyImage {
contents: Vec<u8>,
format: KittyFormat,
dimension: Option<KittyDimension>,
size: Option<PixelSize>,
}

/// The image format (PNG, RGB or RGBA) of the image bytes.
Expand All @@ -267,26 +221,3 @@ impl KittyFormat {
}
}
}

/// The dimension encapsulate the width and height in the pixel unit.
struct KittyDimension {
width: u32,
height: u32,
}

impl KittyDimension {
/// Check whether this dimension entirely contains the specified dimension.
fn contains(&self, other: &KittyDimension) -> bool {
self.width >= other.width && self.height >= other.height
}
}

impl From<(u32, u32)> for KittyDimension {
/// Convert a tuple struct (`u32`, `u32`) ordered by width and height
/// into a `KittyDimension`.
fn from(dimension: (u32, u32)) -> KittyDimension {
let (width, height) = dimension;

KittyDimension { width, height }
}
}
41 changes: 39 additions & 2 deletions src/terminal/size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@

//! Terminal size.

/// The size of a terminal window in pixels
use std::cmp::Ordering;

/// The size of a terminal window in pixels.
///
/// This type is partially ordered; a value is smaller than another if all fields
/// are smaller, and greater if all fields are greater.
///
/// If either field is greater and the other smaller values aren't orderable.
#[derive(Debug, Copy, Clone)]
pub struct PixelSize {
/// The width of the window, in pixels.
Expand All @@ -15,8 +22,38 @@ pub struct PixelSize {
pub y: u32,
}

impl PixelSize {
/// Create a pixel size for a `(x, y)` pair.
pub fn from_xy((x, y): (u32, u32)) -> Self {
Self {
x: x as u32,
y: y as u32,
}
}
}

impl PartialEq for PixelSize {
fn eq(&self, other: &Self) -> bool {
matches!(self.partial_cmp(other), Some(Ordering::Equal))
}
}

impl PartialOrd for PixelSize {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.x == other.x && self.y == other.y {
Some(Ordering::Equal)
} else if self.x < other.x && self.y < other.y {
Some(Ordering::Less)
} else if self.x > other.x && self.y > other.y {
Some(Ordering::Greater)
} else {
None
}
}
}

/// The size of a terminal.
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct TerminalSize {
/// The width of the terminal, in characters aka columns.
pub columns: usize,
Expand Down

0 comments on commit 4335fc6

Please sign in to comment.