diff --git a/lua/modes.lua b/lua/modes.lua new file mode 100644 index 0000000..21826cc --- /dev/null +++ b/lua/modes.lua @@ -0,0 +1,280 @@ +local utils = require('modes.utils') + +local M = {} +local config = {} +local default_config = { + colors = {}, + line_opacity = { + copy = 0.15, + delete = 0.15, + insert = 0.15, + visual = 0.15, + }, + set_cursor = true, + set_cursorline = true, + set_number = true, + ignore_filetypes = { 'NvimTree', 'TelescopePrompt' }, +} +local colors = {} +local blended_colors = {} +local default_colors = {} +local operator_started = false + +M.reset = function() + M.highlight('default') + operator_started = false +end + +---Update highlights +---@param scene 'default'|'insert'|'visual'|'copy'|'delete'| +M.highlight = function(scene) + if scene == 'default' then + vim.cmd('hi CursorLine guibg=' .. default_colors.cursor_line) + if config.set_number then + vim.cmd('hi CursorLineNr guibg=' .. default_colors.cursor_line_nr) + end + vim.cmd('hi ModeMsg guifg=' .. default_colors.mode_msg) + vim.cmd('hi Visual guibg=' .. default_colors.visual) + end + + if scene == 'insert' then + vim.cmd('hi CursorLine guibg=' .. blended_colors.insert) + if config.set_number then + vim.cmd('hi CursorLineNr guibg=' .. blended_colors.insert) + end + vim.cmd('hi ModeMsg guifg=' .. colors.insert) + end + + if scene == 'visual' then + vim.cmd('hi CursorLine guibg=' .. blended_colors.visual) + if config.set_number then + vim.cmd('hi CursorLineNr guibg=' .. blended_colors.visual) + end + vim.cmd('hi ModeMsg guifg=' .. colors.visual) + vim.cmd('hi Visual guibg=' .. blended_colors.visual) + end + + if scene == 'copy' then + vim.cmd('hi CursorLine guibg=' .. blended_colors.copy) + if config.set_number then + vim.cmd('hi CursorLineNr guibg=' .. blended_colors.copy) + end + vim.cmd('hi ModeMsg guifg=' .. colors.copy) + vim.cmd('hi ModesOperator guifg=NONE guibg=NONE') + vim.cmd('hi! link ModesOperator ModesCopy') + end + + if scene == 'delete' then + vim.cmd('hi CursorLine guibg=' .. blended_colors.delete) + if config.set_number then + vim.cmd('hi CursorLineNr guibg=' .. blended_colors.delete) + end + vim.cmd('hi ModeMsg guifg=' .. colors.delete) + vim.cmd('hi ModesOperator guifg=NONE guibg=NONE') + vim.cmd('hi! link ModesOperator ModesDelete') + end +end + +M.define = function() + default_colors = { + cursor_line = utils.get_bg_from_hl('CursorLine', 'CursorLine'), + cursor_line_nr = utils.get_bg_from_hl('CursorLineNr', 'CursorLineNr'), + mode_msg = utils.get_fg_from_hl('ModeMsg', 'ModeMsg'), + normal = utils.get_bg_from_hl('Normal', 'Normal'), + visual = utils.get_bg_from_hl('Visual', 'Visual'), + } + colors = { + copy = config.colors.copy or utils.get_bg_from_hl( + 'ModesCopy', + '#f5c359' + ), + delete = config.colors.delete or utils.get_bg_from_hl( + 'ModesDelete', + '#c75c6a' + ), + insert = config.colors.insert or utils.get_bg_from_hl( + 'ModesInsert', + '#78ccc5' + ), + visual = config.colors.visual or utils.get_bg_from_hl( + 'ModesVisual', + '#9745be' + ), + } + blended_colors = { + copy = utils.blend( + colors.copy, + default_colors.normal, + config.line_opacity.copy + ), + delete = utils.blend( + colors.delete, + default_colors.normal, + config.line_opacity.delete + ), + insert = utils.blend( + colors.insert, + default_colors.normal, + config.line_opacity.insert + ), + visual = utils.blend( + colors.visual, + default_colors.normal, + config.line_opacity.visual + ), + } + + ---Create highlight groups + vim.cmd('hi ModesCopy guibg=' .. colors.copy) + vim.cmd('hi ModesDelete guibg=' .. colors.delete) + vim.cmd('hi ModesInsert guibg=' .. colors.insert) + vim.cmd('hi ModesVisual guibg=' .. colors.visual) +end + +M.enable_managed_ui = function() + if config.set_cursor then + vim.opt.guicursor:append('v-sm:block-ModesVisual') + vim.opt.guicursor:append('i-ci-ve:ver25-ModesInsert') + vim.opt.guicursor:append('r-cr-o:hor20-ModesOperator') + end + + if config.set_cursorline then + vim.opt.cursorline = true + end +end + +M.disable_managed_ui = function() + if config.set_cursor then + vim.opt.guicursor:remove('v-sm:block-ModesVisual') + vim.opt.guicursor:remove('i-ci-ve:ver25-ModesInsert') + vim.opt.guicursor:remove('r-cr-o:hor20-ModesOperator') + end + + if config.set_cursorline then + vim.opt.cursorline = false + end +end + +M.setup = function(opts) + opts = opts or default_config + if opts.focus_only then + print( + 'modes.nvim – `focus_only` has been removed and is now the default behaviour' + ) + end + + config = vim.tbl_deep_extend('force', default_config, opts) + + if type(config.line_opacity) == 'number' then + config.line_opacity = { + copy = config.line_opacity, + delete = config.line_opacity, + insert = config.line_opacity, + visual = config.line_opacity, + } + end + + M.define() + vim.defer_fn(function() + M.define() + end, 15) + + vim.on_key(function(key) + local ok, current_mode = pcall(vim.fn.mode) + if not ok then + M.reset() + end + + if current_mode == 'i' then + if key == utils.get_termcode('') then + M.reset() + end + end + + if current_mode == 'n' then + if key == utils.get_termcode('') then + M.reset() + end + + if key == 'y' then + if operator_started then + M.reset() + else + M.highlight('copy') + operator_started = true + end + end + + if key == 'd' then + if operator_started then + M.reset() + else + M.highlight('delete') + operator_started = true + end + end + + if (key == 'v' or key == 'V') and not operator_started then + M.highlight('visual') + end + end + + if current_mode == 'v' then + if key == utils.get_termcode('') then + M.reset() + end + end + + if current_mode == 'V' then + if key == utils.get_termcode('') then + M.reset() + end + end + end) + + ---Set highlights when colorscheme changes + vim.api.nvim_create_autocmd('ColorScheme', { + pattern = '*', + callback = M.define, + }) + + ---Set insert highlight + vim.api.nvim_create_autocmd('InsertEnter', { + pattern = '*', + callback = function() + M.highlight('insert') + end, + }) + + ---Reset highlights + vim.api.nvim_create_autocmd( + { 'CmdlineLeave', 'InsertLeave', 'TextYankPost', 'WinLeave' }, + { + pattern = '*', + callback = M.reset, + } + ) + + ---Enable managed UI initially + M.enable_managed_ui() + + ---Enable managed UI for current window + vim.api.nvim_create_autocmd('WinEnter', { + pattern = '*', + callback = M.enable_managed_ui, + }) + + ---Disable managed UI for unfocused windows + vim.api.nvim_create_autocmd('WinLeave', { + pattern = '*', + callback = M.disable_managed_ui, + }) + + ---Disable managed UI for ignored filetypes + vim.api.nvim_create_autocmd('FileType', { + pattern = config.ignore_filetypes, + callback = M.disable_managed_ui, + }) +end + +return M diff --git a/lua/modes/init.lua b/lua/modes/init.lua deleted file mode 100644 index 8e264ba..0000000 --- a/lua/modes/init.lua +++ /dev/null @@ -1,251 +0,0 @@ -local util = require('modes.util') - -local modes = {} -local config = {} -local colors = {} -local dim_colors = {} -local init_colors = {} -local operator_started = false - -function modes.reset() - modes.set_highlights('init') - operator_started = false -end - -function modes.set_highlights(style) - if style == 'init' then - vim.cmd('hi CursorLine guibg=' .. init_colors.cursor_line) - vim.cmd('hi CursorLineNr guifg=' .. init_colors.cursor_line_nr) - vim.cmd('hi ModeMsg guifg=' .. init_colors.mode_msg) - end - - if style == 'copy' then - vim.cmd('hi CursorLine guibg=' .. dim_colors.copy) - vim.cmd('hi CursorLineNr guifg=' .. colors.copy) - vim.cmd('hi ModeMsg guifg=' .. colors.copy) - vim.cmd('hi ModesOperator guifg=NONE guibg=NONE') - vim.cmd('hi! link ModesOperator ModesCopy') - end - - if style == 'delete' then - vim.cmd('hi CursorLine guibg=' .. dim_colors.delete) - vim.cmd('hi CursorLineNr guifg=' .. colors.delete) - vim.cmd('hi ModeMsg guifg=' .. colors.delete) - vim.cmd('hi ModesOperator guifg=NONE guibg=NONE') - vim.cmd('hi! link ModesOperator ModesDelete') - end - - if style == 'insert' then - vim.cmd('hi CursorLine guibg=' .. dim_colors.insert) - vim.cmd('hi CursorLineNr guifg=' .. colors.insert) - vim.cmd('hi ModeMsg guifg=' .. colors.insert) - end - - if style == 'visual' then - vim.cmd('hi CursorLine guibg=' .. dim_colors.visual) - vim.cmd('hi CursorLineNr guifg=' .. colors.visual) - vim.cmd('hi ModeMsg guifg=' .. colors.visual) - end -end - -function modes.set_colors() - init_colors = { - cursor_line = util.get_bg_from_hl('CursorLine', 'CursorLine'), - cursor_line_nr = util.get_fg_from_hl('CursorLineNr', 'CursorLineNr'), - mode_msg = util.get_fg_from_hl('ModeMsg', 'ModeMsg'), - normal = util.get_bg_from_hl('Normal', 'Normal'), - } - colors = { - copy = config.colors.copy or util.get_bg_from_hl( - 'ModesCopy', - '#f5c359' - ), - delete = config.colors.delete or util.get_bg_from_hl( - 'ModesDelete', - '#c75c6a' - ), - insert = config.colors.insert or util.get_bg_from_hl( - 'ModesInsert', - '#78ccc5' - ), - visual = config.colors.visual or util.get_bg_from_hl( - 'ModesVisual', - '#9745be' - ), - } - dim_colors = { - copy = util.blend( - colors.copy, - init_colors.normal, - config.line_opacity.copy - ), - delete = util.blend( - colors.delete, - init_colors.normal, - config.line_opacity.delete - ), - insert = util.blend( - colors.insert, - init_colors.normal, - config.line_opacity.insert - ), - visual = util.blend( - colors.visual, - init_colors.normal, - config.line_opacity.visual - ), - } - - vim.cmd('hi ModesCopy guibg=' .. colors.copy) - vim.cmd('hi ModesDelete guibg=' .. colors.delete) - vim.cmd('hi ModesInsert guibg=' .. colors.insert) - vim.cmd('hi ModesVisual guibg=' .. colors.visual) -end - ----@class Colors ----@field copy string ----@field delete string ----@field insert string ----@field visual string - ----@class Opacity ----@field copy number between 0 and 1 ----@field delete number between 0 and 1 ----@field insert number between 0 and 1 ----@field visual number between 0 and 1 - ----@class Config ----@field colors Colors ----@field line_opacity Opacity ----@field set_cursor boolean ----@field focus_only boolean - ----@param opts Config -function modes.setup(opts) - local default_config = { - -- Colors intentionally set to {} to prioritise theme values - colors = {}, - line_opacity = { - copy = 0.15, - delete = 0.15, - insert = 0.15, - visual = 0.15, - }, - set_cursor = true, - focus_only = false, - } - opts = opts or default_config - - -- Resolve configs in the following order: - -- 1. User config - -- 2. Theme highlights if present (eg. ModesCopy) - -- 3. Default config - config = vim.tbl_deep_extend('force', default_config, opts) - - -- Allow overriding line opacity per colour - if type(config.line_opacity) == 'number' then - config.line_opacity = { - copy = config.line_opacity, - delete = config.line_opacity, - insert = config.line_opacity, - visual = config.line_opacity, - } - end - - -- Hack to ensure theme colors get loaded properly - modes.set_colors() - vim.defer_fn(function() - modes.set_colors() - end, 15) - - -- Set common highlights - vim.cmd('hi Visual guibg=' .. dim_colors.visual) - - -- Set guicursor modes - if config.set_cursor then - vim.opt.guicursor:append('v-sm:block-ModesVisual') - vim.opt.guicursor:append('i-ci-ve:ver25-ModesInsert') - vim.opt.guicursor:append('r-cr-o:hor20-ModesOperator') - end - - local on_key = vim.on_key or vim.register_keystroke_callback - on_key(function(key) - local ok, current_mode = pcall(vim.fn.mode) - if not ok then - modes.reset() - end - - -- Insert mode - if current_mode == 'i' then - if key == util.get_termcode('') then - modes.reset() - end - end - - -- Normal mode - if current_mode == 'n' then - if key == util.get_termcode('') then - modes.reset() - end - - if key == 'y' then - if operator_started then - modes.reset() - else - modes.set_highlights('copy') - operator_started = true - end - end - - if key == 'd' then - if operator_started then - modes.reset() - else - modes.set_highlights('delete') - operator_started = true - end - end - - if (key == 'v' or key == 'V') and not operator_started then - modes.set_highlights('visual') - end - end - - -- Visual mode - if current_mode == 'v' then - if key == util.get_termcode('') then - modes.reset() - end - end - - -- Visual line mode - if current_mode == 'V' then - if key == util.get_termcode('') then - modes.reset() - end - end - end) - - local autocmds = { - { 'ColorScheme', '*', 'lua require("modes").set_colors()' }, - { - 'InsertEnter', - '*', - 'lua require("modes").set_highlights("insert")', - }, - { - 'CmdlineLeave,InsertLeave,TextYankPost,WinLeave', - '*', - 'lua require("modes").reset()', - }, - } - - if config.focus_only then - autocmds['cl'] = { 'WinEnter', '*', 'set cursorline' } - autocmds['nocl'] = { 'WinLeave', '*', 'set nocursorline' } - end - - util.define_augroups({ _modes = autocmds }) -end - -return modes diff --git a/lua/modes/util.lua b/lua/modes/utils.lua similarity index 74% rename from lua/modes/util.lua rename to lua/modes/utils.lua index bc657c8..122d3ea 100644 --- a/lua/modes/util.lua +++ b/lua/modes/utils.lua @@ -1,4 +1,4 @@ -local util = {} +local M = {} local function get_byte(value, offset) return bit.band(bit.rshift(value, offset), 0xFF) @@ -17,7 +17,7 @@ end ---@param fg string foreground color ---@param bg string background color ---@param alpha number number between 0 and 1. 0 results in bg, 1 results in fg -function util.blend(fg, bg, alpha) +M.blend = function(fg, bg, alpha) bg = get_color(bg) fg = get_color(fg) @@ -34,7 +34,7 @@ function util.blend(fg, bg, alpha) ) end -function util.hl(group, color) +M.hl = function(group, color) local fg = color.fg and 'guifg=' .. color.fg or 'guifg=NONE' local bg = color.bg and 'guibg=' .. color.bg or 'guibg=NONE' @@ -46,7 +46,7 @@ function util.hl(group, color) end end -function util.get_fg_from_hl(hl_name, fallback) +M.get_fg_from_hl = function(hl_name, fallback) local id = vim.api.nvim_get_hl_id_by_name(hl_name) if not id then return fallback @@ -60,7 +60,7 @@ function util.get_fg_from_hl(hl_name, fallback) return foreground end -function util.get_bg_from_hl(hl_name, fallback) +M.get_bg_from_hl = function(hl_name, fallback) local id = vim.api.nvim_get_hl_id_by_name(hl_name) if not id then return fallback @@ -74,25 +74,8 @@ function util.get_bg_from_hl(hl_name, fallback) return background end -function util.get_termcode(key) +M.get_termcode = function(key) return vim.api.nvim_replace_termcodes(key, true, true, true) end -function util.define_augroups(definitions) - for group_name, definition in pairs(definitions) do - vim.cmd('augroup ' .. group_name) - vim.cmd('autocmd!') - - for _, def in pairs(definition) do - local command = table.concat( - vim.tbl_flatten({ 'autocmd', def }), - ' ' - ) - vim.cmd(command) - end - - vim.cmd('augroup END') - end -end - -return util +return M