├── .github └── workflows │ ├── dependency-update.yaml │ └── publish.yml ├── .gitignore ├── .idea ├── .gitignore ├── modules.xml ├── nu_plugin_image.iml └── vcs.xml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.nu ├── nupm.nuon ├── resources └── fonts │ ├── Anonymous_Pro │ ├── Bold.ttf │ ├── BoldItalic.ttf │ ├── Italic.ttf │ └── Regular.ttf │ ├── IosevkaTerm │ ├── Bold.ttf │ ├── BoldItalic.ttf │ ├── Italic.ttf │ └── Medium.ttf │ ├── SourceCodePro │ ├── Bold.otf │ ├── BoldItalic.otf │ ├── Italic.otf │ └── Regular.otf │ └── Ubuntu │ ├── Bold.ttf │ ├── BoldItalic.ttf │ ├── Italic.ttf │ └── Regular.ttf ├── scripts └── theme_exporter.nu └── src ├── ansi_to_image ├── ansi_to_image.rs ├── color.rs ├── escape.rs ├── escape_parser.rs ├── font_family.rs ├── internal_scale.rs ├── mod.rs ├── nu_plugin.rs ├── palette.rs └── printer.rs ├── image_to_ansi ├── mod.rs ├── nu_plugin.rs └── writer │ ├── block.rs │ ├── lib.rs │ ├── mod.rs │ └── string_writer.rs ├── lib.rs ├── logging ├── logger.rs ├── macros.rs ├── mod.rs └── runtime_filter.rs └── main.rs /.github/workflows/dependency-update.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | schedule: 4 | - cron: '0 0 */2 * *' 5 | 6 | name: Update dependencies 7 | 8 | jobs: 9 | update: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Setup Nushell 13 | uses: hustcer/setup-nu@main 14 | with: 15 | version: "*" 16 | - uses: actions/checkout@v2 17 | - name: prepare 18 | shell: nu {0} 19 | run: | 20 | nu -c ' 21 | cargo install cargo-edit cargo-upgrades nu_plugin_inc -f 22 | ' 23 | - name: Update Dependencies 24 | shell: nu {0} 25 | run: | 26 | nu -c ' 27 | register /home/runner/.cargo/bin/nu_plugin_inc 28 | cargo upgrade 29 | let changed = git status -s | is-empty | not $in 30 | if ($changed) { 31 | open Cargo.toml 32 | | upsert package.version ( $in 33 | | get package.version 34 | | inc --patch 35 | ) 36 | | save Cargo.toml -f 37 | 38 | open package.nuon 39 | | upsert version ( open Cargo.toml | get package.version ) 40 | | save package.nuon -f 41 | cargo upgrade 42 | } 43 | 44 | echo { "changed": $changed } 45 | ' 46 | 47 | - uses: EndBug/add-and-commit@v9 48 | with: 49 | author_name: GitHub-Action 50 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Crate 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - Cargo.tom 9 | release: 10 | workflow_dispatch: 11 | 12 | 13 | jobs: 14 | publish: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Rust toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: stable 23 | override: true 24 | 25 | - name: Publish to crates.io 26 | uses: katyo/publish-crates@v2 27 | with: 28 | registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} 29 | env: 30 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode 3 | *.png 4 | *.ansi 5 | *.tmp 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/nu_plugin_image.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | slog = "2.7.0" 3 | termcolor = "1.4.1" 4 | ansi_colours = "1.2.3" 5 | crossterm = "0.29.0" 6 | image = "0.25.6" 7 | imageproc = "0.25.0" 8 | include-flate = "0.3.0" 9 | ab_glyph = "0.2.29" 10 | vte = "0.15.0" 11 | lazy_static = "1.5.0" 12 | slog-term = "2.9.1" 13 | slog-async = "2.8.0" 14 | 15 | [dependencies.clap] 16 | features = ["derive"] 17 | version = "4.5.37" 18 | 19 | [dependencies.nu-plugin] 20 | version = "0.104.0" 21 | 22 | [dependencies.nu-protocol] 23 | features = ["plugin"] 24 | version = "0.104.0" 25 | 26 | [features] 27 | all-fonts = ["font-iosevka_term", "font-anonymous_pro", "font-ubuntu"] 28 | default = [] 29 | font-anonymous_pro = [] 30 | font-iosevka_term = [] 31 | font-ubuntu = [] 32 | 33 | with-debug = ["slog/max_level_debug", "slog/release_max_level_debug"] 34 | with-trace = ["slog/max_level_trace", "slog/release_max_level_trace"] 35 | 36 | [package] 37 | authors = ["Motalleb Fallahnezhad "] 38 | description = "A nushell plugin to open png images in the shell and save ansi string as images (like tables or ...)" 39 | edition = "2021" 40 | homepage = "https://github.com/FMotalleb/nu_plugin_image" 41 | keywords = ["nushell", "image", "render", "plugin"] 42 | license = "MIT" 43 | name = "nu_plugin_image" 44 | readme = "README.md" 45 | repository = "https://github.com/FMotalleb/nu_plugin_image" 46 | version = "0.104.0" 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 "Motalleb Fallahnezhad" 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🖼 nu_plugin_image 2 | 3 | A [Nushell](https://www.nushell.sh/) plugin to convert ANSI strings into PNG images and create ANSI text from images. 4 | 5 | --- 6 | 7 | ## ✨ Features 8 | 9 | This plugin allows you to: 10 | 11 | - Convert ANSI strings to PNG images with customizable fonts and themes. 12 | - Create ANSI text from an image, enabling you to transform visual data into a textual representation. 13 | 14 | --- 15 | 16 | ### **`to png`** – Convert ANSI String to PNG Image 17 | 18 | The `to png` command converts an ANSI string into a PNG image. Customizable font and theme options are available, with custom flags overriding the default settings. 19 | 20 | #### 📌 Usage 21 | 22 | ```bash 23 | > to png {flags} (output-path) 24 | ``` 25 | 26 | #### ⚙️ Available Flags 27 | 28 | - `-h, --help` → Display the help message for this command. 29 | - `-w, --width ` → Output width. 30 | - `-t, --theme ` → Select the theme of the output. Available themes: ["vscode", "xterm", "ubuntu", "eclipse", "mirc", "putty", "winxp", "terminal", "win10", "win_power-shell", "win_ps"]. Defaults to `vscode`. 31 | - `--font ` → Select the font from one of ["SourceCodePro", "Ubuntu", "IosevkaTerm", "AnonymousPro"]. Defaults to the first font in the list. 32 | - `--custom-font-regular ` → Path to a custom regular font. 33 | - `--custom-font-bold ` → Path to a custom bold font. 34 | - `--custom-font-italic ` → Path to a custom italic font. 35 | - `--custom-font-bold_italic ` → Path to a custom bold italic font. 36 | - `--custom-theme-fg ` → Custom foreground color in hex format (e.g., `#FFFFFF` for white). 37 | - `--custom-theme-bg ` → Custom background color in hex format (e.g., `#00000000` for transparent). 38 | - `--custom-theme-black ` → Custom black color in hex format (e.g., `#1C1C1C`). 39 | - `--custom-theme-red ` → Custom red color in hex format (e.g., `#FF0000`). 40 | - `--custom-theme-green ` → Custom green color in hex format (e.g., `#00FF00`). 41 | - `--custom-theme-yellow ` → Custom yellow color in hex format (e.g., `#FFFF00`). 42 | - `--custom-theme-blue ` → Custom blue color in hex format (e.g., `#0000FF`). 43 | - `--custom-theme-magenta ` → Custom magenta color in hex format (e.g., `#FF00FF`). 44 | - `--custom-theme-cyan ` → Custom cyan color in hex format (e.g., `#00FFFF`). 45 | - `--custom-theme-white ` → Custom white color in hex format (e.g., `#FFFFFF`). 46 | - `--custom-theme-bright_black ` → Custom bright black color in hex format (e.g., `#808080`). 47 | - `--custom-theme-bright_red ` → Custom bright red color in hex format (e.g., `#FF5555`). 48 | - `--custom-theme-bright_green ` → Custom bright green color in hex format (e.g., `#55FF55`). 49 | - `--custom-theme-bright_yellow ` → Custom bright yellow color in hex format (e.g., `#FFFF55`). 50 | - `--custom-theme-bright_blue ` → Custom bright blue color in hex format (e.g., `#5555FF`). 51 | - `--custom-theme-bright_magenta ` → Custom bright magenta color in hex format (e.g., `#FF55FF`). 52 | - `--custom-theme-bright_cyan ` → Custom bright cyan color in hex format (e.g., `#55FFFF`). 53 | - `--custom-theme-bright_white ` → Custom bright white color in hex format (e.g., `#FFFFFF`). 54 | - `--log-level ` → Set log level. Options: `CRITICAL (c)`, `ERROR (e)`, `WARN (w)`, `INFO (i)`, `DEBUG (d)`, `TRACE (t)`. Defaults to `INFO`. 55 | 56 | #### 📊 Example: Convert ANSI String to PNG with Custom Theme 57 | 58 | ```bash 59 | > to png --theme "xterm" --custom-theme-fg "#FF00FF" --custom-theme-bg "#00000000" output.png 60 | ``` 61 | 62 | --- 63 | 64 | ### **`from png`** – Create ANSI Text from an Image 65 | 66 | The `from png` command converts an image into its corresponding ANSI text representation. 67 | 68 | #### 📌 Usage 69 | 70 | ```bash 71 | > from png {flags} 72 | ``` 73 | 74 | #### ⚙️ Available Flags 75 | 76 | - `-h, --help` → Display the help message for this command. 77 | - `-x, --width ` → Output width, in characters. 78 | - `-y, --height ` → Output height, in characters. 79 | - `--log-level ` → Set log level. Options: `CRITICAL (c)`, `ERROR (e)`, `WARN (w)`, `INFO (i)`, `DEBUG (d)`, `TRACE (t)`. Defaults to `INFO`. 80 | 81 | #### 📊 Example: Convert PNG Image to ANSI Text 82 | 83 | ```bash 84 | > from png --width 80 --height 20 image.png 85 | ``` 86 | 87 | --- 88 | 89 | ## 🔧 Installation 90 | 91 | ### 🚀 Recommended: Using [nupm](https://github.com/nushell/nupm) 92 | 93 | This method automatically handles dependencies and features. 94 | 95 | ```bash 96 | git clone https://github.com/FMotalleb/nu_plugin_image.git 97 | nupm install --path nu_plugin_image -f 98 | ``` 99 | 100 | ### 🛠️ Manual Compilation 101 | 102 | ```bash 103 | git clone https://github.com/FMotalleb/nu_plugin_image.git 104 | cd nu_plugin_image 105 | cargo build -r 106 | plugin add target/release/nu_plugin_image 107 | ``` 108 | 109 | ### 📦 Install via Cargo (using git) 110 | 111 | ```bash 112 | cargo install --git https://github.com/FMotalleb/nu_plugin_image.git 113 | plugin add ~/.cargo/bin/nu_plugin_image 114 | ``` 115 | 116 | -------------------------------------------------------------------------------- /build.nu: -------------------------------------------------------------------------------- 1 | use std log 2 | 3 | 4 | # TODO add licenses 5 | let fonts = [ 6 | [name, feature]; 7 | [ 8 | "AnonymousPro Font", 9 | font-anonymous_pro 10 | ], 11 | [ 12 | "IosevkaTerm Font", 13 | font-iosevka_term 14 | ], 15 | [ 16 | "Ubuntu Font", 17 | font-ubuntu 18 | ], 19 | [ 20 | "Debug log level (only used for debuging)", 21 | with-debug 22 | ], 23 | [ 24 | "Trace log level (only used for advanced debuging)", 25 | with-trace 26 | ], 27 | ] 28 | 29 | 30 | def main [package_file: path] { 31 | let repo_root = $package_file | path dirname 32 | let install_root = $env.NUPM_HOME | path join "plugins" 33 | let selected_fonts = $fonts 34 | | input list -d name -m "select features to install" 35 | | get feature 36 | 37 | let name = open ($repo_root | path join "Cargo.toml") | get package.name 38 | let ext = if ($nu.os-info.name == 'windows') { '.exe' } else { '' } 39 | let command = $"cargo install --path ($repo_root) --root ($install_root) --features=\"($selected_fonts | str join ',')\"" 40 | log info $"building using `($command)`" 41 | nu --commands $"($command)" 42 | plugin add $"($install_root | path join "bin" $name)($ext)" 43 | log info "do not forget to restart Nushell for the plugin to be fully available!" 44 | nu ($repo_root | path join scripts theme_exporter.nu) 45 | } 46 | -------------------------------------------------------------------------------- /nupm.nuon: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nu_plugin_image", 3 | "version": "0.104.0", 4 | "description": "A nushell plugin to open png images in the shell and save ansi string as images (like tables or ...)", 5 | "license": "LICENSE", 6 | "type": "custom" 7 | } -------------------------------------------------------------------------------- /resources/fonts/Anonymous_Pro/Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/Anonymous_Pro/Bold.ttf -------------------------------------------------------------------------------- /resources/fonts/Anonymous_Pro/BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/Anonymous_Pro/BoldItalic.ttf -------------------------------------------------------------------------------- /resources/fonts/Anonymous_Pro/Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/Anonymous_Pro/Italic.ttf -------------------------------------------------------------------------------- /resources/fonts/Anonymous_Pro/Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/Anonymous_Pro/Regular.ttf -------------------------------------------------------------------------------- /resources/fonts/IosevkaTerm/Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/IosevkaTerm/Bold.ttf -------------------------------------------------------------------------------- /resources/fonts/IosevkaTerm/BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/IosevkaTerm/BoldItalic.ttf -------------------------------------------------------------------------------- /resources/fonts/IosevkaTerm/Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/IosevkaTerm/Italic.ttf -------------------------------------------------------------------------------- /resources/fonts/IosevkaTerm/Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/IosevkaTerm/Medium.ttf -------------------------------------------------------------------------------- /resources/fonts/SourceCodePro/Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/SourceCodePro/Bold.otf -------------------------------------------------------------------------------- /resources/fonts/SourceCodePro/BoldItalic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/SourceCodePro/BoldItalic.otf -------------------------------------------------------------------------------- /resources/fonts/SourceCodePro/Italic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/SourceCodePro/Italic.otf -------------------------------------------------------------------------------- /resources/fonts/SourceCodePro/Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/SourceCodePro/Regular.otf -------------------------------------------------------------------------------- /resources/fonts/Ubuntu/Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/Ubuntu/Bold.ttf -------------------------------------------------------------------------------- /resources/fonts/Ubuntu/BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/Ubuntu/BoldItalic.ttf -------------------------------------------------------------------------------- /resources/fonts/Ubuntu/Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/Ubuntu/Italic.ttf -------------------------------------------------------------------------------- /resources/fonts/Ubuntu/Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FMotalleb/nu_plugin_image/edf74fdd571eccfd736d2d82c0bcc435c98b6945/resources/fonts/Ubuntu/Regular.ttf -------------------------------------------------------------------------------- /scripts/theme_exporter.nu: -------------------------------------------------------------------------------- 1 | use std log 2 | def confirm [message: string] : any -> bool { 3 | ["yes","no"] | input list $message | $in == "yes" 4 | } 5 | def "from binary" [] : binary -> string { 6 | $in | encode base64 | base64 -d 7 | } 8 | 9 | def get-terminal-colors [] { 10 | 11 | let colors = (0..15 | each {|i| $"4;($i);?" 12 | | (term query $"(ansi osc)(ansi -o $in)(ansi st)" --terminator "\e\\" 13 | | from binary 14 | | split row : 15 | | get 1 16 | | split row /) 17 | }) 18 | 19 | let env_vars = [ 20 | [NU_PLUGIN_IMAGE_FG, ($colors | get 07)] 21 | [NU_PLUGIN_IMAGE_BG , ($colors | get 00)] 22 | [NU_PLUGIN_IMAGE_BLACK , ($colors | get 00)] 23 | [NU_PLUGIN_IMAGE_RED, ($colors | get 01)] 24 | [NU_PLUGIN_IMAGE_GREEN, ($colors | get 02)] 25 | [NU_PLUGIN_IMAGE_YELLOW, ($colors | get 03)] 26 | [NU_PLUGIN_IMAGE_BLUE, ($colors | get 04)] 27 | [NU_PLUGIN_IMAGE_MAGENTA, ($colors | get 05)] 28 | [NU_PLUGIN_IMAGE_CYAN, ($colors | get 06)] 29 | [NU_PLUGIN_IMAGE_WHITE, ($colors | get 07)] 30 | [NU_PLUGIN_IMAGE_BRIGHT_BLACK, ($colors | get 08)] 31 | [NU_PLUGIN_IMAGE_BRIGHT_RED, ($colors | get 09)] 32 | [NU_PLUGIN_IMAGE_BRIGHT_GREEN, ($colors | get 10)] 33 | [NU_PLUGIN_IMAGE_BRIGHT_YELLOW, ($colors | get 11)] 34 | [NU_PLUGIN_IMAGE_BRIGHT_BLUE, ($colors | get 12)] 35 | [NU_PLUGIN_IMAGE_BRIGHT_MAGENTA, ($colors | get 13)] 36 | [NU_PLUGIN_IMAGE_BRIGHT_CYAN, ($colors | get 14)] 37 | [NU_PLUGIN_IMAGE_BRIGHT_WHITE, ($colors | get 15)] 38 | ] | each {|col| 39 | let rgb = $col | get 1 40 | # 16bit rgb to 8bit = 0xe7e7 | bits and 0x00ff 41 | let red = ($"0x($rgb | get 0)" | into int | bits and 0x00ff) 42 | let green = ($"0x($rgb | get 1)" | into int | bits and 0x00ff) 43 | let blue = ($"0x($rgb | get 2)" | into int | bits and 0x00ff) 44 | let red_hx = ($red | fmt).lowerhex | str substring 2.. 45 | let green_hx = ($green | fmt).lowerhex | str substring 2.. 46 | let blue_hx = ($blue | fmt).lowerhex | str substring 2.. 47 | $"$env.($col | first) = 0x($red_hx)($green_hx)($blue_hx)" 48 | } 49 | 50 | if (confirm "write config to the env file?") { 51 | 52 | let default = ($nu.env-path | path dirname | path join nu_image_plugin_conf.nu) 53 | let config_path = input $"where should i save the env file? \(default: ($default)\)\n~> " 54 | | if (not ($in | is-empty)) { 55 | $in 56 | } else { 57 | ($default) 58 | } 59 | 60 | 61 | if (not ( $config_path | path exists)) { 62 | $"source ($config_path)" | save $nu.env-path --append 63 | } 64 | 65 | $"# Auto generated code\n($env_vars | str join "\n")" | save $config_path -f 66 | 67 | log info "Please restart the shell" 68 | } else { 69 | for i in $env_vars { 70 | print $"($i)\n" 71 | } 72 | print "add thse values to environment variables using `config env`" 73 | } 74 | } 75 | 76 | if (confirm "do you want to save your current shell's theme as default for `to png`?") { 77 | print (get-terminal-colors) 78 | } -------------------------------------------------------------------------------- /src/ansi_to_image/ansi_to_image.rs: -------------------------------------------------------------------------------- 1 | use image::RgbaImage; 2 | use std::{ 3 | io::{BufReader, Read}, 4 | path::Path, 5 | }; 6 | use vte::Parser; 7 | 8 | use crate::{ 9 | ansi_to_image::{ 10 | font_family::FontFamily, 11 | palette::Palette, 12 | printer::{self, Settings}, 13 | }, 14 | warn, 15 | }; 16 | 17 | use super::internal_scale::InternalScale; 18 | 19 | pub fn make_image( 20 | output_path: &Path, 21 | font_family: FontFamily, 22 | png_width: Option, 23 | input: &[u8], 24 | palette: Palette, 25 | ) { 26 | // let = FontFamily::default(); 27 | 28 | let font = font_family.regular; 29 | let font_bold = font_family.bold; 30 | let font_italic = font_family.italic; 31 | let font_italic_bold = font_family.bold_italic; 32 | 33 | let font_height = 50.0; 34 | let scale = InternalScale { 35 | x: font_height, 36 | y: font_height, 37 | }; 38 | 39 | // let palette = Palette::Vscode; 40 | 41 | let mut state_machine = Parser::new(); 42 | let mut performer = printer::new(Settings { 43 | font, 44 | font_bold, 45 | font_italic, 46 | font_italic_bold, 47 | font_height, 48 | scale, 49 | palette, 50 | png_width, 51 | }); 52 | let reader = &mut BufReader::new(input); 53 | let mut buf = [0; 2048]; 54 | 55 | loop { 56 | match reader.read(&mut buf) { 57 | Ok(0) => break, 58 | 59 | Ok(n) => state_machine.advance(&mut performer, &buf[..n]), 60 | 61 | Err(err) => { 62 | warn!("{err}"); 63 | break; 64 | } 65 | } 66 | } 67 | 68 | let image: RgbaImage = performer.into(); 69 | 70 | image.save(output_path).unwrap(); 71 | } 72 | -------------------------------------------------------------------------------- /src/ansi_to_image/color.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy)] 2 | pub(super) enum ColorType { 3 | PrimaryForeground, 4 | PrimaryBackground, 5 | Normal(Color), 6 | Bright(Color), 7 | Fixed(u8), 8 | Rgb { field1: (u8, u8, u8) }, 9 | } 10 | 11 | #[derive(Debug, Clone, Copy)] 12 | pub(super) enum Color { 13 | Black, 14 | Red, 15 | Green, 16 | Yellow, 17 | Blue, 18 | Magenta, 19 | Cyan, 20 | White, 21 | } 22 | -------------------------------------------------------------------------------- /src/ansi_to_image/escape.rs: -------------------------------------------------------------------------------- 1 | //! From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters 2 | 3 | use crate::ansi_to_image::color::{Color, ColorType}; 4 | 5 | #[derive(Debug)] 6 | pub(super) enum EscapeSequence { 7 | Reset, 8 | 9 | BlackLetterFont, 10 | Bold, 11 | Faint, 12 | Italic, 13 | RapidBlink, 14 | SlowBlink, 15 | Underline, 16 | 17 | NotBold, 18 | NotUnderline, 19 | NormalIntensity, 20 | NotItalicNorBlackLetter, 21 | NotBlinking, 22 | 23 | ReverseVideo, 24 | Conceal, 25 | CrossedOut, 26 | 27 | DefaultForegroundColor, 28 | DefaultBackgroundColor, 29 | 30 | PrimaryFont, 31 | SetAlternativeFont, 32 | 33 | ForegroundColor(ColorType), 34 | BackgroundColor(ColorType), 35 | 36 | DisableProportionalSpacing, 37 | NeitherSuperscriptNorSubscript, 38 | 39 | NotReserved, 40 | 41 | Unimplemented(Vec), 42 | } 43 | 44 | impl EscapeSequence { 45 | pub(super) fn parse_params(params: Vec<&u16>) -> Vec { 46 | // let params_slice: Vec<&u16> = ; 47 | 48 | match params.as_slice() { 49 | // Set foreground (38) or background (48) color 50 | [fg_or_bg, 5, n] => { 51 | let color = match n { 52 | 0 => ColorType::Normal(Color::Black), 53 | 1 => ColorType::Normal(Color::Red), 54 | 2 => ColorType::Normal(Color::Green), 55 | 3 => ColorType::Normal(Color::Yellow), 56 | 4 => ColorType::Normal(Color::Blue), 57 | 5 => ColorType::Normal(Color::Magenta), 58 | 6 => ColorType::Normal(Color::Cyan), 59 | 7 => ColorType::Normal(Color::White), 60 | 61 | 8 => ColorType::Bright(Color::Black), 62 | 9 => ColorType::Bright(Color::Red), 63 | 10 => ColorType::Bright(Color::Green), 64 | 11 => ColorType::Bright(Color::Yellow), 65 | 12 => ColorType::Bright(Color::Blue), 66 | 13 => ColorType::Bright(Color::Magenta), 67 | 14 => ColorType::Bright(Color::Cyan), 68 | 15 => ColorType::Bright(Color::White), 69 | 70 | // These are fixed colors and could be used like ansi 38;5;numberm or 48;5;numberm 71 | 16..=255 => ColorType::Fixed(**n as u8), 72 | 73 | _ => return vec![Self::Unimplemented(vec![0, **fg_or_bg, 5, **n])], 74 | }; 75 | 76 | match fg_or_bg { 77 | // foreground 78 | 38 => vec![Self::ForegroundColor(color)], 79 | 80 | // background 81 | 48 => vec![Self::BackgroundColor(color)], 82 | 83 | _ => vec![Self::Unimplemented(vec![1, **fg_or_bg, 5, **n])], 84 | } 85 | } 86 | 87 | [fg_or_bg, 2, r, g, b] => { 88 | let color = ColorType::Rgb { 89 | field1: (**r as u8, **g as u8, **b as u8), 90 | }; 91 | match fg_or_bg { 92 | // foreground 93 | 38 => vec![Self::ForegroundColor(color)], 94 | 95 | // background 96 | 48 => vec![Self::BackgroundColor(color)], 97 | 98 | _ => vec![Self::Unimplemented(vec![2, **fg_or_bg, 2, **r, **g, **b])], 99 | } 100 | } 101 | 102 | v => { 103 | if v.len() > 0 { 104 | match v.split_at(1) { 105 | ([item, ..], rest) => { 106 | // let ve = Vec::from(rest); 107 | let mut result = vec![Self::parse_single(item)]; 108 | let next = Vec::from(rest); 109 | for value in Self::parse_params(next) { 110 | result.push(value); 111 | } 112 | return result; 113 | } 114 | _ => { 115 | return vec![]; 116 | } 117 | } 118 | } 119 | return vec![]; 120 | } 121 | } 122 | } 123 | 124 | fn parse_single(value: &&u16) -> Self { 125 | match value { 126 | 0 => Self::Reset, 127 | 1 => Self::Bold, 128 | 2 => Self::Faint, 129 | 3 => Self::Italic, 130 | 4 => Self::Underline, 131 | 5 => Self::SlowBlink, 132 | 6 => Self::RapidBlink, 133 | 134 | 7 => Self::ReverseVideo, 135 | 8 => Self::Conceal, 136 | 9 => Self::CrossedOut, 137 | 138 | 10 => Self::PrimaryFont, 139 | 140 | 11 => Self::SetAlternativeFont, 141 | 12 => Self::SetAlternativeFont, 142 | 13 => Self::SetAlternativeFont, 143 | 14 => Self::SetAlternativeFont, 144 | 15 => Self::SetAlternativeFont, 145 | 16 => Self::SetAlternativeFont, 146 | 17 => Self::SetAlternativeFont, 147 | 18 => Self::SetAlternativeFont, 148 | 19 => Self::SetAlternativeFont, 149 | 150 | 20 => Self::BlackLetterFont, 151 | 21 => Self::NotBold, 152 | 22 => Self::NormalIntensity, 153 | 23 => Self::NotItalicNorBlackLetter, 154 | 24 => Self::NotUnderline, 155 | 25 => Self::NotBlinking, 156 | 157 | 27 => Self::NotReserved, 158 | 159 | 30 => Self::ForegroundColor(ColorType::Normal(Color::Black)), 160 | 31 => Self::ForegroundColor(ColorType::Normal(Color::Red)), 161 | 32 => Self::ForegroundColor(ColorType::Normal(Color::Green)), 162 | 33 => Self::ForegroundColor(ColorType::Normal(Color::Yellow)), 163 | 34 => Self::ForegroundColor(ColorType::Normal(Color::Blue)), 164 | 35 => Self::ForegroundColor(ColorType::Normal(Color::Magenta)), 165 | 36 => Self::ForegroundColor(ColorType::Normal(Color::Cyan)), 166 | 37 => Self::ForegroundColor(ColorType::Normal(Color::White)), 167 | 168 | 39 => Self::DefaultForegroundColor, 169 | 170 | 40 => Self::BackgroundColor(ColorType::Normal(Color::Black)), 171 | 41 => Self::BackgroundColor(ColorType::Normal(Color::Red)), 172 | 42 => Self::BackgroundColor(ColorType::Normal(Color::Green)), 173 | 43 => Self::BackgroundColor(ColorType::Normal(Color::Yellow)), 174 | 44 => Self::BackgroundColor(ColorType::Normal(Color::Blue)), 175 | 45 => Self::BackgroundColor(ColorType::Normal(Color::Magenta)), 176 | 46 => Self::BackgroundColor(ColorType::Normal(Color::Cyan)), 177 | 47 => Self::BackgroundColor(ColorType::Normal(Color::White)), 178 | 179 | 49 => Self::DefaultBackgroundColor, 180 | 50 => Self::DisableProportionalSpacing, 181 | 53 => Self::CrossedOut, 182 | 183 | 75 => Self::NeitherSuperscriptNorSubscript, 184 | 185 | 90 => Self::BackgroundColor(ColorType::Bright(Color::Black)), 186 | 91 => Self::BackgroundColor(ColorType::Bright(Color::Red)), 187 | 92 => Self::BackgroundColor(ColorType::Bright(Color::Green)), 188 | 93 => Self::BackgroundColor(ColorType::Bright(Color::Yellow)), 189 | 94 => Self::BackgroundColor(ColorType::Bright(Color::Blue)), 190 | 95 => Self::BackgroundColor(ColorType::Bright(Color::Magenta)), 191 | 96 => Self::BackgroundColor(ColorType::Bright(Color::Cyan)), 192 | 97 => Self::BackgroundColor(ColorType::Bright(Color::White)), 193 | 194 | 100 => Self::BackgroundColor(ColorType::Bright(Color::Black)), 195 | 101 => Self::BackgroundColor(ColorType::Bright(Color::Red)), 196 | 102 => Self::BackgroundColor(ColorType::Bright(Color::Green)), 197 | 103 => Self::BackgroundColor(ColorType::Bright(Color::Yellow)), 198 | 104 => Self::BackgroundColor(ColorType::Bright(Color::Blue)), 199 | 105 => Self::BackgroundColor(ColorType::Bright(Color::Magenta)), 200 | 106 => Self::BackgroundColor(ColorType::Bright(Color::Cyan)), 201 | 107 => Self::BackgroundColor(ColorType::Bright(Color::White)), 202 | 203 | v => Self::Unimplemented(vec![3, **v]), 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/ansi_to_image/escape_parser.rs: -------------------------------------------------------------------------------- 1 | //! From https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters 2 | 3 | use std::slice::Iter; 4 | 5 | use crate::warn; 6 | 7 | use crate::ansi_to_image::color::{Color, ColorType}; 8 | 9 | #[derive(Debug)] 10 | pub(super) enum EscapeSequence { 11 | Reset, 12 | 13 | BlackLetterFont, 14 | Bold, 15 | Faint, 16 | Italic, 17 | RapidBlink, 18 | SlowBlink, 19 | Underline, 20 | 21 | NotBold, 22 | NotUnderline, 23 | NormalIntensity, 24 | NotItalicNorBlackLetter, 25 | NotBlinking, 26 | 27 | ReverseVideo, 28 | Conceal, 29 | CrossedOut, 30 | 31 | DefaultForegroundColor, 32 | DefaultBackgroundColor, 33 | 34 | PrimaryFont, 35 | SetAlternativeFont, 36 | 37 | ForegroundColor(ColorType), 38 | BackgroundColor(ColorType), 39 | 40 | DisableProportionalSpacing, 41 | NeitherSuperscriptNorSubscript, 42 | 43 | NotReserved, 44 | 45 | Unimplemented(Vec), 46 | Ignore, 47 | } 48 | 49 | impl EscapeSequence { 50 | pub(super) fn parse_params(params: Vec<&u16>) -> Vec { 51 | let iter = &mut params.iter(); 52 | let mut result = vec![]; 53 | while iter.len() > 0 { 54 | result.push(Self::consume_and_parse(iter)) 55 | } 56 | result 57 | } 58 | fn consume_and_parse(iter: &mut Iter<&u16>) -> Self { 59 | if let Some(current) = iter.next() { 60 | return match *current { 61 | 0 => Self::Reset, 62 | 1 => Self::Bold, 63 | 2 => Self::Faint, 64 | 3 => Self::Italic, 65 | 4 => Self::Underline, 66 | 5 => Self::SlowBlink, 67 | 6 => Self::RapidBlink, 68 | 69 | 7 => Self::ReverseVideo, 70 | 8 => Self::Conceal, 71 | 9 => Self::CrossedOut, 72 | 73 | 10 => Self::PrimaryFont, 74 | 75 | 11 => Self::SetAlternativeFont, 76 | 12 => Self::SetAlternativeFont, 77 | 13 => Self::SetAlternativeFont, 78 | 14 => Self::SetAlternativeFont, 79 | 15 => Self::SetAlternativeFont, 80 | 16 => Self::SetAlternativeFont, 81 | 17 => Self::SetAlternativeFont, 82 | 18 => Self::SetAlternativeFont, 83 | 19 => Self::SetAlternativeFont, 84 | 85 | 20 => Self::BlackLetterFont, 86 | 21 => Self::NotBold, 87 | 22 => Self::NormalIntensity, 88 | 23 => Self::NotItalicNorBlackLetter, 89 | 24 => Self::NotUnderline, 90 | 25 => Self::NotBlinking, 91 | 92 | 26 => Self::Ignore, // Proportional spacing 93 | 94 | 27 => Self::NotReserved, 95 | 28 => Self::Ignore, // Reveal 96 | 29 => Self::Ignore, // Not crossed out 97 | 98 | 30 => Self::ForegroundColor(ColorType::Normal(Color::Black)), 99 | 31 => Self::ForegroundColor(ColorType::Normal(Color::Red)), 100 | 32 => Self::ForegroundColor(ColorType::Normal(Color::Green)), 101 | 33 => Self::ForegroundColor(ColorType::Normal(Color::Yellow)), 102 | 34 => Self::ForegroundColor(ColorType::Normal(Color::Blue)), 103 | 35 => Self::ForegroundColor(ColorType::Normal(Color::Magenta)), 104 | 36 => Self::ForegroundColor(ColorType::Normal(Color::Cyan)), 105 | 37 => Self::ForegroundColor(ColorType::Normal(Color::White)), 106 | 38 => match iter.next() { 107 | Some(mode) => Self::ForegroundColor(parse_color(mode, iter)), 108 | None => { 109 | warn!( 110 | "[SEQUENCE_PARSER] foreground color mode is not supplied, parse_color(null, ...)", 111 | ); 112 | Self::Ignore 113 | } 114 | }, 115 | 39 => Self::DefaultForegroundColor, 116 | 117 | 40 => Self::BackgroundColor(ColorType::Normal(Color::Black)), 118 | 41 => Self::BackgroundColor(ColorType::Normal(Color::Red)), 119 | 42 => Self::BackgroundColor(ColorType::Normal(Color::Green)), 120 | 43 => Self::BackgroundColor(ColorType::Normal(Color::Yellow)), 121 | 44 => Self::BackgroundColor(ColorType::Normal(Color::Blue)), 122 | 45 => Self::BackgroundColor(ColorType::Normal(Color::Magenta)), 123 | 46 => Self::BackgroundColor(ColorType::Normal(Color::Cyan)), 124 | 47 => Self::BackgroundColor(ColorType::Normal(Color::White)), 125 | 48 => match iter.next() { 126 | Some(mode) => Self::BackgroundColor(parse_color(mode, iter)), 127 | None => { 128 | warn!( 129 | "[SEQUENCE_PARSER] background color mode is not supplied, parse_color(null, ...)", 130 | ); 131 | Self::Ignore 132 | } 133 | }, 134 | 49 => Self::DefaultBackgroundColor, 135 | 50 => Self::DisableProportionalSpacing, 136 | 53 => Self::CrossedOut, 137 | 138 | 75 => Self::NeitherSuperscriptNorSubscript, 139 | 140 | 90 => Self::ForegroundColor(ColorType::Bright(Color::Black)), 141 | 91 => Self::ForegroundColor(ColorType::Bright(Color::Red)), 142 | 92 => Self::ForegroundColor(ColorType::Bright(Color::Green)), 143 | 93 => Self::ForegroundColor(ColorType::Bright(Color::Yellow)), 144 | 94 => Self::ForegroundColor(ColorType::Bright(Color::Blue)), 145 | 95 => Self::ForegroundColor(ColorType::Bright(Color::Magenta)), 146 | 96 => Self::ForegroundColor(ColorType::Bright(Color::Cyan)), 147 | 97 => Self::ForegroundColor(ColorType::Bright(Color::White)), 148 | 149 | 100 => Self::BackgroundColor(ColorType::Bright(Color::Black)), 150 | 101 => Self::BackgroundColor(ColorType::Bright(Color::Red)), 151 | 102 => Self::BackgroundColor(ColorType::Bright(Color::Green)), 152 | 103 => Self::BackgroundColor(ColorType::Bright(Color::Yellow)), 153 | 104 => Self::BackgroundColor(ColorType::Bright(Color::Blue)), 154 | 105 => Self::BackgroundColor(ColorType::Bright(Color::Magenta)), 155 | 106 => Self::BackgroundColor(ColorType::Bright(Color::Cyan)), 156 | 107 => Self::BackgroundColor(ColorType::Bright(Color::White)), 157 | 158 | v => Self::Unimplemented(vec![3, *v]), 159 | }; 160 | } 161 | Self::Ignore 162 | } 163 | } 164 | 165 | fn parse_color(mode: &u16, iter: &mut Iter<&u16>) -> ColorType { 166 | match mode { 167 | 5 => { 168 | let color = iter.next(); 169 | if let Some(color) = color { 170 | let color = match color { 171 | 0 => ColorType::Normal(Color::Black), 172 | 1 => ColorType::Normal(Color::Red), 173 | 2 => ColorType::Normal(Color::Green), 174 | 3 => ColorType::Normal(Color::Yellow), 175 | 4 => ColorType::Normal(Color::Blue), 176 | 5 => ColorType::Normal(Color::Magenta), 177 | 6 => ColorType::Normal(Color::Cyan), 178 | 7 => ColorType::Normal(Color::White), 179 | 180 | 8 => ColorType::Bright(Color::Black), 181 | 9 => ColorType::Bright(Color::Red), 182 | 10 => ColorType::Bright(Color::Green), 183 | 11 => ColorType::Bright(Color::Yellow), 184 | 12 => ColorType::Bright(Color::Blue), 185 | 13 => ColorType::Bright(Color::Magenta), 186 | 14 => ColorType::Bright(Color::Cyan), 187 | 15 => ColorType::Bright(Color::White), 188 | 189 | // These are fixed colors and could be used like ansi 38;5;numberm or 48;5;numberm 190 | 16..=255 => ColorType::Fixed(**color as u8), 191 | 192 | v => { 193 | warn!("[COLOR_PARSER] fixed color value out of range, parse_fixed_color(code: {})",v); 194 | return ColorType::PrimaryForeground; 195 | } 196 | }; 197 | return color; 198 | } else { 199 | warn!( 200 | "[COLOR_PARSER] fixed color value not supplied, parse_fixed_color(code: null)" 201 | ); 202 | 203 | return ColorType::PrimaryForeground; 204 | } 205 | } 206 | 2 => match (iter.next(), iter.next(), iter.next()) { 207 | (Some(r), Some(g), Some(b)) => { 208 | let color = ColorType::Rgb { 209 | field1: (**r as u8, **g as u8, **b as u8), 210 | }; 211 | return color; 212 | } 213 | (r, g, b) => { 214 | warn!( 215 | "[COLOR_PARSER] rgb color value not supplied (correctly), parse_rgb_color({}, {}, {})", 216 | r.map(|i| i.to_string() ).unwrap_or("null".to_string()), 217 | g.map(|i| i.to_string() ).unwrap_or("null".to_string()), 218 | b.map(|i| i.to_string() ).unwrap_or("null".to_string()) 219 | ); 220 | return ColorType::PrimaryForeground; 221 | } 222 | }, 223 | v => { 224 | warn!( 225 | "[COLOR_PARSER] color mode is not supplied correctly, parse_color({}, ...)", 226 | v 227 | ); 228 | return ColorType::PrimaryForeground; 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/ansi_to_image/font_family.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | use ab_glyph::FontRef; 5 | use include_flate::flate; 6 | type FontBuilder = fn() -> FontFamily<'static>; 7 | #[derive(Debug)] 8 | pub struct FontFamily<'a> { 9 | pub name: String, 10 | pub regular: FontRef<'a>, 11 | pub bold: FontRef<'a>, 12 | pub italic: FontRef<'a>, 13 | pub bold_italic: FontRef<'a>, 14 | } 15 | 16 | impl FontFamily<'static> { 17 | fn all_fonts() -> Vec<(String, FontBuilder)> { 18 | let mut result: Vec<(String, FontBuilder)> = vec![]; 19 | result.push(("SourceCodePro".to_string(), Self::source_code_pro)); 20 | #[cfg(feature = "font-ubuntu")] 21 | result.push(("Ubuntu".to_string(), Self::ubuntu)); 22 | #[cfg(feature = "font-iosevka_term")] 23 | result.push(("IosevkaTerm".to_string(), Self::iosevka_term)); 24 | #[cfg(feature = "font-anonymous_pro")] 25 | result.push(("AnonymousPro".to_string(), Self::anonymous_pro)); 26 | result 27 | } 28 | pub fn list() -> Vec { 29 | Self::all_fonts().into_iter().map(|i| i.0).collect() 30 | } 31 | 32 | pub fn from_name(name: String) -> Self { 33 | for value in Self::all_fonts() { 34 | if name == value.0 { 35 | return value.1(); 36 | } 37 | } 38 | return Self::default(); 39 | } 40 | pub fn try_from_bytes( 41 | name: Option, 42 | regular: &'static [u8], 43 | bold: &'static [u8], 44 | italic: &'static [u8], 45 | bold_italic: &'static [u8], 46 | ) -> Option> { 47 | let regular = FontRef::try_from_slice(regular); 48 | let bold = FontRef::try_from_slice(bold); 49 | let italic = FontRef::try_from_slice(italic); 50 | let bold_italic = FontRef::try_from_slice(bold_italic); 51 | match (regular, bold, italic, bold_italic) { 52 | (Ok(regular), Ok(bold), Ok(italic), Ok(bold_italic)) => { 53 | return Some(FontFamily { 54 | name: name.unwrap_or("Custom".to_string()), 55 | regular, 56 | bold, 57 | italic, 58 | bold_italic, 59 | }) 60 | } 61 | _ => None, 62 | } 63 | } 64 | pub fn source_code_pro() -> Self { 65 | flate!(static REGULAR: [u8] from 66 | "resources/fonts/SourceCodePro/Regular.otf"); 67 | flate!(static BOLD: [u8] from 68 | "resources/fonts/SourceCodePro/Bold.otf"); 69 | flate!(static ITALIC: [u8] from 70 | "resources/fonts/SourceCodePro/Italic.otf"); 71 | flate!(static BOLD_ITALIC: [u8] from 72 | "resources/fonts/SourceCodePro/BoldItalic.otf"); 73 | FontFamily::try_from_bytes( 74 | Some("SourceCodePro".to_string()), 75 | ®ULAR, 76 | &BOLD, 77 | &ITALIC, 78 | &BOLD_ITALIC, 79 | ) 80 | .unwrap() 81 | } 82 | 83 | #[cfg(feature = "font-ubuntu")] 84 | pub fn ubuntu() -> Self { 85 | flate!(static REGULAR: [u8] from 86 | "resources/fonts/Ubuntu/Regular.ttf"); 87 | flate!(static BOLD: [u8] from 88 | "resources/fonts/Ubuntu/Bold.ttf"); 89 | flate!(static ITALIC: [u8] from 90 | "resources/fonts/Ubuntu/Italic.ttf"); 91 | flate!(static BOLD_ITALIC: [u8] from 92 | "resources/fonts/Ubuntu/BoldItalic.ttf"); 93 | FontFamily::try_from_bytes( 94 | Some("Ubunto".to_string()), 95 | ®ULAR, 96 | &BOLD, 97 | &ITALIC, 98 | &BOLD_ITALIC, 99 | ) 100 | .unwrap() 101 | } 102 | 103 | #[cfg(feature = "font-iosevka_term")] 104 | pub fn iosevka_term() -> Self { 105 | flate!(static REGULAR: [u8] from 106 | "resources/fonts/IosevkaTerm/Medium.ttf"); 107 | flate!(static BOLD: [u8] from 108 | "resources/fonts/IosevkaTerm/Bold.ttf"); 109 | flate!(static ITALIC: [u8] from 110 | "resources/fonts/IosevkaTerm/Italic.ttf"); 111 | flate!(static BOLD_ITALIC: [u8] from 112 | "resources/fonts/IosevkaTerm/BoldItalic.ttf"); 113 | FontFamily::try_from_bytes( 114 | Some("IosevkaTerm".to_string()), 115 | ®ULAR, 116 | &BOLD, 117 | &ITALIC, 118 | &BOLD_ITALIC, 119 | ) 120 | .unwrap() 121 | } 122 | 123 | #[cfg(feature = "font-anonymous_pro")] 124 | pub fn anonymous_pro() -> Self { 125 | flate!(static REGULAR: [u8] from 126 | "resources/fonts/Anonymous_Pro/Regular.ttf"); 127 | flate!(static BOLD: [u8] from 128 | "resources/fonts/Anonymous_Pro/Bold.ttf"); 129 | flate!(static ITALIC: [u8] from 130 | "resources/fonts/Anonymous_Pro/Italic.ttf"); 131 | flate!(static BOLD_ITALIC: [u8] from 132 | "resources/fonts/Anonymous_Pro/BoldItalic.ttf"); 133 | FontFamily::try_from_bytes( 134 | Some("AnonymousPro".to_string()), 135 | ®ULAR, 136 | &BOLD, 137 | &ITALIC, 138 | &BOLD_ITALIC, 139 | ) 140 | .unwrap() 141 | } 142 | } 143 | 144 | impl Default for FontFamily<'static> { 145 | fn default() -> Self { 146 | Self::source_code_pro() 147 | } 148 | } 149 | 150 | impl Display for FontFamily<'_> { 151 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 152 | write!(f, "{}", self.name) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/ansi_to_image/internal_scale.rs: -------------------------------------------------------------------------------- 1 | use ab_glyph::PxScale; 2 | 3 | #[derive(Clone, Copy)] 4 | pub struct InternalScale { 5 | /// Horizontal scale, in pixels. 6 | pub x: f32, 7 | /// Vertical scale, in pixels. 8 | pub y: f32, 9 | } 10 | 11 | // impl Into for InternalScale { 12 | // fn into(self) -> Scale { 13 | // Scale { 14 | // x: self.x, 15 | // y: self.y, 16 | // } 17 | // } 18 | // } 19 | 20 | impl Into for InternalScale { 21 | fn into(self) -> PxScale { 22 | PxScale { 23 | x: self.x, 24 | y: self.y, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ansi_to_image/mod.rs: -------------------------------------------------------------------------------- 1 | mod ansi_to_image; 2 | mod color; 3 | // mod escape; 4 | mod escape_parser; 5 | mod font_family; 6 | mod internal_scale; 7 | mod nu_plugin; 8 | mod palette; 9 | mod printer; 10 | pub use font_family::FontFamily; 11 | pub use nu_plugin::ansi_to_image; 12 | pub use palette::Palette; 13 | -------------------------------------------------------------------------------- /src/ansi_to_image/nu_plugin.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Read, path::PathBuf, time::SystemTime}; 2 | 3 | use crate::{debug, error, warn}; 4 | use ab_glyph::FontRef; 5 | use nu_plugin::EvaluatedCall; 6 | use nu_protocol::{LabeledError, Span, Value}; 7 | 8 | use crate::FontFamily; 9 | 10 | use super::{ 11 | ansi_to_image::make_image, 12 | palette::{strhex_to_rgba, Palette}, 13 | }; 14 | 15 | pub fn ansi_to_image( 16 | engine: &nu_plugin::EngineInterface, 17 | call: &EvaluatedCall, 18 | input: &Value, 19 | ) -> Result { 20 | let i: &[u8] = match input { 21 | Value::String { 22 | val, 23 | internal_span: _, 24 | } => val.as_bytes(), 25 | Value::Binary { 26 | val, 27 | internal_span: _, 28 | } => val, 29 | _ => { 30 | return Err(make_params_err( 31 | "cannot read input as binary data (maybe its empty)".to_string(), 32 | input.span(), 33 | )) 34 | } 35 | }; 36 | let size = match call.get_flag_value("width") { 37 | Some(val) => match val.as_int().ok() { 38 | Some(value) => Some(value as u32), 39 | _ => None, 40 | }, 41 | _ => None, 42 | }; 43 | let font: FontFamily<'_> = resolve_font(call); 44 | let out_path = call.opt::(0); 45 | 46 | let out = match out_path { 47 | Ok(Some(path)) => { 48 | debug!("received output name `{}`", path); 49 | if let Ok(value) = engine.get_current_dir() { 50 | let mut absolute = PathBuf::from(value); 51 | absolute.extend(PathBuf::from(path).iter()); 52 | debug!( 53 | "absolute output name `{}`", 54 | absolute.to_str().unwrap_or("cannot convert path to string") 55 | ); 56 | Some(absolute) 57 | } else { 58 | warn!("failed to fetch current directories path"); 59 | Some(PathBuf::from(path)) 60 | } 61 | } 62 | _ => { 63 | let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH); 64 | let current = engine.get_current_dir().map(|p| PathBuf::from(p)); 65 | if let (Ok(now), Ok(current)) = (now, current) { 66 | let current = &mut current.clone(); 67 | current.push(PathBuf::from(format!("nu-image-{}.png", now.as_secs()))); 68 | Some(current.to_owned()) 69 | } else { 70 | None 71 | } 72 | } 73 | }; 74 | if let None = out { 75 | return Err(make_params_err( 76 | format!("cannot use time stamp as the file name timestamp please provide output path explicitly"), 77 | call.head, 78 | )); 79 | } 80 | let theme = match call 81 | .get_flag_value("theme") 82 | .map(|i| i.as_str().map(|f| f.to_string())) 83 | { 84 | Some(Ok(name)) => { 85 | if let Some(theme) = Palette::from_name(name.to_string()) { 86 | theme 87 | } else { 88 | error!("No theme found that matches the given name"); 89 | Palette::default() 90 | } 91 | } 92 | _ => Palette::default(), 93 | }; 94 | let theme = load_custom_theme(call, theme); 95 | 96 | let path = PathBuf::from(out.unwrap()); 97 | make_image(path.as_path(), font, size, i, theme); 98 | 99 | Ok(Value::string( 100 | path.to_str().unwrap_or("error reading path").to_owned(), 101 | call.head, 102 | )) 103 | } 104 | 105 | fn resolve_font(call: &EvaluatedCall) -> FontFamily<'static> { 106 | let mut font: FontFamily<'static> = match call.get_flag_value("font").map(|value| match value { 107 | Value::String { val, .. } => Some(FontFamily::from_name(val)), 108 | _ => None, 109 | }) { 110 | Some(value) => { 111 | if let Some(font) = value { 112 | font 113 | } else { 114 | FontFamily::default() 115 | } 116 | } 117 | None => FontFamily::default(), 118 | }; 119 | // TODO custom fonts disabled for now 120 | if let Some(path) = call.get_flag_value("font-regular") { 121 | let buffer = load_file(path); 122 | font.regular = FontRef::try_from_slice(buffer).unwrap(); 123 | } 124 | if let Some(path) = call.get_flag_value("font-bold") { 125 | let buffer = load_file(path); 126 | font.bold = FontRef::try_from_slice(buffer).unwrap(); 127 | } 128 | if let Some(path) = call.get_flag_value("font-italic") { 129 | let buffer = load_file(path); 130 | font.italic = FontRef::try_from_slice(buffer).unwrap(); 131 | } 132 | if let Some(path) = call.get_flag_value("bold-italic") { 133 | let buffer = load_file(path); 134 | font.bold_italic = FontRef::try_from_slice(buffer).unwrap(); 135 | } 136 | font 137 | } 138 | 139 | // fn load_file<'a>(path: Value) -> &'a [u8] { 140 | // let path = path.as_str().unwrap(); 141 | // let mut file = File::open(PathBuf::from(path)).unwrap(); 142 | // let mut buffer = Vec::new(); 143 | 144 | // // read the whole file 145 | // let _ = file.read_to_end(&mut buffer); 146 | // buffer.as_slice() 147 | // } 148 | 149 | fn load_file<'a>(path: Value) -> &'a [u8] { 150 | let path = path.as_str().unwrap(); 151 | let mut file = File::open(PathBuf::from(path)).unwrap(); 152 | let mut buffer: Box> = Box::new(vec![]); 153 | file.read_to_end(&mut *buffer).unwrap(); 154 | Box::leak(buffer) 155 | } 156 | 157 | fn make_params_err(text: String, span: Span) -> LabeledError { 158 | LabeledError::new(text).with_label("faced an error when tried to parse the params", span) 159 | } 160 | fn load_custom_theme(call: &EvaluatedCall, theme: Palette) -> Palette { 161 | let result = theme.palette().copy_with( 162 | read_hex_to_array(call, "custom-theme-fg"), 163 | read_hex_to_array(call, "custom-theme-bg"), 164 | read_hex_to_array(call, "custom-theme-black"), 165 | read_hex_to_array(call, "custom-theme-red"), 166 | read_hex_to_array(call, "custom-theme-green"), 167 | read_hex_to_array(call, "custom-theme-yellow"), 168 | read_hex_to_array(call, "custom-theme-blue"), 169 | read_hex_to_array(call, "custom-theme-magenta"), 170 | read_hex_to_array(call, "custom-theme-cyan"), 171 | read_hex_to_array(call, "custom-theme-white"), 172 | read_hex_to_array(call, "custom-theme-bright_black"), 173 | read_hex_to_array(call, "custom-theme-bright_red"), 174 | read_hex_to_array(call, "custom-theme-bright_green"), 175 | read_hex_to_array(call, "custom-theme-bright_yellow"), 176 | read_hex_to_array(call, "custom-theme-bright_blue"), 177 | read_hex_to_array(call, "custom-theme-bright_magenta"), 178 | read_hex_to_array(call, "custom-theme-bright_cyan"), 179 | read_hex_to_array(call, "custom-theme-bright_white"), 180 | ); 181 | Palette::Custom(result) 182 | } 183 | 184 | fn read_hex_to_array(call: &EvaluatedCall, name: &str) -> Option<[u8; 4]> { 185 | if let Some(Value::String { val, .. }) = call.get_flag_value(name) { 186 | return strhex_to_rgba(val); 187 | } 188 | None 189 | } 190 | -------------------------------------------------------------------------------- /src/ansi_to_image/palette.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::warn; 4 | 5 | use crate::ansi_to_image::color::{Color, ColorType}; 6 | type ColorOption = Option<[u8; 4]>; 7 | #[allow(dead_code)] 8 | #[derive(Debug)] 9 | pub enum Palette { 10 | Vscode, 11 | Xterm, 12 | Eclipse, 13 | Ubuntu, 14 | MIRC, 15 | Putty, 16 | WinXp, 17 | WinTerminal, 18 | Win10, 19 | WinPs, 20 | Env, 21 | Custom(PaletteData), 22 | Test, 23 | } 24 | 25 | impl Default for Palette { 26 | fn default() -> Self { 27 | if let Ok(_) = std::env::var("NU_PLUGIN_IMAGE_FG") { 28 | return Palette::Env; 29 | } 30 | return Palette::Vscode; 31 | } 32 | } 33 | 34 | #[derive(Debug, Clone, Copy)] 35 | pub struct PaletteData { 36 | pub primary_foreground: [u8; 4], 37 | pub primary_background: [u8; 4], 38 | 39 | pub black: [u8; 4], 40 | pub red: [u8; 4], 41 | pub green: [u8; 4], 42 | pub yellow: [u8; 4], 43 | pub blue: [u8; 4], 44 | pub magenta: [u8; 4], 45 | pub cyan: [u8; 4], 46 | pub white: [u8; 4], 47 | 48 | pub bright_black: [u8; 4], 49 | pub bright_red: [u8; 4], 50 | pub bright_green: [u8; 4], 51 | pub bright_yellow: [u8; 4], 52 | pub bright_blue: [u8; 4], 53 | pub bright_magenta: [u8; 4], 54 | pub bright_cyan: [u8; 4], 55 | pub bright_white: [u8; 4], 56 | 57 | pub fixed: [[u8; 4]; 256], 58 | } 59 | impl PaletteData { 60 | pub fn copy_with( 61 | &self, 62 | primary_foreground: ColorOption, 63 | primary_background: ColorOption, 64 | 65 | black: ColorOption, 66 | red: ColorOption, 67 | green: ColorOption, 68 | yellow: ColorOption, 69 | blue: ColorOption, 70 | magenta: ColorOption, 71 | cyan: ColorOption, 72 | white: ColorOption, 73 | 74 | bright_black: ColorOption, 75 | bright_red: ColorOption, 76 | bright_green: ColorOption, 77 | bright_yellow: ColorOption, 78 | bright_blue: ColorOption, 79 | bright_magenta: ColorOption, 80 | bright_cyan: ColorOption, 81 | bright_white: ColorOption, 82 | ) -> PaletteData { 83 | let result = &mut self.clone(); 84 | if let Some(fg) = primary_foreground { 85 | result.primary_foreground = fg; 86 | } 87 | 88 | if let Some(bg) = primary_background { 89 | result.primary_background = bg; 90 | } 91 | 92 | if let Some(color) = black { 93 | result.black = color; 94 | } 95 | 96 | if let Some(color) = red { 97 | result.red = color; 98 | } 99 | 100 | if let Some(color) = green { 101 | result.green = color; 102 | } 103 | 104 | if let Some(color) = yellow { 105 | result.yellow = color; 106 | } 107 | 108 | if let Some(color) = blue { 109 | result.blue = color; 110 | } 111 | 112 | if let Some(color) = magenta { 113 | result.magenta = color; 114 | } 115 | 116 | if let Some(color) = cyan { 117 | result.cyan = color; 118 | } 119 | 120 | if let Some(color) = white { 121 | result.white = color; 122 | } 123 | 124 | if let Some(color) = bright_black { 125 | result.bright_black = color; 126 | } 127 | 128 | if let Some(color) = bright_red { 129 | result.bright_red = color; 130 | } 131 | 132 | if let Some(color) = bright_green { 133 | result.bright_green = color; 134 | } 135 | 136 | if let Some(color) = bright_yellow { 137 | result.bright_yellow = color; 138 | } 139 | 140 | if let Some(color) = bright_blue { 141 | result.bright_blue = color; 142 | } 143 | 144 | if let Some(color) = bright_magenta { 145 | result.bright_magenta = color; 146 | } 147 | 148 | if let Some(color) = bright_cyan { 149 | result.bright_cyan = color; 150 | } 151 | 152 | if let Some(color) = bright_white { 153 | result.bright_white = color; 154 | } 155 | 156 | result.to_owned() 157 | } 158 | } 159 | 160 | impl Palette { 161 | pub(super) fn palette(&self) -> PaletteData { 162 | match self { 163 | Palette::Vscode => palette_vscode(), 164 | Palette::Xterm => palette_xterm(), 165 | Palette::Ubuntu => palette_ubuntu(), 166 | Palette::Eclipse => palette_eclipse(), 167 | Palette::MIRC => palette_mirc(), 168 | Palette::Putty => palette_putty(), 169 | Palette::WinXp => palette_win_xp(), 170 | Palette::WinTerminal => palette_terminal_app(), 171 | Palette::Win10 => palette_win_10(), 172 | Palette::WinPs => palette_win_power_shell(), 173 | Palette::Test => palette_test(), 174 | Palette::Env => palette_env(), 175 | Palette::Custom(p) => *p, 176 | } 177 | } 178 | pub(super) fn from_name(name: String) -> Option { 179 | match name.to_lowercase().as_str() { 180 | "vscode" => Some(Palette::Vscode), 181 | "xterm" => Some(Palette::Xterm), 182 | "eclipse" => Some(Palette::Eclipse), 183 | "ubuntu" => Some(Palette::Ubuntu), 184 | "mirc" => Some(Palette::MIRC), 185 | "putty" => Some(Palette::Putty), 186 | "winxp" => Some(Palette::WinXp), 187 | "terminal" => Some(Palette::WinTerminal), 188 | "winterm" => Some(Palette::WinTerminal), 189 | "win10" => Some(Palette::Win10), 190 | "win_power-shell" => Some(Palette::WinPs), 191 | "win_ps" => Some(Palette::WinPs), 192 | _ => None, 193 | } 194 | } 195 | pub fn list() -> Vec { 196 | vec![ 197 | "vscode".to_string(), 198 | "xterm".to_string(), 199 | "ubuntu".to_string(), 200 | "eclipse".to_string(), 201 | "mirc".to_string(), 202 | "putty".to_string(), 203 | "winxp".to_string(), 204 | "terminal".to_string(), 205 | "win10".to_string(), 206 | "win_power-shell".to_string(), 207 | "win_ps".to_string(), 208 | ] 209 | } 210 | 211 | pub(super) fn get_color(&self, color: ColorType) -> [u8; 4] { 212 | let palette = self.palette(); 213 | 214 | match color { 215 | ColorType::PrimaryForeground => palette.primary_foreground, 216 | ColorType::PrimaryBackground => palette.primary_background, 217 | ColorType::Rgb { field1: rgb } => [rgb.0, rgb.1, rgb.2, 255], 218 | ColorType::Normal(color) => match color { 219 | Color::Black => palette.black, 220 | Color::Red => palette.red, 221 | Color::Green => palette.green, 222 | Color::Yellow => palette.yellow, 223 | Color::Blue => palette.blue, 224 | Color::Magenta => palette.magenta, 225 | Color::Cyan => palette.cyan, 226 | Color::White => palette.white, 227 | }, 228 | 229 | ColorType::Bright(color) => match color { 230 | Color::Black => palette.bright_black, 231 | Color::Red => palette.bright_red, 232 | Color::Green => palette.bright_green, 233 | Color::Yellow => palette.bright_yellow, 234 | Color::Blue => palette.bright_blue, 235 | Color::Magenta => palette.bright_magenta, 236 | Color::Cyan => palette.bright_cyan, 237 | Color::White => palette.bright_white, 238 | }, 239 | 240 | ColorType::Fixed(num) => palette.fixed[num as usize], 241 | } 242 | } 243 | } 244 | fn palette_env() -> PaletteData { 245 | PaletteData { 246 | primary_foreground: hex_from_env("NU_PLUGIN_IMAGE_FG"), 247 | primary_background: hex_from_env("NU_PLUGIN_IMAGE_BG"), 248 | 249 | black: hex_from_env("NU_PLUGIN_IMAGE_BLACK"), 250 | red: hex_from_env("NU_PLUGIN_IMAGE_RED"), 251 | green: hex_from_env("NU_PLUGIN_IMAGE_GREEN"), 252 | yellow: hex_from_env("NU_PLUGIN_IMAGE_YELLOW"), 253 | blue: hex_from_env("NU_PLUGIN_IMAGE_BLUE"), 254 | magenta: hex_from_env("NU_PLUGIN_IMAGE_MAGENTA"), 255 | cyan: hex_from_env("NU_PLUGIN_IMAGE_CYAN"), 256 | white: hex_from_env("NU_PLUGIN_IMAGE_WHITE"), 257 | 258 | bright_black: hex_from_env("NU_PLUGIN_IMAGE_BRIGHT_BLACK"), 259 | bright_red: hex_from_env("NU_PLUGIN_IMAGE_BRIGHT_RED"), 260 | bright_green: hex_from_env("NU_PLUGIN_IMAGE_BRIGHT_GREEN"), 261 | bright_yellow: hex_from_env("NU_PLUGIN_IMAGE_BRIGHT_YELLOW"), 262 | bright_blue: hex_from_env("NU_PLUGIN_IMAGE_BRIGHT_BLUE"), 263 | bright_magenta: hex_from_env("NU_PLUGIN_IMAGE_BRIGHT_MAGENTA"), 264 | bright_cyan: hex_from_env("NU_PLUGIN_IMAGE_BRIGHT_CYAN"), 265 | bright_white: hex_from_env("NU_PLUGIN_IMAGE_BRIGHT_WHITE"), 266 | 267 | fixed: fixed_colors(), 268 | } 269 | } 270 | fn palette_vscode() -> PaletteData { 271 | PaletteData { 272 | // primary_background: "0x161616".parse().unwrap() 273 | // primary_foreground: "0xf2f2f2".parse().unwrap() 274 | primary_foreground: [229, 229, 229, 255], 275 | primary_background: [24, 24, 24, 255], 276 | 277 | black: [0, 0, 0, 255], 278 | red: [205, 49, 49, 255], 279 | green: [13, 188, 121, 255], 280 | yellow: [229, 229, 16, 255], 281 | blue: [36, 114, 200, 255], 282 | magenta: [188, 63, 188, 255], 283 | cyan: [17, 168, 205, 255], 284 | white: [229, 229, 229, 255], 285 | 286 | bright_black: [102, 102, 102, 255], 287 | bright_red: [241, 76, 76, 255], 288 | bright_green: [35, 209, 139, 255], 289 | bright_yellow: [245, 245, 67, 255], 290 | bright_blue: [59, 142, 234, 255], 291 | bright_magenta: [214, 112, 214, 255], 292 | bright_cyan: [41, 184, 219, 255], 293 | bright_white: [229, 229, 229, 255], 294 | 295 | fixed: fixed_colors(), 296 | } 297 | } 298 | fn palette_xterm() -> PaletteData { 299 | PaletteData { 300 | // primary_background: "0x161616".parse().unwrap() 301 | // primary_foreground: "0xf2f2f2".parse().unwrap() 302 | primary_foreground: [229, 229, 229, 255], 303 | primary_background: [0, 0, 0, 255], 304 | 305 | black: [0, 0, 0, 255], 306 | red: [205, 0, 0, 255], 307 | green: [0, 205, 0, 255], 308 | yellow: [205, 205, 0, 255], 309 | blue: [0, 0, 238, 255], 310 | magenta: [205, 0, 205, 255], 311 | cyan: [0, 205, 205, 255], 312 | white: [229, 229, 229, 255], 313 | 314 | bright_black: [127, 127, 127, 255], 315 | bright_red: [255, 0, 0, 255], 316 | bright_green: [0, 255, 0, 255], 317 | bright_yellow: [255, 255, 0, 255], 318 | bright_blue: [0, 0, 252, 255], 319 | bright_magenta: [255, 0, 255, 255], 320 | bright_cyan: [0, 255, 255, 255], 321 | bright_white: [255, 255, 255, 255], 322 | 323 | fixed: fixed_colors(), 324 | } 325 | } 326 | fn palette_eclipse() -> PaletteData { 327 | PaletteData { 328 | // primary_background: "0x161616".parse().unwrap() 329 | // primary_foreground: "0xf2f2f2".parse().unwrap() 330 | primary_foreground: [229, 229, 229, 255], 331 | primary_background: [0, 0, 0, 255], 332 | 333 | black: [0, 0, 0, 255], 334 | red: [205, 0, 0, 255], 335 | green: [0, 205, 0, 255], 336 | yellow: [205, 205, 0, 255], 337 | blue: [0, 0, 238, 255], 338 | magenta: [205, 0, 205, 255], 339 | cyan: [0, 205, 205, 255], 340 | white: [229, 229, 229, 255], 341 | 342 | bright_black: [0, 0, 0, 255], 343 | bright_red: [255, 0, 0, 255], 344 | bright_green: [0, 255, 0, 255], 345 | bright_yellow: [255, 255, 0, 255], 346 | bright_blue: [0, 0, 252, 255], 347 | bright_magenta: [255, 0, 255, 255], 348 | bright_cyan: [0, 255, 255, 255], 349 | bright_white: [255, 255, 255, 255], 350 | 351 | fixed: fixed_colors(), 352 | } 353 | } 354 | fn palette_ubuntu() -> PaletteData { 355 | PaletteData { 356 | // primary_background: "0x161616".parse().unwrap() 357 | // primary_foreground: "0xf2f2f2".parse().unwrap() 358 | primary_foreground: [204, 204, 204, 255], 359 | primary_background: [1, 1, 1, 255], 360 | 361 | black: [1, 1, 1, 255], 362 | red: [222, 56, 43, 255], 363 | green: [57, 181, 74, 255], 364 | yellow: [255, 199, 6, 255], 365 | blue: [0, 111, 184, 255], 366 | magenta: [118, 38, 113, 255], 367 | cyan: [44, 181, 233, 255], 368 | white: [204, 204, 204, 255], 369 | 370 | bright_black: [128, 128, 128, 255], 371 | bright_red: [255, 0, 0, 255], 372 | bright_green: [0, 255, 0, 255], 373 | bright_yellow: [255, 255, 0, 255], 374 | bright_blue: [0, 0, 255, 255], 375 | bright_magenta: [255, 0, 255, 255], 376 | bright_cyan: [0, 255, 255, 255], 377 | bright_white: [255, 255, 255, 255], 378 | 379 | fixed: fixed_colors(), 380 | } 381 | } 382 | fn palette_mirc() -> PaletteData { 383 | PaletteData { 384 | // primary_background: "0x161616".parse().unwrap() 385 | // primary_foreground: "0xf2f2f2".parse().unwrap() 386 | primary_foreground: [210, 210, 210, 255], 387 | primary_background: [0, 0, 0, 255], 388 | 389 | black: [0, 0, 0, 255], 390 | red: [127, 0, 0, 255], 391 | green: [0, 147, 0, 255], 392 | yellow: [252, 127, 0, 255], 393 | blue: [0, 0, 127, 255], 394 | magenta: [156, 0, 156, 255], 395 | cyan: [0, 147, 147, 255], 396 | white: [210, 210, 210, 255], 397 | 398 | bright_black: [127, 127, 127, 255], 399 | bright_red: [255, 0, 0, 255], 400 | bright_green: [0, 252, 0, 255], 401 | bright_yellow: [255, 255, 0, 255], 402 | bright_blue: [0, 0, 252, 255], 403 | bright_magenta: [255, 0, 255, 255], 404 | bright_cyan: [0, 255, 255, 255], 405 | bright_white: [255, 255, 255, 255], 406 | 407 | fixed: fixed_colors(), 408 | } 409 | } 410 | fn palette_putty() -> PaletteData { 411 | PaletteData { 412 | // primary_background: "0x161616".parse().unwrap() 413 | // primary_foreground: "0xf2f2f2".parse().unwrap() 414 | primary_foreground: [187, 187, 187, 255], 415 | primary_background: [0, 0, 0, 255], 416 | 417 | black: [0, 0, 0, 255], 418 | red: [187, 0, 0, 255], 419 | green: [0, 187, 0, 255], 420 | yellow: [187, 187, 0, 255], 421 | blue: [0, 0, 187, 255], 422 | magenta: [187, 0, 187, 255], 423 | cyan: [0, 187, 187, 255], 424 | white: [187, 187, 187, 255], 425 | 426 | bright_black: [85, 85, 85, 255], 427 | bright_red: [255, 85, 85, 255], 428 | bright_green: [85, 255, 85, 255], 429 | bright_yellow: [255, 255, 85, 255], 430 | bright_blue: [85, 85, 255, 255], 431 | bright_magenta: [255, 85, 255, 255], 432 | bright_cyan: [85, 255, 255, 255], 433 | bright_white: [255, 255, 255, 255], 434 | 435 | fixed: fixed_colors(), 436 | } 437 | } 438 | fn palette_terminal_app() -> PaletteData { 439 | PaletteData { 440 | // primary_background: "0x161616".parse().unwrap() 441 | // primary_foreground: "0xf2f2f2".parse().unwrap() 442 | primary_foreground: [203, 204, 205, 255], 443 | primary_background: [0, 0, 0, 255], 444 | 445 | black: [0, 0, 0, 255], 446 | red: [194, 54, 33, 255], 447 | green: [37, 188, 36, 255], 448 | yellow: [173, 173, 39, 255], 449 | blue: [73, 46, 225, 255], 450 | magenta: [211, 56, 211, 255], 451 | cyan: [51, 187, 200, 255], 452 | white: [203, 204, 205, 255], 453 | 454 | bright_black: [129, 131, 131, 255], 455 | bright_red: [252, 57, 31, 255], 456 | bright_green: [49, 231, 34, 255], 457 | bright_yellow: [234, 236, 35, 255], 458 | bright_blue: [88, 51, 255, 255], 459 | bright_magenta: [249, 53, 248, 255], 460 | bright_cyan: [20, 240, 240, 255], 461 | bright_white: [233, 235, 235, 255], 462 | 463 | fixed: fixed_colors(), 464 | } 465 | } 466 | fn palette_win_10() -> PaletteData { 467 | PaletteData { 468 | // primary_background: "0x161616".parse().unwrap() 469 | // primary_foreground: "0xf2f2f2".parse().unwrap() 470 | primary_foreground: [204, 204, 204, 255], 471 | primary_background: [12, 12, 12, 255], 472 | 473 | black: [12, 12, 12, 255], 474 | red: [197, 15, 31, 255], 475 | green: [19, 161, 14, 255], 476 | yellow: [193, 156, 0, 255], 477 | blue: [0, 55, 218, 255], 478 | magenta: [136, 23, 152, 255], 479 | cyan: [58, 150, 221, 255], 480 | white: [204, 204, 204, 255], 481 | 482 | bright_black: [118, 118, 118, 255], 483 | bright_red: [231, 72, 86, 255], 484 | bright_green: [22, 198, 12, 255], 485 | bright_yellow: [249, 241, 165, 255], 486 | bright_blue: [59, 120, 255, 255], 487 | bright_magenta: [180, 0, 158, 255], 488 | bright_cyan: [97, 214, 214, 255], 489 | bright_white: [242, 242, 242, 255], 490 | 491 | fixed: fixed_colors(), 492 | } 493 | } 494 | fn palette_win_xp() -> PaletteData { 495 | PaletteData { 496 | // primary_background: "0x161616".parse().unwrap() 497 | // primary_foreground: "0xf2f2f2".parse().unwrap() 498 | primary_foreground: [192, 192, 192, 255], 499 | primary_background: [0, 0, 0, 255], 500 | 501 | black: [0, 0, 0, 255], 502 | red: [128, 0, 0, 255], 503 | green: [0, 128, 0, 255], 504 | yellow: [128, 128, 0, 255], 505 | blue: [0, 0, 128, 255], 506 | magenta: [128, 0, 128, 255], 507 | cyan: [0, 128, 128, 255], 508 | white: [192, 192, 192, 255], 509 | 510 | bright_black: [128, 128, 128, 255], 511 | bright_red: [255, 0, 0, 255], 512 | bright_green: [0, 255, 0, 255], 513 | bright_yellow: [255, 255, 0, 255], 514 | bright_blue: [0, 0, 255, 255], 515 | bright_magenta: [255, 0, 255, 255], 516 | bright_cyan: [0, 255, 255, 255], 517 | bright_white: [255, 255, 255, 255], 518 | 519 | fixed: fixed_colors(), 520 | } 521 | } 522 | fn palette_win_power_shell() -> PaletteData { 523 | PaletteData { 524 | // primary_background: "0x161616".parse().unwrap() 525 | // primary_foreground: "0xf2f2f2".parse().unwrap() 526 | primary_foreground: [192, 192, 192, 255], 527 | primary_background: [1, 36, 86, 255], 528 | 529 | black: [0, 0, 0, 255], 530 | red: [128, 0, 0, 255], 531 | green: [0, 128, 0, 255], 532 | yellow: [238, 237, 240, 255], 533 | blue: [0, 0, 128, 255], 534 | magenta: [1, 36, 86, 255], 535 | cyan: [0, 128, 128, 255], 536 | white: [192, 192, 192, 255], 537 | 538 | bright_black: [128, 128, 128, 255], 539 | bright_red: [255, 0, 0, 255], 540 | bright_green: [0, 255, 0, 255], 541 | bright_yellow: [255, 255, 0, 255], 542 | bright_blue: [0, 0, 255, 255], 543 | bright_magenta: [255, 0, 255, 255], 544 | bright_cyan: [0, 255, 255, 255], 545 | bright_white: [255, 255, 255, 255], 546 | 547 | fixed: fixed_colors(), 548 | } 549 | } 550 | 551 | fn palette_test() -> PaletteData { 552 | PaletteData { 553 | // primary_background: "0x161616".parse().unwrap() 554 | // primary_foreground: "0xf2f2f2".parse().unwrap() 555 | primary_foreground: [0, 0, 0, 255], 556 | primary_background: [255, 255, 255, 255], 557 | 558 | black: [0, 0, 0, 255], 559 | red: [255, 0, 0, 255], 560 | green: [0, 255, 0, 255], 561 | yellow: [249, 168, 37, 255], 562 | blue: [0, 0, 255, 255], 563 | magenta: [168, 37, 191, 255], 564 | cyan: [0, 131, 143, 255], 565 | white: [255, 255, 255, 255], 566 | 567 | bright_black: [44, 44, 44, 255], 568 | bright_red: [198, 40, 40, 255], 569 | bright_green: [85, 139, 46, 255], 570 | bright_yellow: [249, 168, 37, 255], 571 | bright_blue: [21, 101, 193, 255], 572 | bright_magenta: [168, 37, 191, 255], 573 | bright_cyan: [0, 131, 143, 255], 574 | bright_white: [255, 255, 255, 255], 575 | 576 | fixed: fixed_colors(), 577 | } 578 | } 579 | 580 | fn fixed_colors() -> [[u8; 4]; 256] { 581 | [ 582 | [0, 0, 0, 255], 583 | [128, 0, 0, 255], 584 | [0, 128, 0, 255], 585 | [128, 128, 0, 255], 586 | [0, 0, 128, 255], 587 | [128, 0, 128, 255], 588 | [0, 128, 128, 255], 589 | [192, 192, 192, 255], 590 | [128, 128, 128, 255], 591 | [255, 0, 0, 255], 592 | [0, 255, 0, 255], 593 | [255, 255, 0, 255], 594 | [0, 0, 255, 255], 595 | [255, 0, 255, 255], 596 | [0, 255, 255, 255], 597 | [255, 255, 255, 255], 598 | [0, 0, 0, 255], 599 | [0, 0, 95, 255], 600 | [0, 0, 135, 255], 601 | [0, 0, 175, 255], 602 | [0, 0, 215, 255], 603 | [0, 0, 255, 255], 604 | [0, 95, 0, 255], 605 | [0, 95, 95, 255], 606 | [0, 95, 135, 255], 607 | [0, 95, 175, 255], 608 | [0, 95, 215, 255], 609 | [0, 95, 255, 255], 610 | [0, 135, 0, 255], 611 | [0, 135, 95, 255], 612 | [0, 135, 135, 255], 613 | [0, 135, 175, 255], 614 | [0, 135, 215, 255], 615 | [0, 135, 255, 255], 616 | [0, 175, 0, 255], 617 | [0, 175, 95, 255], 618 | [0, 175, 135, 255], 619 | [0, 175, 175, 255], 620 | [0, 175, 215, 255], 621 | [0, 175, 255, 255], 622 | [0, 215, 0, 255], 623 | [0, 215, 95, 255], 624 | [0, 215, 135, 255], 625 | [0, 215, 175, 255], 626 | [0, 215, 215, 255], 627 | [0, 215, 255, 255], 628 | [0, 255, 0, 255], 629 | [0, 255, 95, 255], 630 | [0, 255, 135, 255], 631 | [0, 255, 175, 255], 632 | [0, 255, 215, 255], 633 | [0, 255, 255, 255], 634 | [95, 0, 0, 255], 635 | [95, 0, 95, 255], 636 | [95, 0, 135, 255], 637 | [95, 0, 175, 255], 638 | [95, 0, 215, 255], 639 | [95, 0, 255, 255], 640 | [95, 95, 0, 255], 641 | [95, 95, 95, 255], 642 | [95, 95, 135, 255], 643 | [95, 95, 175, 255], 644 | [95, 95, 215, 255], 645 | [95, 95, 255, 255], 646 | [95, 135, 0, 255], 647 | [95, 135, 95, 255], 648 | [95, 135, 135, 255], 649 | [95, 135, 175, 255], 650 | [95, 135, 215, 255], 651 | [95, 135, 255, 255], 652 | [95, 175, 0, 255], 653 | [95, 175, 95, 255], 654 | [95, 175, 135, 255], 655 | [95, 175, 175, 255], 656 | [95, 175, 215, 255], 657 | [95, 175, 255, 255], 658 | [95, 215, 0, 255], 659 | [95, 215, 95, 255], 660 | [95, 215, 135, 255], 661 | [95, 215, 175, 255], 662 | [95, 215, 215, 255], 663 | [95, 215, 255, 255], 664 | [95, 255, 0, 255], 665 | [95, 255, 95, 255], 666 | [95, 255, 135, 255], 667 | [95, 255, 175, 255], 668 | [95, 255, 215, 255], 669 | [95, 255, 255, 255], 670 | [135, 0, 0, 255], 671 | [135, 0, 95, 255], 672 | [135, 0, 135, 255], 673 | [135, 0, 175, 255], 674 | [135, 0, 215, 255], 675 | [135, 0, 255, 255], 676 | [135, 95, 0, 255], 677 | [135, 95, 95, 255], 678 | [135, 95, 135, 255], 679 | [135, 95, 175, 255], 680 | [135, 95, 215, 255], 681 | [135, 95, 255, 255], 682 | [135, 135, 0, 255], 683 | [135, 135, 95, 255], 684 | [135, 135, 135, 255], 685 | [135, 135, 175, 255], 686 | [135, 135, 215, 255], 687 | [135, 135, 255, 255], 688 | [135, 175, 0, 255], 689 | [135, 175, 95, 255], 690 | [135, 175, 135, 255], 691 | [135, 175, 175, 255], 692 | [135, 175, 215, 255], 693 | [135, 175, 255, 255], 694 | [135, 215, 0, 255], 695 | [135, 215, 95, 255], 696 | [135, 215, 135, 255], 697 | [135, 215, 175, 255], 698 | [135, 215, 215, 255], 699 | [135, 215, 255, 255], 700 | [135, 255, 0, 255], 701 | [135, 255, 95, 255], 702 | [135, 255, 135, 255], 703 | [135, 255, 175, 255], 704 | [135, 255, 215, 255], 705 | [135, 255, 255, 255], 706 | [175, 0, 0, 255], 707 | [175, 0, 95, 255], 708 | [175, 0, 135, 255], 709 | [175, 0, 175, 255], 710 | [175, 0, 215, 255], 711 | [175, 0, 255, 255], 712 | [175, 95, 0, 255], 713 | [175, 95, 95, 255], 714 | [175, 95, 135, 255], 715 | [175, 95, 175, 255], 716 | [175, 95, 215, 255], 717 | [175, 95, 255, 255], 718 | [175, 135, 0, 255], 719 | [175, 135, 95, 255], 720 | [175, 135, 135, 255], 721 | [175, 135, 175, 255], 722 | [175, 135, 215, 255], 723 | [175, 135, 255, 255], 724 | [175, 175, 0, 255], 725 | [175, 175, 95, 255], 726 | [175, 175, 135, 255], 727 | [175, 175, 175, 255], 728 | [175, 175, 215, 255], 729 | [175, 175, 255, 255], 730 | [175, 215, 0, 255], 731 | [175, 215, 95, 255], 732 | [175, 215, 135, 255], 733 | [175, 215, 175, 255], 734 | [175, 215, 215, 255], 735 | [175, 215, 255, 255], 736 | [175, 255, 0, 255], 737 | [175, 255, 95, 255], 738 | [175, 255, 135, 255], 739 | [175, 255, 175, 255], 740 | [175, 255, 215, 255], 741 | [175, 255, 255, 255], 742 | [215, 0, 0, 255], 743 | [215, 0, 95, 255], 744 | [215, 0, 135, 255], 745 | [215, 0, 175, 255], 746 | [215, 0, 215, 255], 747 | [215, 0, 255, 255], 748 | [215, 95, 0, 255], 749 | [215, 95, 95, 255], 750 | [215, 95, 135, 255], 751 | [215, 95, 175, 255], 752 | [215, 95, 215, 255], 753 | [215, 95, 255, 255], 754 | [215, 135, 0, 255], 755 | [215, 135, 95, 255], 756 | [215, 135, 135, 255], 757 | [215, 135, 175, 255], 758 | [215, 135, 215, 255], 759 | [215, 135, 255, 255], 760 | [215, 175, 0, 255], 761 | [215, 175, 95, 255], 762 | [215, 175, 135, 255], 763 | [215, 175, 175, 255], 764 | [215, 175, 215, 255], 765 | [215, 175, 255, 255], 766 | [215, 215, 0, 255], 767 | [215, 215, 95, 255], 768 | [215, 215, 135, 255], 769 | [215, 215, 175, 255], 770 | [215, 215, 215, 255], 771 | [215, 215, 255, 255], 772 | [215, 255, 0, 255], 773 | [215, 255, 95, 255], 774 | [215, 255, 135, 255], 775 | [215, 255, 175, 255], 776 | [215, 255, 215, 255], 777 | [215, 255, 255, 255], 778 | [255, 0, 0, 255], 779 | [255, 0, 95, 255], 780 | [255, 0, 135, 255], 781 | [255, 0, 175, 255], 782 | [255, 0, 215, 255], 783 | [255, 0, 255, 255], 784 | [255, 95, 0, 255], 785 | [255, 95, 95, 255], 786 | [255, 95, 135, 255], 787 | [255, 95, 175, 255], 788 | [255, 95, 215, 255], 789 | [255, 95, 255, 255], 790 | [255, 135, 0, 255], 791 | [255, 135, 95, 255], 792 | [255, 135, 135, 255], 793 | [255, 135, 175, 255], 794 | [255, 135, 215, 255], 795 | [255, 135, 255, 255], 796 | [255, 175, 0, 255], 797 | [255, 175, 95, 255], 798 | [255, 175, 135, 255], 799 | [255, 175, 175, 255], 800 | [255, 175, 215, 255], 801 | [255, 175, 255, 255], 802 | [255, 215, 0, 255], 803 | [255, 215, 95, 255], 804 | [255, 215, 135, 255], 805 | [255, 215, 175, 255], 806 | [255, 215, 215, 255], 807 | [255, 215, 255, 255], 808 | [255, 255, 0, 255], 809 | [255, 255, 95, 255], 810 | [255, 255, 135, 255], 811 | [255, 255, 175, 255], 812 | [255, 255, 215, 255], 813 | [255, 255, 255, 255], 814 | [8, 8, 8, 255], 815 | [18, 18, 18, 255], 816 | [28, 28, 28, 255], 817 | [38, 38, 38, 255], 818 | [48, 48, 48, 255], 819 | [58, 58, 58, 255], 820 | [68, 68, 68, 255], 821 | [78, 78, 78, 255], 822 | [88, 88, 88, 255], 823 | [98, 98, 98, 255], 824 | [108, 108, 108, 255], 825 | [118, 118, 118, 255], 826 | [128, 128, 128, 255], 827 | [138, 138, 138, 255], 828 | [148, 148, 148, 255], 829 | [158, 158, 158, 255], 830 | [168, 168, 168, 255], 831 | [178, 178, 178, 255], 832 | [188, 188, 188, 255], 833 | [198, 198, 198, 255], 834 | [208, 208, 208, 255], 835 | [218, 218, 218, 255], 836 | [228, 228, 228, 255], 837 | [238, 238, 238, 255], 838 | ] 839 | } 840 | 841 | fn hex_from_env(var_name: &str) -> [u8; 4] { 842 | let val = std::env::var(var_name); 843 | match val { 844 | Ok(code) => match strhex_to_rgba(code) { 845 | Some(color) => color, 846 | None => { 847 | warn!("invalid hex value for env var {}", var_name); 848 | [0, 0, 0, 0] 849 | } 850 | }, 851 | Err(err) => { 852 | warn!("cannot read env var {}, err: {}", var_name, err.to_string()); 853 | [0, 0, 0, 0] 854 | } 855 | } 856 | } 857 | pub(crate) fn hex_to_rgba(hex: i64, alpha: Option) -> [u8; 4] { 858 | let r = ((hex >> 16) & 0xFF) as u8; 859 | let g = ((hex >> 8) & 0xFF) as u8; 860 | let b = (hex & 0xFF) as u8; 861 | let a = alpha.unwrap_or(255); // Default alpha to 255 (full) if not provided 862 | [r, g, b, a] 863 | } 864 | 865 | pub(crate) fn strhex_to_rgba(hex: String) -> Option<[u8; 4]> { 866 | let hex = hex.trim_start_matches("0x").trim_start_matches("#"); 867 | 868 | let has_alpha = hex.len() == 8; 869 | 870 | if let Ok(hex) = i64::from_str_radix(&hex, 16) { 871 | let alpha = if has_alpha { 872 | Some((hex >> 24) as u8) 873 | } else { 874 | None 875 | }; 876 | Some(hex_to_rgba(hex, alpha)) 877 | } else { 878 | warn!("invalid hex {}", hex); 879 | None 880 | } 881 | } 882 | -------------------------------------------------------------------------------- /src/ansi_to_image/printer.rs: -------------------------------------------------------------------------------- 1 | use crate::{trace, warn}; 2 | use ab_glyph::{Font, FontRef, Glyph, Point}; 3 | use image::{Rgba, RgbaImage}; 4 | use imageproc::drawing::draw_text_mut; 5 | use std::collections::BTreeMap; 6 | use vte::{Params, Perform}; 7 | 8 | use crate::ansi_to_image::{color::ColorType, escape_parser::EscapeSequence, palette::Palette}; 9 | 10 | use super::internal_scale::InternalScale; 11 | 12 | pub(super) struct Settings<'a> { 13 | pub(super) font: FontRef<'a>, 14 | pub(super) font_bold: FontRef<'a>, 15 | pub(super) font_italic: FontRef<'a>, 16 | pub(super) font_italic_bold: FontRef<'a>, 17 | pub(super) font_height: f32, 18 | pub(super) scale: InternalScale, 19 | pub(super) palette: Palette, 20 | pub(super) png_width: Option, 21 | } 22 | 23 | #[derive(Debug, Default)] 24 | struct SettingsInternal { 25 | glyph_advance_width: f32, 26 | new_line_distance: u32, 27 | png_width: Option, 28 | } 29 | 30 | #[derive(Debug)] 31 | struct TextEntry { 32 | character: char, 33 | foreground_color: ColorType, 34 | background_color: ColorType, 35 | font: FontState, 36 | underline: bool, 37 | } 38 | 39 | #[derive(Debug, Clone, Copy)] 40 | enum FontState { 41 | Normal, 42 | Bold, 43 | Italic, 44 | ItalicBold, 45 | } 46 | 47 | #[derive(Debug)] 48 | struct State { 49 | text: BTreeMap<(u32, u32), TextEntry>, 50 | current_x: u32, 51 | current_y: u32, 52 | foreground_color: ColorType, 53 | background_color: ColorType, 54 | font: FontState, 55 | last_execute_byte: Option, 56 | underline: bool, 57 | } 58 | 59 | pub(super) struct Printer<'a> { 60 | settings: Settings<'a>, 61 | settings_internal: SettingsInternal, 62 | state: State, 63 | } 64 | 65 | pub(super) fn new(settings: Settings) -> Printer { 66 | let glyph_advance_width = settings 67 | .font 68 | .glyph_bounds(&Glyph { 69 | id: settings.font.glyph_id('_'), 70 | scale: settings.scale.into(), 71 | position: Point { x: 0.0, y: 0.0 }, 72 | }) 73 | .width(); 74 | 75 | let new_line_distance = settings.font_height as u32; 76 | 77 | let png_width = settings.png_width; 78 | 79 | let settings_internal = SettingsInternal { 80 | glyph_advance_width, 81 | new_line_distance, 82 | png_width, 83 | }; 84 | 85 | Printer { 86 | settings, 87 | settings_internal, 88 | state: State::default(), 89 | } 90 | } 91 | 92 | impl Default for State { 93 | fn default() -> Self { 94 | Self { 95 | text: BTreeMap::new(), 96 | current_x: 0, 97 | current_y: 0, 98 | foreground_color: ColorType::PrimaryForeground, 99 | background_color: ColorType::PrimaryBackground, 100 | font: FontState::Normal, 101 | last_execute_byte: None, 102 | underline: false, 103 | } 104 | } 105 | } 106 | 107 | impl<'a> Perform for Printer<'a> { 108 | fn print(&mut self, character: char) { 109 | self.state.text.insert( 110 | (self.state.current_x, self.state.current_y), 111 | TextEntry { 112 | character, 113 | foreground_color: self.state.foreground_color, 114 | background_color: self.state.background_color, 115 | font: self.state.font, 116 | underline: self.state.underline, 117 | }, 118 | ); 119 | 120 | self.state.current_x += self.settings_internal.glyph_advance_width as u32; 121 | 122 | if let Some(png_width) = self.settings_internal.png_width { 123 | if self.state.current_x > png_width { 124 | self.state.current_x = 0; 125 | self.state.current_y += self.settings_internal.new_line_distance; 126 | } 127 | } 128 | } 129 | 130 | fn execute(&mut self, byte: u8) { 131 | match byte { 132 | // ^M 0x0D CR Carriage Return Moves the cursor to column zero. 133 | 0x0d => { 134 | self.state.current_x = 0; 135 | } 136 | 137 | // ^J 0x0A LF Line Feed Moves to next line, scrolls the display up if at bottom of the 138 | // screen. Usually does not move horizontally, though programs should not rely on this. 139 | 0x0a => { 140 | self.state.current_x = 0; 141 | self.state.current_y += self.settings_internal.new_line_distance; 142 | } 143 | 144 | _ => trace!("[execute] {byte}, {byte:02x}"), 145 | } 146 | 147 | self.state.last_execute_byte = Some(byte) 148 | } 149 | 150 | fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, c: char) { 151 | trace!( 152 | "[hook] params={params:?}, intermediates={intermediates:?}, ignore={ignore:?}, \ 153 | char={c:?}" 154 | ); 155 | } 156 | 157 | fn put(&mut self, byte: u8) { 158 | trace!("[put] {byte:02x}"); 159 | } 160 | 161 | fn unhook(&mut self) { 162 | trace!("[unhook]"); 163 | } 164 | 165 | fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { 166 | trace!("[osc_dispatch] params={params:?} bell_terminated={bell_terminated}2"); 167 | } 168 | 169 | fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, _c: char) { 170 | // trace!( 171 | // "[csi_dispatch] params={params:?}, intermediates={intermediates:?}, ignore={ignore:?}, char={c:?}" 172 | // ); 173 | let actions = EscapeSequence::parse_params(params.iter().flatten().collect::>()); 174 | 175 | for action in actions { 176 | match action { 177 | EscapeSequence::Reset => { 178 | let defaults = State::default(); 179 | 180 | self.state.foreground_color = defaults.foreground_color; 181 | self.state.background_color = defaults.background_color; 182 | self.state.font = defaults.font; 183 | self.state.underline = false; 184 | } 185 | 186 | EscapeSequence::Bold => self.state.font += FontState::Bold, 187 | EscapeSequence::Italic => self.state.font += FontState::Italic, 188 | EscapeSequence::Underline => self.state.underline = true, 189 | 190 | EscapeSequence::NotBold => self.state.font -= FontState::Bold, 191 | EscapeSequence::NotItalicNorBlackLetter => self.state.font -= FontState::Italic, 192 | EscapeSequence::NotUnderline => self.state.underline = false, 193 | 194 | EscapeSequence::ForegroundColor(color_type) => { 195 | self.state.foreground_color = color_type 196 | } 197 | EscapeSequence::BackgroundColor(color_type) => { 198 | self.state.background_color = color_type 199 | } 200 | 201 | EscapeSequence::DefaultForegroundColor => { 202 | self.state.foreground_color = ColorType::PrimaryForeground 203 | } 204 | 205 | EscapeSequence::DefaultBackgroundColor => { 206 | self.state.background_color = ColorType::PrimaryBackground 207 | } 208 | 209 | EscapeSequence::BlackLetterFont 210 | | EscapeSequence::Faint 211 | | EscapeSequence::SlowBlink 212 | | EscapeSequence::NotBlinking 213 | | EscapeSequence::ReverseVideo 214 | | EscapeSequence::Conceal 215 | | EscapeSequence::CrossedOut 216 | | EscapeSequence::PrimaryFont 217 | | EscapeSequence::SetAlternativeFont 218 | | EscapeSequence::DisableProportionalSpacing 219 | | EscapeSequence::NeitherSuperscriptNorSubscript 220 | | EscapeSequence::NotReserved 221 | | EscapeSequence::NormalIntensity 222 | | EscapeSequence::RapidBlink => { 223 | warn!("not implemented for action: {action:?}") 224 | } 225 | EscapeSequence::Unimplemented(value) => { 226 | warn!("not implemented for value: {value:?}") 227 | } 228 | EscapeSequence::Ignore => trace!("ignored sequence"), 229 | } 230 | } 231 | } 232 | 233 | fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {} 234 | } 235 | 236 | impl<'a> From> for RgbaImage { 237 | fn from(printer: Printer) -> Self { 238 | let width = printer 239 | .state 240 | .text 241 | .keys() 242 | .map(|(x, _)| x) 243 | .max() 244 | .unwrap_or(&0) 245 | + printer.settings_internal.glyph_advance_width as u32; 246 | 247 | let height = printer 248 | .state 249 | .text 250 | .keys() 251 | .map(|(_, y)| y) 252 | .max() 253 | .unwrap_or(&0) 254 | + printer.settings_internal.new_line_distance; 255 | 256 | let mut image = RgbaImage::new(width, height); 257 | 258 | // Set primary background 259 | for (_x, _y, pixel) in image.enumerate_pixels_mut() { 260 | *pixel = image::Rgba( 261 | printer 262 | .settings 263 | .palette 264 | .get_color(ColorType::PrimaryBackground), 265 | ); 266 | } 267 | 268 | // Render background before foreground from bottom to top to make it look better 269 | printer.state.text.iter().rev().for_each(|((x, y), entry)| { 270 | let background_end_x = x + printer.settings_internal.glyph_advance_width as u32; 271 | let background_end_y = y + printer.settings.font_height as u32; 272 | 273 | for x in *x..background_end_x { 274 | for y in *y..background_end_y { 275 | let pixel = 276 | image::Rgba(printer.settings.palette.get_color(entry.background_color)); 277 | 278 | image.put_pixel(x, y, pixel); 279 | } 280 | } 281 | }); 282 | 283 | printer.state.text.iter().for_each(|((x, y), entry)| { 284 | let font = match entry.font { 285 | FontState::Normal => &printer.settings.font, 286 | FontState::Bold => &printer.settings.font_bold, 287 | FontState::Italic => &printer.settings.font_italic, 288 | FontState::ItalicBold => &printer.settings.font_italic_bold, 289 | }; 290 | 291 | draw_text_mut( 292 | &mut image, 293 | Rgba(printer.settings.palette.get_color(entry.foreground_color)), 294 | (*x).try_into().unwrap(), 295 | (*y).try_into().unwrap(), 296 | printer.settings.scale, 297 | font, 298 | &entry.character.to_string(), 299 | ); 300 | 301 | if entry.underline { 302 | // let underline_start = *x; 303 | // let underline_end = x + printer.settings_internal.glyph_advance_width as u32; 304 | // let underline_y = (y - 6) + printer.settings.font_height as u32; 305 | 306 | // for underline_x in underline_start..underline_end { 307 | // let pixel = 308 | // image::Rgb(printer.settings.palette.get_color(entry.foreground_color)); 309 | 310 | // image.put_pixel(underline_x, underline_y - 1, pixel); 311 | // image.put_pixel(underline_x, underline_y, pixel); 312 | // } 313 | } 314 | }); 315 | 316 | image 317 | } 318 | } 319 | 320 | impl std::ops::AddAssign for FontState { 321 | fn add_assign(&mut self, other: Self) { 322 | let new_self = match (&self, other) { 323 | (Self::Normal, Self::Normal) => Self::Normal, 324 | 325 | (Self::Bold, Self::Bold) | (Self::Bold, Self::Normal) | (Self::Normal, Self::Bold) => { 326 | Self::Bold 327 | } 328 | 329 | (Self::Italic, Self::Italic) 330 | | (Self::Italic, Self::Normal) 331 | | (Self::Normal, Self::Italic) => Self::Italic, 332 | 333 | (Self::Bold, Self::Italic) 334 | | (Self::Bold, Self::ItalicBold) 335 | | (Self::ItalicBold, Self::Bold) 336 | | (Self::ItalicBold, Self::Italic) 337 | | (Self::ItalicBold, Self::ItalicBold) 338 | | (Self::ItalicBold, Self::Normal) 339 | | (Self::Italic, Self::Bold) 340 | | (Self::Italic, Self::ItalicBold) 341 | | (Self::Normal, Self::ItalicBold) => Self::ItalicBold, 342 | }; 343 | 344 | *self = new_self 345 | } 346 | } 347 | 348 | impl std::ops::SubAssign for FontState { 349 | fn sub_assign(&mut self, other: Self) { 350 | let new_self = match (&self, other) { 351 | (Self::Italic, Self::Italic) 352 | | (Self::ItalicBold, Self::ItalicBold) 353 | | (Self::Bold, Self::Bold) 354 | | (Self::Normal, Self::Normal) 355 | | (Self::Normal, Self::Bold) 356 | | (Self::Normal, Self::Italic) 357 | | (Self::Bold, Self::ItalicBold) 358 | | (Self::Italic, Self::ItalicBold) 359 | | (Self::Normal, Self::ItalicBold) => Self::Normal, 360 | 361 | (Self::Bold, Self::Normal) 362 | | (Self::Bold, Self::Italic) 363 | | (Self::ItalicBold, Self::Italic) => Self::Bold, 364 | 365 | (Self::Italic, Self::Normal) 366 | | (Self::Italic, Self::Bold) 367 | | (Self::ItalicBold, Self::Bold) => Self::Italic, 368 | 369 | (Self::ItalicBold, Self::Normal) => Self::ItalicBold, 370 | }; 371 | 372 | *self = new_self 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /src/image_to_ansi/mod.rs: -------------------------------------------------------------------------------- 1 | mod nu_plugin; 2 | mod writer; 3 | pub use nu_plugin::image_to_ansi; 4 | -------------------------------------------------------------------------------- /src/image_to_ansi/nu_plugin.rs: -------------------------------------------------------------------------------- 1 | use std::{env, io::Cursor}; 2 | 3 | use image::codecs::png::PngDecoder; 4 | use nu_plugin::EvaluatedCall; 5 | use nu_protocol::{LabeledError, Span, Value}; 6 | 7 | pub fn image_to_ansi(call: &EvaluatedCall, input: &Value) -> Result { 8 | match build_params(call, input) { 9 | Ok(params) => { 10 | let img = PngDecoder::new(Cursor::new(params.file.as_slice())) 11 | .map(|img| image::DynamicImage::from_decoder(img)); 12 | match img { 13 | Ok(img) => { 14 | let result = super::writer::lib::to_ansi(&img.unwrap(), ¶ms); 15 | 16 | return result 17 | .map(|value| Value::string(value, call.head)) 18 | .map_err(|err| response_error(err, call.head)); 19 | } 20 | Err(er) => Err(response_error(er.to_string(), call.head)), 21 | } 22 | } 23 | Err(err) => Err(err), 24 | } 25 | } 26 | 27 | pub(super) struct IntoAnsiParams { 28 | file: Vec, 29 | pub width: Option, 30 | pub height: Option, 31 | pub truecolor: bool, 32 | } 33 | 34 | pub fn truecolor_available() -> bool { 35 | if let Ok(value) = env::var("COLORTERM") { 36 | value.contains("truecolor") || value.contains("24bit") 37 | } else { 38 | false 39 | } 40 | } 41 | 42 | fn build_params(call: &EvaluatedCall, input: &Value) -> Result { 43 | let mut params = IntoAnsiParams { 44 | file: [].to_vec(), 45 | // verbose: false, 46 | height: None, 47 | width: None, 48 | truecolor: truecolor_available(), 49 | }; 50 | match input.as_binary() { 51 | Ok(file) => params.file = file.to_owned(), 52 | Err(err) => return Err(make_params_err(err.to_string(), call.head)), 53 | }; 54 | params.width = match load_u32(call, "width") { 55 | Ok(value) => Some(value), 56 | Err(_) => None, 57 | }; 58 | params.height = match load_u32(call, "height") { 59 | Ok(value) => Some(value), 60 | Err(_) => None, 61 | }; 62 | 63 | Ok(params) 64 | } 65 | 66 | fn load_u32(call: &EvaluatedCall, flag_name: &str) -> Result { 67 | match call.get_flag_value(flag_name) { 68 | Some(val) => match val { 69 | Value::Int { .. } => match val.as_int().unwrap().try_into() { 70 | Ok(value) => Ok(value), 71 | Err(err) => Err(make_params_err(err.to_string(), call.head)), 72 | }, 73 | _ => Err(make_params_err( 74 | format!("value of `{}` is not an integer", flag_name), 75 | call.head, 76 | )), 77 | }, 78 | None => Err(make_params_err( 79 | format!("cannot find `{}` parameter", flag_name), 80 | call.head, 81 | )), 82 | } 83 | } 84 | 85 | fn make_params_err(text: String, span: Span) -> LabeledError { 86 | return LabeledError::new(text) 87 | .with_label("faced an error when tried to parse the params", span); 88 | } 89 | 90 | fn response_error(text: String, span: Span) -> LabeledError { 91 | return LabeledError::new(text).with_label("cannot create image", span); 92 | } 93 | -------------------------------------------------------------------------------- /src/image_to_ansi/writer/block.rs: -------------------------------------------------------------------------------- 1 | use std::io::Error; 2 | 3 | use crate::image_to_ansi::nu_plugin::IntoAnsiParams; 4 | 5 | use ansi_colours::ansi256_from_rgb; 6 | use image::{DynamicImage, GenericImageView, Rgba}; 7 | use termcolor::{Color, ColorSpec, WriteColor}; 8 | 9 | use crossterm::cursor::MoveRight; 10 | use crossterm::execute; 11 | 12 | const UPPER_HALF_BLOCK: &str = "\u{2580}"; 13 | const LOWER_HALF_BLOCK: &str = "\u{2584}"; 14 | 15 | // const CHECKERBOARD_BACKGROUND_LIGHT: (u8, u8, u8) = (153, 153, 153); 16 | // const CHECKERBOARD_BACKGROUND_DARK: (u8, u8, u8) = (102, 102, 102); 17 | 18 | pub fn make_ansi( 19 | stdout: &mut impl WriteColor, 20 | img: &DynamicImage, 21 | config: &IntoAnsiParams, 22 | ) -> Result<(), Error> { 23 | print_to_writecolor(stdout, img, config) 24 | } 25 | 26 | fn print_to_writecolor( 27 | stdout: &mut impl WriteColor, 28 | img: &DynamicImage, 29 | config: &IntoAnsiParams, 30 | ) -> Result<(), Error> { 31 | // adjust with x=0 and handle horizontal offset entirely below 32 | // adjust_offset(stdout, &Config { x: 0, ..*config })?; 33 | 34 | // resize the image so that it fits in the constraints, if any 35 | let img = super::resize(img, config.width, config.height); 36 | let (width, height) = img.dimensions(); 37 | 38 | let mut row_color_buffer: Vec = vec![ColorSpec::new(); width as usize]; 39 | let img_buffer = img.to_rgba8(); //TODO: Can conversion be avoided? 40 | 41 | for (curr_row, img_row) in img_buffer.enumerate_rows() { 42 | let is_even_row = curr_row % 2 == 0; 43 | let is_last_row = curr_row == height - 1; 44 | 45 | for pixel in img_row { 46 | // choose the half block's color 47 | let color = if is_pixel_transparent(pixel) { 48 | // TODO bg color 49 | // if config.transparent { 50 | None 51 | // } else { 52 | // Some(get_transparency_color(curr_row, pixel.0, config.truecolor)) 53 | // } 54 | } else { 55 | Some(get_color_from_pixel(pixel, config.truecolor)) 56 | }; 57 | 58 | // Even rows modify the background, odd rows the foreground 59 | // because lower half blocks are used by default 60 | let colorspec = &mut row_color_buffer[pixel.0 as usize]; 61 | if is_even_row { 62 | colorspec.set_bg(color); 63 | if is_last_row { 64 | write_colored_character(stdout, colorspec, true)?; 65 | } 66 | } else { 67 | colorspec.set_fg(color); 68 | write_colored_character(stdout, colorspec, false)?; 69 | } 70 | } 71 | 72 | if !is_even_row && !is_last_row { 73 | stdout.reset()?; 74 | writeln!(stdout, "\r")?; 75 | } 76 | } 77 | 78 | stdout.reset()?; 79 | writeln!(stdout)?; 80 | stdout.flush()?; 81 | Ok(()) 82 | } 83 | 84 | fn write_colored_character( 85 | stdout: &mut impl WriteColor, 86 | c: &ColorSpec, 87 | is_last_row: bool, 88 | ) -> Result<(), Error> { 89 | let out_color; 90 | let out_char; 91 | let mut new_color; 92 | 93 | // On the last row use upper blocks and leave the bottom half empty (transparent) 94 | if is_last_row { 95 | new_color = ColorSpec::new(); 96 | if let Some(bg) = c.bg() { 97 | new_color.set_fg(Some(*bg)); 98 | out_char = UPPER_HALF_BLOCK; 99 | } else { 100 | execute!(stdout, MoveRight(1))?; 101 | return Ok(()); 102 | } 103 | out_color = &new_color; 104 | } else { 105 | match (c.fg(), c.bg()) { 106 | (None, None) => { 107 | // completely transparent 108 | execute!(stdout, MoveRight(1))?; 109 | return Ok(()); 110 | } 111 | (Some(bottom), None) => { 112 | // only top transparent 113 | new_color = ColorSpec::new(); 114 | new_color.set_fg(Some(*bottom)); 115 | out_color = &new_color; 116 | out_char = LOWER_HALF_BLOCK; 117 | } 118 | (None, Some(top)) => { 119 | // only bottom transparent 120 | new_color = ColorSpec::new(); 121 | new_color.set_fg(Some(*top)); 122 | out_color = &new_color; 123 | out_char = UPPER_HALF_BLOCK; 124 | } 125 | (Some(_top), Some(_bottom)) => { 126 | // both parts have a color 127 | out_color = c; 128 | out_char = LOWER_HALF_BLOCK; 129 | } 130 | } 131 | } 132 | stdout.set_color(out_color)?; 133 | write!(stdout, "{}", out_char)?; 134 | 135 | Ok(()) 136 | } 137 | 138 | fn is_pixel_transparent(pixel: (u32, u32, &Rgba)) -> bool { 139 | pixel.2[3] == 0 140 | } 141 | 142 | // fn get_transparency_color(row: u32, col: u32, truecolor: bool) -> Color { 143 | // //imitate the transparent chess board pattern 144 | // let rgb = if row % 2 == col % 2 { 145 | // CHECKERBOARD_BACKGROUND_DARK 146 | // } else { 147 | // CHECKERBOARD_BACKGROUND_LIGHT 148 | // }; 149 | // if truecolor { 150 | // Color::Rgb(rgb.0, rgb.1, rgb.2) 151 | // } else { 152 | // Color::Ansi256(ansi256_from_rgb(rgb)) 153 | // } 154 | // } 155 | 156 | fn get_color_from_pixel(pixel: (u32, u32, &Rgba), truecolor: bool) -> Color { 157 | let (_x, _y, data) = pixel; 158 | let rgb = (data[0], data[1], data[2]); 159 | if truecolor { 160 | Color::Rgb(rgb.0, rgb.1, rgb.2) 161 | } else { 162 | Color::Ansi256(ansi256_from_rgb(rgb)) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/image_to_ansi/writer/lib.rs: -------------------------------------------------------------------------------- 1 | use image::DynamicImage; 2 | 3 | use crate::image_to_ansi::nu_plugin::IntoAnsiParams; 4 | 5 | use super::{make_ansi, string_writer::StringWriter}; 6 | 7 | pub fn to_ansi(img: &DynamicImage, config: &IntoAnsiParams) -> Result { 8 | let stdout = &mut StringWriter::new(); 9 | let _ = make_ansi(stdout, img, config); 10 | 11 | // if config.restore_cursor { 12 | // execute!(&mut stdout, RestorePosition)?; 13 | // }; 14 | 15 | Ok(stdout.read().to_string()) 16 | } 17 | -------------------------------------------------------------------------------- /src/image_to_ansi/writer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod lib; 2 | mod string_writer; 3 | 4 | use image::{DynamicImage, GenericImageView}; 5 | 6 | mod block; 7 | pub use block::make_ansi; 8 | 9 | /// Resize a [image::DynamicImage] so that it fits within optional width and height bounds. 10 | /// If none are provided, terminal size is used instead. 11 | pub fn resize(img: &DynamicImage, width: Option, height: Option) -> DynamicImage { 12 | let (w, h) = find_best_fit(img, width, height); 13 | 14 | // find_best_fit returns values in terminal cells. Hence, we multiply by two 15 | // because a 5x10 image can fit in 5x5 cells. However, a 5x9 image will also 16 | // fit in 5x5 and 1 is deducted in such cases. 17 | img.resize_exact( 18 | w, 19 | 2 * h - img.height() % 2, 20 | image::imageops::FilterType::Triangle, 21 | ) 22 | } 23 | 24 | /// Find the best dimensions for the printed image, based on user's input. 25 | /// Returns the dimensions of how the image should be printed in **terminal cells**. 26 | /// 27 | /// The behaviour is different based on the provided width and height: 28 | /// - If both are None, the image will be resized to fit in the terminal. Aspect ratio is preserved. 29 | /// - If only one is provided and the other is None, it will fit the image in the provided boundary. Aspect ratio is preserved. 30 | /// - If both are provided, the image will be resized to match the new size. Aspect ratio is **not** preserved. 31 | /// 32 | /// Example: 33 | /// Use None for both dimensions to use terminal size (80x24) instead. 34 | /// The image ratio is 2:1, the terminal can be split into 80x46 squares. 35 | /// The best fit would be to use the whole width (80) and 40 vertical squares, 36 | /// which is equivalent to 20 terminal cells. 37 | /// 38 | /// let img = image::DynamicImage::ImageRgba8(image::RgbaImage::new(160, 80)); 39 | /// let (w, h) = find_best_fit(&img, None, None); 40 | /// assert_eq!(w, 80); 41 | /// assert_eq!(h, 20); 42 | //TODO: it might make more sense to change signiture from img to (width, height) 43 | fn find_best_fit(img: &DynamicImage, width: Option, height: Option) -> (u32, u32) { 44 | let (img_width, img_height) = img.dimensions(); 45 | 46 | // Match user's width and height preferences 47 | match (width, height) { 48 | (None, None) => { 49 | let (term_w, term_h) = terminal_size(); 50 | let (w, h) = fit_dimensions(img_width, img_height, term_w as u32, term_h as u32); 51 | 52 | // One less row because two reasons: 53 | // - the prompt after executing the command will take a line 54 | // - gifs flicker 55 | let h = if h == term_h as u32 { h - 1 } else { h }; 56 | (w, h) 57 | } 58 | // Either width or height is specified, will fit and preserve aspect ratio. 59 | (Some(w), None) => fit_dimensions(img_width, img_height, w, img_height), 60 | (None, Some(h)) => fit_dimensions(img_width, img_height, img_width, h), 61 | 62 | // Both width and height are specified, will resize to match exactly 63 | (Some(w), Some(h)) => (w, h), 64 | } 65 | } 66 | 67 | /// Given width & height of an image, scale the size so that it can fit within given bounds 68 | /// while preserving aspect ratio. Will only scale down - if dimensions are smaller than the 69 | /// bounds, they will be returned unmodified. 70 | /// 71 | /// Note: input bounds are meant to hold dimensions of a terminal, where the height of a cell is 72 | /// twice it's width. It is best illustrated in an example: 73 | /// 74 | /// Trying to fit a 100x100 image in 40x15 terminal cells. The best fit, while having an aspect 75 | /// ratio of 1:1, would be to use all of the available height, 15, which is 76 | /// equivalent in size to 30 vertical cells. Hence, the returned dimensions will be 30x15. 77 | /// 78 | /// assert_eq!((30, 15), viuer::fit_dimensions(100, 100, 40, 15)); 79 | fn fit_dimensions(width: u32, height: u32, bound_width: u32, bound_height: u32) -> (u32, u32) { 80 | let bound_height = 2 * bound_height; 81 | 82 | if width <= bound_width && height <= bound_height { 83 | return (width, std::cmp::max(1, height / 2 + height % 2)); 84 | } 85 | 86 | let ratio = width * bound_height; 87 | let nratio = bound_width * height; 88 | 89 | let use_width = nratio <= ratio; 90 | let intermediate = if use_width { 91 | height * bound_width / width 92 | } else { 93 | width * bound_height / height 94 | }; 95 | 96 | if use_width { 97 | (bound_width, std::cmp::max(1, intermediate / 2)) 98 | } else { 99 | (intermediate, std::cmp::max(1, bound_height / 2)) 100 | } 101 | } 102 | 103 | const DEFAULT_TERM_SIZE: (u16, u16) = (80, 24); 104 | 105 | pub fn terminal_size() -> (u16, u16) { 106 | match crossterm::terminal::size() { 107 | Ok(s) => s, 108 | Err(_) => DEFAULT_TERM_SIZE, 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/image_to_ansi/writer/string_writer.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use termcolor::{Color, ColorSpec, WriteColor}; 4 | 5 | /// Override Output stream 6 | pub struct StringWriter { 7 | inner_buf: Vec, 8 | } 9 | 10 | impl StringWriter { 11 | pub fn new() -> StringWriter { 12 | StringWriter { inner_buf: vec![] } 13 | } 14 | pub fn read(&mut self) -> String { 15 | let result = String::from_utf8_lossy(self.inner_buf.as_slice()); 16 | return result.to_string(); 17 | } 18 | 19 | fn write_str(&mut self, s: &str) -> io::Result<()> { 20 | self.write(s.as_bytes()).map(|_| ()) 21 | } 22 | 23 | fn write_color(&mut self, fg: bool, c: &Color, intense: bool) -> io::Result<()> { 24 | macro_rules! write_intense { 25 | ($clr:expr) => { 26 | if fg { 27 | self.write_str(concat!("\x1B[38;5;", $clr, "m")) 28 | } else { 29 | self.write_str(concat!("\x1B[48;5;", $clr, "m")) 30 | } 31 | }; 32 | } 33 | macro_rules! write_normal { 34 | ($clr:expr) => { 35 | if fg { 36 | self.write_str(concat!("\x1B[3", $clr, "m")) 37 | } else { 38 | self.write_str(concat!("\x1B[4", $clr, "m")) 39 | } 40 | }; 41 | } 42 | macro_rules! write_var_ansi_code { 43 | ($pre:expr, $($code:expr),+) => {{ 44 | // The loop generates at worst a literal of the form 45 | // '255,255,255m' which is 12-bytes. 46 | // The largest `pre` expression we currently use is 7 bytes. 47 | // This gives us the maximum of 19-bytes for our work buffer. 48 | let pre_len = $pre.len(); 49 | assert!(pre_len <= 7); 50 | let mut fmt = [0u8; 19]; 51 | fmt[..pre_len].copy_from_slice($pre); 52 | let mut i = pre_len - 1; 53 | $( 54 | let c1: u8 = ($code / 100) % 10; 55 | let c2: u8 = ($code / 10) % 10; 56 | let c3: u8 = $code % 10; 57 | let mut printed = false; 58 | 59 | if c1 != 0 { 60 | printed = true; 61 | i += 1; 62 | fmt[i] = b'0' + c1; 63 | } 64 | if c2 != 0 || printed { 65 | i += 1; 66 | fmt[i] = b'0' + c2; 67 | } 68 | // If we received a zero value we must still print a value. 69 | i += 1; 70 | fmt[i] = b'0' + c3; 71 | i += 1; 72 | fmt[i] = b';'; 73 | )+ 74 | 75 | fmt[i] = b'm'; 76 | self.write_all(&fmt[0..i+1]) 77 | }} 78 | } 79 | macro_rules! write_custom { 80 | ($ansi256:expr) => { 81 | if fg { 82 | write_var_ansi_code!(b"\x1B[38;5;", $ansi256) 83 | } else { 84 | write_var_ansi_code!(b"\x1B[48;5;", $ansi256) 85 | } 86 | }; 87 | 88 | ($r:expr, $g:expr, $b:expr) => {{ 89 | if fg { 90 | write_var_ansi_code!(b"\x1B[38;2;", $r, $g, $b) 91 | } else { 92 | write_var_ansi_code!(b"\x1B[48;2;", $r, $g, $b) 93 | } 94 | }}; 95 | } 96 | if intense { 97 | match *c { 98 | Color::Black => write_intense!("8"), 99 | Color::Blue => write_intense!("12"), 100 | Color::Green => write_intense!("10"), 101 | Color::Red => write_intense!("9"), 102 | Color::Cyan => write_intense!("14"), 103 | Color::Magenta => write_intense!("13"), 104 | Color::Yellow => write_intense!("11"), 105 | Color::White => write_intense!("15"), 106 | Color::Ansi256(c) => write_custom!(c), 107 | Color::Rgb(r, g, b) => write_custom!(r, g, b), 108 | Color::__Nonexhaustive => unreachable!(), 109 | } 110 | } else { 111 | match *c { 112 | Color::Black => write_normal!("0"), 113 | Color::Blue => write_normal!("4"), 114 | Color::Green => write_normal!("2"), 115 | Color::Red => write_normal!("1"), 116 | Color::Cyan => write_normal!("6"), 117 | Color::Magenta => write_normal!("5"), 118 | Color::Yellow => write_normal!("3"), 119 | Color::White => write_normal!("7"), 120 | Color::Ansi256(c) => write_custom!(c), 121 | Color::Rgb(r, g, b) => write_custom!(r, g, b), 122 | Color::__Nonexhaustive => unreachable!(), 123 | } 124 | } 125 | } 126 | } 127 | 128 | impl Write for StringWriter { 129 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 130 | self.inner_buf.write(buf) 131 | } 132 | 133 | fn flush(&mut self) -> std::io::Result<()> { 134 | self.inner_buf.flush() 135 | } 136 | } 137 | 138 | impl WriteColor for StringWriter { 139 | fn supports_color(&self) -> bool { 140 | return true; 141 | } 142 | 143 | fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { 144 | if spec.reset() { 145 | self.reset()?; 146 | } 147 | if spec.bold() { 148 | self.write_str("\x1B[1m")?; 149 | } 150 | if spec.dimmed() { 151 | self.write_str("\x1B[2m")?; 152 | } 153 | if spec.italic() { 154 | self.write_str("\x1B[3m")?; 155 | } 156 | if spec.underline() { 157 | self.write_str("\x1B[4m")?; 158 | } 159 | if spec.strikethrough() { 160 | self.write_str("\x1B[9m")?; 161 | } 162 | if let Some(ref c) = spec.fg() { 163 | self.write_color(true, c, spec.intense())?; 164 | } 165 | if let Some(ref c) = spec.bg() { 166 | self.write_color(false, c, spec.intense())?; 167 | } 168 | Ok(()) 169 | } 170 | fn reset(&mut self) -> io::Result<()> { 171 | self.write_str("\x1B[0m") 172 | } 173 | 174 | fn is_synchronous(&self) -> bool { 175 | false 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod ansi_to_image; 2 | mod image_to_ansi; 3 | pub mod logging; 4 | pub use ansi_to_image::*; 5 | pub use image_to_ansi::*; 6 | -------------------------------------------------------------------------------- /src/logging/logger.rs: -------------------------------------------------------------------------------- 1 | use include_flate::lazy_static; 2 | use slog::{Drain, Logger}; 3 | use slog_async::Async; 4 | use slog_term::{CompactFormat, TermDecorator}; 5 | use std::sync::{ 6 | atomic::{self, AtomicU8}, 7 | Arc, 8 | }; 9 | 10 | use crate::logging::runtime_filter::RuntimeLevelFilter; 11 | 12 | lazy_static! { 13 | pub static ref INTERNAL_LOGGER: Logger = { 14 | let decorator = TermDecorator::new().build(); 15 | let drain = CompactFormat::new(decorator).build().fuse(); 16 | let drain = RuntimeLevelFilter { 17 | drain: drain, 18 | on: LOG_LEVEL.clone(), 19 | } 20 | .fuse(); 21 | let drain = Async::new(drain).build().fuse(); 22 | Logger::root(drain, slog::o!()) 23 | }; 24 | pub static ref LOG_LEVEL: Arc = Arc::new(AtomicU8::new(0)); 25 | } 26 | 27 | pub fn set_verbose(level: String) { 28 | let level_id = match level.as_str() { 29 | "TRACE" | "trace" | "t" => 0, 30 | "DEBUG" | "debug" | "d" => 1, 31 | "INFO" | "info" | "i" => 2, 32 | "WARNING" | "warning" | "w" => 3, 33 | "ERROR" | "error" | "e" => 4, 34 | "CRITICAL" | "critical" | "c" => 5, 35 | _ => 2, 36 | }; 37 | 38 | LOG_LEVEL.store(level_id, atomic::Ordering::SeqCst); 39 | } 40 | -------------------------------------------------------------------------------- /src/logging/macros.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! trace { 3 | ( #$tag:expr, $($args:tt)+) => { 4 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Trace, $tag, $($args)+) 5 | }; 6 | ( $($args:tt)+) => { 7 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Trace, "", $($args)+) 8 | }; 9 | } 10 | 11 | #[macro_export] 12 | macro_rules! debug { 13 | ( #$tag:expr, $($args:tt)+) => { 14 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Debug, $tag, $($args)+) 15 | }; 16 | ( $($args:tt)+) => { 17 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Debug, "", $($args)+) 18 | }; 19 | } 20 | #[macro_export] 21 | macro_rules! info { 22 | ( #$tag:expr, $($args:tt)+) => { 23 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Info, $tag, $($args)+) 24 | }; 25 | ( $($args:tt)+) => { 26 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Info, "", $($args)+) 27 | }; 28 | } 29 | #[macro_export] 30 | macro_rules! warn( 31 | ( #$tag:expr, $($args:tt)+) => { 32 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Warning, $tag, $($args)+) 33 | }; 34 | ( $($args:tt)+) => { 35 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Warning, "", $($args)+) 36 | }; 37 | ); 38 | #[macro_export] 39 | macro_rules! error { 40 | ( #$tag:expr, $($args:tt)+) => { 41 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Error, $tag, $($args)+) 42 | }; 43 | ( $($args:tt)+) => { 44 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Error, "", $($args)+) 45 | }; 46 | } 47 | #[macro_export] 48 | macro_rules! critical { 49 | ( #$tag:expr, $($args:tt)+) => { 50 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Critical, $tag, $($args)+) 51 | }; 52 | ( $($args:tt)+) => { 53 | slog::log!(crate::logging::logger::INTERNAL_LOGGER, slog::Level::Critical, "", $($args)+) 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/logging/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod logger; 2 | mod macros; 3 | mod runtime_filter; 4 | -------------------------------------------------------------------------------- /src/logging/runtime_filter.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | result, 3 | sync::{ 4 | atomic::{self, Ordering}, 5 | Arc, 6 | }, 7 | }; 8 | 9 | use slog::Drain; 10 | 11 | /// Custom Drain logic 12 | pub struct RuntimeLevelFilter { 13 | pub drain: D, 14 | pub on: Arc, 15 | } 16 | 17 | unsafe impl Sync for RuntimeLevelFilter {} 18 | impl Drain for RuntimeLevelFilter 19 | where 20 | D: Drain, 21 | { 22 | type Ok = Option; 23 | type Err = Option; 24 | 25 | fn log( 26 | &self, 27 | record: &slog::Record, 28 | values: &slog::OwnedKVList, 29 | ) -> result::Result { 30 | let level_id = self.on.load(Ordering::SeqCst); 31 | let current_level = match level_id { 32 | 0 => slog::Level::Trace, 33 | 1 => slog::Level::Debug, 34 | 2 => slog::Level::Info, 35 | 3 => slog::Level::Warning, 36 | 4 => slog::Level::Error, 37 | 5 => slog::Level::Critical, 38 | _ => slog::Level::Info, 39 | }; 40 | if record.level().is_at_least(current_level) { 41 | self.drain.log(record, values).map(Some).map_err(Some) 42 | } else { 43 | Ok(None) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use nu_plugin::{self, EvaluatedCall, Plugin, PluginCommand, SimplePluginCommand}; 2 | use nu_plugin_image::{ansi_to_image, image_to_ansi, logging::logger, FontFamily, Palette}; 3 | use nu_protocol::{Category, Signature, SyntaxShape, Type, Value}; 4 | 5 | pub struct ImageConversionPlugin; 6 | 7 | impl Plugin for ImageConversionPlugin { 8 | fn commands(&self) -> Vec>> { 9 | vec![ 10 | Box::new(FromPngCommand::new()), 11 | Box::new(ToPngCommand::new()), 12 | ] 13 | } 14 | 15 | fn version(&self) -> String { 16 | env!("CARGO_PKG_VERSION").into() 17 | } 18 | } 19 | struct FromPngCommand; 20 | 21 | impl FromPngCommand { 22 | pub fn new() -> FromPngCommand { 23 | FromPngCommand {} 24 | } 25 | } 26 | impl SimplePluginCommand for FromPngCommand { 27 | type Plugin = ImageConversionPlugin; 28 | 29 | fn name(&self) -> &str { 30 | "from png" 31 | } 32 | 33 | fn signature(&self) -> Signature { 34 | Signature::build("from png") 35 | .named( 36 | "width", 37 | SyntaxShape::Int, 38 | "Output width, in characters.", 39 | Some('x'), 40 | ) 41 | .named( 42 | "height", 43 | SyntaxShape::Int, 44 | "Output height, in characters.", 45 | Some('y'), 46 | ) 47 | .named( 48 | "log-level", 49 | SyntaxShape::String, 50 | "sets log level (CRITICAL (c) ERROR (e) WARN (w) INFO (i) DEBUG (d) TRACE (t)) defaults to INFO", 51 | None, 52 | ) 53 | .input_output_type(Type::Binary, Type::String) 54 | .category(Category::Conversions) 55 | } 56 | 57 | fn description(&self) -> &str { 58 | "create ansi text from an image" 59 | } 60 | 61 | fn run( 62 | &self, 63 | _plugin: &Self::Plugin, 64 | _engine: &nu_plugin::EngineInterface, 65 | call: &EvaluatedCall, 66 | input: &Value, 67 | ) -> Result { 68 | if let Some(Value::String { val, .. }) = call.get_flag_value("log-level") { 69 | logger::set_verbose(val); 70 | } 71 | image_to_ansi(call, input) 72 | } 73 | } 74 | struct ToPngCommand; 75 | impl ToPngCommand { 76 | pub fn new() -> ToPngCommand { 77 | ToPngCommand {} 78 | } 79 | } 80 | impl SimplePluginCommand for ToPngCommand { 81 | type Plugin = ImageConversionPlugin; 82 | 83 | fn name(&self) -> &str { 84 | "to png" 85 | } 86 | 87 | fn signature(&self) -> nu_protocol::Signature { 88 | Signature::build("to png") 89 | .optional( 90 | "output-path", 91 | SyntaxShape::Filepath, 92 | "output file path (by default uses current timestamp)", 93 | ) 94 | .named("width", SyntaxShape::Int, "output width", Some('w')) 95 | .named("theme",SyntaxShape::String,format!("select theme of the output, one of: {:?}\n\t\tby default uses `vscode` theme and you can mix this flag with custom theme colors every other colors will be from the selected theme",Palette::list()),Some('t')) 96 | .named( 97 | "font", 98 | SyntaxShape::String, 99 | format!( 100 | "Select the font from one of {:?}, by default the first font in the list will be used", 101 | FontFamily::list() 102 | ), 103 | None, 104 | ) 105 | .named("custom-font-regular", SyntaxShape::Filepath, "custom font Regular font path", None) 106 | .named("custom-font-bold", SyntaxShape::Filepath, "custom font Bold font path", None) 107 | .named("custom-font-italic", SyntaxShape::Filepath, "custom font Italic font path", None) 108 | .named("custom-font-bold_italic", SyntaxShape::Filepath, "custom font Bold Italic font path", None) 109 | .named("custom-theme-fg", SyntaxShape::String, "custom foreground color in hex format (0x040404)", None) 110 | .named("custom-theme-bg", SyntaxShape::String, "custom background color in hex format (0x040404)", None) 111 | .named("custom-theme-black", SyntaxShape::String, "custom black color in hex format (0x040404)", None) 112 | .named("custom-theme-red", SyntaxShape::String, "custom red color in hex format (0x040404)", None) 113 | .named("custom-theme-green", SyntaxShape::String, "custom green color in hex format (0x040404)", None) 114 | .named("custom-theme-yellow", SyntaxShape::String, "custom yellow color in hex format (0x040404)", None) 115 | .named("custom-theme-blue", SyntaxShape::String, "custom blue color in hex format (0x040404)", None) 116 | .named("custom-theme-magenta", SyntaxShape::String, "custom magenta color in hex format (0x040404)", None) 117 | .named("custom-theme-cyan", SyntaxShape::String, "custom cyan color in hex format (0x040404)", None) 118 | .named("custom-theme-white", SyntaxShape::String, "custom white color in hex format (0x040404)", None) 119 | .named("custom-theme-bright_black", SyntaxShape::String, "custom bright black color in hex format (0x040404)", None) 120 | .named("custom-theme-bright_red", SyntaxShape::String, "custom bright red color in hex format (0x040404)", None) 121 | .named("custom-theme-bright_green", SyntaxShape::String, "custom bright green color in hex format (0x040404)", None) 122 | .named("custom-theme-bright_yellow", SyntaxShape::String, "custom bright yellow color in hex format (0x040404)", None) 123 | .named("custom-theme-bright_blue", SyntaxShape::String, "custom bright blue color in hex format (0x040404)", None) 124 | .named("custom-theme-bright_magenta", SyntaxShape::String, "custom bright magenta color in hex format (0x040404)", None) 125 | .named("custom-theme-bright_cyan", SyntaxShape::String, "custom bright cyan color in hex format (0x040404)", None) 126 | .named("custom-theme-bright_white", SyntaxShape::String, "custom bright white color in hex format (0x040404)", None) 127 | .named( 128 | "log-level", 129 | SyntaxShape::String, 130 | "sets log level (CRITICAL (c) ERROR (e) WARN (w) INFO (i) DEBUG (d) TRACE (t)) defaults to INFO", 131 | None, 132 | ) 133 | .input_output_type(Type::String, Type::String) 134 | // .plugin_examples( 135 | // vec![ 136 | // PluginExample{ 137 | // description: "creates image of `ls` command's output and save it in the `ls.png` file".to_string(), 138 | // example: "ls | table -c | to png --theme ubuntu --font Ubuntu --output-path ls.png".to_string(), 139 | // result: None, 140 | // }, 141 | // PluginExample{ 142 | // description: "creates image of `ls` command's output and save it in the `ls.png` file with custom greenish background color".to_string(), 143 | // example: "ls | table -c | to png --theme ubuntu --font Ubuntu --custom-theme-bg 0x112411 --output-path ls.png".to_string(), 144 | // result: None, 145 | // }, 146 | // ] 147 | // ) 148 | .category(Category::Conversions) 149 | } 150 | 151 | fn description(&self) -> &str { 152 | "converts ansi string into png image" 153 | } 154 | fn extra_description(&self) -> &str { 155 | "if you change font and theme they will be used as base theme of the output and every custom flag you provide will override the selected theme or font" 156 | } 157 | 158 | fn run( 159 | &self, 160 | _plugin: &Self::Plugin, 161 | engine: &nu_plugin::EngineInterface, 162 | call: &EvaluatedCall, 163 | input: &Value, 164 | ) -> Result { 165 | if let Some(Value::String { val, .. }) = call.get_flag_value("log-level") { 166 | logger::set_verbose(val); 167 | } 168 | ansi_to_image(engine, call, input) 169 | } 170 | } 171 | 172 | fn main() { 173 | nu_plugin::serve_plugin( 174 | &mut ImageConversionPlugin {}, 175 | nu_plugin::MsgPackSerializer {}, 176 | ) 177 | } 178 | --------------------------------------------------------------------------------