solorice/.config/yazi/plugins/augment-command.yazi/init.lua
2024-07-16 22:42:32 +03:00

994 lines
29 KiB
Lua

-- Plugin to make some Yazi commands smarter
-- Written in Lua 5.4
-- The enum for which group of items to operate on
local ItemGroup = {
Hovered = "hovered",
Selected = "selected",
None = "none",
Prompt = "prompt",
}
-- The enum for the archive extraction behaviour
local ExtractBehaviour = {
Overwrite = "overwrite",
Rename = "rename",
Skip = "skip",
}
-- The enum for the flags for the archive extraction behaviour
local ExtractBehaviourFlags = {
[ExtractBehaviour.Overwrite] = "-f",
[ExtractBehaviour.Rename] = "-r",
[ExtractBehaviour.Skip] = "-s",
}
-- The enum for the supported commands
local Commands = {
Open = "open",
Enter = "enter",
Leave = "leave",
Rename = "rename",
Remove = "remove",
Paste = "paste",
Arrow = "arrow",
ParentArrow = "parent-arrow",
Editor = "editor",
Pager = "pager",
}
-- The default configuration for the plugin
local DEFAULT_CONFIG = {
prompt = false,
default_item_group_for_prompt = ItemGroup.Hovered,
smart_enter = true,
smart_paste = false,
enter_archives = true,
extract_behaviour = ExtractBehaviour.Skip,
must_have_hovered_item = true,
skip_single_subdirectory_on_enter = true,
skip_single_subdirectory_on_leave = true,
ignore_hidden_items = false,
wraparound_file_navigation = false,
}
-- The default notification options for this plugin
local DEFAULT_NOTIFICATION_OPTIONS = {
title = "Augment Command Plugin",
timeout = 5.0
}
-- The default input options for this plugin
local DEFAULT_INPUT_OPTIONS = {
position = { "top-center", y = 2, w = 50 }
}
-- The table of input options for the prompt
local INPUT_OPTIONS_TABLE = {
[ItemGroup.Hovered] = "(H/s)",
[ItemGroup.Selected] = "(h/S)",
[ItemGroup.None] = "(h/s)",
}
-- The list of archive mime types
local ARCHIVE_MIME_TYPES = {
"application/zip",
"application/gzip",
"application/x-tar",
"application/x-bzip",
"application/x-bzip2",
"application/x-7z-compressed",
"application/x-rar",
"application/x-xz",
}
-- The pattern to get the double dash from the front of the argument
local double_dash_pattern = "^%-%-"
-- The pattern to get the parent directory of the current directory
local get_parent_directory_pattern = "(.*)/.*"
-- The pattern to get if a file path is a directory
local is_directory_pattern = "(.*)/$"
-- The pattern to get the filename of a file
local get_filename_pattern = "(.*)%.[^%.]+$"
-- Function to merge tables.
-- The tables given later in the argument list WILL OVERRIDE
-- the tables given earlier in the argument list.
local function merge_tables(...)
-- Initialise a new table
local new_table = {}
-- Iterates over the tables given
for _, table in ipairs({...}) do
-- Iterate over all of the keys and values
for key, value in pairs(table) do
-- Set the key in the new table to the value given
new_table[key] = value
end
end
-- Return the new table
return new_table
end
-- Function to check if a list contains a given value
local function list_contains(list, value)
-- Iterate over all of the items in the list
for _, item in ipairs(list) do
-- If the item is equal to the given value,
-- then return true
if item == value then return true end
end
-- Otherwise, return false if the item isn't in the list
return false
end
-- Function to split a string into a list
local function string_split(given_string, separator)
-- If the separator isn't given, set it to the whitespace character
if separator == nil then
separator = "%s"
end
-- Initialise the list of splitted strings
local splitted_strings = {}
-- Iterate over all of the strings found by pattern
for string in string.gmatch(given_string, "([^" .. separator .. "]+)") do
-- Add the string to the list of splitted strings
table.insert(splitted_strings, string)
end
-- Return the list of splitted strings
return splitted_strings
end
-- Function to parse the arguments given.
-- This function takes the arguments passed to the entry function
local function parse_args(args)
-- The table of options to pass to ya.manager_emit
local options = {}
-- Iterates over the arguments given
for index, argument in ipairs(args) do
-- If the index isn't 1,
-- which means it is the arguments to the command given
if index ~= 1 then
-- If the argument doesn't start with a double dash
if not argument:match(double_dash_pattern) then
-- Try to convert the argument to a number
local number_argument = tonumber(argument)
-- Add the argument to the list of options
table.insert(
options,
number_argument and number_argument or argument
)
-- Continue the loop
goto continue
end
-- Otherwise, remove the double dash from the front of the argument
local cleaned_argument =
argument:gsub(double_dash_pattern, "")
-- Replace all of the dashes with underscores
cleaned_argument = cleaned_argument:gsub("%-", "_")
-- Split the arguments at the = character
local arg_name, arg_value = table.unpack(
string_split(cleaned_argument, "=")
)
-- If the argument value is nil
if arg_value == nil then
-- Set the argument name to the cleaned argument
arg_name = cleaned_argument
-- Set the argument value to true
arg_value = true
-- Otherwise
else
-- Try to convert the argument value to a number
local number_arg_value = tonumber(arg_value)
-- Set the argument value to the number
-- if the the argument value can be converted to a number
arg_value = number_arg_value and number_arg_value or arg_value
end
-- Add the argument name and value to the options
options[arg_name] = arg_value
end
-- The label to continue the loop
::continue::
end
-- Return the table of options
return options
end
-- Function to initialise the configuration
local initialise_config = ya.sync(function(state, opts)
-- Merge the default configuration with the given one
-- and set it to the state.
state.config = merge_tables(DEFAULT_CONFIG, opts)
-- Return the configuration object for async functions
return state.config
end)
-- Function to get the configuration from an async function
local get_config = ya.sync(function(state)
-- Returns the configuration object
return state.config
end)
-- Function to get the current working directory
local get_current_directory = ya.sync(function(_)
return tostring(cx.active.current.cwd)
end)
-- Function to get the parent working directory
local get_parent_directory = ya.sync(function(_)
-- Get the parent directory
local parent_directory = cx.active.parent
-- If the parent directory doesn't exist,
-- return nil
if not parent_directory then return nil end
-- Otherwise, return the path of the parent directory
return tostring(parent_directory.cwd)
end)
-- Function to get the hovered item path
local get_hovered_item_path = ya.sync(function(_)
-- Get the hovered item
local hovered_item = cx.active.current.hovered
-- If the hovered item exists
if hovered_item then
-- Return the path of the hovered item
return tostring(cx.active.current.hovered.url)
-- Otherwise, return nil
else return nil end
end)
-- Function to get if the hovered item is a directory
local hovered_item_is_dir = ya.sync(function(_)
-- Get the hovered item
local hovered_item = cx.active.current.hovered
-- Return if the hovered item exists and is a directory
return hovered_item and hovered_item.cha.is_dir
end)
-- Function to get if the hovered item is an archive
local hovered_item_is_archive = ya.sync(function(_)
-- Get the hovered item
local hovered_item = cx.active.current.hovered
-- Return if the hovered item exists and is an archive
return hovered_item and list_contains(
ARCHIVE_MIME_TYPES, hovered_item:mime()
)
end)
-- Function to choose which group of items to operate on.
-- It returns ItemGroup.Hovered for the hovered item,
-- ItemGroup.Selected for the selected items,
-- and ItemGroup.Prompt to tell the calling function
-- to prompt the user.
local get_item_group_from_state = ya.sync(function(state)
-- Get the hovered item
local hovered_item = cx.active.current.hovered
-- The boolean representing that there are no selected items
local no_selected_items = #cx.active.selected == 0
-- If there is no hovered item
if not hovered_item then
-- If there are no selected items, exit the function
if no_selected_items then return
-- Otherwise, if the configuration is set to have a hovered item,
-- exit the function
elseif state.config.must_have_hovered_item then return
-- Otherwise, return the enum for the selected items
else return ItemGroup.Selected end
-- Otherwise, there is a hovered item
-- and if there are no selected items,
-- return the enum for the hovered item.
elseif no_selected_items then return ItemGroup.Hovered
-- Otherwise if there are selected items and the user wants a prompt,
-- then tells the calling function to prompt them
elseif state.config.prompt then
return ItemGroup.Prompt
-- Otherwise, if the hovered item is selected,
-- then return the enum for the selected items
elseif hovered_item:is_selected() then return ItemGroup.Selected
-- Otherwise, return the enum for the hovered item
else return ItemGroup.Hovered end
end)
-- Function to prompt the user for their desired item group
local function prompt_for_desired_item_group()
-- Get the configuration
local config = get_config()
-- Get the default item group
local default_item_group = config.default_item_group_for_prompt
-- Get the input options
local input_options = INPUT_OPTIONS_TABLE[default_item_group]
-- If the default item group is None, then set it to nil
if default_item_group == ItemGroup.None then
default_item_group = nil
end
-- Prompt the user for their input
local user_input, event = ya.input(merge_tables(DEFAULT_INPUT_OPTIONS, {
title = "Operate on hovered or selected items? " .. input_options
}))
-- Lowercase the user's input
user_input = user_input:lower()
-- If the user did not confirm the input, exit the function
if event ~= 1 then return
-- Otherwise, if the user's input starts with "h",
-- return the item group representing the hovered item
elseif user_input:find("^h") then return ItemGroup.Hovered
-- Otherwise, if the user's input starts with "s",
-- return the item group representing the selected items
elseif user_input:find("^s") then return ItemGroup.Selected
-- Otherwise, return the default item group
else return default_item_group end
end
-- Function to get the item group
local function get_item_group()
-- Get the item group from the state
local item_group = get_item_group_from_state()
-- If the item group isn't the prompt one,
-- then return the item group immediately
if item_group ~= ItemGroup.Prompt then return item_group
-- Otherwise, prompt the user for the desired item group
else return prompt_for_desired_item_group() end
end
-- The ls command to get the items in the directory
local function ls_command(directory, ignore_hidden_items)
return Command("ls")
:args({
directory,
ignore_hidden_items and "-1p" or "-1pA",
"--group-directories-first",
})
:stdout(Command.PIPED)
:stderr(Command.PIPED)
:output()
end
-- Function to skip child directories with only one directory
local function skip_single_child_directories(args, config, initial_directory)
-- If the user doesn't want to skip single subdirectories on enter,
-- or one of the arguments passed is no skip,
-- then exit the function
if not config.skip_single_subdirectory_on_enter or args.no_skip then
return
end
-- Initialise the directory variable to the initial directory given
local directory = initial_directory
-- Start an infinite loop
while true do
-- Run the ls command to get the items in the directory
local output, _ = ls_command(directory, config.ignore_hidden_items)
-- If there is no output, then break out of the loop
if not output then break end
-- Get the list of items in the directory
local directory_items = string_split(output.stdout, "\n")
-- If the number of directory items is not 1,
-- then break out of the loop
if #directory_items ~= 1 then break end
-- Otherwise, get the item in the directory
local directory_item = table.unpack(directory_items)
-- Match the directory item against the pattern to
-- check if it is a directory
directory_item = directory_item:match(is_directory_pattern)
-- If the directory item isn't a directory, break the loop
if directory_item == nil then break end
-- Otherwise, set the directory to the inner directory
directory = directory .. "/" .. directory_item
end
-- Emit the change directory command to change to the directory variable
ya.manager_emit("cd", { directory })
end
-- Function to handle the open command
local function handle_open(args, config, command_table)
-- Call the function to get the item group
local item_group = get_item_group()
-- If no item group is returned, exit the function
if not item_group then return end
-- If the item group is the selected items,
-- then execute the command and exit the function
if item_group == ItemGroup.Selected then
-- Emit the command and exit the function
return ya.manager_emit("open", args)
end
-- Otherwise, the item group is the hovered item.
-- Get the function to handle the enter command.
local enter_command = command_table[Commands.Enter]
-- If the hovered item is a directory
if hovered_item_is_dir() then
-- If smart enter is wanted,
-- calls the function to enter the directory
-- and exit the function
if config.smart_enter then
return enter_command(args, config, command_table)
-- Otherwise, just exit the function
else return end
end
-- Otherwise, if the hovered item is not an archive,
-- or entering archives isn't wanted
if not hovered_item_is_archive() or not config.enter_archives then
-- Simply emit the open command and exit the function
return ya.manager_emit("open", args)
end
-- Otherwise, the hovered item is an archive
-- and entering archives is wanted,
-- so get the path of the hovered item
local archive_path = get_hovered_item_path()
-- If the archive path somehow doesn't exist, then exit the function
if not archive_path then return end
-- Run the command to extract the archive
local output, err = Command("unar")
:args({
"-d",
ExtractBehaviourFlags[config.extract_behaviour],
archive_path
})
:stdout(Command.PIPED)
:stderr(Command.PIPED)
:output()
-- If the command isn't successful, notify the user
if not output then
return ya.notify(merge_tables(DEFAULT_NOTIFICATION_OPTIONS, {
content = "Failed to extract archive at: "
.. archive_path
.. "\nError code: "
.. tostring(err),
level = "error"
}))
end
-- Get the filename of the archive
local archive_filename = archive_path:match(get_filename_pattern)
-- Enter the archive directory
ya.manager_emit("cd", { archive_filename })
-- Calls the function to skip child directories
-- with only a single directory inside
skip_single_child_directories(args, config, archive_filename)
end
-- Function to handle the enter command
local function handle_enter(args, config, command_table)
-- Get the function for the open command
local open_command = command_table[Commands.Open]
-- If the hovered item is not a directory
if not hovered_item_is_dir() and config.smart_enter then
-- If smart enter is wanted,
-- call the function for the open command
-- and exit the function
if config.smart_enter then
return open_command(args, config, command_table)
-- Otherwise, just exit the function
else return end
end
-- Otherwise, always emit the enter command,
ya.manager_emit("enter", args)
-- Calls the function to skip child directories
-- with only a single directory inside
skip_single_child_directories(args, config, get_current_directory())
end
-- Function to handle the leave command
local function handle_leave(args, config)
-- Always emit the leave command
ya.manager_emit("leave", args)
-- If the user doesn't want to skip single subdirectories on leave,
-- or one of the arguments passed is no skip,
-- then exit the function
if not config.skip_single_subdirectory_on_leave or args.no_skip then
return
end
-- Otherwise, initialise the directory to the current directory
local directory = get_current_directory()
-- Otherwise, start an infinite loop
while true do
-- Run the ls command to get the items in the directory
local output, _ = ls_command(directory, config.ignore_hidden_items)
-- If there is no output, then break out of the loop
if not output then break end
-- Get the list of items in the directory
local directory_items = string_split(output.stdout, "\n")
-- If the number of directory items is not 1,
-- then break out of the loop
if #directory_items ~= 1 then break end
-- Otherwise, set the new directory
-- to the parent of the current directory
directory = directory:match(get_parent_directory_pattern)
end
-- Emit the change directory command to change to the directory variable
ya.manager_emit("cd", { directory })
end
-- Function to handle the a command
local function handle_command(command, args)
-- Call the function to get the item group
local item_group = get_item_group()
-- If no item group is returned, exit the function
if not item_group then return end
-- If the item group is the selected items
if item_group == ItemGroup.Selected then
-- Emit the command to operate on the selected items
ya.manager_emit(command, args)
-- If the item group is the hovered item
elseif item_group == ItemGroup.Hovered then
-- Emit the command with the hovered option
ya.manager_emit(command, merge_tables(args, { hovered = true }))
-- Otherwise, exit the function
else return end
end
-- Function to handle a shell command
local function handle_shell_command(command, args)
-- Call the function to get the item group
local item_group = get_item_group()
-- If no item group is returned, exit the function
if not item_group then return end
-- If the item group is the selected items
if item_group == ItemGroup.Selected then
-- Merge the arguments for the shell command with additional ones
args = merge_tables({
command .. " $@",
confirm = true,
block = true,
}, args)
-- Emit the command to operate the selected items
ya.manager_emit("shell", args)
-- If the item group is the hovered item
elseif item_group == ItemGroup.Hovered then
-- Merge the arguments for the shell command with additional ones
args = merge_tables({
command .. " $0",
confirm = true,
block = true,
}, args)
-- Emit the command to operate on the hovered item
ya.manager_emit("shell", args)
-- Otherwise, exit the function
else return end
end
-- Function to handle the paste command
local function handle_paste(args, config)
-- If the hovered item is a directory and smart paste is wanted
if hovered_item_is_dir() and (config.smart_paste or args.smart) then
-- Enter the directory
ya.manager_emit("enter", {})
-- Paste the items inside the directory
ya.manager_emit("paste", args)
-- Leave the directory
ya.manager_emit("leave", {})
-- Otherwise, just paste the items inside the current directory
else
ya.manager_emit("paste", args)
end
end
-- Function to do the wraparound for the arrow command
local wraparound_arrow = ya.sync(function(_, args)
-- Get the current tab
local current_tab = cx.active.current
-- Get the step from the arguments given
local step = table.remove(args, 1)
-- Get the number of files in the current tab
local number_of_files = #current_tab.files
-- If there are no files in the current tab, exit the function
if number_of_files == 0 then return end
-- Get the new cursor index,
-- which is the current cursor position plus the step given
-- to the arrow function, modulus the number of files in
-- the current tab
local new_cursor_index = (current_tab.cursor + step) % number_of_files
-- Emit the arrow function with the new cursor index minus
-- the current cursor index to determine how to move the cursor
ya.manager_emit("arrow", merge_tables(
args,
{ new_cursor_index - current_tab.cursor }
))
end)
-- Function to handle the arrow command
local function handle_arrow(args, config)
-- If wraparound file navigation isn't wanted,
-- then execute the arrow command
if not config.wraparound_file_navigation then
ya.manager_emit("arrow", args)
-- Otherwise, call the wraparound arrow function
else wraparound_arrow(args) end
end
-- Function to execute the parent arrow command
local execute_parent_arrow_command = ya.sync(
function(state, args, number_of_directories)
-- Gets the parent directory
local parent_directory = cx.active.parent
-- If the parent directory doesn't exist,
-- then exit the function
if not parent_directory then return end
-- Get the step from the arguments given
local step = table.remove(args, 1)
-- Initialise the new cursor index
-- to the current parent cursor index
local new_cursor_index = parent_directory.cursor
-- Otherwise, if wraparound file navigation is wanted
-- and the number of directories is given and isn't 0
if
state.config.wraparound_file_navigation
and number_of_directories
and number_of_directories ~= 0
then
-- Get the new cursor index by adding the step,
-- and modding the whole thing by the number of
-- directories given.
new_cursor_index = (parent_directory.cursor + step)
% number_of_directories
else
-- Otherwise, get the new cursor index normally.
new_cursor_index = parent_directory.cursor + step
end
-- Increment the cursor index by 1.
-- The cursor index needs to be increased by 1
-- as the cursor index is 0-based, while Lua
-- tables are 1-based.
new_cursor_index = new_cursor_index + 1
-- Get the target directory
local target_directory = parent_directory.files[new_cursor_index]
-- If the target directory exists and is a directory
if target_directory and target_directory.cha.is_dir then
-- Emit the command to change directory
-- to the target directory
ya.manager_emit("cd", { target_directory.url })
end
end
)
-- Function to handle the parent arrow command
local function handle_parent_arrow(args, config)
-- If wraparound file navigation isn't wanted,
-- then execute the parent arrow command and exit the function
if not config.wraparound_file_navigation then
return execute_parent_arrow_command(args)
end
-- Otherwise, get the path of the parent directory
local parent_directory_path = get_parent_directory()
-- If there is no parent directory, exit the function
if not parent_directory_path then return end
-- Call the ls command to get the number of directories
local output, _ = ls_command(
parent_directory_path,
config.ignore_hidden_items
)
-- If there is no output, exit the function
if not output then return end
-- Get the item in the parent directory
local directory_items = string_split(output.stdout, "\n")
-- Initialise the number of directories
local number_of_directories = 0
-- Iterate over the directory items
for _, item in ipairs(directory_items) do
-- If the item is a directory
if item:match(is_directory_pattern) then
-- Increment the number of directories by 1
number_of_directories = number_of_directories + 1
-- Otherwise, break out of the loop,
-- as the directories are grouped together
else break end
end
-- Call the function to execute the parent arrow command
execute_parent_arrow_command(args, number_of_directories)
end
-- Function to handle the pager command
local function handle_pager(args)
-- Call the function to get the item group
local item_group = get_item_group()
-- If no item group is returned, exit the function
if not item_group then return end
-- If the item group is the selected items,
-- then execute the command and exit the function
if item_group == ItemGroup.Selected then
-- Combine the arguments with additional ones
args = merge_tables({
"$PAGER $@",
confirm = true,
block = true
}, args)
-- Emit the command and exit the function
return ya.manager_emit("shell", args)
end
-- Otherwise, the item group is the hovered item,
-- and if the hovered item is a directory, exit the function
if hovered_item_is_dir() then return
-- Otherwise
else
-- Combine the arguments with additional ones
args = merge_tables({
"$PAGER $0",
confirm = true,
block = true
}, args)
-- Emit the command and exit the function
return ya.manager_emit("shell", args)
end
end
-- Function to run the commands given
local function run_command_func(command, args, config)
-- The command table
local command_table = {
[Commands.Open] = handle_open,
[Commands.Enter] = handle_enter,
[Commands.Leave] = handle_leave,
[Commands.Rename] = function(_)
handle_command("rename", args)
end,
[Commands.Remove] = function(_)
handle_command("remove", args)
end,
[Commands.Paste] = handle_paste,
[Commands.Arrow] = handle_arrow,
[Commands.ParentArrow] = handle_parent_arrow,
[Commands.Editor] = function(_)
handle_shell_command("$EDITOR", args)
end,
[Commands.Pager] = handle_pager,
}
-- Get the function for the command
local command_func = command_table[command]
-- If the function isn't found, notify the user and exit the function
if not command_func then
return ya.notify(
merge_tables(DEFAULT_NOTIFICATION_OPTIONS, {
content = "Unknown command: " .. command,
level = "error"
})
)
end
-- Parse the arguments and set it to the args variable
args = parse_args(args)
-- Otherwise, call the function for the command
command_func(args, config, command_table)
end
-- The setup function to setup the plugin
local function setup(_, opts)
-- Initialise the configuration with the default configuration
initialise_config(opts)
end
-- The function to be called to use the plugin
local function entry(_, args)
-- Gets the command passed to the plugin
local command = args[1]
-- If the command isn't given, exit the function
if not command then return end
-- Gets the configuration object
local config = get_config()
-- If the configuration hasn't been initialised,
-- then initialise the configuration
if not config then
config = initialise_config()
end
-- Call the function to handle the commands
run_command_func(command, args, config)
end
-- Returns the table required for Yazi to run the plugin
return {
setup = setup,
entry = entry,
}