Regenerate nvim config
This commit is contained in:
@ -0,0 +1,117 @@
|
||||
local api = vim.api
|
||||
local bit = require("bit")
|
||||
-----------------------------------------------------------
|
||||
-- Export
|
||||
-----------------------------------------------------------
|
||||
local M = {}
|
||||
-----------------------------------------------------------
|
||||
-- Helpers
|
||||
-----------------------------------------------------------
|
||||
---Convert a hex color to an rgb color
|
||||
---@param color string
|
||||
---@return number
|
||||
---@return number
|
||||
---@return number
|
||||
local function to_rgb(color)
|
||||
return tonumber(color:sub(2, 3), 16), tonumber(color:sub(4, 5), 16), tonumber(color:sub(6), 16)
|
||||
end
|
||||
|
||||
-- SOURCE: https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors
|
||||
-- @see: https://stackoverflow.com/questions/37796287/convert-decimal-to-hex-in-lua-4
|
||||
--- Shade Color generate
|
||||
--- @param color string hex color
|
||||
--- @param percent number
|
||||
--- @return string
|
||||
function M.shade_color(color, percent)
|
||||
local r, g, b = to_rgb(color)
|
||||
-- If any of the colors are missing return "NONE" i.e. no highlight
|
||||
if not r or not g or not b then return "NONE" end
|
||||
r = math.floor(tonumber(r * (100 + percent) / 100) or 0)
|
||||
g = math.floor(tonumber(g * (100 + percent) / 100) or 0)
|
||||
b = math.floor(tonumber(b * (100 + percent) / 100) or 0)
|
||||
r, g, b = r < 255 and r or 255, g < 255 and g or 255, b < 255 and b or 255
|
||||
|
||||
return "#" .. string.format("%02x%02x%02x", r, g, b)
|
||||
end
|
||||
|
||||
--- Determine whether to use black or white text
|
||||
--- Ref:
|
||||
--- 1. https://stackoverflow.com/a/1855903/837964
|
||||
--- 2. https://stackoverflow.com/a/596243
|
||||
function M.color_is_bright(hex)
|
||||
if not hex then return false end
|
||||
local r, g, b = to_rgb(hex)
|
||||
-- If any of the colors are missing return false
|
||||
if not r or not g or not b then return false end
|
||||
-- Counting the perceptive luminance - human eye favors green color
|
||||
local luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
||||
-- If luminance is > 0.5 -> Bright colors, black font else Dark colors, white font
|
||||
return luminance > 0.5
|
||||
end
|
||||
|
||||
--- Get hex color
|
||||
---@param name string highlight group name
|
||||
---@param attr string attr name 'bg', 'fg'
|
||||
---@return string
|
||||
function M.get_hex(name, attr)
|
||||
local ok, hl = pcall(api.nvim_get_hl_by_name, name, true)
|
||||
if not ok then return "NONE" end
|
||||
hl.foreground = hl.foreground and "#" .. bit.tohex(hl.foreground, 6)
|
||||
hl.background = hl.background and "#" .. bit.tohex(hl.background, 6)
|
||||
attr = ({ bg = "background", fg = "foreground" })[attr] or attr
|
||||
return hl[attr] or "NONE"
|
||||
end
|
||||
|
||||
--- Check if background is bright
|
||||
--- @return boolean
|
||||
function M.is_bright_background()
|
||||
local bg_color = M.get_hex("Normal", "bg")
|
||||
return M.color_is_bright(bg_color)
|
||||
end
|
||||
|
||||
-----------------------------------------------------------
|
||||
-- Darken Terminal
|
||||
-----------------------------------------------------------
|
||||
|
||||
local function convert_attributes(result, key, value)
|
||||
local target = result
|
||||
if key == "cterm" then
|
||||
result.cterm = {}
|
||||
target = result.cterm
|
||||
end
|
||||
if value:find(",") then
|
||||
for _, v in vim.split(value, ",") do
|
||||
target[v] = true
|
||||
end
|
||||
else
|
||||
target[value] = true
|
||||
end
|
||||
end
|
||||
|
||||
local function convert_options(opts)
|
||||
local keys = {
|
||||
gui = true,
|
||||
guifg = "foreground",
|
||||
guibg = "background",
|
||||
guisp = "sp",
|
||||
cterm = "cterm",
|
||||
ctermfg = "ctermfg",
|
||||
ctermbg = "ctermbg",
|
||||
link = "link",
|
||||
}
|
||||
local result = {}
|
||||
for key, value in pairs(opts) do
|
||||
if keys[key] then
|
||||
if key == "gui" or key == "cterm" then
|
||||
if value ~= "NONE" then convert_attributes(result, key, value) end
|
||||
else
|
||||
result[keys[key]] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function M.set_hl(name, opts) api.nvim_set_hl(0, name, convert_options(opts)) end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,198 @@
|
||||
local fn = vim.fn
|
||||
local u = require("toggleterm.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
local p = {
|
||||
single = "'(.-)'",
|
||||
double = '"(.-)"',
|
||||
}
|
||||
|
||||
local is_windows = vim.loop.os_uname().version:match("Windows")
|
||||
|
||||
---@class ParsedArgs
|
||||
---@field direction string?
|
||||
---@field cmd string?
|
||||
---@field dir string?
|
||||
---@field size number?
|
||||
---@field name string?
|
||||
---@field go_back boolean?
|
||||
---@field open boolean?
|
||||
|
||||
---Take a users command arguments in the format "cmd='git commit' dir=~/dotfiles"
|
||||
---and parse this into a table of arguments
|
||||
---{cmd = "git commit", dir = "~/dotfiles"}
|
||||
---@see https://stackoverflow.com/a/27007701
|
||||
---@param args string
|
||||
---@return ParsedArgs
|
||||
function M.parse(args)
|
||||
local result = {}
|
||||
if args then
|
||||
local quotes = args:match(p.single) and p.single or args:match(p.double) and p.double or nil
|
||||
if quotes then
|
||||
-- 1. extract the quoted command
|
||||
local pattern = "(%S+)=" .. quotes
|
||||
for key, value in args:gmatch(pattern) do
|
||||
-- Check if the current OS is Windows so we can determine if +shellslash
|
||||
-- exists and if it exists, then determine if it is enabled. In that way,
|
||||
-- we can determine if we should match the value with single or double quotes.
|
||||
if is_windows then
|
||||
quotes = not vim.opt.shellslash:get() and quotes or p.single
|
||||
else
|
||||
quotes = p.single
|
||||
end
|
||||
value = fn.shellescape(value)
|
||||
result[vim.trim(key)] = fn.expandcmd(value:match(quotes))
|
||||
end
|
||||
-- 2. then remove it from the rest of the argument string
|
||||
args = args:gsub(pattern, "")
|
||||
end
|
||||
|
||||
for _, part in ipairs(vim.split(args, " ")) do
|
||||
if #part > 1 then
|
||||
local arg = vim.split(part, "=")
|
||||
local key, value = arg[1], arg[2]
|
||||
if key == "size" then
|
||||
value = tonumber(value)
|
||||
elseif key == "go_back" or key == "open" then
|
||||
value = value ~= "0"
|
||||
end
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Get a valid base path for a user provided path
|
||||
-- and an optional search term
|
||||
---@param typed_path string
|
||||
---@return string|nil, string|nil
|
||||
function M.get_path_parts(typed_path)
|
||||
if vim.fn.isdirectory(typed_path ~= "" and typed_path or ".") == 1 then
|
||||
-- The string is a valid path, we just need to drop trailing slashes to
|
||||
-- ease joining the base path with the suggestions
|
||||
return typed_path:gsub("/$", ""), nil
|
||||
elseif typed_path:find("/", 2) ~= nil then
|
||||
-- Maybe the typed path is looking for a nested directory
|
||||
-- we need to make sure it has at least one slash in it, and that is not
|
||||
-- from a root path
|
||||
local base_path = vim.fn.fnamemodify(typed_path, ":h")
|
||||
local search_term = vim.fn.fnamemodify(typed_path, ":t")
|
||||
if vim.fn.isdirectory(base_path) then return base_path, search_term end
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local term_exec_options = {
|
||||
--- Suggests commands
|
||||
---@param typed_cmd string|nil
|
||||
cmd = function(typed_cmd)
|
||||
local paths = vim.split(vim.env.PATH, ":")
|
||||
local commands = {}
|
||||
|
||||
for _, path in ipairs(paths) do
|
||||
local glob_str
|
||||
if string.match(path, "%s*") then
|
||||
--path with spaces
|
||||
glob_str = path:gsub(" ", "\\ ") .. "/" .. (typed_cmd or "") .. "*"
|
||||
else
|
||||
-- path without spaces
|
||||
glob_str = path .. "/" .. (typed_cmd or "") .. "*"
|
||||
end
|
||||
local dir_cmds = vim.split(vim.fn.glob(glob_str), "\n")
|
||||
|
||||
for _, cmd in ipairs(dir_cmds) do
|
||||
if not u.str_is_empty(cmd) then table.insert(commands, vim.fn.fnamemodify(cmd, ":t")) end
|
||||
end
|
||||
end
|
||||
|
||||
return commands
|
||||
end,
|
||||
--- Suggests paths in the cwd
|
||||
---@param typed_path string
|
||||
dir = function(typed_path)
|
||||
-- Read the typed path as the base for the directory search
|
||||
local base_path, search_term = M.get_path_parts(typed_path or "")
|
||||
local safe_path = base_path ~= "" and base_path or "."
|
||||
|
||||
local paths = vim.fn.readdir(
|
||||
safe_path,
|
||||
function(entry) return vim.fn.isdirectory(safe_path .. "/" .. entry) end
|
||||
)
|
||||
|
||||
if not u.str_is_empty(search_term) then
|
||||
paths = vim.tbl_filter(
|
||||
function(path) return path:match("^" .. search_term .. "*") ~= nil end,
|
||||
paths
|
||||
)
|
||||
end
|
||||
|
||||
return vim.tbl_map(
|
||||
function(path) return u.concat_without_empty({ base_path, path }, "/") end,
|
||||
paths
|
||||
)
|
||||
end,
|
||||
--- Suggests directions for the term
|
||||
---@param typed_direction string
|
||||
direction = function(typed_direction)
|
||||
local directions = {
|
||||
"float",
|
||||
"horizontal",
|
||||
"tab",
|
||||
"vertical",
|
||||
}
|
||||
if u.str_is_empty(typed_direction) then return directions end
|
||||
return vim.tbl_filter(
|
||||
function(direction) return direction:match("^" .. typed_direction .. "*") ~= nil end,
|
||||
directions
|
||||
)
|
||||
end,
|
||||
--- The size param takes in arbitrary numbers, we keep this function only to
|
||||
--- match the signature of other options
|
||||
size = function() return {} end,
|
||||
--- The name param takes in arbitrary strings, we keep this function only to
|
||||
--- match the signature of other options
|
||||
name = function() return {} end,
|
||||
}
|
||||
|
||||
local toggle_term_options = {
|
||||
dir = term_exec_options.dir,
|
||||
direction = term_exec_options.direction,
|
||||
size = term_exec_options.size,
|
||||
name = term_exec_options.name,
|
||||
}
|
||||
|
||||
---@param options table a dictionary of key to function
|
||||
---@return fun(lead: string, command: string, _: number)
|
||||
local function complete(options)
|
||||
---@param lead string the leading portion of the argument currently being completed on
|
||||
---@param command string the entire command line
|
||||
---@param _ number the cursor position in it (byte index)
|
||||
return function(lead, command, _)
|
||||
local parts = vim.split(lead, "=")
|
||||
local key = parts[1]
|
||||
local value = parts[2]
|
||||
if options[key] then
|
||||
return vim.tbl_map(function(option) return key .. "=" .. option end, options[key](value))
|
||||
end
|
||||
|
||||
local available_options = vim.tbl_filter(
|
||||
function(option) return command:match(" " .. option .. "=") == nil end,
|
||||
vim.tbl_keys(options)
|
||||
)
|
||||
|
||||
table.sort(available_options)
|
||||
|
||||
return vim.tbl_map(function(option) return option .. "=" end, available_options)
|
||||
end
|
||||
end
|
||||
|
||||
--- See :h :command-completion-custom
|
||||
M.term_exec_complete = complete(term_exec_options)
|
||||
|
||||
--- See :h :command-completion-custom
|
||||
M.toggle_term_complete = complete(toggle_term_options)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,146 @@
|
||||
local colors = require("toggleterm.colors")
|
||||
local constants = require("toggleterm.constants")
|
||||
local utils = require("toggleterm.utils")
|
||||
|
||||
local M = {}
|
||||
|
||||
local fmt = string.format
|
||||
|
||||
local function shade(color, factor) return colors.shade_color(color, factor) end
|
||||
|
||||
--- @alias ToggleTermHighlights table<string, table<string, string>>
|
||||
|
||||
---@class WinbarOpts
|
||||
---@field name_formatter fun(term: Terminal):string
|
||||
---@field enabled boolean
|
||||
|
||||
--- @class ToggleTermConfig
|
||||
--- @field size number
|
||||
--- @field shade_filetypes string[]
|
||||
--- @field hide_numbers boolean
|
||||
--- @field open_mapping string | string[]
|
||||
--- @field shade_terminals boolean
|
||||
--- @field insert_mappings boolean
|
||||
--- @field terminal_mappings boolean
|
||||
--- @field start_in_insert boolean
|
||||
--- @field persist_size boolean
|
||||
--- @field persist_mode boolean
|
||||
--- @field close_on_exit boolean
|
||||
--- @field direction '"horizontal"' | '"vertical"' | '"float"'
|
||||
--- @field shading_factor number
|
||||
--- @field shell string|fun():string
|
||||
--- @field auto_scroll boolean
|
||||
--- @field float_opts table<string, any>
|
||||
--- @field highlights ToggleTermHighlights
|
||||
--- @field winbar WinbarOpts
|
||||
--- @field autochdir boolean
|
||||
--- @field title_pos '"left"' | '"center"' | '"right"'
|
||||
|
||||
---@type ToggleTermConfig
|
||||
local config = {
|
||||
size = 12,
|
||||
shade_filetypes = {},
|
||||
hide_numbers = true,
|
||||
shade_terminals = true,
|
||||
insert_mappings = true,
|
||||
terminal_mappings = true,
|
||||
start_in_insert = true,
|
||||
persist_size = true,
|
||||
persist_mode = true,
|
||||
close_on_exit = true,
|
||||
direction = "horizontal",
|
||||
shading_factor = constants.shading_amount,
|
||||
shell = vim.o.shell,
|
||||
autochdir = false,
|
||||
auto_scroll = true,
|
||||
winbar = {
|
||||
enabled = false,
|
||||
name_formatter = function(term) return fmt("%d:%s", term.id, term:_display_name()) end,
|
||||
},
|
||||
float_opts = {
|
||||
winblend = 0,
|
||||
title_pos = "left",
|
||||
},
|
||||
}
|
||||
|
||||
---Derive the highlights for a toggleterm and merge these with the user's preferences
|
||||
---A few caveats must be noted. Since I link the normal and float border to the Normal
|
||||
---highlight this has to be done carefully as if the user has specified any Float highlights
|
||||
---themselves merging will result in a mix of user highlights and the link key which is invalid
|
||||
---so I check that they have not attempted to highlight these themselves. Also
|
||||
---if they have chosen to shade the terminal then this takes priority over their own highlights
|
||||
---since they can't have it both ways i.e. custom highlighting and shading
|
||||
---@param conf ToggleTermConfig
|
||||
---@return ToggleTermHighlights
|
||||
local function get_highlights(conf)
|
||||
local user = conf.highlights
|
||||
local defaults = {
|
||||
NormalFloat = vim.F.if_nil(user.NormalFloat, { link = "Normal" }),
|
||||
FloatBorder = vim.F.if_nil(user.FloatBorder, { link = "Normal" }),
|
||||
StatusLine = { gui = "NONE" },
|
||||
StatusLineNC = { cterm = "italic", gui = "NONE" },
|
||||
}
|
||||
local overrides = {}
|
||||
local nightly = utils.is_nightly()
|
||||
|
||||
local comment_fg = colors.get_hex("Comment", "fg")
|
||||
local dir_fg = colors.get_hex("Directory", "fg")
|
||||
|
||||
local winbar_inactive_opts = { guifg = comment_fg }
|
||||
local winbar_active_opts = { guifg = dir_fg, gui = "underline" }
|
||||
|
||||
if conf.shade_terminals then
|
||||
local is_bright = colors.is_bright_background()
|
||||
local degree = is_bright and -3 or 1
|
||||
local amount = conf.shading_factor * degree
|
||||
local normal_bg = colors.get_hex("Normal", "bg")
|
||||
local terminal_bg = conf.shade_terminals and shade(normal_bg, amount) or normal_bg
|
||||
|
||||
overrides = {
|
||||
Normal = { guibg = terminal_bg },
|
||||
SignColumn = { guibg = terminal_bg },
|
||||
EndOfBuffer = { guibg = terminal_bg },
|
||||
StatusLine = { guibg = terminal_bg },
|
||||
StatusLineNC = { guibg = terminal_bg },
|
||||
}
|
||||
-- TODO: Move this to the main overrides block once nvim 0.8 is stable
|
||||
if nightly then
|
||||
winbar_inactive_opts.guibg = terminal_bg
|
||||
winbar_active_opts.guibg = terminal_bg
|
||||
overrides.WinBarNC = { guibg = terminal_bg }
|
||||
overrides.WinBar = { guibg = terminal_bg }
|
||||
end
|
||||
end
|
||||
|
||||
if nightly and conf.winbar.enabled then
|
||||
colors.set_hl("WinBarActive", winbar_active_opts)
|
||||
colors.set_hl("WinBarInactive", winbar_inactive_opts)
|
||||
end
|
||||
|
||||
return vim.tbl_deep_extend("force", defaults, conf.highlights, overrides)
|
||||
end
|
||||
|
||||
--- get the full user config or just a specified value
|
||||
---@param key string?
|
||||
---@return any
|
||||
function M.get(key)
|
||||
if key then return config[key] end
|
||||
return config
|
||||
end
|
||||
|
||||
function M.reset_highlights() config.highlights = get_highlights(config) end
|
||||
|
||||
---@param user_conf ToggleTermConfig
|
||||
---@return ToggleTermConfig
|
||||
function M.set(user_conf)
|
||||
user_conf = user_conf or {}
|
||||
user_conf.highlights = user_conf.highlights or {}
|
||||
config = vim.tbl_deep_extend("force", config, user_conf)
|
||||
config.highlights = get_highlights(config)
|
||||
return config
|
||||
end
|
||||
|
||||
---@return ToggleTermConfig
|
||||
return setmetatable(M, {
|
||||
__index = function(_, k) return config[k] end,
|
||||
})
|
||||
@ -0,0 +1,11 @@
|
||||
local M = {}
|
||||
-----------------------------------------------------------
|
||||
-- Constants
|
||||
-----------------------------------------------------------
|
||||
M.FILETYPE = "toggleterm"
|
||||
-- -30 is a magic number based on manual testing of what looks good
|
||||
M.shading_amount = -30
|
||||
-- Highlight group name prefix
|
||||
M.highlight_group_name_prefix = "ToggleTerm"
|
||||
|
||||
return M
|
||||
@ -0,0 +1,17 @@
|
||||
local lazy = {}
|
||||
|
||||
--- Require on index.
|
||||
---
|
||||
--- Will only require the module after the first index of a module.
|
||||
--- Only works for modules that export a table.
|
||||
---@param require_path string
|
||||
---@return table
|
||||
lazy.require = function(require_path)
|
||||
return setmetatable({}, {
|
||||
__index = function(_, key) return require(require_path)[key] end,
|
||||
|
||||
__newindex = function(_, key, value) require(require_path)[key] = value end,
|
||||
})
|
||||
end
|
||||
|
||||
return lazy
|
||||
@ -0,0 +1,595 @@
|
||||
local M = {}
|
||||
|
||||
local lazy = require("toggleterm.lazy")
|
||||
---@module "toggleterm.ui"
|
||||
local ui = lazy.require("toggleterm.ui")
|
||||
---@module "toggleterm.config"
|
||||
local config = lazy.require("toggleterm.config")
|
||||
---@module "toggleterm.utils"
|
||||
local utils = lazy.require("toggleterm.utils")
|
||||
---@module "toggleterm.constants"
|
||||
local constants = lazy.require("toggleterm.constants")
|
||||
|
||||
local api = vim.api
|
||||
local fmt = string.format
|
||||
local fn = vim.fn
|
||||
|
||||
local mode = {
|
||||
INSERT = "i",
|
||||
NORMAL = "n",
|
||||
UNSUPPORTED = "?",
|
||||
}
|
||||
|
||||
local AUGROUP = api.nvim_create_augroup("ToggleTermBuffer", { clear = true })
|
||||
|
||||
local is_windows = fn.has("win32") == 1
|
||||
local function is_cmd(shell) return shell:find("cmd") end
|
||||
|
||||
local function is_pwsh(shell) return shell:find("pwsh") or shell:find("powershell") end
|
||||
|
||||
local function is_nushell(shell) return shell:find("nu") end
|
||||
|
||||
local function get_command_sep() return is_windows and is_cmd(vim.o.shell) and "&" or ";" end
|
||||
|
||||
local function get_comment_sep() return is_windows and is_cmd(vim.o.shell) and "::" or "#" end
|
||||
|
||||
local function get_newline_chr()
|
||||
local shell = config.get("shell")
|
||||
if type(shell) == "function" then shell = shell() end
|
||||
if is_windows then
|
||||
return is_pwsh(shell) and "\r" or "\r\n"
|
||||
elseif is_nushell(shell) then
|
||||
return "\r"
|
||||
else
|
||||
return "\n"
|
||||
end
|
||||
end
|
||||
|
||||
---@alias Mode "n" | "i" | "?"
|
||||
|
||||
--- @class TerminalState
|
||||
--- @field mode Mode
|
||||
|
||||
---@type Terminal[]
|
||||
local terminals = {}
|
||||
|
||||
--- @class TermCreateArgs
|
||||
--- @field newline_chr? string user specified newline chararacter
|
||||
--- @field cmd? string a custom command to run
|
||||
--- @field direction? string the layout style for the terminal
|
||||
--- @field id number?
|
||||
--- @field highlights table<string, table<string, string>>?
|
||||
--- @field dir string? the directory for the terminal
|
||||
--- @field count number? the count that triggers that specific terminal
|
||||
--- @field display_name string?
|
||||
--- @field hidden boolean? whether or not to include this terminal in the terminals list
|
||||
--- @field close_on_exit boolean? whether or not to close the terminal window when the process exits
|
||||
--- @field auto_scroll boolean? whether or not to scroll down on terminal output
|
||||
--- @field float_opts table<string, any>?
|
||||
--- @field on_stdout fun(t: Terminal, job: number, data: string[]?, name: string?)?
|
||||
--- @field on_stderr fun(t: Terminal, job: number, data: string[], name: string)?
|
||||
--- @field on_exit fun(t: Terminal, job: number, exit_code: number?, name: string?)?
|
||||
--- @field on_create fun(term:Terminal)?
|
||||
--- @field on_open fun(term:Terminal)?
|
||||
--- @field on_close fun(term:Terminal)?
|
||||
|
||||
--- @class Terminal
|
||||
--- @field newline_chr string
|
||||
--- @field cmd string
|
||||
--- @field direction string the layout style for the terminal
|
||||
--- @field id number
|
||||
--- @field bufnr number
|
||||
--- @field window number
|
||||
--- @field job_id number
|
||||
--- @field highlights table<string, table<string, string>>
|
||||
--- @field dir string the directory for the terminal
|
||||
--- @field name string the name of the terminal
|
||||
--- @field count number the count that triggers that specific terminal
|
||||
--- @field hidden boolean whether or not to include this terminal in the terminals list
|
||||
--- @field close_on_exit boolean? whether or not to close the terminal window when the process exits
|
||||
--- @field auto_scroll boolean? whether or not to scroll down on terminal output
|
||||
--- @field float_opts table<string, any>?
|
||||
--- @field display_name string?
|
||||
--- @field env table<string, string> environmental variables passed to jobstart()
|
||||
--- @field clear_env boolean use clean job environment, passed to jobstart()
|
||||
--- @field on_stdout fun(t: Terminal, job: number, data: string[]?, name: string?)?
|
||||
--- @field on_stderr fun(t: Terminal, job: number, data: string[], name: string)?
|
||||
--- @field on_exit fun(t: Terminal, job: number, exit_code: number?, name: string?)?
|
||||
--- @field on_create fun(term:Terminal)?
|
||||
--- @field on_open fun(term:Terminal)?
|
||||
--- @field on_close fun(term:Terminal)?
|
||||
--- @field _display_name fun(term: Terminal): string
|
||||
--- @field __state TerminalState
|
||||
local Terminal = {}
|
||||
|
||||
--- Get the next available id based on the next number in the sequence that
|
||||
--- hasn't already been allocated e.g. in a list of {1,2,5,6} the next id should
|
||||
--- be 3 then 4 then 7
|
||||
---@return integer
|
||||
local function next_id()
|
||||
local all = M.get_all(true)
|
||||
for index, term in pairs(all) do
|
||||
if index ~= term.id then return index end
|
||||
end
|
||||
return #all + 1
|
||||
end
|
||||
|
||||
---Get an opened (valid) toggle terminal by id, defaults to the first opened
|
||||
---@param position number?
|
||||
---@return number?
|
||||
function M.get_toggled_id(position)
|
||||
position = position or 1
|
||||
local t = M.get_all()
|
||||
return t[position] and t[position].id or nil
|
||||
end
|
||||
|
||||
---Return currently focused terminal id.
|
||||
---@return number?
|
||||
function M.get_focused_id()
|
||||
for _, term in pairs(terminals) do
|
||||
if term:is_focused() then return term.id end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function M.get_last_focused()
|
||||
local last_focus = ui.get_terminal_view().focus_term_id
|
||||
return M.get(last_focus, true)
|
||||
end
|
||||
|
||||
--- @param bufnr number
|
||||
local function setup_buffer_mappings(bufnr)
|
||||
local mapping = config.open_mapping
|
||||
if mapping and config.terminal_mappings then
|
||||
utils.key_map("t", mapping, "<Cmd>ToggleTerm<CR>", { buffer = bufnr, silent = true })
|
||||
end
|
||||
end
|
||||
|
||||
---@param id number terminal id
|
||||
local function on_vim_resized(id)
|
||||
local term = M.get(id, true)
|
||||
if not term or not term:is_float() or not term:is_open() then return end
|
||||
ui.update_float(term)
|
||||
end
|
||||
|
||||
--- Remove the in memory reference to the no longer open terminal
|
||||
--- @param num number
|
||||
local function delete(num)
|
||||
if terminals[num] then terminals[num] = nil end
|
||||
end
|
||||
|
||||
---Terminal buffer autocommands
|
||||
---@param term Terminal
|
||||
local function setup_buffer_autocommands(term)
|
||||
api.nvim_create_autocmd("TermClose", {
|
||||
buffer = term.bufnr,
|
||||
group = AUGROUP,
|
||||
callback = function() delete(term.id) end,
|
||||
})
|
||||
if term:is_float() then
|
||||
api.nvim_create_autocmd("VimResized", {
|
||||
buffer = term.bufnr,
|
||||
group = AUGROUP,
|
||||
callback = function() on_vim_resized(term.id) end,
|
||||
})
|
||||
end
|
||||
|
||||
if config.start_in_insert then
|
||||
-- Avoid entering insert mode when spawning terminal in the background
|
||||
if term.window == api.nvim_get_current_win() then vim.cmd("startinsert") end
|
||||
end
|
||||
end
|
||||
|
||||
---get the directory for the terminal parsing special arguments
|
||||
---@param dir string?
|
||||
---@return string
|
||||
local function _get_dir(dir)
|
||||
if dir == "git_dir" then dir = utils.git_dir() end
|
||||
if dir then
|
||||
return fn.expand(dir)
|
||||
else
|
||||
return vim.loop.cwd()
|
||||
end
|
||||
end
|
||||
|
||||
---Create a new terminal object
|
||||
---@param term TermCreateArgs?
|
||||
---@return Terminal
|
||||
function Terminal:new(term)
|
||||
term = term or {}
|
||||
--- If we try to create a new terminal, but the id is already
|
||||
--- taken, return the terminal with the containing id
|
||||
local id = term.count or term.id
|
||||
if id and terminals[id] then return terminals[id] end
|
||||
local conf = config.get()
|
||||
self.__index = self
|
||||
term.newline_chr = term.newline_chr or get_newline_chr()
|
||||
term.direction = term.direction or conf.direction
|
||||
term.id = id or next_id()
|
||||
term.display_name = term.display_name
|
||||
term.float_opts = vim.tbl_deep_extend("keep", term.float_opts or {}, conf.float_opts)
|
||||
term.clear_env = term.clear_env
|
||||
term.auto_scroll = vim.F.if_nil(term.auto_scroll, conf.auto_scroll)
|
||||
term.env = vim.F.if_nil(term.env, conf.env)
|
||||
term.hidden = vim.F.if_nil(term.hidden, false)
|
||||
term.on_create = vim.F.if_nil(term.on_create, conf.on_create)
|
||||
term.on_open = vim.F.if_nil(term.on_open, conf.on_open)
|
||||
term.on_close = vim.F.if_nil(term.on_close, conf.on_close)
|
||||
term.on_stdout = vim.F.if_nil(term.on_stdout, conf.on_stdout)
|
||||
term.on_stderr = vim.F.if_nil(term.on_stderr, conf.on_stderr)
|
||||
term.on_exit = vim.F.if_nil(term.on_exit, conf.on_exit)
|
||||
term.__state = { mode = "?" }
|
||||
if term.close_on_exit == nil then term.close_on_exit = conf.close_on_exit end
|
||||
-- Add the newly created terminal to the list of all terminals
|
||||
---@diagnostic disable-next-line: return-type-mismatch
|
||||
return setmetatable(term, self)
|
||||
end
|
||||
|
||||
---@package
|
||||
---Add a terminal to the list of terminals
|
||||
function Terminal:__add()
|
||||
if terminals[self.id] and terminals[self.id] ~= self then self.id = next_id() end
|
||||
if not terminals[self.id] then terminals[self.id] = self end
|
||||
return self
|
||||
end
|
||||
|
||||
function Terminal:is_float() return self.direction == "float" and ui.is_float(self.window) end
|
||||
|
||||
function Terminal:is_split()
|
||||
return (self.direction == "vertical" or self.direction == "horizontal")
|
||||
and not ui.is_float(self.window)
|
||||
end
|
||||
|
||||
function Terminal:is_tab() return self.direction == "tab" and not ui.is_float(self.window) end
|
||||
|
||||
function Terminal:resize(size)
|
||||
if self:is_split() then ui.resize_split(self, size) end
|
||||
end
|
||||
|
||||
function Terminal:is_open()
|
||||
if not self.window then return false end
|
||||
local win_type = fn.win_gettype(self.window)
|
||||
-- empty string window type corresponds to a normal window
|
||||
local win_open = win_type == "" or win_type == "popup"
|
||||
return win_open and api.nvim_win_get_buf(self.window) == self.bufnr
|
||||
end
|
||||
|
||||
---@package
|
||||
function Terminal:__restore_mode() self:set_mode(self.__state.mode) end
|
||||
|
||||
--- Set the terminal's mode
|
||||
---@param m Mode
|
||||
function Terminal:set_mode(m)
|
||||
if m == mode.INSERT then
|
||||
vim.cmd("startinsert")
|
||||
elseif m == mode.NORMAL then
|
||||
vim.cmd("stopinsert")
|
||||
elseif m == mode.UNSUPPORTED and config.get("start_in_insert") then
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
end
|
||||
|
||||
function Terminal:persist_mode()
|
||||
local raw_mode = api.nvim_get_mode().mode
|
||||
local m = "?"
|
||||
if raw_mode:match("nt") then -- nt is normal mode in the terminal
|
||||
m = mode.NORMAL
|
||||
elseif raw_mode:match("t") then -- t is insert mode in the terminal
|
||||
m = mode.INSERT
|
||||
end
|
||||
self.__state.mode = m
|
||||
end
|
||||
|
||||
---@package
|
||||
function Terminal:_display_name() return self.display_name or vim.split(self.name, ";")[1] end
|
||||
|
||||
function Terminal:close()
|
||||
if self.on_close then self:on_close() end
|
||||
ui.close(self)
|
||||
ui.stopinsert()
|
||||
ui.update_origin_window(self.window)
|
||||
end
|
||||
|
||||
function Terminal:shutdown()
|
||||
if self:is_open() then self:close() end
|
||||
ui.delete_buf(self)
|
||||
delete(self.id)
|
||||
end
|
||||
|
||||
---Combine arguments into strings separated by new lines
|
||||
---@vararg string
|
||||
---@param newline_chr string
|
||||
---@return string
|
||||
local function with_cr(newline_chr, ...)
|
||||
local result = {}
|
||||
for _, str in ipairs({ ... }) do
|
||||
table.insert(result, str .. newline_chr)
|
||||
end
|
||||
return table.concat(result, "")
|
||||
end
|
||||
|
||||
function Terminal:scroll_bottom()
|
||||
if not api.nvim_buf_is_loaded(self.bufnr) or not api.nvim_buf_is_valid(self.bufnr) then return end
|
||||
if ui.term_has_open_win(self) then api.nvim_buf_call(self.bufnr, ui.scroll_to_bottom) end
|
||||
end
|
||||
|
||||
function Terminal:is_focused() return self.window == api.nvim_get_current_win() end
|
||||
|
||||
function Terminal:focus()
|
||||
if ui.term_has_open_win(self) then api.nvim_set_current_win(self.window) end
|
||||
end
|
||||
|
||||
---Send a command to a running terminal
|
||||
---@param cmd string|string[]
|
||||
---@param go_back boolean? whether or not to return to original window
|
||||
function Terminal:send(cmd, go_back)
|
||||
cmd = type(cmd) == "table" and with_cr(self.newline_chr, unpack(cmd))
|
||||
or with_cr(self.newline_chr, cmd --[[@as string]])
|
||||
fn.chansend(self.job_id, cmd)
|
||||
self:scroll_bottom()
|
||||
if go_back and self:is_focused() then
|
||||
ui.goto_previous()
|
||||
ui.stopinsert()
|
||||
elseif not go_back and not self:is_focused() then
|
||||
self:focus()
|
||||
end
|
||||
end
|
||||
|
||||
--check for os type and perform os specific clear command
|
||||
function Terminal:clear()
|
||||
local clear = is_windows and "cls" or "clear"
|
||||
self:send(clear)
|
||||
end
|
||||
|
||||
---Update the directory of an already opened terminal
|
||||
---@param dir string
|
||||
function Terminal:change_dir(dir, go_back)
|
||||
dir = _get_dir(dir)
|
||||
if self.dir == dir then return end
|
||||
self:send({ fmt("cd %s", dir), self:clear() }, go_back)
|
||||
self.dir = dir
|
||||
end
|
||||
|
||||
---Update the direction of an already opened terminal
|
||||
---@param direction string
|
||||
function Terminal:change_direction(direction)
|
||||
self.direction = direction
|
||||
self.window = nil
|
||||
end
|
||||
|
||||
--- Handle when a terminal process exits
|
||||
---@param term Terminal
|
||||
local function __handle_exit(term)
|
||||
return function(...)
|
||||
if term.on_exit then term:on_exit(...) end
|
||||
if term.close_on_exit then
|
||||
term:close()
|
||||
if api.nvim_buf_is_loaded(term.bufnr) then
|
||||
api.nvim_buf_delete(term.bufnr, { force = true })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---Prepare callback for terminal output handling
|
||||
---If `auto_scroll` is active, will create a handler that scrolls on terminal output
|
||||
---If `handler` is present, will call it passing `self` as the first parameter
|
||||
---If none of the above is applicable, will not return a handler
|
||||
---@param handler function? a custom callback function for output handling
|
||||
function Terminal:__make_output_handler(handler)
|
||||
if self.auto_scroll or handler then
|
||||
return function(...)
|
||||
if self.auto_scroll then self:scroll_bottom() end
|
||||
if handler then handler(self, ...) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function Terminal:__spawn()
|
||||
local cmd = self.cmd or config.get("shell")
|
||||
if type(cmd) == "function" then cmd = cmd() end
|
||||
local command_sep = get_command_sep()
|
||||
local comment_sep = get_comment_sep()
|
||||
cmd = table.concat({
|
||||
cmd,
|
||||
command_sep,
|
||||
comment_sep,
|
||||
constants.FILETYPE,
|
||||
comment_sep,
|
||||
self.id,
|
||||
})
|
||||
local dir = _get_dir(self.dir)
|
||||
self.job_id = fn.termopen(cmd, {
|
||||
detach = 1,
|
||||
cwd = dir,
|
||||
on_exit = __handle_exit(self),
|
||||
on_stdout = self:__make_output_handler(self.on_stdout),
|
||||
on_stderr = self:__make_output_handler(self.on_stderr),
|
||||
env = self.env,
|
||||
clear_env = self.clear_env,
|
||||
})
|
||||
self.name = cmd
|
||||
self.dir = dir
|
||||
end
|
||||
|
||||
---@package
|
||||
---Add an orphaned terminal to the list of terminal and re-apply settings
|
||||
function Terminal:__resurrect()
|
||||
self:__add()
|
||||
if self:is_split() then ui.resize_split(self) end
|
||||
-- set the window options including fixing height or width once the window is resized
|
||||
self:__set_options()
|
||||
ui.hl_term(self)
|
||||
end
|
||||
|
||||
---@package
|
||||
function Terminal:__set_ft_options()
|
||||
local buf = vim.bo[self.bufnr]
|
||||
buf.filetype = constants.FILETYPE
|
||||
buf.buflisted = false
|
||||
end
|
||||
|
||||
---@package
|
||||
function Terminal:__set_win_options()
|
||||
if self:is_split() then
|
||||
local field = self.direction == "vertical" and "winfixwidth" or "winfixheight"
|
||||
utils.wo_setlocal(self.window, field, true)
|
||||
end
|
||||
|
||||
if config.hide_numbers then
|
||||
utils.wo_setlocal(self.window, "number", false)
|
||||
utils.wo_setlocal(self.window, "relativenumber", false)
|
||||
end
|
||||
end
|
||||
|
||||
---@package
|
||||
function Terminal:__set_options()
|
||||
self:__set_ft_options()
|
||||
self:__set_win_options()
|
||||
vim.b[self.bufnr].toggle_number = self.id
|
||||
end
|
||||
|
||||
---Open a terminal in a type of window i.e. a split,full window or tab
|
||||
---@param size number
|
||||
---@param term table
|
||||
local function opener(size, term)
|
||||
local direction = term.direction
|
||||
if term:is_split() then
|
||||
ui.open_split(size, term)
|
||||
elseif direction == "tab" then
|
||||
ui.open_tab(term)
|
||||
elseif direction == "float" then
|
||||
ui.open_float(term)
|
||||
else
|
||||
error("Invalid terminal direction")
|
||||
end
|
||||
end
|
||||
|
||||
---Spawn terminal background job in a buffer without a window
|
||||
function Terminal:spawn()
|
||||
if not self.bufnr or not api.nvim_buf_is_valid(self.bufnr) then self.bufnr = ui.create_buf() end
|
||||
self:__add()
|
||||
if api.nvim_get_current_buf() ~= self.bufnr then
|
||||
api.nvim_buf_call(self.bufnr, function() self:__spawn() end)
|
||||
else
|
||||
self:__spawn()
|
||||
end
|
||||
setup_buffer_autocommands(self)
|
||||
setup_buffer_mappings(self.bufnr)
|
||||
if self.on_create then self:on_create() end
|
||||
end
|
||||
|
||||
---Open a terminal window
|
||||
---@param size number?
|
||||
---@param direction string?
|
||||
function Terminal:open(size, direction)
|
||||
local cwd = fn.getcwd()
|
||||
self.dir = _get_dir(config.autochdir and cwd or self.dir)
|
||||
ui.set_origin_window()
|
||||
if direction then self:change_direction(direction) end
|
||||
if not self.bufnr or not api.nvim_buf_is_valid(self.bufnr) then
|
||||
local ok, err = pcall(opener, size, self)
|
||||
if not ok and err then return utils.notify(err, "error") end
|
||||
self:spawn()
|
||||
else
|
||||
local ok, err = pcall(opener, size, self)
|
||||
if not ok and err then return utils.notify(err, "error") end
|
||||
ui.switch_buf(self.bufnr)
|
||||
if config.autochdir and self.dir ~= cwd then self:change_dir(cwd) end
|
||||
end
|
||||
ui.hl_term(self)
|
||||
-- NOTE: it is important that this function is called at this point. i.e. the buffer has been correctly assigned
|
||||
if self.on_open then self:on_open() end
|
||||
end
|
||||
|
||||
---Open if closed and close if opened
|
||||
---@param size number?
|
||||
---@param direction string?
|
||||
function Terminal:toggle(size, direction)
|
||||
if self:is_open() then
|
||||
self:close()
|
||||
else
|
||||
self:open(size, direction)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
--- get the toggle term number from
|
||||
--- the name e.g. term://~/.dotfiles//3371887:/usr/bin/zsh;#toggleterm#1
|
||||
--- the number in this case is 1
|
||||
--- @param name string?
|
||||
--- @return number?
|
||||
--- @return Terminal?
|
||||
function M.identify(name)
|
||||
name = name or api.nvim_buf_get_name(api.nvim_get_current_buf())
|
||||
local comment_sep = get_comment_sep()
|
||||
local parts = vim.split(name, comment_sep)
|
||||
local id = tonumber(parts[#parts])
|
||||
return id, terminals[id]
|
||||
end
|
||||
|
||||
---get existing terminal or create an empty term table
|
||||
---@param num number?
|
||||
---@param dir string?
|
||||
---@param direction string?
|
||||
---@param name string?
|
||||
---@return Terminal
|
||||
---@return boolean
|
||||
function M.get_or_create_term(num, dir, direction, name)
|
||||
local term = M.get(num)
|
||||
if term then return term, false end
|
||||
if dir and fn.isdirectory(fn.expand(dir)) == 0 then dir = nil end
|
||||
return Terminal:new({ id = num, dir = dir, direction = direction, display_name = name }), true
|
||||
end
|
||||
|
||||
---Get a single terminal by id, unless it is hidden
|
||||
---@param id number?
|
||||
---@param include_hidden boolean? whether or nor to filter out hidden
|
||||
---@return Terminal?
|
||||
function M.get(id, include_hidden)
|
||||
local term = terminals[id]
|
||||
return (term and (include_hidden == true or not term.hidden)) and term or nil
|
||||
end
|
||||
|
||||
---Get the first terminal that matches a predicate
|
||||
---@param predicate fun(term: Terminal): boolean
|
||||
---@return Terminal?
|
||||
function M.find(predicate)
|
||||
if type(predicate) ~= "function" then
|
||||
utils.notify("terminal.find expects a function, got " .. type(predicate), "error")
|
||||
return
|
||||
end
|
||||
for _, term in pairs(terminals) do
|
||||
if predicate(term) then return term end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---Return the potentially non contiguous map of terminals as a sorted array
|
||||
---@param include_hidden boolean? whether or nor to filter out hidden
|
||||
---@return Terminal[]
|
||||
function M.get_all(include_hidden)
|
||||
local result = {}
|
||||
for _, v in pairs(terminals) do
|
||||
if include_hidden or (not include_hidden and not v.hidden) then table.insert(result, v) end
|
||||
end
|
||||
table.sort(result, function(a, b) return a.id < b.id end)
|
||||
return result
|
||||
end
|
||||
|
||||
if _G.IS_TEST then
|
||||
function M.__reset()
|
||||
for _, term in pairs(terminals) do
|
||||
term:shutdown()
|
||||
end
|
||||
end
|
||||
|
||||
M.__next_id = next_id
|
||||
end
|
||||
|
||||
M.Terminal = Terminal
|
||||
M.mode = mode
|
||||
|
||||
return M
|
||||
@ -0,0 +1,472 @@
|
||||
local M = {}
|
||||
|
||||
local lazy = require("toggleterm.lazy")
|
||||
---@module "toggleterm.constants"
|
||||
local constants = lazy.require("toggleterm.constants")
|
||||
---@module "toggleterm.utils"
|
||||
local utils = lazy.require("toggleterm.utils")
|
||||
---@module "toggleterm.colors"
|
||||
local colors = lazy.require("toggleterm.colors")
|
||||
---@module "toggleterm.config"
|
||||
local config = lazy.require("toggleterm.config")
|
||||
---@module "toggleterm.terminal"
|
||||
local terms = lazy.require("toggleterm.terminal")
|
||||
|
||||
local fn = vim.fn
|
||||
local fmt = string.format
|
||||
local api = vim.api
|
||||
|
||||
local origin_window
|
||||
local persistent = {}
|
||||
|
||||
---@alias TerminalView {terminals: number[], focus_term_id: number}
|
||||
|
||||
---@type TerminalView
|
||||
local terminal_view = {
|
||||
---@type number[]
|
||||
-- A list of terminal IDs that are saved from the view on smart toggle.
|
||||
terminals = {},
|
||||
---@type number
|
||||
---Last focused terminal ID in the view.
|
||||
focus_term_id = nil,
|
||||
}
|
||||
|
||||
--- @class TerminalWindow
|
||||
--- @field term_id number ID for the terminal in the window
|
||||
--- @field window number window handle
|
||||
--
|
||||
--- Save the size of a split window before it is hidden
|
||||
--- @param direction string
|
||||
--- @param window number
|
||||
function M.save_window_size(direction, window)
|
||||
if direction == "horizontal" then
|
||||
persistent.horizontal = api.nvim_win_get_height(window)
|
||||
elseif direction == "vertical" then
|
||||
persistent.vertical = api.nvim_win_get_width(window)
|
||||
end
|
||||
end
|
||||
|
||||
--- Explicitly set the persistent size of a direction
|
||||
--- @param direction string
|
||||
--- @param size number
|
||||
function M.save_direction_size(direction, size) persistent[direction] = size end
|
||||
|
||||
--- @param direction string
|
||||
--- @return boolean
|
||||
function M.has_saved_size(direction) return persistent[direction] ~= nil end
|
||||
|
||||
--- Get the size of the split. Order of priority is as follows:
|
||||
--- 1. The size argument is a valid number > 0
|
||||
--- 2. There is persistent width/height information from prev open state
|
||||
--- 3. Default/base case config size
|
||||
---
|
||||
--- If `config.persist_size = false` then option `2` in the
|
||||
--- list is skipped.
|
||||
--- @param size number?
|
||||
--- @param direction string?
|
||||
function M.get_size(size, direction)
|
||||
local valid_size = size ~= nil and size > 0
|
||||
if not config.persist_size then return valid_size and size or config.size end
|
||||
return valid_size and size or persistent[direction] or config.size
|
||||
end
|
||||
|
||||
local function hl(name) return "%#" .. name .. "#" end
|
||||
|
||||
local hl_end = "%*"
|
||||
|
||||
--- Create terminal window bar
|
||||
---@param id number
|
||||
---@return string
|
||||
function M.winbar(id)
|
||||
local terms = require("toggleterm.terminal").get_all()
|
||||
local str = " "
|
||||
for _, t in pairs(terms) do
|
||||
local h = id == t.id and "WinBarActive" or "WinBarInactive"
|
||||
str = str
|
||||
.. fmt("%%%d@v:lua.___toggleterm_winbar_click@", t.id)
|
||||
.. hl(h)
|
||||
.. config.winbar.name_formatter(t)
|
||||
.. hl_end
|
||||
.. " "
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
---@param term Terminal?
|
||||
function M.set_winbar(term)
|
||||
if
|
||||
not config.winbar.enabled
|
||||
or not term
|
||||
or term:is_float() -- TODO: make this configurable
|
||||
or fn.exists("+winbar") ~= 1
|
||||
or not term.window
|
||||
or not api.nvim_win_is_valid(term.window)
|
||||
then
|
||||
return
|
||||
end
|
||||
local value = fmt('%%{%%v:lua.require("toggleterm.ui").winbar(%d)%%}', term.id)
|
||||
utils.wo_setlocal(term.window, "winbar", value)
|
||||
end
|
||||
|
||||
---apply highlights to a terminal
|
||||
---if no term is passed in we use default values instead
|
||||
---@param term Terminal?
|
||||
function M.hl_term(term)
|
||||
local hls = (term and term.highlights and not vim.tbl_isempty(term.highlights))
|
||||
and term.highlights
|
||||
or config.highlights
|
||||
|
||||
if not hls or vim.tbl_isempty(hls) then return end
|
||||
|
||||
local window = term and term.window or api.nvim_get_current_win()
|
||||
local id = term and term.id or "Default"
|
||||
local is_float = M.is_float(window)
|
||||
|
||||
-- If the terminal is a floating window we only want to set the background and border
|
||||
-- not the statusline etc. which are not applicable to floating windows
|
||||
local hl_names = vim.tbl_filter(
|
||||
function(name)
|
||||
return not is_float or (is_float and vim.tbl_contains({ "FloatBorder", "NormalFloat" }, name))
|
||||
end,
|
||||
vim.tbl_keys(hls)
|
||||
)
|
||||
|
||||
local highlights = vim.tbl_map(function(hl_group_name)
|
||||
local name = constants.highlight_group_name_prefix .. id .. hl_group_name
|
||||
local hi_target = fmt("%s:%s", hl_group_name, name)
|
||||
local attrs = hls[hl_group_name]
|
||||
attrs.default = true
|
||||
colors.set_hl(name, attrs)
|
||||
return hi_target
|
||||
end, hl_names)
|
||||
|
||||
utils.wo_setlocal(window, "winhighlight", table.concat(highlights, ","))
|
||||
end
|
||||
|
||||
---Create a terminal buffer with the correct buffer/window options
|
||||
---then set it to current window
|
||||
---@param term Terminal
|
||||
local function create_term_buf_if_needed(term)
|
||||
local valid_win = term.window and api.nvim_win_is_valid(term.window)
|
||||
local window = valid_win and term.window or api.nvim_get_current_win()
|
||||
-- If the buffer doesn't exist create a new one
|
||||
local valid_buf = term.bufnr and api.nvim_buf_is_valid(term.bufnr)
|
||||
local bufnr = valid_buf and term.bufnr or api.nvim_create_buf(false, false)
|
||||
-- Assign buf to window to ensure window options are set correctly
|
||||
api.nvim_win_set_buf(window, bufnr)
|
||||
term.window, term.bufnr = window, bufnr
|
||||
term:__set_options()
|
||||
api.nvim_set_current_buf(bufnr)
|
||||
end
|
||||
|
||||
function M.create_buf() return api.nvim_create_buf(false, false) end
|
||||
|
||||
function M.delete_buf(term)
|
||||
if term.bufnr and api.nvim_buf_is_valid(term.bufnr) then
|
||||
api.nvim_buf_delete(term.bufnr, { force = true })
|
||||
end
|
||||
end
|
||||
|
||||
function M.set_origin_window() origin_window = api.nvim_get_current_win() end
|
||||
|
||||
function M.get_origin_window() return origin_window end
|
||||
|
||||
function M.update_origin_window(term_window)
|
||||
local curr_win = api.nvim_get_current_win()
|
||||
if term_window ~= curr_win then origin_window = curr_win end
|
||||
end
|
||||
|
||||
function M.scroll_to_bottom()
|
||||
local info = vim.api.nvim_get_mode()
|
||||
if info and (info.mode == "n" or info.mode == "nt") then vim.cmd("normal! G") end
|
||||
end
|
||||
|
||||
function M.goto_previous() vim.cmd("wincmd p") end
|
||||
|
||||
function M.stopinsert() vim.cmd("stopinsert!") end
|
||||
|
||||
---@param buf integer
|
||||
---@return boolean
|
||||
local function default_compare(buf)
|
||||
return vim.bo[buf].filetype == constants.FILETYPE or vim.b[buf].toggle_number ~= nil
|
||||
end
|
||||
|
||||
--- Find the first open terminal window
|
||||
--- by iterating all windows and matching the
|
||||
--- containing buffers filetype with the passed in
|
||||
--- comparator function or the default which matches
|
||||
--- the filetype
|
||||
--- @param comparator function?
|
||||
--- @return boolean, TerminalWindow[]
|
||||
function M.find_open_windows(comparator)
|
||||
comparator = comparator or default_compare
|
||||
local term_wins, is_open = {}, false
|
||||
for _, tab in ipairs(api.nvim_list_tabpages()) do
|
||||
for _, win in pairs(api.nvim_tabpage_list_wins(tab)) do
|
||||
local buf = api.nvim_win_get_buf(win)
|
||||
if comparator(buf) then
|
||||
is_open = true
|
||||
table.insert(term_wins, { window = win, term_id = vim.b[buf].toggle_number })
|
||||
end
|
||||
end
|
||||
end
|
||||
return is_open, term_wins
|
||||
end
|
||||
|
||||
---Switch to the given buffer without changing the alternate
|
||||
---@param buf number
|
||||
function M.switch_buf(buf)
|
||||
-- don't change the alternate buffer so that <c-^><c-^> does nothing in the terminal split
|
||||
local cur_buf = api.nvim_get_current_buf()
|
||||
if cur_buf ~= buf then vim.cmd(fmt("keepalt buffer %d", buf)) end
|
||||
end
|
||||
|
||||
local split_commands = {
|
||||
horizontal = {
|
||||
existing = "rightbelow vsplit",
|
||||
new = "botright split",
|
||||
resize = "resize",
|
||||
},
|
||||
vertical = {
|
||||
existing = "rightbelow split",
|
||||
new = "botright vsplit",
|
||||
resize = "vertical resize",
|
||||
},
|
||||
}
|
||||
|
||||
---Guess whether or not the window is a horizontal or vertical split
|
||||
---this only works if either of the two are full size
|
||||
---@return string?
|
||||
function M.guess_direction()
|
||||
-- current window is full height vertical split
|
||||
-- NOTE: add one for tabline and one for status
|
||||
local ui_lines = (vim.o.tabline ~= "" and 1 or 0) + (vim.o.laststatus ~= 0 and 1 or 0)
|
||||
if api.nvim_win_get_height(0) + vim.o.cmdheight + ui_lines == vim.o.lines then
|
||||
return "vertical"
|
||||
end
|
||||
-- current window is full width horizontal split
|
||||
if api.nvim_win_get_width(0) == vim.o.columns then return "horizontal" end
|
||||
return nil
|
||||
end
|
||||
|
||||
--- @private
|
||||
--- @param size number|function
|
||||
--- @param term Terminal?
|
||||
--- @return number?
|
||||
function M._resolve_size(size, term)
|
||||
if not size then
|
||||
return
|
||||
elseif type(size) == "number" then
|
||||
return size
|
||||
elseif term and type(size) == "function" then
|
||||
return size(term)
|
||||
end
|
||||
utils.notify(fmt('The input %s is not of type "number" or "function".', size), "error")
|
||||
end
|
||||
|
||||
local curved = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }
|
||||
|
||||
--- @private
|
||||
--- @param term Terminal
|
||||
--- @param opening boolean
|
||||
function M._get_float_config(term, opening)
|
||||
local opts = term.float_opts or {}
|
||||
local border = opts.border == "curved" and curved or opts.border or "single"
|
||||
|
||||
local width = math.ceil(math.min(vim.o.columns, math.max(80, vim.o.columns - 20)))
|
||||
local height = math.ceil(math.min(vim.o.lines, math.max(20, vim.o.lines - 10)))
|
||||
|
||||
width = vim.F.if_nil(M._resolve_size(opts.width, term), width)
|
||||
height = vim.F.if_nil(M._resolve_size(opts.height, term), height)
|
||||
|
||||
local row = math.ceil(vim.o.lines - height) * 0.5 - 1
|
||||
local col = math.ceil(vim.o.columns - width) * 0.5 - 1
|
||||
|
||||
row = vim.F.if_nil(M._resolve_size(opts.row, term), row)
|
||||
col = vim.F.if_nil(M._resolve_size(opts.col, term), col)
|
||||
|
||||
local version = vim.version()
|
||||
|
||||
local float_config = {
|
||||
row = row,
|
||||
col = col,
|
||||
relative = opts.relative or "editor",
|
||||
style = opening and "minimal" or nil,
|
||||
width = width,
|
||||
height = height,
|
||||
border = opening and border or nil,
|
||||
zindex = opts.zindex or nil,
|
||||
}
|
||||
if version.major > 0 or version.minor >= 9 then
|
||||
float_config.title_pos = term.display_name and opts.title_pos or nil
|
||||
float_config.title = term.display_name
|
||||
end
|
||||
return float_config
|
||||
end
|
||||
|
||||
--- @param size number
|
||||
--- @param term Terminal
|
||||
function M.open_split(size, term)
|
||||
local has_open, windows = M.find_open_windows()
|
||||
local commands = split_commands[term.direction]
|
||||
|
||||
if has_open then
|
||||
-- we need to be in the terminal window most recently opened
|
||||
-- in order to split it
|
||||
local split_win = windows[#windows]
|
||||
if config.persist_size then M.save_window_size(term.direction, split_win.window) end
|
||||
api.nvim_set_current_win(split_win.window)
|
||||
vim.cmd(commands.existing)
|
||||
else
|
||||
vim.cmd(commands.new)
|
||||
end
|
||||
|
||||
M.resize_split(term, size)
|
||||
create_term_buf_if_needed(term)
|
||||
end
|
||||
|
||||
--- @param term Terminal
|
||||
function M.open_tab(term)
|
||||
-- Open the current buffer in a tab (use tabnew due to issue #95)
|
||||
vim.cmd("tabedit new")
|
||||
-- tabnew creates an empty no name buffer so we set it to be wiped once it's replaced
|
||||
-- by the terminal buffer
|
||||
vim.bo.bufhidden = "wipe"
|
||||
-- Replace the current window with a tab
|
||||
create_term_buf_if_needed(term)
|
||||
end
|
||||
|
||||
---@param term Terminal
|
||||
local function close_tab(term)
|
||||
if #vim.api.nvim_list_tabpages() == 1 then
|
||||
return utils.notify("You cannot close the last tab! This will exit neovim", "error")
|
||||
end
|
||||
api.nvim_win_close(term.window, true)
|
||||
end
|
||||
|
||||
---Close terminal window
|
||||
---@param term Terminal
|
||||
local function close_split(term)
|
||||
if term.window and api.nvim_win_is_valid(term.window) then
|
||||
local persist_size = require("toggleterm.config").get("persist_size")
|
||||
if persist_size then M.save_window_size(term.direction, term.window) end
|
||||
api.nvim_win_close(term.window, true)
|
||||
end
|
||||
if origin_window and api.nvim_win_is_valid(origin_window) then
|
||||
api.nvim_set_current_win(origin_window)
|
||||
else
|
||||
origin_window = nil
|
||||
end
|
||||
end
|
||||
|
||||
---Open a floating window
|
||||
---@param term Terminal
|
||||
function M.open_float(term)
|
||||
local opts = term.float_opts or {}
|
||||
local valid_buf = term.bufnr and api.nvim_buf_is_valid(term.bufnr)
|
||||
local buf = valid_buf and term.bufnr or api.nvim_create_buf(false, false)
|
||||
local win = api.nvim_open_win(buf, true, M._get_float_config(term, true))
|
||||
|
||||
term.window, term.bufnr = win, buf
|
||||
-- partial fix for #391
|
||||
utils.wo_setlocal(win, "sidescrolloff", 0)
|
||||
|
||||
if opts.winblend then utils.wo_setlocal(win, "winblend", opts.winblend) end
|
||||
term:__set_options()
|
||||
end
|
||||
|
||||
---Updates the floating terminal size
|
||||
---@param term Terminal
|
||||
function M.update_float(term)
|
||||
if not vim.api.nvim_win_is_valid(term.window) then return end
|
||||
vim.api.nvim_win_set_config(term.window, M._get_float_config(term, false))
|
||||
end
|
||||
|
||||
---Close given terminal's ui
|
||||
---@param term Terminal
|
||||
function M.close(term)
|
||||
if term:is_split() then
|
||||
close_split(term)
|
||||
elseif term:is_tab() then
|
||||
close_tab(term)
|
||||
elseif term.window and api.nvim_win_is_valid(term.window) then
|
||||
api.nvim_win_close(term.window, true)
|
||||
end
|
||||
end
|
||||
|
||||
---Resize a split window
|
||||
---@param term Terminal
|
||||
---@param size number?
|
||||
function M.resize_split(term, size)
|
||||
size = M._resolve_size(M.get_size(size, term.direction), term)
|
||||
if config.persist_size and size then M.save_direction_size(term.direction, size) end
|
||||
vim.cmd(split_commands[term.direction].resize .. " " .. size)
|
||||
end
|
||||
|
||||
---Determine if a window is a float
|
||||
---@param window number
|
||||
function M.is_float(window) return fn.win_gettype(window) == "popup" end
|
||||
|
||||
--- @param bufnr number
|
||||
function M.find_windows_by_bufnr(bufnr) return fn.win_findbuf(bufnr) end
|
||||
|
||||
---Return whether or not the terminal passed in has an open window
|
||||
---@param term Terminal
|
||||
---@return boolean
|
||||
function M.term_has_open_win(term)
|
||||
if not term.window then return false end
|
||||
local wins = {}
|
||||
for _, tab in ipairs(api.nvim_list_tabpages()) do
|
||||
vim.list_extend(wins, api.nvim_tabpage_list_wins(tab))
|
||||
end
|
||||
return vim.tbl_contains(wins, term.window)
|
||||
end
|
||||
|
||||
---Close and save terminals that are currently in view.
|
||||
---@param windows TerminalWindow[]
|
||||
function M.close_and_save_terminal_view(windows)
|
||||
local terminals = {}
|
||||
local focused_term_id = terms.get_focused_id()
|
||||
-- NOTE: Use windows to close terminals in order they are being shown on
|
||||
-- the view.
|
||||
for _, window in pairs(windows) do
|
||||
local term = terms.get(window.term_id)
|
||||
if term then
|
||||
table.insert(terminals, term.id)
|
||||
term:close()
|
||||
end
|
||||
end
|
||||
M.save_terminal_view(terminals, focused_term_id)
|
||||
end
|
||||
|
||||
---Open terminals that were saved in the last terminal view.
|
||||
---@return boolean
|
||||
function M.open_terminal_view(size, direction)
|
||||
local opened = false
|
||||
if not vim.tbl_isempty(terminal_view.terminals) then
|
||||
for _, term_id in pairs(terminal_view.terminals) do
|
||||
local term = terms.get(term_id)
|
||||
if term then
|
||||
term:open(size, direction)
|
||||
opened = true
|
||||
end
|
||||
end
|
||||
local focus_term = terms.get(terminal_view.focus_term_id)
|
||||
if focus_term then focus_term:focus() end
|
||||
M.save_terminal_view({}, nil)
|
||||
end
|
||||
return opened
|
||||
end
|
||||
|
||||
---Save the terminal view with the just closed terminals and the previously
|
||||
--focused terminal.
|
||||
---@param terminals number[]
|
||||
---@param focus_term_id number?
|
||||
function M.save_terminal_view(terminals, focus_term_id)
|
||||
terminal_view = { terminals = terminals, focus_term_id = focus_term_id }
|
||||
end
|
||||
|
||||
---@return TerminalView
|
||||
function M.get_terminal_view() return terminal_view end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,135 @@
|
||||
local M = {}
|
||||
|
||||
local fn, api, opt = vim.fn, vim.api, vim.opt
|
||||
local fmt = string.format
|
||||
local levels = vim.log.levels
|
||||
|
||||
function M.is_nightly()
|
||||
local v = vim.version()
|
||||
return v.minor >= 8
|
||||
end
|
||||
|
||||
---@alias error_types 'error' | 'info' | 'warn'
|
||||
---Inform a user about something
|
||||
---@param msg string
|
||||
---@param level error_types
|
||||
function M.notify(msg, level)
|
||||
local err = level:upper()
|
||||
level = level and levels[err] or levels.INFO
|
||||
vim.schedule(function() vim.notify(msg, level, { title = "Toggleterm" }) end)
|
||||
end
|
||||
|
||||
---@private
|
||||
---Helper function to derive the current git directory path
|
||||
---@return string|nil
|
||||
function M.git_dir()
|
||||
local gitdir = fn.system(fmt("git -C %s rev-parse --show-toplevel", fn.expand("%:p:h")))
|
||||
local isgitdir = fn.matchstr(gitdir, "^fatal:.*") == ""
|
||||
if not isgitdir then return end
|
||||
return vim.trim(gitdir)
|
||||
end
|
||||
|
||||
---@param str string|nil
|
||||
---@return boolean
|
||||
function M.str_is_empty(str) return str == nil or str == "" end
|
||||
|
||||
---@param tbl table
|
||||
---@return table
|
||||
function M.tbl_filter_empty(tbl)
|
||||
return vim.tbl_filter(
|
||||
---@param str string|nil
|
||||
function(str) return not M.str_is_empty(str) end,
|
||||
tbl
|
||||
)
|
||||
end
|
||||
|
||||
--- Concats a table ignoring empty entries
|
||||
---@param tbl table
|
||||
---@param sep string
|
||||
function M.concat_without_empty(tbl, sep) return table.concat(M.tbl_filter_empty(tbl), sep) end
|
||||
|
||||
-- Key mapping function
|
||||
---@param mod string | string[]
|
||||
---@param lhs string | string[]
|
||||
---@param rhs string | function
|
||||
---@param opts table?
|
||||
function M.key_map(mod, lhs, rhs, opts)
|
||||
if type(lhs) == "string" then
|
||||
vim.keymap.set(mod, lhs, rhs, opts)
|
||||
elseif type(lhs) == "table" then
|
||||
for _, key in pairs(lhs) do
|
||||
vim.keymap.set(mod, key, rhs, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param mode "visual" | "motion"
|
||||
---@return table
|
||||
function M.get_line_selection(mode)
|
||||
local start_char, end_char = unpack(({
|
||||
visual = { "'<", "'>" },
|
||||
motion = { "'[", "']" },
|
||||
})[mode])
|
||||
-- '< marks are only updated when one leaves visual mode.
|
||||
-- When calling lua functions directly from a mapping, need to
|
||||
-- explicitly exit visual with the escape key to ensure those marks are
|
||||
-- accurate.
|
||||
vim.cmd("normal! ")
|
||||
|
||||
-- Get the start and the end of the selection
|
||||
local start_line, start_col = unpack(fn.getpos(start_char), 2, 3)
|
||||
local end_line, end_col = unpack(fn.getpos(end_char), 2, 3)
|
||||
local selected_lines = api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
|
||||
return {
|
||||
start_pos = { start_line, start_col },
|
||||
end_pos = { end_line, end_col },
|
||||
selected_lines = selected_lines,
|
||||
}
|
||||
end
|
||||
|
||||
function M.get_visual_selection(res, motion)
|
||||
motion = motion or false
|
||||
local mode = fn.visualmode()
|
||||
if motion then mode = "v" end
|
||||
|
||||
-- line-visual
|
||||
-- return lines encompassed by the selection; already in res object
|
||||
if mode == "V" then return res.selected_lines end
|
||||
|
||||
if mode == "v" then
|
||||
-- regular-visual
|
||||
-- return the buffer text encompassed by the selection
|
||||
local start_line, start_col = unpack(res.start_pos)
|
||||
local end_line, end_col = unpack(res.end_pos)
|
||||
-- exclude the last char in text if "selection" is set to "exclusive"
|
||||
if opt.selection:get() == "exclusive" then end_col = end_col - 1 end
|
||||
return api.nvim_buf_get_text(0, start_line - 1, start_col - 1, end_line - 1, end_col, {})
|
||||
end
|
||||
|
||||
-- block-visual
|
||||
-- return the lines encompassed by the selection, each truncated by the start and end columns
|
||||
if mode == "\x16" then
|
||||
local _, start_col = unpack(res.start_pos)
|
||||
local _, end_col = unpack(res.end_pos)
|
||||
-- exclude the last col of the block if "selection" is set to "exclusive"
|
||||
if opt.selection:get() == "exclusive" then end_col = end_col - 1 end
|
||||
-- exchange start and end columns for proper substring indexing if needed
|
||||
-- e.g. instead of str:sub(10, 5), do str:sub(5, 10)
|
||||
if start_col > end_col then
|
||||
start_col, end_col = end_col, start_col
|
||||
end
|
||||
-- iterate over lines, truncating each one
|
||||
return vim.tbl_map(function(line) return line:sub(start_col, end_col) end, res.selected_lines)
|
||||
end
|
||||
end
|
||||
|
||||
--- Sets a local window option, like `:setlocal`
|
||||
--- TODO: replace with double-indexing on `vim.wo` when neovim/neovim#20288 (hopefully) merges
|
||||
---@param win number
|
||||
---@param option string
|
||||
---@param value any
|
||||
function M.wo_setlocal(win, option, value)
|
||||
api.nvim_set_option_value(option, value, { scope = "local", win = win })
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user