diff --git a/assets/CascadiaCode-Bold.ttf b/assets/CascadiaCode-Bold.ttf new file mode 100644 index 0000000..19e9961 Binary files /dev/null and b/assets/CascadiaCode-Bold.ttf differ diff --git a/assets/CascadiaCode-BoldItalic.ttf b/assets/CascadiaCode-BoldItalic.ttf new file mode 100644 index 0000000..4f7be4a Binary files /dev/null and b/assets/CascadiaCode-BoldItalic.ttf differ diff --git a/assets/CascadiaCode-Italic.ttf b/assets/CascadiaCode-Italic.ttf new file mode 100644 index 0000000..0c7ff10 Binary files /dev/null and b/assets/CascadiaCode-Italic.ttf differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..42ff0c8 --- /dev/null +++ b/main.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 + +WIDTH = 79 + +# Reset all attributes +RESET = "\033[0m" + +# Text styles +BOLD = "\033[1m" +DIM = "\033[2m" +ITALIC = "\033[3m" # May not be supported in all terminals. +UNDERLINE = "\033[4m" # Standard single underline +REVERSE = "\033[7m" +STRIKETHROUGH = "\033[9m" # Not supported in all terminal emulators. + +# Extended underline styles (if supported) +# Using the extended SGR syntax for underline styles: +# Single: \033[4m (or \033[4:1m), Double: \033[4:2m, Curly: \033[4:3m, Dotted: \033[4:4m +DOUBLE_UNDERLINE = "\033[4:2m" +CURLY_UNDERLINE = "\033[4:3m" +DOTTED_UNDERLINE = "\033[4:4m" + +# Basic palette colors (foreground) +BLACK = "\033[30m" +RED = "\033[31m" +GREEN = "\033[32m" +YELLOW = "\033[33m" +BLUE = "\033[34m" +MAGENTA = "\033[35m" +CYAN = "\033[36m" +WHITE = "\033[37m" +BRIGHT_BLACK = "\033[90m" +BRIGHT_RED = "\033[91m" +BRIGHT_GREEN = "\033[92m" +BRIGHT_YELLOW = "\033[93m" +BRIGHT_BLUE = "\033[94m" +BRIGHT_MAGENTA = "\033[95m" +BRIGHT_CYAN = "\033[96m" +BRIGHT_WHITE = "\033[97m" + +# Basic background colors +BG_BLACK = "\033[40m" +BG_RED = "\033[41m" +BG_GREEN = "\033[42m" +BG_YELLOW = "\033[43m" +BG_BLUE = "\033[44m" +BG_MAGENTA = "\033[45m" +BG_CYAN = "\033[46m" +BG_WHITE = "\033[47m" +BG_BRIGHT_BLACK = "\033[100m" +BG_BRIGHT_RED = "\033[101m" +BG_BRIGHT_GREEN = "\033[102m" +BG_BRIGHT_YELLOW = "\033[103m" +BG_BRIGHT_BLUE = "\033[104m" +BG_BRIGHT_MAGENTA = "\033[105m" +BG_BRIGHT_CYAN = "\033[106m" +BG_BRIGHT_WHITE = "\033[107m" + +# 256-color palette +PITCH_BLACK = "\033[38;5;16m" +SNOW_WHITE = "\033[38;5;231m" + +# List of text styles +print(f"{BOLD}Available Text Styles:{RESET}") +print(f"• Bold: {BOLD}This is bold text{RESET}") +print(f"• Dim: {DIM}This is dim text{RESET}") +print(f"• Italic: {ITALIC}This is italic text{RESET}") +print(f"• Underline: {UNDERLINE}This is underlined text{RESET}") +print(f"• Strikethrough: {STRIKETHROUGH}This is strikethrough text{RESET}") +print(f"• Reverse: {REVERSE}This is reversed text{RESET}") +print() + +# Basic Foreground Colors +print(f"{BOLD}Basic Foreground Colors:{RESET}") +print("• Normal: ", end="") +print(f"{BLACK}Black{RESET} ", end="") +print(f"{RED}Red{RESET} ", end="") +print(f"{GREEN}Green{RESET} ", end="") +print(f"{YELLOW}Yellow{RESET} ", end="") +print(f"{BLUE}Blue{RESET} ", end="") +print(f"{MAGENTA}Magenta{RESET} ", end="") +print(f"{CYAN}Cyan{RESET} ", end="") +print(f"{WHITE}White{RESET}") +print("• Bright: ", end="") +print(f"{BRIGHT_BLACK}Black{RESET} ", end="") +print(f"{BRIGHT_RED}Red{RESET} ", end="") +print(f"{BRIGHT_GREEN}Green{RESET} ", end="") +print(f"{BRIGHT_YELLOW}Yellow{RESET} ", end="") +print(f"{BRIGHT_BLUE}Blue{RESET} ", end="") +print(f"{BRIGHT_MAGENTA}Magenta{RESET} ", end="") +print(f"{BRIGHT_CYAN}Cyan{RESET} ", end="") +print(f"{BRIGHT_WHITE}White{RESET}") +print() + +# Mixing styles with colors +print(f"{BOLD}Mixed Styles with Colors:{RESET}") +print(f"• {BOLD}{RED}Bold red text{RESET}") +print(f"• {ITALIC}{GREEN}Italic green text{RESET}") +print(f"• {UNDERLINE}{BLUE}Underlined blue text{RESET}") +print(f"• {BOLD}{UNDERLINE}{MAGENTA}Bold underlined magenta text{RESET}") +print() + +import sys + +FG = PITCH_BLACK +if len(sys.argv) > 1 and sys.argv[1] == "light": + FG = SNOW_WHITE + +# Basic Background Colors with contrasting text +print(f"{BOLD}Basic Background Colors:{RESET}") +print(f"{BG_BLACK}{SNOW_WHITE} Black BG {RESET}", end="") +print(f"{BG_RED}{FG} Red BG {RESET}", end="") +print(f"{BG_GREEN}{FG} Green BG {RESET}", end="") +print(f"{BG_YELLOW}{FG} Yellow BG {RESET}", end="") +print(f"{BG_BLUE}{FG} Blue BG {RESET}", end="") +print(f"{BG_MAGENTA}{FG} Magenta BG {RESET}", end="") +print(f"{BG_CYAN}{FG} Cyan BG {RESET}", end="") +print(f"{BG_WHITE}{PITCH_BLACK} White BG {RESET}") +print() + +# 24-bit True Color Background Gradient (79 characters wide) +print(f"{BOLD}24-bit True Color Background:{RESET}") +# Loop WIDTH times to print exactly WIDTH blocks. +for i in range(WIDTH): + # Calculate red value decreasing from 255 to 0 across the gradient + red = int(255 - (255 * i / (WIDTH-1)) + 0.5) + # Calculate blue value increasing from 0 to 255 across the gradient + blue = int(255 * i / (WIDTH-1) + 0.5) + # Print one block (a space with the background color set) without a newline. + print(f"\033[48;2;{red};0;{blue}m \033[0m", end="") +print() \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 00db371..22f3a71 100644 --- a/src/app.rs +++ b/src/app.rs @@ -74,7 +74,7 @@ pub struct Args { #[arg(long, short = 'W', default_value = "auto")] pub width: Dimension, - /// Final image height in terminal lines, or 'auto' + /// Final image height in terminal rows, or 'auto' #[arg(long, short = 'H', default_value = "auto")] pub height: Dimension, diff --git a/src/image_renderer.rs b/src/image_renderer.rs index ce26c84..d9bb718 100644 --- a/src/image_renderer.rs +++ b/src/image_renderer.rs @@ -8,7 +8,6 @@ use unicode_width::UnicodeWidthChar; use crate::constants::{FONT_SIZE, IMAGE_QUALITY_MULTIPLIER}; use crate::image_renderer::canvas::Canvas; use crate::image_renderer::render_size::{calculate_char_size, calculate_image_size}; -use crate::image_renderer::utils::resolve_rgba_with_palette; use crate::window_decoration::{WindowDecoration, WindowMetrics}; pub mod canvas; @@ -74,7 +73,7 @@ impl ImageRenderer { let font = window_decoration.font()?; let scale = PxScale::from((FONT_SIZE * IMAGE_QUALITY_MULTIPLIER) as f32); - let char_size = calculate_char_size(font, scale); + let char_size = calculate_char_size(&font.regular, scale); let command_line = window_decoration.build_command_line(&command.join(" ")); let metrics = window_decoration.compute_metrics(char_size); @@ -113,7 +112,6 @@ impl ImageRenderer { self.metrics.border_width + self.metrics.title_bar_height + self.metrics.padding; let color_palette = self.window_decoration.get_color_palette(); - let default_fg_color = color_palette[7]; let command_line = self .window_decoration @@ -125,11 +123,9 @@ impl ImageRenderer { let x = i32::try_from(start_x + x_offset)?; let text = cell.str(); - let color = resolve_rgba_with_palette(&color_palette, cell.attrs().foreground()) - .unwrap_or(default_fg_color); - let background = resolve_rgba_with_palette(&color_palette, cell.attrs().background()); - self.canvas.draw_text(text, x, y, color, background); + self.canvas + .draw_text(text, x, y, &color_palette, cell.attrs()); let text_width = text .chars() @@ -147,7 +143,6 @@ impl ImageRenderer { self.metrics.border_width + self.metrics.title_bar_height + self.metrics.padding; let color_palette = self.window_decoration.get_color_palette(); - let default_fg_color = color_palette[7]; for (row_idx, line) in screen.screen_lines().iter().enumerate() { let row_idx = u32::try_from(row_idx + 1)?; @@ -158,12 +153,9 @@ impl ImageRenderer { let x = i32::try_from(start_x + x_offset)?; let text = cell.str(); - let color = resolve_rgba_with_palette(&color_palette, cell.attrs().foreground()) - .unwrap_or(default_fg_color); - let background = - resolve_rgba_with_palette(&color_palette, cell.attrs().background()); - self.canvas.draw_text(text, x, y, color, background); + self.canvas + .draw_text(text, x, y, &color_palette, cell.attrs()); let text_width = text .chars() diff --git a/src/image_renderer/canvas.rs b/src/image_renderer/canvas.rs index 36ce76f..0a1340a 100644 --- a/src/image_renderer/canvas.rs +++ b/src/image_renderer/canvas.rs @@ -1,10 +1,15 @@ -use crate::image_renderer::{ - render_size::{calculate_char_size, Size}, - ImageRendererError, +use crate::{ + image_renderer::{ + render_size::{calculate_char_size, Size}, + utils::resolve_rgba_with_palette, + ImageRendererError, + }, + window_decoration::Fonts, }; -use ab_glyph::{FontArc, PxScale}; +use ab_glyph::PxScale; use image::{Rgba, RgbaImage}; use imageproc::drawing::draw_text_mut; +use termwiz::cell::{CellAttributes, Intensity, Underline}; use tiny_skia::{Color, FillRule, Paint, PathBuilder, Pixmap, Rect, Transform}; use tracing::warn; @@ -22,7 +27,7 @@ bitflags::bitflags! { pub struct Canvas { pixmap: Pixmap, image_for_text: RgbaImage, - font: FontArc, + font: Fonts, scale: PxScale, char_size: Size, } @@ -31,12 +36,12 @@ impl Canvas { pub fn new( width: u32, height: u32, - font: FontArc, + font: Fonts, scale: PxScale, ) -> Result { let pixmap = Pixmap::new(width, height).ok_or(ImageRendererError::CanvasInitFailed)?; let image_for_text = RgbaImage::new(width, height); - let char_size = calculate_char_size(&font, scale); + let char_size = calculate_char_size(&font.regular, scale); Ok(Self { pixmap, @@ -162,10 +167,56 @@ impl Canvas { text: &str, x: i32, y: i32, - color: Rgba, - background: Option>, + color_palette: &[Rgba; 256], + attributes: &CellAttributes, + ) { + let foreground_color = if attributes.reverse() { + color_palette[0] + } else { + resolve_rgba_with_palette(color_palette, attributes.foreground()) + .unwrap_or(color_palette[7]) + }; + + let is_bold = matches!(attributes.intensity(), Intensity::Bold); + + let font = if is_bold && attributes.italic() { + &self.font.bold_italic + } else if is_bold { + &self.font.bold + } else if attributes.italic() { + &self.font.italic + } else { + &self.font.regular + }; + + draw_text_mut( + &mut self.image_for_text, + foreground_color, + x, + y, + self.scale, + &font, + text, + ); + + self.draw_cell_attributes(text, x, y, color_palette, attributes); + } + + pub fn draw_cell_attributes( + &mut self, + text: &str, + x: i32, + y: i32, + color_palette: &[Rgba; 256], + attributes: &CellAttributes, ) { - if let Some(bg_color) = background { + let bg_color = if attributes.reverse() { + resolve_rgba_with_palette(color_palette, attributes.foreground()) + .or(Some(color_palette[7])) + } else { + resolve_rgba_with_palette(color_palette, attributes.background()) + }; + if let Some(bg_color) = bg_color { self.fill_rect( x, y, @@ -175,15 +226,28 @@ impl Canvas { ); } - draw_text_mut( - &mut self.image_for_text, - color, - x, - y, - self.scale, - &self.font, - text, - ); + if attributes.underline() != Underline::None { + let underline_color = + resolve_rgba_with_palette(color_palette, attributes.underline_color()) + .unwrap_or(color_palette[7]); + self.fill_rect( + x, + y + self.char_height() as i32, + text.chars().count() as u32 * self.char_width(), + 1, + underline_color, + ); + } + + if attributes.strikethrough() { + self.fill_rect( + x, + y + (self.char_height() as i32 / 2), + text.chars().count() as u32 * self.char_width(), + 1, + color_palette[7], + ); + } } pub fn width(&self) -> u32 { @@ -229,13 +293,12 @@ impl Canvas { #[cfg(test)] mod tests { - use crate::window_decoration::common::default_font; + use crate::window_decoration::common::{default_font, get_default_color_palette}; use super::*; - use ab_glyph::FontArc; use image::Rgba; - fn make_font() -> FontArc { + fn make_font() -> Fonts { default_font().unwrap().clone() } @@ -267,8 +330,8 @@ mod tests { "Hello", 5, 5, - Rgba([0, 0, 0, 255]), - Some(Rgba([255, 255, 255, 255])), + &get_default_color_palette(), + &CellAttributes::default(), ); } diff --git a/src/lib.rs b/src/lib.rs index 568fdcd..7f7944d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,3 +12,4 @@ mod terminal_builder; mod window_decoration; pub use app::{run_shellshot, Args}; +pub use window_decoration::WindowDecorationType; diff --git a/src/terminal_builder.rs b/src/terminal_builder.rs index b698175..67bb6d5 100644 --- a/src/terminal_builder.rs +++ b/src/terminal_builder.rs @@ -38,7 +38,7 @@ impl TerminalBuilder { }; terminal.run_loop()?; - match (rows, cols) { + match (cols, rows) { (Dimension::Auto, Dimension::Auto) => terminal.resize_surface(true, true), (Dimension::Auto, Dimension::Value(_)) => terminal.resize_surface(true, false), (Dimension::Value(_), Dimension::Auto) => terminal.resize_surface(false, true), @@ -103,8 +103,15 @@ impl TerminalBuilder { let new_rows = if resize_rows { lines .iter() - .rposition(|line| !line.is_whitespace()) - .map_or(0, |idx| idx + 1) + .enumerate() + .rev() + .find(|(_, line)| { + line.visible_cells().any(|cell| { + !cell.str().chars().all(char::is_whitespace) + || !matches!(cell.attrs().background(), ColorAttribute::Default) + }) + }) + .map_or(0, |(idx, _)| idx + 1) } else { current_rows }; diff --git a/src/terminal_builder/action/csi.rs b/src/terminal_builder/action/csi.rs index 2e3a77c..f38d262 100644 --- a/src/terminal_builder/action/csi.rs +++ b/src/terminal_builder/action/csi.rs @@ -29,12 +29,11 @@ fn process_edit(surface: &mut Surface, edit: &Edit) -> SequenceNo { match edit { Edit::EraseCharacter(n) => { let (x, y) = surface.cursor_position(); - let new_x = x.saturating_sub(*n as usize); + surface.add_change(Change::Text(" ".repeat(*n as usize))); surface.add_change(Change::CursorPosition { - x: Position::Absolute(new_x), + x: Position::Absolute(x), y: Position::Absolute(y), - }); - surface.add_change(Change::Text(" ".repeat(*n as usize))) + }) } Edit::EraseInLine(_) | Edit::InsertCharacter(_) @@ -93,7 +92,7 @@ mod tests { let csi = CSI::Edit(Edit::EraseCharacter(3)); process_csi(&mut s, &mut std::io::sink(), &csi); let content = s.screen_chars_to_string(); - println!("{content}"); - assert!(content.starts_with("AB ")); + println!("{content:?}"); + assert!(content.starts_with("ABCDE")); } } diff --git a/src/window_decoration.rs b/src/window_decoration.rs index d0b126a..c6dacaf 100644 --- a/src/window_decoration.rs +++ b/src/window_decoration.rs @@ -27,6 +27,14 @@ pub struct WindowMetrics { pub title_bar_height: u32, } +#[derive(Debug, Clone)] +pub struct Fonts { + pub regular: FontArc, + pub bold: FontArc, + pub italic: FontArc, + pub bold_italic: FontArc, +} + pub trait WindowDecoration: std::fmt::Debug { fn build_command_line(&self, command: &str) -> Vec; @@ -34,7 +42,7 @@ pub trait WindowDecoration: std::fmt::Debug { fn get_color_palette(&self) -> [Rgba; 256]; - fn font(&self) -> Result<&FontArc, ImageRendererError>; + fn font(&self) -> Result; fn draw_window( &self, @@ -107,7 +115,7 @@ mod tests { let font = window_decoration.font().expect("Font should be available"); - let char_size = calculate_char_size(font, scale); + let char_size = calculate_char_size(&font.regular, scale); let metrics = window_decoration.compute_metrics(char_size); let mut canvas = Canvas::new(canvas_width, canvas_height, font.clone(), scale) diff --git a/src/window_decoration/classic.rs b/src/window_decoration/classic.rs index dba42f0..4f62440 100644 --- a/src/window_decoration/classic.rs +++ b/src/window_decoration/classic.rs @@ -1,4 +1,3 @@ -use ab_glyph::FontArc; use image::Rgba; use termwiz::cell::Cell; @@ -9,6 +8,7 @@ use crate::image_renderer::ImageRendererError; use crate::window_decoration::common::default_build_command_line; use crate::window_decoration::common::default_font; use crate::window_decoration::common::get_default_color_palette; +use crate::window_decoration::Fonts; use crate::window_decoration::WindowMetrics; use super::WindowDecoration; @@ -45,7 +45,7 @@ impl WindowDecoration for Classic { get_default_color_palette() } - fn font(&self) -> Result<&FontArc, ImageRendererError> { + fn font(&self) -> Result { default_font() } diff --git a/src/window_decoration/common.rs b/src/window_decoration/common.rs index e1a9fa5..e8221d3 100644 --- a/src/window_decoration/common.rs +++ b/src/window_decoration/common.rs @@ -1,5 +1,3 @@ -use std::sync::OnceLock; - use ab_glyph::FontArc; use image::Rgba; use termwiz::{ @@ -7,10 +5,15 @@ use termwiz::{ color::ColorAttribute, }; -use crate::image_renderer::ImageRendererError; +use crate::{image_renderer::ImageRendererError, window_decoration::Fonts}; pub static CASCADIA_CODE_FONT_DATA: &[u8] = include_bytes!("../../assets/CascadiaCode.ttf"); -pub static CASCADIA_CODE_FONT: OnceLock> = OnceLock::new(); +pub static CASCADIA_CODE_BOLD_FONT_DATA: &[u8] = + include_bytes!("../../assets/CascadiaCode-Bold.ttf"); +pub static CASCADIA_CODE_BOLDITALIC_FONT_DATA: &[u8] = + include_bytes!("../../assets/CascadiaCode-BoldItalic.ttf"); +pub static CASCADIA_CODE_ITALIC_FONT_DATA: &[u8] = + include_bytes!("../../assets/CascadiaCode-Italic.ttf"); pub fn default_build_command_line(command: &str) -> Vec { let mut cells = Vec::with_capacity(2 + command.len()); @@ -33,14 +36,17 @@ pub fn default_build_command_line(command: &str) -> Vec { cells } -pub fn default_font() -> Result<&'static FontArc, ImageRendererError> { - CASCADIA_CODE_FONT - .get_or_init(|| { - FontArc::try_from_slice(CASCADIA_CODE_FONT_DATA) - .map_err(|_| ImageRendererError::FontLoadError) - }) - .as_ref() - .map_err(|_| ImageRendererError::FontLoadError) +pub fn default_font() -> Result { + Ok(Fonts { + regular: FontArc::try_from_slice(CASCADIA_CODE_FONT_DATA) + .map_err(|_| ImageRendererError::FontLoadError)?, + bold: FontArc::try_from_slice(CASCADIA_CODE_BOLD_FONT_DATA) + .map_err(|_| ImageRendererError::FontLoadError)?, + italic: FontArc::try_from_slice(CASCADIA_CODE_ITALIC_FONT_DATA) + .map_err(|_| ImageRendererError::FontLoadError)?, + bold_italic: FontArc::try_from_slice(CASCADIA_CODE_BOLDITALIC_FONT_DATA) + .map_err(|_| ImageRendererError::FontLoadError)?, + }) } pub fn get_default_color_palette() -> [Rgba; 256] { diff --git a/src/window_decoration/no_decoration.rs b/src/window_decoration/no_decoration.rs index 1f4781f..e9466b4 100644 --- a/src/window_decoration/no_decoration.rs +++ b/src/window_decoration/no_decoration.rs @@ -1,4 +1,3 @@ -use ab_glyph::FontArc; use image::Rgba; use termwiz::cell::Cell; @@ -8,6 +7,7 @@ use crate::image_renderer::ImageRendererError; use crate::window_decoration::common::default_build_command_line; use crate::window_decoration::common::default_font; use crate::window_decoration::common::get_default_color_palette; +use crate::window_decoration::Fonts; use crate::window_decoration::WindowMetrics; use super::WindowDecoration; @@ -34,7 +34,7 @@ impl WindowDecoration for NoDecoration { get_default_color_palette() } - fn font(&self) -> Result<&FontArc, ImageRendererError> { + fn font(&self) -> Result { default_font() } diff --git a/test.png b/test.png new file mode 100644 index 0000000..8076c40 Binary files /dev/null and b/test.png differ