Skip to content

Commit

Permalink
Add image positioning using terminal dimensions
Browse files Browse the repository at this point in the history
  • Loading branch information
dhruvkb committed Sep 11, 2024
1 parent c771ea6 commit 78a24e6
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 17 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ env_logger = { version = "0.11.5", default-features = false }
figment = { version = "0.10.10", features = ["yaml", "test"] }
git2 = { version = "0.19.0", default-features = false }
home = "0.5.5"
libc = "0.2.158"
log = { version = "0.4.19", features = ["release_max_level_off"] }
number_prefix = "0.4.0"
path-clean = "1.0.1"
regex = { version = "1.8.4", default-features = false, features = ["std", "perf"] }
resvg = { version = "0.43.0", default-features = false }
serde = { version = "1.0.164", features = ["derive"] }
serde_regex = "1.1.0"
terminal_size = "0.3.0"
time = { version = "0.3.22", default-features = false, features = ["std", "alloc", "local-offset", "formatting"] }
unicode-segmentation = "1.10.1"
uzers = { version = "0.12.1", default-features = false, features = ["cache"] }
Expand Down
6 changes: 4 additions & 2 deletions src/gfx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
//!
//! The public interface of the module consists of three functions:
//!
//! * [`icon_size`]
//! * [`is_supported`]
//! * [`get_rgba`]
//! * [`render_image`]
//! * [`strip_image`]
//! * [`get_rgba`]
mod kitty;
mod svg;

pub use kitty::{is_supported, render_image};
pub use kitty::{icon_size, is_supported, render_image, strip_image};
pub use svg::get_rgba;
56 changes: 51 additions & 5 deletions src/gfx/kitty.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
use crate::PLS;
use base64::prelude::*;
use log::debug;
use regex::Regex;
use std::env;
use std::ops::Mul;
use std::sync::LazyLock;

static KITTY_IMAGE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\x1b_G.*?\x1b\\").unwrap());
const CHUNK_SIZE: usize = 4096;

/// Get the size of the icon in pixels.
///
/// The icon size is determined by the width of a cell in the terminal
/// multiplied by a scaling factor.
pub fn icon_size() -> u8 {
std::env::var("PLS_ICON_SCALE")
.ok()
.and_then(|string| string.parse().ok())
.unwrap_or(1.0f32)
.min(2.0) // We only allocate two cells for an icon.
.mul(PLS.window.as_ref().unwrap().cell_width() as f32) // Convert to px.
.round() as u8
}

