solorice/config/yazi/plugins/relative-motions.yazi/main.lua
Kristofers Solo 1a19d1112b
Update 2025-06-30
Update 2025-06-06

Update 2025-06-08

Update 2025-06-11

Update 2025-06-11

Update 2025-06-18

Update 2025-06-24
2025-07-16 15:34:39 +03:00

387 lines
9.7 KiB
Lua

--- @since 25.5.28
-- stylua: ignore
local MOTIONS_AND_OP_KEYS = {
{ on = "0" }, { on = "1" }, { on = "2" }, { on = "3" }, { on = "4" },
{ on = "5" }, { on = "6" }, { on = "7" }, { on = "8" }, { on = "9" },
-- commands
{ on = "d" }, { on = "v" }, { on = "y" }, { on = "x" },
-- tab commands
{ on = "t" }, { on = "L" }, { on = "H" }, { on = "w" },
{ on = "W" }, { on = "<" }, { on = ">" }, { on = "~" },
-- movement
{ on = "g" }, { on = "j" }, { on = "k" }, { on = "h" }, { on = "l" }, { on = "<Down>" }, { on = "<Up>" }, { on = "<Left>" }, { on = "<Right>" }
}
-- stylua: ignore
local MOTION_KEYS = {
{ on = "0" }, { on = "1" }, { on = "2" }, { on = "3" }, { on = "4" },
{ on = "5" }, { on = "6" }, { on = "7" }, { on = "8" }, { on = "9" },
-- movement
{ on = "g" }, { on = "j" }, { on = "k" }, { on = "h" }, { on = "l" }, { on = "<Down>" }, { on = "<Up>" }, { on = "<Left>" }, { on = "<Right>" }
}
-- stylua: ignore
local DIRECTION_KEYS = {
{ on = "j" }, { on = "k" }, { on = "<Down>" }, { on = "<Up>" },
-- tab movement
{ on = "t" }
}
local SHOW_NUMBERS_ABSOLUTE = 0
local SHOW_NUMBERS_RELATIVE = 1
local SHOW_NUMBERS_RELATIVE_ABSOLUTE = 2
local ENTER_MODE_FIRST = 0
local ENTER_MODE_CACHE = 1
local ENTER_MODE_CACHE_OR_FIRST = 2
-----------------------------------------------
----------------- R E N D E R -----------------
-----------------------------------------------
local render_motion_setup = ya.sync(function(_)
ya.render()
Status.motion = function() return ui.Span("") end
Status.children_redraw = function(self, side)
local lines = {}
if side == self.RIGHT then
lines[1] = self:motion(self)
end
for _, c in ipairs(side == self.RIGHT and self._right or self._left) do
lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self)
end
return ui.Line(lines)
end
-- TODO: check why it doesn't work line this
-- Status:children_add(function() return ui.Span("") end, 1000, Status.RIGHT)
end)
local render_motion = ya.sync(function(_, motion_num, motion_cmd)
ya.render()
Status.motion = function(self)
if not motion_num then
return ui.Span("")
end
local style = self:style()
local motion_span
if not motion_cmd then
motion_span = ui.Span(string.format(" %3d ", motion_num))
else
motion_span = ui.Span(string.format(" %3d%s ", motion_num, motion_cmd))
end
local status_config = th.status
local separator_open = status_config.sep_right.open
local separator_close = status_config.sep_right.close
return ui.Line {
ui.Span(separator_open):fg(style.main.bg),
motion_span:style(style.main),
ui.Span(separator_close):fg(style.main.bg),
ui.Span(" "),
}
end
end)
local render_numbers = ya.sync(function(_, mode)
ya.render()
Entity.number = function(_, index, total, file, hovered)
local idx
if mode == SHOW_NUMBERS_RELATIVE then
idx = math.abs(hovered - index)
elseif mode == SHOW_NUMBERS_ABSOLUTE then
idx = file.idx
else -- SHOW_NUMBERS_RELATIVE_ABSOLUTE
if hovered == index then
idx = file.idx
else
idx = math.abs(hovered - index)
end
end
local num_format = "%" .. #tostring(total) .. "d"
-- emulate vim's hovered offset
if hovered == index then
return ui.Span(string.format(num_format .. " ", idx))
else
return ui.Span(string.format(" " .. num_format, idx))
end
end
Current.redraw = function(self)
local files = self._folder.window
if #files == 0 then
return self:empty()
end
local hovered_index
for i, f in ipairs(files) do
if f.is_hovered then
hovered_index = i
break
end
end
local entities, linemodes = {}, {}
for i, f in ipairs(files) do
linemodes[#linemodes + 1] = Linemode:new(f):redraw()
local entity = Entity:new(f)
entities[#entities + 1] = ui.Line({ Entity:number(i, #self._folder.files, f, hovered_index), entity:redraw() })
:style(entity:style())
end
return {
ui.List(entities):area(self._area),
ui.Text(linemodes):area(self._area):align(ui.Align.RIGHT),
}
end
end)
local function render_clear() render_motion() end
-----------------------------------------------
--------- C O M M A N D P A R S E R ---------
-----------------------------------------------
local get_keys = ya.sync(function(state) return state._only_motions and MOTION_KEYS or MOTIONS_AND_OP_KEYS end)
local function normal_direction(dir)
if dir == "<Down>" then
return "j"
elseif dir == "<Up>" then
return "k"
elseif dir == "<Left>" then
return "h"
elseif dir == "<Right>" then
return "l"
end
return dir
end
local function get_cmd(first_char, keys)
local last_key
local lines = first_char or ""
while true do
render_motion(tonumber(lines))
local key = ya.which { cands = keys, silent = true }
if not key then
return nil, nil, nil
end
last_key = keys[key].on
if not tonumber(last_key) then
last_key = normal_direction(last_key)
break
end
lines = lines .. last_key
end
render_motion(tonumber(lines), last_key)
-- command direction
local direction
if last_key == "g" or last_key == "v" or last_key == "d" or last_key == "y" or last_key == "x" then
DIRECTION_KEYS[#DIRECTION_KEYS + 1] = {
on = last_key,
}
local direction_key = ya.which { cands = DIRECTION_KEYS, silent = true }
if not direction_key then
return nil, nil, nil
end
direction = DIRECTION_KEYS[direction_key].on
direction = normal_direction(direction)
end
return tonumber(lines), last_key, direction
end
local function is_tab_command(command)
local tab_commands = { "t", "L", "H", "w", "W", "<", ">", "~" }
for _, cmd in ipairs(tab_commands) do
if command == cmd then
return true
end
end
return false
end
local get_active_tab = ya.sync(function(_) return cx.tabs.idx end)
local get_cache_or_first_dir = ya.sync(function(state)
if state._enter_mode == ENTER_MODE_CACHE then
return nil
elseif state._enter_mode == ENTER_MODE_CACHE_OR_FIRST then
local hovered_file = cx.active.current.hovered
if hovered_file ~= nil and hovered_file.cha.is_dir then
return cx.active.current.cursor
end
end
local files = cx.active.current.files
local index = 1
for i = 1, #files do
if files[i].cha.is_dir then
index = i
break
end
end
return index - 1
end)
-----------------------------------------------
---------- E N T R Y / S E T U P ----------
-----------------------------------------------
return {
entry = function(_, job)
local initial_value
local args = job.args
-- this is checking if the argument is a valid number
if #args > 0 then
initial_value = tostring(tonumber(args[1]))
if initial_value == "nil" then
return
end
end
local lines, cmd, direction = get_cmd(initial_value, get_keys())
if not lines or not cmd then
-- command was cancelled
render_clear()
return
end
if cmd == "g" then
if direction == "g" then
ya.mgr_emit("arrow", { "top" })
ya.mgr_emit("arrow", { lines - 1 })
render_clear()
return
elseif direction == "j" then
cmd = "j"
elseif direction == "k" then
cmd = "k"
elseif direction == "t" then
ya.mgr_emit("tab_switch", { lines - 1 })
render_clear()
return
else
-- no valid direction
render_clear()
return
end
end
if cmd == "j" then
ya.mgr_emit("arrow", { lines })
elseif cmd == "k" then
ya.mgr_emit("arrow", { -lines })
elseif cmd == "h" then
for _ = 1, lines do
ya.mgr_emit("leave", {})
end
elseif cmd == "l" then
for _ = 1, lines do
ya.mgr_emit("enter", {})
local file_idx = get_cache_or_first_dir()
if file_idx then
ya.mgr_emit("arrow", { "top" })
ya.mgr_emit("arrow", { file_idx })
end
end
elseif is_tab_command(cmd) then
if cmd == "t" then
for _ = 1, lines do
ya.mgr_emit("tab_create", {})
end
elseif cmd == "H" then
ya.mgr_emit("tab_switch", { -lines, relative = true })
elseif cmd == "L" then
ya.mgr_emit("tab_switch", { lines, relative = true })
elseif cmd == "w" then
ya.mgr_emit("tab_close", { lines - 1 })
elseif cmd == "W" then
local curr_tab = get_active_tab()
local del_tab = curr_tab + lines - 1
for _ = curr_tab, del_tab do
ya.mgr_emit("tab_close", { curr_tab - 1 })
end
ya.mgr_emit("tab_switch", { curr_tab - 1 })
elseif cmd == "<" then
ya.mgr_emit("tab_swap", { -lines })
elseif cmd == ">" then
ya.mgr_emit("tab_swap", { lines })
elseif cmd == "~" then
local jump = lines - get_active_tab()
ya.mgr_emit("tab_swap", { jump })
end
else
ya.mgr_emit("visual_mode", {})
-- invert direction when user specifies it
if direction == "k" then
ya.mgr_emit("arrow", { -lines })
elseif direction == "j" then
ya.mgr_emit("arrow", { lines })
else
ya.mgr_emit("arrow", { lines - 1 })
end
ya.mgr_emit("escape", {})
if cmd == "d" then
ya.mgr_emit("remove", {})
elseif cmd == "y" then
ya.mgr_emit("yank", {})
elseif cmd == "x" then
ya.mgr_emit("yank", { cut = true })
end
end
render_clear()
end,
setup = function(state, args)
if not args then
return
end
-- initialize state variables
state._only_motions = args["only_motions"] or false
if args["show_motion"] then
render_motion_setup()
end
if args["enter_mode"] == "cache" then
state._enter_mode = ENTER_MODE_CACHE
elseif args["enter_mode"] == "first" then
state._enter_mode = ENTER_MODE_FIRST
elseif args["enter_mode"] == "cache_or_first" then
state._enter_mode = ENTER_MODE_CACHE_OR_FIRST
else
state._enter_mode = ENTER_MODE_CACHE_OR_FIRST
end
if args["show_numbers"] == "absolute" then
render_numbers(SHOW_NUMBERS_ABSOLUTE)
elseif args["show_numbers"] == "relative" then
render_numbers(SHOW_NUMBERS_RELATIVE)
elseif args["show_numbers"] == "relative_absolute" then
render_numbers(SHOW_NUMBERS_RELATIVE_ABSOLUTE)
end
end,
}