├── .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 | [](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
--------------------------------------------------------------------------------