/// Check if the terminal supports Kitty's terminal graphics protocol.
///
Expand Down Expand Up @@ -65,23 +86,48 @@ pub fn is_supported() -> bool {
///
/// * `rgba_data` - the RGBA data to render
/// * `size` - the size of the image, in pixels
/// * `width` - the width of the icon cell, in columns
pub fn render_image(rgba_data: &[u8], size: u8, width: u8) -> String {
const CHUNK_SIZE: usize = 4096;
pub fn render_image(rgba_data: &[u8], size: u8) -> String {
let cell_height = PLS.window.as_ref().unwrap().cell_height();
let off_y = if cell_height > size {
(cell_height - size) / 2
} else {
0
};

let encoded = BASE64_STANDARD.encode(rgba_data);
let mut iter = encoded.chars().peekable();

let first_chunk: String = iter.by_ref().take(CHUNK_SIZE).collect();
let mut output = format!("\x1b_Gf=32,t=d,a=T,C=1,m=1,s={size},v={size};{first_chunk}\x1b\\");
let mut output = format!(
"\x1b_G\
f=32,t=d,a=T,C=1,m=1,s={size},v={size},Y={off_y};\
{first_chunk}\
\x1b\\"
);

while iter.peek().is_some() {
let chunk: String = iter.by_ref().take(CHUNK_SIZE).collect();
output.push_str(&format!("\x1b_Gm=1;{chunk}\x1b\\"));
}

output.push_str("\x1b_Gm=0;\x1b\\");

output.push_str(&format!("\x1b[{}C", width + 1));
output.push_str("\x1b[2C");

output
}

/// Strip the image data from the text.
///
/// This function removes the all terminal graphics from the string,
/// leaving only the text content.
///
/// # Arguments
///
/// * `text` - the text to strip the image data from
pub fn strip_image<S>(text: S) -> String
where
S: AsRef<str>,
{
KITTY_IMAGE.replace_all(text.as_ref(), "").to_string()
}
15 changes: 14 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,25 @@ mod output;
mod traits;
mod utils;

use crate::gfx::is_supported;
use crate::models::Pls;
use crate::models::Window;

use log::debug;
use std::sync::LazyLock;

static PLS: LazyLock<Pls> = LazyLock::new(Pls::default);
static PLS: LazyLock<Pls> = LazyLock::new(|| {
let (supports_gfx, window) = match (is_supported(), Window::try_new()) {
(true, Some(window)) => (true, Some(window)),
_ => (false, None),
};

Pls {
supports_gfx,
window,
..Pls::default()
}
});

/// Create a `Pls` instance and immediately delegate to it.
///
Expand Down
2 changes: 2 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ mod owner;
mod perm;
mod pls;
mod spec;
mod window;

pub use node::Node;
pub use owner::OwnerMan;
pub use perm::Perm;
pub use pls::Pls;
pub use spec::Spec;
pub use window::Window;
9 changes: 8 additions & 1 deletion src/models/pls.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
use crate::args::{Group, Input};
use crate::config::{Args, ConfMan};
use crate::fmt::render;
use crate::models::OwnerMan;
use crate::models::{OwnerMan, Window};

/// Represents the entire application state.
///
/// This struct also holds various globals that are used across the
/// application.
#[derive(Default)]
pub struct Pls {
/// configuration manager for `.pls.yml` files
pub conf_man: ConfMan,
/// command-line arguments
pub args: Args,
/// whether the terminal supports Kitty's terminal graphics protocol
pub supports_gfx: bool,
/// the width and height of a terminal cell in pixels
pub window: Option<Window>,
}

impl Pls {
Expand Down
36 changes: 36 additions & 0 deletions src/models/window.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use libc::{c_ushort, ioctl, STDOUT_FILENO, TIOCGWINSZ};
use log::warn;

/// See http://www.delorie.com/djgpp/doc/libc/libc_495.html
#[repr(C)]
#[derive(Default)]
pub struct Window {
pub ws_row: c_ushort, /* rows, in characters */
pub ws_col: c_ushort, /* columns, in characters */
pub ws_xpixel: c_ushort, /* horizontal size, pixels */
pub ws_ypixel: c_ushort, /* vertical size, pixels */
}

impl Window {
/// Get a new `Window` instance with the terminal measurements.
///
/// This function returns `None` if the ioctl call fails.
pub fn try_new() -> Option<Self> {
let mut win = Self::default();
#[allow(clippy::useless_conversion)]
let r = unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ.into(), &mut win) };
if r == 0 && win.ws_xpixel > win.ws_col && win.ws_ypixel > win.ws_row {
return Some(win);
}
warn!("Could not determine cell dimensions.");
None
}

pub fn cell_width(&self) -> u8 {
(self.ws_xpixel / self.ws_col) as u8
}

pub fn cell_height(&self) -> u8 {
(self.ws_ypixel / self.ws_row) as u8
}
}
3 changes: 2 additions & 1 deletion src/output/cell.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::fmt::{len, render};
use crate::gfx::strip_image;
use std::fmt::Alignment;

/// Represents one cell in the rendered output.
Expand Down Expand Up @@ -45,7 +46,7 @@ impl Cell {
S: AsRef<str>,
{
let text = text.as_ref();
let text_len = len(text); // This `len` can understand markup.
let text_len = len(strip_image(text)); // This `len` can understand markup.

let (left, right): (usize, usize) = match width {
Some(width) if *width > text_len => {
Expand Down
13 changes: 7 additions & 6 deletions src/output/grid.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use crate::config::AppConst;
use crate::enums::DetailField;
use crate::fmt::len;
use crate::gfx::strip_image;
use crate::output::Cell;
use crate::PLS;
use std::collections::HashMap;
use std::fmt::Alignment;
use terminal_size::{terminal_size, Width};

/// The grid view renders the node names in a two dimensional layout to minimise
/// scrolling. It does not support rendering of node metadata.
Expand Down Expand Up @@ -33,7 +33,7 @@ impl Grid {

/// Render the grid to STDOUT.
pub fn render(&self, _app_const: &AppConst) {
let max_width = self.entries.iter().map(len).max();
let max_width = self.entries.iter().map(strip_image).map(len).max();
let max_cols = self.columns(max_width);

let entry_len = self.entries.len();
Expand Down Expand Up @@ -96,13 +96,14 @@ impl Grid {

/// Get the terminal width.
///
/// If the `PLS_COLUMNS` environment variable is set, the value of that
/// variable is used as the terminal width. Otherwise, the terminal width is
/// determined using the `terminal_size` crate.
/// The terminal width is determined from two sources:
///
/// * the `PLS_COLUMNS` environment variable, if it is set
/// * the result of an ioctl call, if it succeeds
fn term_width() -> Option<u16> {
std::env::var("PLS_COLUMNS") // development hack
.ok()
.and_then(|width_str| width_str.parse::<u16>().ok())
.or_else(|| terminal_size().map(|(Width(term_width), _)| term_width))
.or_else(|| PLS.window.as_ref().map(|win| win.ws_col))
}
}

0 comments on commit 78a24e6

Please sign in to comment.