diff --git a/Cargo.lock b/Cargo.lock index 1c515b40..a78ca088 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -805,6 +805,7 @@ dependencies = [ "signal-hook-registry", "smallvec", "structopt", + "svg", "termion", "toml", "unicode-segmentation", @@ -1613,6 +1614,12 @@ dependencies = [ "syn", ] +[[package]] +name = "svg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b65a64d32a41db2a8081aa03c1ccca26f246ff681add693f8b01307b137da79" + [[package]] name = "syn" version = "1.0.30" diff --git a/Cargo.toml b/Cargo.toml index eca68f9c..f26581ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ smallvec = { version = "1.1.0", features = ["serde", ] } bitflags = "1.0" pcre2 = { version = "0.2.3", optional = true } structopt = { version = "0.3.14", default-features = false } +svg_crate = { version = "0.8.0", optional = true, package = "svg" } [profile.release] @@ -67,6 +68,7 @@ jmap = ["melib/jmap_backend",] sqlite3 = ["melib/sqlite3"] regexp = ["pcre2"] cli-docs = [] +svgscreenshot = ["svg_crate"] # Print tracing logs as meli runs in stderr # enable for debug tracing logs: build with --features=debug-tracing diff --git a/src/bin.rs b/src/bin.rs index 3e32379d..c9e1cedb 100644 --- a/src/bin.rs +++ b/src/bin.rs @@ -323,6 +323,8 @@ fn run_app(opt: Opt) -> Result<()> { state.register_component(Box::new(EnvelopeView::new(wrapper, None, None, 0))); } else { state = State::new(None, sender, receiver.clone())?; + #[cfg(feature = "svgscreenshot")] + state.register_component(Box::new(components::svg::SVGScreenshotFilter::new())); let window = Box::new(Tabbed::new( vec![ Box::new(listing::Listing::new(&mut state.context)), diff --git a/src/components.rs b/src/components.rs index e0ad87a7..8ec0085d 100644 --- a/src/components.rs +++ b/src/components.rs @@ -40,6 +40,9 @@ pub use self::utilities::*; pub mod contacts; pub use crate::contacts::*; +#[cfg(feature = "svgscreenshot")] +pub mod svg; + use std::fmt; use std::fmt::{Debug, Display}; diff --git a/src/components/svg.rs b/src/components/svg.rs new file mode 100644 index 00000000..e7a49088 --- /dev/null +++ b/src/components/svg.rs @@ -0,0 +1,645 @@ +/* + * meli - svg screenshot + * + * Copyright Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ +use super::*; +use std::io::Write; + +#[derive(Debug)] +pub struct SVGScreenshotFilter { + save_screenshot: bool, +} + +impl fmt::Display for SVGScreenshotFilter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "svg screenshot filter") + } +} + +impl SVGScreenshotFilter { + pub fn new() -> Self { + SVGScreenshotFilter { + save_screenshot: false, + } + } +} + +impl Component for SVGScreenshotFilter { + fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, context: &mut Context) { + if !self.save_screenshot { + return; + } + self.save_screenshot = false; + let grid: &CellBuffer = _grid; + use svg_crate::node::element::{Definitions, Group, Rectangle, Style, Text, Use}; + use svg_crate::node::Text as TextNode; + use svg_crate::Document; + + let (width, height) = grid.size(); + /* + * Format frame as follows: + * - The entire background is a big rectangle. + * - Every text piece with unified foreground color is a text element inserted into the + * `definitions` field of the svg, and then `use`ed as a reference + * - Every background piece (a slice of unified backgrund color) is a rectangle element + * inserted along with the `use` elements + * + * Each row is arbritarily set at 17px high, and each character cell is 8 pixels wide. + * Rectangle cells each have one extra pixel (so 18px * 9px) in their dimensions in order + * to cover the spacing between cells. + */ + let mut definitions = Definitions::new(); + let mut rows_group = Group::new(); + let mut text = String::with_capacity(width); + for (row_idx, row) in grid.bounds_iter(((0, 0), (width, height))).enumerate() { + text.clear(); + let mut row_group = Group::new().set("id", format!("g{}", row_idx + 1)); + let mut cur_fg = Color::Default; + let mut cur_bg = Color::Default; + let mut cur_attrs = Attr::DEFAULT; + let mut prev_x_fg = 0; + let mut is_start = true; + let mut prev_x_bg = 0; + for (x, c) in row.enumerate() { + if cur_bg != grid[c].bg() || cur_fg != grid[c].fg() || cur_attrs != grid[c].attrs() + { + if cur_bg != Color::Default { + let mut rect = Rectangle::new() + .set("x", prev_x_bg * 8) + .set("y", 17 * row_idx) + .set("width", (x - prev_x_bg) * 8 + 1) + .set("bgname", format!("{:?}", cur_bg)) + .set("height", 18); + match cur_bg { + Color::Rgb(r, g, b) => { + rect = rect + .set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str()); + } + Color::Default => { + unreachable!(); + } + c if c.as_byte() < 16 => { + rect = rect.set("class", format!("color{}", c.as_byte()).as_str()); + } + c => { + let c = c.as_byte(); + let (r, g, b) = XTERM_COLORS[c as usize]; + rect = rect + .set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str()); + } + } + rows_group = rows_group.add(rect); + } + prev_x_bg = x; + cur_bg = grid[c].bg(); + if !text.is_empty() { + let text_length = text.split_graphemes().len(); + let mut text_el = Text::new() + .add(TextNode::new(&text)) + .set("x", prev_x_fg * 8) + .set("textLength", text_length * 8) + .set("fgname", format!("{:?}", cur_fg)); + if cur_attrs.intersects(Attr::BOLD) { + text_el = text_el.set("font-weight", "bold"); + } + if cur_attrs.intersects(Attr::ITALICS) { + text_el = text_el.set("font-style", "italic"); + } + if cur_attrs.intersects(Attr::UNDERLINE) { + text_el = text_el.set("text-decoration", "underline"); + } + if cur_attrs.intersects(Attr::DIM) { + text_el = text_el.set("font-weight", "lighter"); + } + if cur_attrs.intersects(Attr::HIDDEN) { + text_el = text_el.set("display", "none"); + } + match cur_fg { + Color::Default if cur_attrs.intersects(Attr::REVERSE) => { + text_el = text_el.set("class", "background"); + } + Color::Default => { + text_el = text_el.set("class", "foreground"); + } + Color::Rgb(r, g, b) => { + text_el = text_el + .set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str()); + } + c if c.as_byte() < 16 => { + text_el = + text_el.set("class", format!("color{}", c.as_byte()).as_str()); + } + c => { + let c = c.as_byte(); + let (r, g, b) = XTERM_COLORS[c as usize]; + text_el = text_el + .set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str()); + } + }; + row_group = row_group.add(text_el); + text.clear(); + } + prev_x_fg = x; + cur_fg = grid[c].fg(); + cur_attrs = grid[c].attrs(); + } + match grid[c].ch() { + '"' => text.push_str("""), + '&' => text.push_str("&"), + '\'' => text.push_str("'"), + '<' => text.push_str("<"), + '>' => text.push_str(">"), + ' ' if is_start => { + prev_x_fg = x + 1; + } + c => text.push(c), + } + if grid[c].ch() != ' ' { + is_start = false; + } + } + if cur_bg != Color::Default { + let mut rect = Rectangle::new() + .set("x", prev_x_bg * 8) + .set("y", 17 * row_idx) + .set("width", (width - prev_x_bg) * 8 + 1) + .set("bgname", format!("{:?}", cur_bg)) + .set("height", 18); + match cur_bg { + Color::Rgb(r, g, b) => { + rect = rect.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str()); + } + Color::Default => { + unreachable!(); + } + c if c.as_byte() < 16 => { + rect = rect.set("class", format!("color{}", c.as_byte()).as_str()); + } + c => { + let c = c.as_byte(); + let (r, g, b) = XTERM_COLORS[c as usize]; + rect = rect.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str()); + } + } + rows_group = rows_group.add(rect); + } + if !text.is_empty() { + let text_length = text.split_graphemes().len(); + let mut text_el = Text::new() + .add(TextNode::new(&text)) + .set("x", prev_x_fg * 8) + .set("textLength", text_length * 8) + .set("fgname", format!("{:?}", cur_fg)); + if cur_attrs.intersects(Attr::BOLD) { + text_el = text_el.set("font-weight", "bold"); + } + if cur_attrs.intersects(Attr::ITALICS) { + text_el = text_el.set("font-style", "italic"); + } + if cur_attrs.intersects(Attr::UNDERLINE) { + text_el = text_el.set("text-decoration", "underline"); + } + if cur_attrs.intersects(Attr::DIM) { + text_el = text_el.set("font-weight", "lighter"); + } + if cur_attrs.intersects(Attr::HIDDEN) { + text_el = text_el.set("display", "none"); + } + match cur_fg { + Color::Default if cur_attrs.intersects(Attr::REVERSE) => { + text_el = text_el.set("class", "background"); + } + Color::Default => { + text_el = text_el.set("class", "foreground"); + } + Color::Rgb(r, g, b) => { + text_el = + text_el.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str()); + } + c if c.as_byte() < 16 => { + text_el = text_el.set("class", format!("color{}", c.as_byte()).as_str()); + } + c => { + let c = c.as_byte(); + let (r, g, b) = XTERM_COLORS[c as usize]; + text_el = + text_el.set("fill", format!("#{:02x}{:02x}{:02x}", r, g, b).as_str()); + } + } + row_group = row_group.add(text_el); + text.clear(); + } + definitions = definitions.add(row_group); + rows_group = rows_group.add( + Use::new() + .set("xlink:href", format!("#g{}", row_idx + 1)) + .set("y", 17 * row_idx), + ); + } + let document = Document::new() + .set("viewBox", (0, 0, width * 8, height * 17 + 2)) + .set("width", width * 8) + .set("height", height * 17 + 2) + .add( + Definitions::new().add( + Style::new(CSS_STYLE) + .set("id", "generated-style") + .set("type", "text/css"), + ), + ) + .add( + Document::new() + .set("id", "terminal") + .set("preserveAspectRatio", "xMidYMin slice") + .set("viewBox", (0, 0, width * 8, height * 17)) + .set("width", width * 8) + .set("height", height * 17) + .add( + Rectangle::new() + .set("class", "background") + .set("height", "100%") + .set("width", "100%") + .set("x", 0) + .set("y", 0), + ) + .add(definitions) + .add(rows_group), + ) + .set("xmlns", "http://www.w3.org/2000/svg") + .set("baseProfile", "full") + .set("xmlns:xlink", "http://www.w3.org/1999/xlink") + .set("version", "1.1"); + + let mut s = Vec::new(); + svg_crate::write(&mut s, &document).unwrap(); + let mut res = Vec::new(); + /* + * svg crate formats text nodes like this: + * + * + * actual content + * + * + * But we don't want any extra newlines before/after the tags: + * + * actual content + * + * So remove all new lines from SVG file. + */ + for b in s { + if b == b'\n' { + continue; + } + res.push(b); + } + let mut filename = melib::datetime::timestamp_to_string( + melib::datetime::now(), + Some("meli Screenshot - %e %h %Y %H:%M:%S.svg"), + ); + while std::path::Path::new(&filename).exists() { + filename.pop(); + filename.pop(); + filename.pop(); + filename.pop(); + filename.push_str("_.svg"); + } + std::fs::File::create(&filename) + .unwrap() + .write_all(&res) + .unwrap(); + context.replies.push_back(UIEvent::Notification( + Some("Screenshot saved".into()), + format!("Screenshot saved to {}", filename), + None, + )); + } + fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool { + if let UIEvent::Input(Key::F(6)) = event { + self.save_screenshot = true; + true + } else if let UIEvent::ExInput(Key::F(6)) = event { + self.save_screenshot = true; + true + } else if let UIEvent::EmbedInput((Key::F(6), _)) = event { + self.save_screenshot = true; + false + } else { + false + } + } + fn set_dirty(&mut self, _value: bool) {} + + fn is_dirty(&self) -> bool { + self.save_screenshot + } + + fn id(&self) -> ComponentId { + ComponentId::nil() + } + fn set_id(&mut self, _id: ComponentId) {} +} + +const CSS_STYLE: &'static str = r#"#terminal { + font-family: 'DejaVu Sans Mono', monospace; + font-style: normal; + font-size: 14px; +} + +text { + dominant-baseline: text-before-edge; + white-space: pre; +} +/* The colors defined below are the default 16 colors used for rendering text of the terminal. Adjust them as needed. xterm colors based on https://en.wikipedia.org/wiki/ANSI_escape_code#Colors */ +.foreground {fill: #e5e5e5;} +.background {fill: #000000;} +.color0 {fill: #000000;} +.color1 {fill: #cd0000;} +.color2 {fill: #00cd00;} +.color3 {fill: #cdcd00;} +.color4 {fill: #0000ee;} +.color5 {fill: #cd00cd;} +.color6 {fill: #00cdcd;} +.color7 {fill: #e5e5e5;} +.color8 {fill: #7f7f7f;} +.color9 {fill: #ff0000;} +.color10 {fill: #00ff00;} +.color11 {fill: #ffff00;} +.color12 {fill: #5c5cff;} +.color13 {fill: #ff00ff;} +.color14 {fill: #00ffff;} +.color15 {fill: #ffffff;}"#; + +const XTERM_COLORS: &'static [(u8, u8, u8)] = &[ + /*0*/ (0, 0, 0), + /*1*/ (128, 0, 0), + /*2*/ (0, 128, 0), + /*3*/ (128, 128, 0), + /*4*/ (0, 0, 128), + /*5*/ (128, 0, 128), + /*6*/ (0, 128, 128), + /*7*/ (192, 192, 192), + /*8*/ (128, 128, 128), + /*9*/ (255, 0, 0), + /*10*/ (0, 255, 0), + /*11*/ (255, 255, 0), + /*12*/ (0, 0, 255), + /*13*/ (255, 0, 255), + /*14*/ (0, 255, 255), + /*15*/ (255, 255, 255), + /*16*/ (0, 0, 0), + /*17*/ (0, 0, 95), + /*18*/ (0, 0, 135), + /*19*/ (0, 0, 175), + /*20*/ (0, 0, 215), + /*21*/ (0, 0, 255), + /*22*/ (0, 95, 0), + /*23*/ (0, 95, 95), + /*24*/ (0, 95, 135), + /*25*/ (0, 95, 175), + /*26*/ (0, 95, 215), + /*27*/ (0, 95, 255), + /*28*/ (0, 135, 0), + /*29*/ (0, 135, 95), + /*30*/ (0, 135, 135), + /*31*/ (0, 135, 175), + /*32*/ (0, 135, 215), + /*33*/ (0, 135, 255), + /*34*/ (0, 175, 0), + /*35*/ (0, 175, 95), + /*36*/ (0, 175, 135), + /*37*/ (0, 175, 175), + /*38*/ (0, 175, 215), + /*39*/ (0, 175, 255), + /*40*/ (0, 215, 0), + /*41*/ (0, 215, 95), + /*42*/ (0, 215, 135), + /*43*/ (0, 215, 175), + /*44*/ (0, 215, 215), + /*45*/ (0, 215, 255), + /*46*/ (0, 255, 0), + /*47*/ (0, 255, 95), + /*48*/ (0, 255, 135), + /*49*/ (0, 255, 175), + /*50*/ (0, 255, 215), + /*51*/ (0, 255, 255), + /*52*/ (95, 0, 0), + /*53*/ (95, 0, 95), + /*54*/ (95, 0, 135), + /*55*/ (95, 0, 175), + /*56*/ (95, 0, 215), + /*57*/ (95, 0, 255), + /*58*/ (95, 95, 0), + /*59*/ (95, 95, 95), + /*60*/ (95, 95, 135), + /*61*/ (95, 95, 175), + /*62*/ (95, 95, 215), + /*63*/ (95, 95, 255), + /*64*/ (95, 135, 0), + /*65*/ (95, 135, 95), + /*66*/ (95, 135, 135), + /*67*/ (95, 135, 175), + /*68*/ (95, 135, 215), + /*69*/ (95, 135, 255), + /*70*/ (95, 175, 0), + /*71*/ (95, 175, 95), + /*72*/ (95, 175, 135), + /*73*/ (95, 175, 175), + /*74*/ (95, 175, 215), + /*75*/ (95, 175, 255), + /*76*/ (95, 215, 0), + /*77*/ (95, 215, 95), + /*78*/ (95, 215, 135), + /*79*/ (95, 215, 175), + /*80*/ (95, 215, 215), + /*81*/ (95, 215, 255), + /*82*/ (95, 255, 0), + /*83*/ (95, 255, 95), + /*84*/ (95, 255, 135), + /*85*/ (95, 255, 175), + /*86*/ (95, 255, 215), + /*87*/ (95, 255, 255), + /*88*/ (135, 0, 0), + /*89*/ (135, 0, 95), + /*90*/ (135, 0, 135), + /*91*/ (135, 0, 175), + /*92*/ (135, 0, 215), + /*93*/ (135, 0, 255), + /*94*/ (135, 95, 0), + /*95*/ (135, 95, 95), + /*96*/ (135, 95, 135), + /*97*/ (135, 95, 175), + /*98*/ (135, 95, 215), + /*99*/ (135, 95, 255), + /*100*/ (135, 135, 0), + /*101*/ (135, 135, 95), + /*102*/ (135, 135, 135), + /*103*/ (135, 135, 175), + /*104*/ (135, 135, 215), + /*105*/ (135, 135, 255), + /*106*/ (135, 175, 0), + /*107*/ (135, 175, 95), + /*108*/ (135, 175, 135), + /*109*/ (135, 175, 175), + /*110*/ (135, 175, 215), + /*111*/ (135, 175, 255), + /*112*/ (135, 215, 0), + /*113*/ (135, 215, 95), + /*114*/ (135, 215, 135), + /*115*/ (135, 215, 175), + /*116*/ (135, 215, 215), + /*117*/ (135, 215, 255), + /*118*/ (135, 255, 0), + /*119*/ (135, 255, 95), + /*120*/ (135, 255, 135), + /*121*/ (135, 255, 175), + /*122*/ (135, 255, 215), + /*123*/ (135, 255, 255), + /*124*/ (175, 0, 0), + /*125*/ (175, 0, 95), + /*126*/ (175, 0, 135), + /*127*/ (175, 0, 175), + /*128*/ (175, 0, 215), + /*129*/ (175, 0, 255), + /*130*/ (175, 95, 0), + /*131*/ (175, 95, 95), + /*132*/ (175, 95, 135), + /*133*/ (175, 95, 175), + /*134*/ (175, 95, 215), + /*135*/ (175, 95, 255), + /*136*/ (175, 135, 0), + /*137*/ (175, 135, 95), + /*138*/ (175, 135, 135), + /*139*/ (175, 135, 175), + /*140*/ (175, 135, 215), + /*141*/ (175, 135, 255), + /*142*/ (175, 175, 0), + /*143*/ (175, 175, 95), + /*144*/ (175, 175, 135), + /*145*/ (175, 175, 175), + /*146*/ (175, 175, 215), + /*147*/ (175, 175, 255), + /*148*/ (175, 215, 0), + /*149*/ (175, 215, 95), + /*150*/ (175, 215, 135), + /*151*/ (175, 215, 175), + /*152*/ (175, 215, 215), + /*153*/ (175, 215, 255), + /*154*/ (175, 255, 0), + /*155*/ (175, 255, 95), + /*156*/ (175, 255, 135), + /*157*/ (175, 255, 175), + /*158*/ (175, 255, 215), + /*159*/ (175, 255, 255), + /*160*/ (215, 0, 0), + /*161*/ (215, 0, 95), + /*162*/ (215, 0, 135), + /*163*/ (215, 0, 175), + /*164*/ (215, 0, 215), + /*165*/ (215, 0, 255), + /*166*/ (215, 95, 0), + /*167*/ (215, 95, 95), + /*168*/ (215, 95, 135), + /*169*/ (215, 95, 175), + /*170*/ (215, 95, 215), + /*171*/ (215, 95, 255), + /*172*/ (215, 135, 0), + /*173*/ (215, 135, 95), + /*174*/ (215, 135, 135), + /*175*/ (215, 135, 175), + /*176*/ (215, 135, 215), + /*177*/ (215, 135, 255), + /*178*/ (215, 175, 0), + /*179*/ (215, 175, 95), + /*180*/ (215, 175, 135), + /*181*/ (215, 175, 175), + /*182*/ (215, 175, 215), + /*183*/ (215, 175, 255), + /*184*/ (215, 215, 0), + /*185*/ (215, 215, 95), + /*186*/ (215, 215, 135), + /*187*/ (215, 215, 175), + /*188*/ (215, 215, 215), + /*189*/ (215, 215, 255), + /*190*/ (215, 255, 0), + /*191*/ (215, 255, 95), + /*192*/ (215, 255, 135), + /*193*/ (215, 255, 175), + /*194*/ (215, 255, 215), + /*195*/ (215, 255, 255), + /*196*/ (255, 0, 0), + /*197*/ (255, 0, 95), + /*198*/ (255, 0, 135), + /*199*/ (255, 0, 175), + /*200*/ (255, 0, 215), + /*201*/ (255, 0, 255), + /*202*/ (255, 95, 0), + /*203*/ (255, 95, 95), + /*204*/ (255, 95, 135), + /*205*/ (255, 95, 175), + /*206*/ (255, 95, 215), + /*207*/ (255, 95, 255), + /*208*/ (255, 135, 0), + /*209*/ (255, 135, 95), + /*210*/ (255, 135, 135), + /*211*/ (255, 135, 175), + /*212*/ (255, 135, 215), + /*213*/ (255, 135, 255), + /*214*/ (255, 175, 0), + /*215*/ (255, 175, 95), + /*216*/ (255, 175, 135), + /*217*/ (255, 175, 175), + /*218*/ (255, 175, 215), + /*219*/ (255, 175, 255), + /*220*/ (255, 215, 0), + /*221*/ (255, 215, 95), + /*222*/ (255, 215, 135), + /*223*/ (255, 215, 175), + /*224*/ (255, 215, 215), + /*225*/ (255, 215, 255), + /*226*/ (255, 255, 0), + /*227*/ (255, 255, 95), + /*228*/ (255, 255, 135), + /*229*/ (255, 255, 175), + /*230*/ (255, 255, 215), + /*231*/ (255, 255, 255), + /*232*/ (8, 8, 8), + /*233*/ (18, 18, 18), + /*234*/ (28, 28, 28), + /*235*/ (38, 38, 38), + /*236*/ (48, 48, 48), + /*237*/ (58, 58, 58), + /*238*/ (68, 68, 68), + /*239*/ (78, 78, 78), + /*240*/ (88, 88, 88), + /*241*/ (98, 98, 98), + /*242*/ (108, 108, 108), + /*243*/ (118, 118, 118), + /*244*/ (128, 128, 128), + /*245*/ (138, 138, 138), + /*246*/ (148, 148, 148), + /*247*/ (158, 158, 158), + /*248*/ (168, 168, 168), + /*249*/ (178, 178, 178), + /*250*/ (188, 188, 188), + /*251*/ (198, 198, 198), + /*252*/ (208, 208, 208), + /*253*/ (218, 218, 218), + /*254*/ (228, 228, 228), + /*255*/ (238, 238, 238), +];