├── .gitignore ├── LICENSE.md ├── README.md ├── doc └── neo-img.txt ├── lua └── neo-img │ ├── autocommands.lua │ ├── config.lua │ ├── health.lua │ ├── image.lua │ ├── init.lua │ ├── others.lua │ ├── tty.lua │ └── utils.lua ├── plugin └── neo-img.lua └── ttyimg └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | ttyimg/* 2 | !ttyimg/.gitkeep 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Neo-Img

2 |

🖼️ A Neovim plugin for viewing images in the terminal. 🖼️

3 |
4 | 5 | [![Static Badge](https://img.shields.io/badge/neovim-1e2029?logo=neovim&logoColor=3CA628&label=built%20for&labelColor=15161b)](https://neovim.io) 6 |
7 | 8 | --- 9 | https://github.com/user-attachments/assets/f7c76789-d57f-437c-b4da-444eebb7eb20 10 | 11 | ## Features ✨ 12 | - Automatically preview supported image files 13 | - Oil.nvim preview support 14 | - Caching 15 | 16 | ## Installation 🚀 17 | 18 | > uses [ttyimg](https://github.com/Skardyy/ttyimg) 19 | > you can install it in 2 ways: 20 | > * via `:NeoImg Install` **(recommended)** 21 | > * globally via `go install github.com/Skardyy/ttyimg@v1.0.5`, make sure you have GOPATH in your path `export PATH="$HOME/go/bin:$PATH` 22 | 23 | Using lazy.nvim: 24 | ```lua 25 | return { 26 | 'skardyy/neo-img', 27 | build = ":NeoImg Install", 28 | config = function() 29 | require('neo-img').setup() 30 | end 31 | } 32 | ``` 33 | 34 | ## Usage 💼 35 | - Images will automatically preview when opening supported files 36 | - Use `:NeoImg DisplayImage` to manually display the current file 37 | - you can also call `require("neo-img.utils").display_image(filepath, win)` to display the image in the given window 38 | 39 | ## Configuration ⚙️ 40 | > document files require 41 | >
42 | > Libreoffice 43 | > 44 | > ```txt 45 | > make sure its installed and in your path 46 | > * window: its called soffice and should be in C:\Program Files\LibreOffice\program 47 | > * linux: should be in the path automatically 48 | > ``` 49 | >
50 | ```lua 51 | require('neo-img').setup({ 52 | supported_extensions = { 53 | png = true, 54 | jpg = true, 55 | jpeg = true, 56 | tiff = true, 57 | tif = true, 58 | svg = true, 59 | webp = true, 60 | bmp = true, 61 | gif = true, -- static only 62 | docx = true, 63 | xlsx = true, 64 | pdf = true, 65 | pptx = true, 66 | odg = true, 67 | odp = true, 68 | ods = true, 69 | odt = true 70 | }, 71 | 72 | ----- Important ones ----- 73 | size = "80%", -- size of the image in percent 74 | center = true, -- rather or not to center the image in the window 75 | ----- Important ones ----- 76 | 77 | ----- Less Important ----- 78 | auto_open = true, -- Automatically open images when buffer is loaded 79 | oil_preview = true, -- changes oil preview of images too 80 | backend = "auto", -- auto / kitty / iterm / sixel 81 | resizeMode = "Fit", -- Fit / Stretch / Crop 82 | offset = "2x3", -- that exmp is 2 cells offset x and 3 y. 83 | ttyimg = "local" -- local / global 84 | ----- Less Important ----- 85 | }) 86 | ``` 87 | -------------------------------------------------------------------------------- /doc/neo-img.txt: -------------------------------------------------------------------------------- 1 | # Neo-Img 2 | A Neovim plugin for viewing images in the terminal. 3 | 4 | --- 5 | 6 | ## Features ✨ 7 | - Automatically preview supported image files 8 | - Oil.nvim preview support 9 | - Caching 10 | 11 | ## Installation 🚀 12 | 13 | > uses [ttyimg](https://github.com/Skardyy/ttyimg) 14 | > you can install it in 2 ways: 15 | > * via `:NeoImg Install` **(recommended)** 16 | > * globally via `go install github.com/Skardyy/ttyimg@v1.0.5`, make sure you have GOPATH in your path `export PATH="$HOME/go/bin:$PATH` 17 | 18 | Using lazy.nvim: 19 | ```lua 20 | return { 21 | 'skardyy/neo-img', 22 | build = ":NeoImg Install", 23 | config = function() 24 | require('neo-img').setup() 25 | end 26 | } 27 | ``` 28 | 29 | ## Usage 💼 30 | - Images will automatically preview when opening supported files 31 | - Use `:NeoImg DisplayImage` to manually display the current file 32 | - you can also call `require("neo-img.utils").display_image(filepath, win)` to display the image in the given window 33 | 34 | ## Configuration ⚙️ 35 | > document files require Libreoffice 36 | > ```txt 37 | > make sure its installed and in your path 38 | > * window: its called soffice and should be in C:\Program Files\LibreOffice\program 39 | > * linux: should be in the path automatically 40 | > ``` 41 | 42 | ```lua 43 | require('neo-img').setup({ 44 | supported_extensions = { 45 | png = true, 46 | jpg = true, 47 | jpeg = true, 48 | tiff = true, 49 | tif = true, 50 | svg = true, 51 | webp = true, 52 | bmp = true, 53 | gif = true, -- static only 54 | docx = true, 55 | xlsx = true, 56 | pdf = true, 57 | pptx = true, 58 | odg = true, 59 | odp = true, 60 | ods = true, 61 | odt = true 62 | }, 63 | 64 | ----- Important ones ----- 65 | size = "80%", -- size of the image in percent 66 | center = true, -- rather or not to center the image in the window 67 | ----- Important ones ----- 68 | 69 | ----- Less Important ----- 70 | auto_open = true, -- Automatically open images when buffer is loaded 71 | oil_preview = true, -- changes oil preview of images too 72 | backend = "auto", -- auto / kitty / iterm / sixel 73 | resizeMode = "Fit", -- Fit / Stretch / Crop 74 | offset = "2x3", -- that exmp is 2 cells offset x and 3 y. 75 | ttyimg = "local" -- local / global 76 | ----- Less Important ----- 77 | }) 78 | ``` 79 | 80 | * supported_extensions: the extension of file to replace with an image. 81 | * size: size of the image compared to the window in percent, must be percent! 82 | * center: centers the image in x axis inside the window. 83 | * auto_open: auto replaces buffers with images. replaces calling :NeoImg DisplayImage every time. only relevant for buffers and not oil! 84 | * oil_preview: replaces oil preview of supported extensions into images. don't use outdated oil version :( 85 | * backend: enforces a graphic protocol if the auto fails to match. 86 | * resizeMode: the resize mode to apply on the size. 87 | * offset: offset from the window top left. works in addition to the center option! 88 | * ttyimg: enforces different installation of ttyimg. global is from go, local is from :NeoImg Install. some PCs may not have supported precompiled binary from ttyimg, so the global installation can solve that. 89 | -------------------------------------------------------------------------------- /lua/neo-img/autocommands.lua: -------------------------------------------------------------------------------- 1 | --- @class NeoImg.Autocommands 2 | local M = {} 3 | local utils = require "neo-img.utils" 4 | local Image = require "neo-img.image" 5 | local main_config = require "neo-img.config" 6 | local others = require "neo-img.others" 7 | 8 | --- setups the main autocommands 9 | local function setup_main(config) 10 | local patterns = {} 11 | for ext, _ in pairs(config.supported_extensions) do 12 | table.insert(patterns, "*." .. ext) 13 | end 14 | 15 | local group = vim.api.nvim_create_augroup("NeoImg", { clear = true }) 16 | 17 | -- lock bufs on read 18 | vim.api.nvim_create_autocmd({ "BufRead" }, { 19 | group = group, 20 | pattern = patterns, 21 | callback = function(ev) 22 | utils.lock_buf(ev.buf) 23 | end 24 | }) 25 | 26 | -- preview image on buf enter 27 | vim.api.nvim_create_autocmd({ "BufEnter" }, { 28 | group = group, 29 | pattern = patterns, 30 | callback = function(ev) 31 | Image.StopJob() 32 | vim.schedule(function() 33 | local filepath = vim.api.nvim_buf_get_name(ev.buf) 34 | local win = vim.fn.bufwinid(ev.buf) 35 | utils.display_image(filepath, win) 36 | end) 37 | end 38 | }) 39 | end 40 | 41 | --- setups the api 42 | local function setup_api() 43 | local config = main_config.get() 44 | vim.api.nvim_create_user_command('NeoImg', function(opts) 45 | local command_name = opts.args 46 | if command_name == 'Install' then 47 | print("Installing Ttyimg...") 48 | require("neo-img").install() 49 | elseif command_name == 'DisplayImage' then 50 | local buf = vim.api.nvim_get_current_buf() 51 | local buf_name = vim.api.nvim_buf_get_name(buf) 52 | local ext = utils.get_extension(buf_name) 53 | if ext and config.supported_extensions[ext:lower()] then 54 | local win = vim.fn.bufwinid(buf) 55 | utils.display_image(buf_name, win) 56 | else 57 | vim.notify("invalid path for image: " .. buf_name) 58 | end 59 | end 60 | end, { 61 | nargs = 1, 62 | complete = function() 63 | return { 'Install', 'DisplayImage' } 64 | end 65 | }) 66 | end 67 | 68 | --- setups all the autocommands for neo-img 69 | function M.setup() 70 | local config = main_config.get() 71 | vim.g.zipPlugin_ext = "zip" -- showing image so no need for unzip 72 | if config.auto_open then 73 | setup_main(config) 74 | end 75 | if config.oil_preview then 76 | others.setup_oil() -- disables preview for files that im already showing image preview 77 | end 78 | setup_api() 79 | end 80 | 81 | return M 82 | -------------------------------------------------------------------------------- /lua/neo-img/config.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | --- @class NeoImg.Config 4 | --- @field supported_extensions table Supported file extensions 5 | --- @field size string Size of the image in percent (e.g., "80%") 6 | --- @field center boolean Whether to center the image in the window 7 | --- @field auto_open boolean Auto-open images on buffer load 8 | --- @field oil_preview boolean Enable oil.nvim preview for images 9 | --- @field backend "auto"|"kitty"|"iterm"|"sixel" Backend for rendering 10 | --- @field resizeMode "Fit"|"Stretch"|"Crop" Resize mode for images 11 | --- @field offset string Offset for positioning (e.g., "0x3") 12 | --- @field ttyimg "local"|"global" which ttyimg is preferred 13 | --- @field bin_path? string Path to the ttyimg binary (populated at runtime) 14 | --- @field os? string the OS of the machine (populated at runtime) 15 | --- @field window_size? {spx: NeoImg.Size, sc: NeoImg.Size} window size fallbacks in px and cells (populated at runtime) 16 | --- @field ttyimg_version? string the version of ttyimg to scope to (populated at runtime) 17 | 18 | --- Default configuration 19 | ---@type NeoImg.Config 20 | M.defaults = { 21 | supported_extensions = { 22 | png = true, 23 | jpg = true, 24 | jpeg = true, 25 | tiff = true, 26 | tif = true, 27 | svg = true, 28 | webp = true, 29 | bmp = true, 30 | gif = true, -- static only 31 | docx = true, 32 | xlsx = true, 33 | pdf = true, 34 | pptx = true, 35 | odg = true, 36 | odp = true, 37 | ods = true, 38 | odt = true 39 | }, 40 | 41 | ----- Important ones ----- 42 | size = "80%", -- size of the image in percent 43 | center = true, -- rather or not to center the image in the window 44 | ----- Important ones ----- 45 | 46 | ----- Less Important ----- 47 | auto_open = true, -- Automatically open images when buffer is loaded 48 | oil_preview = true, -- changes oil preview of images too 49 | backend = "auto", -- auto / kitty / iterm / sixel 50 | resizeMode = "Fit", -- Fit / Stretch / Crop 51 | offset = "2x3", -- that exmp is 2 cells offset x and 3 y. 52 | ttyimg = "local" -- local / global 53 | ----- Less Important ----- 54 | } 55 | 56 | local config = M.defaults 57 | 58 | --- Get the directory where ttyimg is installed 59 | ---@return string bin_dir Path to the ttyimg binary directory 60 | function M.get_bin_dir() 61 | local bin_name = "ttyimg" 62 | local config_dir = debug.getinfo(1).source:sub(2) 63 | local _, end_idx = config_dir:find("neo%-img") 64 | return config_dir:sub(1, end_idx) .. "/" .. bin_name 65 | end 66 | 67 | --- Get the path to the ttyimg binary 68 | ---@return string bin_path The resolved binary path or an empty string if not found 69 | function M.get_bin_path() 70 | local bin_dir = M.get_bin_dir() 71 | local bin_path = bin_dir .. "/ttyimg" 72 | local fallback_bin = nil 73 | 74 | local local_bin = vim.fn.exepath(bin_path) 75 | if local_bin ~= "" then 76 | if config.ttyimg == "local" then 77 | return local_bin 78 | else 79 | fallback_bin = local_bin 80 | end 81 | end 82 | 83 | local global_binary = vim.fn.exepath("ttyimg") 84 | if global_binary ~= "" then 85 | if config.ttyimg == "global" then 86 | return global_binary 87 | else 88 | if fallback_bin == nil then 89 | fallback_bin = global_binary 90 | end 91 | end 92 | end 93 | if fallback_bin ~= nil then 94 | return fallback_bin 95 | end 96 | print("couldn't find ttyimg, please call :NeoImg Install") 97 | return "" 98 | end 99 | 100 | --- Set the bin_path in config 101 | function M.set_bin_path() 102 | config.bin_path = M.get_bin_path() 103 | end 104 | 105 | --- Setup function to initialize the configuration 106 | ---@param opts? NeoImg.Config Custom user options 107 | function M.setup(opts) 108 | config.ttyimg_version = "1.0.5" 109 | config.bin_path = M.get_bin_path() 110 | local new_opts = opts and M.validate_config(opts) or {} 111 | config = vim.tbl_deep_extend('force', M.defaults, new_opts) 112 | end 113 | 114 | --- Get the current configuration 115 | ---@return NeoImg.Config config The current configuration table 116 | function M.get() 117 | return config 118 | end 119 | 120 | --- Validates and corrects a given NeoImg.Config table. 121 | ---@param opts NeoImg.Config The configuration table to validate. 122 | ---@return NeoImg.Config opts The validated and corrected configuration. 123 | function M.validate_config(opts) 124 | local defaults = M.defaults 125 | 126 | if type(opts) ~= "table" then 127 | return vim.deepcopy(defaults) 128 | end 129 | 130 | local function is_valid_percentage(value) 131 | return type(value) == "string" and value:match("^%d+%%$") 132 | end 133 | 134 | local function is_valid_boolean(value) 135 | return type(value) == "boolean" 136 | end 137 | 138 | local function is_valid_backend(value) 139 | if type(value) == "string" then 140 | local value2 = string.lower(value) 141 | return value2 == "auto" or value2 == "kitty" or value2 == "iterm" or value2 == "sixel" 142 | end 143 | return false 144 | end 145 | 146 | local function is_valid_resize_mode(value) 147 | if type(value) == "string" then 148 | local value2 = string.lower(value) 149 | return value2 == "fit" or value2 == "stretch" or value2 == "crop" 150 | end 151 | return false 152 | end 153 | 154 | local function is_valid_offset(value) 155 | return type(value) == "string" and value:match("^%d+x%d+$") 156 | end 157 | 158 | local function is_valid_ttyimg(value) 159 | return value == "global" or value == "local" 160 | end 161 | 162 | --- @type NeoImg.Config 163 | local validated_config = { 164 | supported_extensions = type(opts.supported_extensions) == "table" and opts.supported_extensions or 165 | defaults.supported_extensions, 166 | size = is_valid_percentage(opts.size) and opts.size or defaults.size, 167 | center = is_valid_boolean(opts.center) and opts.center or defaults.center, 168 | auto_open = is_valid_boolean(opts.auto_open) and opts.auto_open or defaults.auto_open, 169 | oil_preview = is_valid_boolean(opts.oil_preview) and opts.oil_preview or defaults.oil_preview, 170 | backend = is_valid_backend(opts.backend) and opts.backend or defaults.backend, 171 | resizeMode = is_valid_resize_mode(opts.resizeMode) and opts.resizeMode or defaults.resizeMode, 172 | offset = is_valid_offset(opts.offset) and opts.offset or defaults.offset, 173 | ttyimg = is_valid_ttyimg(opts.ttyimg) and opts.ttyimg or defaults.ttyimg, 174 | 175 | bin_path = type(opts.bin_path) == "string" and opts.bin_path or nil, 176 | os = type(opts.os) == "string" and opts.os or nil, 177 | window_size = type(opts.window_size) == "table" and opts.window_size or nil, 178 | } 179 | 180 | return validated_config 181 | end 182 | 183 | return M 184 | -------------------------------------------------------------------------------- /lua/neo-img/health.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local main_config = require "neo-img.config" 3 | local utils = require "neo-img.utils" 4 | 5 | function M.check() 6 | local config = main_config.get() 7 | local version = config.ttyimg_version 8 | vim.health.start("ttyimg Health Check") 9 | 10 | -- Run ttyimg --validate 11 | local result = vim.fn.system({ config.bin_path, "--validate", version }) 12 | local versioned = string.match(result, "version") 13 | local exit_code = vim.v.shell_error 14 | 15 | -- ttyimg install location 16 | local location_type = config.ttyimg == "global" and "global (installed from go)" or 17 | "local (precompiled from `:NeoImg Install`)" 18 | vim.health.ok("expecting ttyimg to be " .. location_type) 19 | vim.health.info("PATH: " .. config.bin_path) 20 | 21 | -- ttyimg installation and version check 22 | if exit_code == 0 then 23 | vim.health.ok("ttyimg is installed and working correctly") 24 | vim.health.info("Validation output:\n" .. result) 25 | local backend_enf = config.backend == "auto" and "None" or config.backend 26 | vim.health.ok("backend enforcement: " .. backend_enf) 27 | vim.health.ok("fallback: " .. "sixel") 28 | else 29 | local install_message = 30 | "- Please call `:NeoImg Install`\n- or install it via `go install github.com/Skardyy/ttyimg@v" .. version .. "`" 31 | if versioned then 32 | vim.health.info("Validation output:\n" .. result) 33 | vim.health.error( 34 | "ttyimg version is invalid\n" .. install_message) 35 | else 36 | vim.health.error("ttyimg is outdated.\n" .. install_message) 37 | end 38 | end 39 | 40 | -- os and arch check 41 | local os, arch = utils.get_os_arch() 42 | if os == nil then 43 | vim.health.warn("OS: not supported on `:NeoImg Install`, please install from go") 44 | else 45 | vim.health.ok("OS (" .. os .. "): supported on both `go install` and `:NeoImg Install`") 46 | end 47 | 48 | if arch == nil then 49 | vim.health.warn("ARCH: not supported on `:NeoImg Install`, please install from go") 50 | else 51 | vim.health.ok("ARCH (" .. arch .. "): supported on both `go install` and `:NeoImg Install`") 52 | end 53 | 54 | -- screen size 55 | if config.window_size.spx.x == 1920 and config.window_size.spx.y == 1080 and os ~= "windows" then 56 | vim.health.warn("SPX: unless your terminal is 1920x1080, its likely failed to query the size") 57 | else 58 | if os == "windows" then 59 | vim.health.ok("SPX: " .. "will be queried from win api") 60 | else 61 | vim.health.ok("SPX: " .. vim.inspect(config.window_size.spx)) 62 | end 63 | end 64 | vim.health.ok("SC: " .. vim.inspect(config.window_size.sc)) 65 | end 66 | 67 | return M 68 | -------------------------------------------------------------------------------- /lua/neo-img/image.lua: -------------------------------------------------------------------------------- 1 | --- @class NeoImg.Image 2 | local Image = { 3 | --- @type integer[] 4 | watch = {}, 5 | --- @type table 6 | draw = {}, 7 | } 8 | local tty = require("neo-img.tty") 9 | 10 | --- image constructor 11 | --- @param win integer the win to draw on 12 | --- @param row integer starting row to draw on 13 | --- @param col integer starting col to draw on 14 | --- @param esc string the content to draw 15 | --- @param watch integer[] bufs to listen for image cleanup 16 | --- @param id string the id of the img to track if its still drawn 17 | function Image.Create(win, row, col, esc, watch, id) 18 | Image.win = win 19 | Image.row = row 20 | Image.col = col 21 | Image.esc = esc 22 | Image.id = id 23 | 24 | for _, buf in ipairs(watch) do 25 | -- 2 should watch, 1 watching, 0 nothing 26 | if Image.watch[buf] ~= 1 then 27 | Image.watch[buf] = 2 28 | end 29 | end 30 | end 31 | 32 | --- @return boolean rather or not there is a image to delete 33 | function Image.Should_Clean() 34 | for _, draw in pairs(Image.draw) do 35 | if draw then 36 | return true 37 | end 38 | end 39 | return false 40 | end 41 | 42 | --- draws the image 43 | function Image.Draw() 44 | local move_cursor = string.format("\27[%d;%dH", Image.row, Image.col) 45 | local image_esc = "\27[s" .. move_cursor .. Image.esc .. "\27[u" 46 | tty.write(image_esc) 47 | Image.draw[Image.id] = true 48 | end 49 | 50 | --- @return integer[] the buffers to watch for image cleanup 51 | function Image.get_watch_list() 52 | local buffers = {} 53 | if vim.api.nvim_win_is_valid(Image.win) then 54 | table.insert(buffers, vim.api.nvim_win_get_buf(Image.win)) 55 | end 56 | for bufnr, status in pairs(Image.watch) do 57 | if status == 2 then 58 | table.insert(buffers, bufnr) 59 | end 60 | end 61 | return buffers 62 | end 63 | 64 | --- prepares image cleanup 65 | function Image.Prepare() 66 | local buffers = Image.get_watch_list() 67 | for _, buf in ipairs(buffers) do 68 | Image.watch[buf] = 1 69 | local group = vim.api.nvim_create_augroup("NeoImg", { clear = false }) 70 | vim.api.nvim_create_autocmd({ "BufLeave" }, { 71 | group = group, 72 | buffer = buf, 73 | once = true, 74 | callback = function() 75 | if Image.watch[buf] == 1 then 76 | Image.Delete() 77 | Image.watch[buf] = 0 78 | Image.draw = {} 79 | end 80 | end, 81 | desc = "Delete image when window or buffer is no longer visible", 82 | }) 83 | end 84 | end 85 | 86 | --- stops jobs to draw an image 87 | function Image.StopJob() 88 | if Image.job ~= nil then 89 | vim.fn.jobstop(Image.job) 90 | Image.job = nil 91 | end 92 | end 93 | 94 | --- cleans the screen if needed 95 | function Image.Delete() 96 | if Image.Should_Clean() then 97 | vim.api.nvim_command("mode") 98 | end 99 | Image.StopJob() 100 | end 101 | 102 | return Image 103 | -------------------------------------------------------------------------------- /lua/neo-img/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local config = require('neo-img.config') 3 | local autocmds = require("neo-img.autocommands") 4 | local utils = require("neo-img.utils") 5 | 6 | --- setups the plugin 7 | function M.setup(opts) 8 | opts = opts or {} 9 | config.setup(opts) 10 | config.get().window_size = utils.get_window_size_fallback() 11 | autocmds.setup() 12 | end 13 | 14 | --- installs ttyimg, which is a dependency for the plugin 15 | function M.install() 16 | local target_dir = config.get_bin_dir() 17 | 18 | local os, arch = require("neo-img.utils").get_os_arch() 19 | if os == nil then 20 | print("Unsupported OS") 21 | return 22 | end 23 | if arch == nil then 24 | print("Unsupported cpu architecture") 25 | return 26 | end 27 | 28 | -- Build file name 29 | local filename = "ttyimg-" .. os .. "-" .. arch 30 | if os == "windows" then 31 | filename = filename .. ".exe" 32 | end 33 | 34 | -- Download URL 35 | local version = "v" .. config.get().ttyimg_version 36 | local url = "https://github.com/Skardyy/ttyimg/releases/download/" .. version .. "/" .. filename 37 | local output_path = target_dir .. "/ttyimg" .. (os == "windows" and ".exe" or "") 38 | 39 | -- Check if curl or wget is available 40 | local function is_command_available(cmd) 41 | return vim.fn.executable(cmd) == 1 42 | end 43 | 44 | local downloader 45 | if is_command_available("curl") then 46 | downloader = { "curl", "-L", "-o", output_path, url } 47 | elseif is_command_available("wget") then 48 | downloader = { "wget", "-O", output_path, url } 49 | else 50 | print("Neither curl nor wget found. Please install one.") 51 | return 52 | end 53 | 54 | -- Run the download command 55 | local handle = vim.loop.spawn(downloader[1], { args = { unpack(downloader, 2) } }, function(code, signal) 56 | if code == 0 then 57 | print("Downloaded ttyimg successfully to " .. output_path) 58 | 59 | -- Perform chmod to make the binary executable (only for non-Windows systems) 60 | if os ~= "windows" then 61 | local chmod_handle = vim.loop.spawn("chmod", { args = { "+x", output_path } }, function(chmod_code) 62 | if chmod_code == 0 then 63 | print("done installing ttyimg!") 64 | vim.schedule(function() 65 | config.set_bin_path() 66 | end) 67 | else 68 | print("Failed to set executable permissions for " .. output_path) 69 | end 70 | end) 71 | 72 | if not chmod_handle then 73 | print("Failed to start chmod process.") 74 | end 75 | else 76 | print("done installing ttyimg!") 77 | vim.schedule(function() 78 | config.set_bin_path() 79 | end) 80 | end 81 | else 82 | print("Failed to download ttyimg. Exit code: " .. code .. ", Signal: " .. signal) 83 | end 84 | end) 85 | 86 | if not handle then 87 | print("Failed to start download process.") 88 | end 89 | end 90 | 91 | return M 92 | -------------------------------------------------------------------------------- /lua/neo-img/others.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local utils = require "neo-img.utils" 3 | local main_config = require "neo-img.config" 4 | 5 | --- @return integer? win the first other win in the same tab 6 | local function get_first_other_win() 7 | local current_win = vim.api.nvim_get_current_win() 8 | local current_tab = vim.api.nvim_get_current_tabpage() 9 | local all_wins = vim.api.nvim_tabpage_list_wins(current_tab) 10 | 11 | for _, win in ipairs(all_wins) do 12 | if win ~= current_win then 13 | return win 14 | end 15 | end 16 | 17 | return nil -- No other windows found in the same tab 18 | end 19 | 20 | --- enables drawing supported_extensions instead of normal oil-preview 21 | function M.setup_oil() 22 | local status_ok, oil = pcall(require, "oil.config") 23 | if not status_ok then return end 24 | 25 | if oil.preview_win ~= nil then 26 | local pre_dis_fn = oil.preview_win.disable_preview 27 | oil.preview_win.disable_preview = function(filepath) 28 | local ext = utils.get_extension(filepath) 29 | if main_config.get().supported_extensions[ext] then 30 | vim.schedule(function() 31 | local win = get_first_other_win() 32 | if not win then return end 33 | utils.display_image(filepath, win) 34 | end) 35 | return true 36 | end 37 | if pre_dis_fn then 38 | return pre_dis_fn(filepath) 39 | else 40 | return false 41 | end 42 | end 43 | end 44 | return false 45 | end 46 | 47 | return M 48 | -------------------------------------------------------------------------------- /lua/neo-img/tty.lua: -------------------------------------------------------------------------------- 1 | --- @class NeoImg.Tty 2 | local M = {} 3 | 4 | --- sends GP escape characters to the terminal 5 | --- @param data string the escape characters to draw 6 | function M.write(data) 7 | vim.fn.chansend(vim.v.stderr, data) 8 | end 9 | 10 | return M 11 | -------------------------------------------------------------------------------- /lua/neo-img/utils.lua: -------------------------------------------------------------------------------- 1 | --- @class NeoImg.Utils 2 | local M = {} 3 | local Image = require("neo-img.image") 4 | local main_config = require("neo-img.config") 5 | 6 | --- returns the os and arch 7 | --- @return "windows"|"linux"|"darwin" os the OS of the machine 8 | --- @return "386"|"amd64"|"arm"|"arm64" arch the arch of the cpu 9 | function M.get_os_arch() 10 | local os_mapper = { 11 | Windows = "windows", 12 | Linux = "linux", 13 | OSX = "darwin", 14 | BSD = nil, 15 | POSIX = nil, 16 | Other = nil 17 | } 18 | 19 | local arch_mapper = { 20 | x86 = "386", 21 | x64 = "amd64", 22 | arm = "arm", 23 | arm64 = "arm64", 24 | arm64be = nil, 25 | ppc = nil, 26 | mips = nil, 27 | mipsel = nil, 28 | mips64 = nil, 29 | mips64el = nil, 30 | mips64r6 = nil, 31 | mips64r6el = nil 32 | } 33 | 34 | return os_mapper[jit.os], arch_mapper[jit.arch] 35 | end 36 | 37 | --- @class NeoImg.Size 38 | --- @field x number 39 | --- @field y number 40 | 41 | --- returns a window size for fallback 42 | --- @return {spx: NeoImg.Size, sc: NeoImg.Size} 43 | function M.get_window_size_fallback() 44 | local config = main_config.get() 45 | local os = M.get_os_arch() 46 | config.os = os 47 | local spx = { 48 | x = 1920, 49 | y = 1080 50 | } 51 | local sc = { 52 | x = vim.o.columns, 53 | y = vim.o.lines 54 | } 55 | if config.os ~= "windows" then 56 | local ffi = require("ffi") 57 | ffi.cdef [[ 58 | struct winsize { 59 | unsigned short ws_row; 60 | unsigned short ws_col; 61 | unsigned short ws_xpixel; 62 | unsigned short ws_ypixel; 63 | }; 64 | 65 | int ioctl(int fd, unsigned long request, void *arg); 66 | ]] 67 | local TIOCGWINSZ = config.os == "linux" and 0x5413 or 0x40087468 68 | local winsize = ffi.new("struct winsize") 69 | local success = ffi.C.ioctl(0, TIOCGWINSZ, winsize) 70 | if success == 0 then 71 | spx.x = winsize.ws_xpixel 72 | spx.y = winsize.ws_ypixel 73 | end 74 | end 75 | return { 76 | spx = spx, 77 | sc = sc 78 | } 79 | end 80 | 81 | --- Normalizes the size of the img 82 | --- @return string value 83 | local function get_scale_factor(value) 84 | local numberString = value:gsub("%%", "") 85 | local number = tonumber(numberString) 86 | if number > 95 then 87 | return 95 .. "%" 88 | else 89 | return value 90 | end 91 | end 92 | 93 | --- Calculates dimensions for the image in the given win 94 | --- @param win integer window id 95 | --- @return {spx: string, sc: string, size: string, scale: string, offset: NeoImg.Size} 96 | function M.get_dims(win) 97 | local config = main_config.get() 98 | 99 | local row, col = unpack(vim.api.nvim_win_get_position(win)) 100 | local ovcol, ovrow = vim.o.columns - col, vim.o.lines - row 101 | 102 | -- gettig factors 103 | local scale_factor = get_scale_factor(config.size) 104 | local win_factor_x = ovcol / vim.o.columns 105 | local win_factor_y = ovrow / vim.o.lines 106 | 107 | -- getting the offset 108 | local offsetx, offsety = 2, 3 109 | local tx, ty = config.offset:match("^(%d+)x(%d+)$") 110 | local offsetx_tmp, offsety_tmp = tonumber(tx), tonumber(ty) 111 | if offsetx_tmp then 112 | offsetx = offsetx_tmp 113 | end 114 | if offsety_tmp then 115 | offsety = offsety_tmp 116 | end 117 | 118 | -- getting size in px 119 | local spx = config.window_size.spx.x .. "x" .. config.window_size.spx.y 120 | if config.os ~= "windows" then 121 | spx = spx .. "xforce" 122 | end 123 | 124 | --getting size in cells 125 | local sc = config.window_size.sc.x .. "x" .. config.window_size.sc.y .. "xforce" 126 | 127 | --getting the scale 128 | local scale = win_factor_x .. "x" .. win_factor_y 129 | 130 | return { 131 | spx = spx, 132 | sc = sc, 133 | size = scale_factor, 134 | scale = scale, 135 | offset = { 136 | x = col + offsetx, 137 | y = row + offsety 138 | } 139 | } 140 | end 141 | 142 | --- @param filename string the filename to get the ext from 143 | --- @return string the ext 144 | function M.get_extension(filename) 145 | return filename:match("^.+%.(.+)$") 146 | end 147 | 148 | --- builds the command to run in order to get the img 149 | --- @param filepath string the img to show 150 | --- @param opts {spx: string, sc: string, scale: string, width: string, height: string} 151 | --- @return table 152 | local function build_command(filepath, opts) 153 | local config = main_config.get() 154 | 155 | local protocol = "auto" 156 | local valid_configs = { iterm = true, kitty = true, sixel = true } 157 | if valid_configs[config.backend] then 158 | protocol = config.backend 159 | end 160 | 161 | local command = { 162 | config.bin_path, 163 | "-m", config.resizeMode, 164 | "-spx", opts.spx, 165 | "-sc", opts.sc, 166 | "-center=" .. tostring(config.center), 167 | "-scale", opts.scale, 168 | "-p", protocol, 169 | "-w", opts.width, "-h", opts.height, 170 | "-f", "sixel", 171 | filepath 172 | } 173 | 174 | return command 175 | end 176 | 177 | --- @return integer? buf the main oil buf in the current tab 178 | local function get_oil_buf() 179 | local current_tab = vim.api.nvim_get_current_tabpage() 180 | local all_wins = vim.api.nvim_tabpage_list_wins(current_tab) 181 | 182 | for _, win in ipairs(all_wins) do 183 | local buf = vim.api.nvim_win_get_buf(win) 184 | if vim.bo[buf].filetype == "oil" then 185 | return buf 186 | end 187 | end 188 | 189 | return nil 190 | end 191 | 192 | --- setup and draws the image 193 | --- @param win integer the window id to listen on remove 194 | --- @param row integer the starting row 195 | --- @param col integer the starting col 196 | --- @param output string the content of the image 197 | --- @param filepath string the filepath to use as id 198 | local function draw_image(win, row, col, output, filepath) 199 | local config = main_config.get() 200 | local watch = config.oil_preview and { get_oil_buf() } or {} 201 | Image.Create(win, row, col, output, watch, filepath) 202 | Image.Prepare() 203 | Image.Draw() 204 | end 205 | 206 | --- draws the image 207 | --- @param filepath string the image to draw 208 | --- @param win integer the window id to draw on 209 | function M.display_image(filepath, win) 210 | local config = main_config.get() 211 | 212 | -- checks before draw 213 | if config.bin_path == "" then 214 | vim.notify("ttyimg isn't installed, call :NeoImg Install", vim.log.levels.ERROR) 215 | return 216 | end 217 | if vim.fn.filereadable(filepath) == 0 then 218 | vim.notify("File not found: " .. filepath, vim.log.levels.ERROR) 219 | return 220 | end 221 | 222 | local opts = M.get_dims(win) 223 | local command = build_command(filepath, { 224 | spx = opts.spx, 225 | sc = opts.sc, 226 | scale = opts.scale, 227 | width = opts.size, 228 | height = opts.size 229 | }) 230 | 231 | Image.Delete() 232 | Image.job = vim.fn.jobstart(command, { 233 | on_stdout = function(_, data) 234 | if data then 235 | local output = table.concat(data, "\n") 236 | -- error 237 | if string.len(vim.inspect(data)) < 100 then 238 | -- if empty probbs just stopjob 239 | if output == "" then return end 240 | vim.notify("error: " .. output) 241 | return 242 | end 243 | draw_image(win, opts.offset.y, opts.offset.x, output, filepath) 244 | end 245 | end, 246 | stdout_buffered = true 247 | }) 248 | end 249 | 250 | --- make a buf empty and unwritable 251 | function M.lock_buf(buf) 252 | -- make it empty and not saveable, dk if all things are needed 253 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, {}) 254 | vim.api.nvim_buf_set_option(buf, "modifiable", false) 255 | vim.api.nvim_buf_set_option(buf, "buftype", "nofile") 256 | vim.api.nvim_buf_set_option(buf, "swapfile", false) 257 | vim.api.nvim_buf_set_option(buf, "bufhidden", "hide") 258 | vim.api.nvim_buf_set_option(buf, "readonly", true) 259 | end 260 | 261 | return M 262 | -------------------------------------------------------------------------------- /plugin/neo-img.lua: -------------------------------------------------------------------------------- 1 | if vim.g.loaded_neo_img then 2 | return 3 | end 4 | vim.g.loaded_neo_img = true 5 | -------------------------------------------------------------------------------- /ttyimg/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skardyy/neo-img/065fa672929696c4f3478ff2fbdb886b5d2d434e/ttyimg/.gitkeep --------------------------------------------------------------------------------