├── .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 |
--------------------------------------------------------------------------------