Initial commit

This commit is contained in:
Oliver Hartmann 2022-02-28 20:15:23 +01:00
commit b925fbc378
17 changed files with 4268 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
clink_history*
clink.log

3
.init.lua Normal file
View File

@ -0,0 +1,3 @@
-- The line below extends package.path with modules
-- directory to allow to require them
package.path = debug.getinfo(1, "S").source:match[[^@?(.*[\/])[^\/]-$]] .."modules/?.lua;".. package.path

5
.inputrc Normal file
View File

@ -0,0 +1,5 @@
$if clink
"\t": clink-popup-complete
$endif

126
clink_settings Normal file
View File

@ -0,0 +1,126 @@
# name: Enable automatic suggestions
# type: boolean
autosuggest.enable = True
# name: Selects default key bindings
# type: enum
# options: bash,windows
clink.default_bindings = windows
# name: Use GUI popup windows
# type: boolean
clink.gui_popups = False
# name: Pressing Ctrl-D exits session
# type: boolean
cmd.ctrld_exits = False
# name: Argument color
# type: color
color.arg = bold
# name: Argument info color
# type: color
color.arginfo = sgr 1;38;5;172
# name: Argmatcher color
# type: color
color.argmatcher = sgr 1;38;5;40
# name: Shell command completions
# type: color
color.cmd = sgr 1;38;5;231
# name: Color for < and > redirection symbols
# type: color
color.cmdredir = sgr 38;5;172
# name: Color for & and | command separators
# type: color
color.cmdsep = sgr 38;5;214
# name: Color for comment row
# type: color
color.comment_row = sgr 38;5;87;48;5;18
# name: Description completion color
# type: color
color.description = sgr 38;5;39
# name: Doskey completions
# type: color
color.doskey = sgr 1;38;5;75
# name: Color for executable command word
# type: color
color.executable = sgr 38;5;33
# name: Filtered completion color
# type: color
color.filtered = sgr 38;5;231
# name: Flag color
# type: color
color.flag = sgr 38;5;117
# name: Hidden file completions
# type: color
color.hidden = sgr 38;5;160
# name: Horizontal scroll marker color
# type: color
color.horizscroll = sgr 38;5;16;48;5;30
# name: Input text color
# type: color
color.input = sgr 38;5;222
# name: For user-interaction prompts
# type: color
color.interact = bold
# name: Message area color
# type: color
color.message = default
# name: Readonly file completions
# type: color
color.readonly = sgr 38;5;28
# name: Selected completion color
# type: color
color.selected_completion = sgr 38;5;16;48;5;254
# name: Selection color
# type: color
color.selection = sgr 38;5;16;48;5;179
# name: Color for suggestion text
# type: color
color.suggestion = sgr 38;5;239
# name: Unexpected argument color
# type: color
color.unexpected = default
# name: Color for unrecognized command word
# type: color
color.unrecognized = sgr 38;5;203
# name: The number of history lines to save
# type: integer
history.max_lines = 25000
# name: Expand envvars when completing
# type: boolean
match.expand_envvars = True
# name: Try substring if no prefix matches
# type: boolean
match.substring = True
# name: Controls when past prompts are collapsed
# type: enum
# options: off,always,same_dir
prompt.transient = off

334
fzf.lua Normal file
View File

@ -0,0 +1,334 @@
--------------------------------------------------------------------------------
-- FZF integration for Clink.
--
-- Clink is available at https://chrisant996.github.io/clink
-- FZF is available from https://nicedoc.io/junegunn/fzf
--
-- To use this:
--
-- 1. Copy this script into your Clink scripts directory.
--
-- 2. Either put fzf.exe in a directory listed in the system PATH environment
-- variable, or run 'clink set fzf.exe_location <directoryname>' to tell
-- Clink where to find fzf.exe.
--
-- 3. The default key bindings are as follows, when using Clink v1.2.46 or
-- higher:
--[[
# Default key bindings for fzf with Clink.
"\C-t": "luafunc:fzf_file" # Ctrl+T lists files recursively; choose one or multiple to insert them.
"\C-r": "luafunc:fzf_history" # Ctrl+R lists history entries; choose one to insert it.
"\M-c": "luafunc:fzf_directory" # Alt+C lists subdirectories; choose one to 'cd /d' to it.
"\M-b": "luafunc:fzf_bindings" # Alt+B lists key bindings; choose one to invoke it.
"\e[27;5;32~": "luafunc:fzf_complete" # Ctrl+Space uses fzf to filter match completions.
]]
-- 4. Optional: You can use your own custom key bindings if you want.
-- Run 'clink set fzf.default_bindings false' and add key bindings to
-- your .inputrc file manually. The default key bindings are listed
-- above in .inputrc format for convenience.
--
-- 5. Optional: You can set the following environment variables to
-- customize the behavior:
--
-- FZF_CTRL_T_OPTS = fzf options for fzf_file() function.
-- FZF_CTRL_R_OPTS = fzf options for fzf_history() function.
-- FZF_ALT_C_OPTS = fzf options for fzf_directory() function.
-- FZF_BINDINGS_OPTS = fzf options for fzf_bindings() function.
-- FZF_COMPLETE_OPTS = fzf options for fzf_complete() function.
--
-- FZF_CTRL_T_COMMAND = command to run for collecting files for fzf_file() function.
-- FZF_ALT_C_COMMAND = command to run for collecting directories for fzf_directory() function.
--------------------------------------------------------------------------------
-- Compatibility check.
if not io.popenrw then
print('fzf.lua requires a newer version of Clink; please upgrade.')
return
end
--------------------------------------------------------------------------------
-- Settings available via 'clink set'.
settings.add('fzf.height', '40%', 'Height to use for the --height flag')
settings.add('fzf.exe_location', '', 'Location of fzf.exe if not on the PATH')
if rl.setbinding then
settings.add('fzf.default_bindings', true, 'Use default key bindings', 'If the default key bindings interfere with your own, you can turn off the\ndefault key bindings and add bindings manually to your .inputrc file.\n\nChanging this takes effect for the next session.')
if settings.get('fzf.default_bindings') then
rl.setbinding([["\C-t"]], [["luafunc:fzf_file"]])
rl.setbinding([["\C-r"]], [["luafunc:fzf_history"]])
rl.setbinding([["\M-c"]], [["luafunc:fzf_directory"]])
rl.setbinding([["\M-b"]], [["luafunc:fzf_bindings"]])
rl.setbinding([["\e[27;5;32~"]], [["luafunc:fzf_complete"]])
end
end
--------------------------------------------------------------------------------
-- Helpers.
local diag = false
local fzf_complete_intercept = false
local function get_fzf(env)
local height = settings.get('fzf.height')
local command = settings.get('fzf.exe_location')
if os.expandenv and command then
-- Expand so that os.getshortpathname() can work even when envvars are
-- present.
command = os.expandenv(command)
end
if not command or command == '' then
command = 'fzf.exe'
else
-- CMD.exe cannot use pipe redirection with a quoted program name, so
-- try to use a short name.
local short = os.getshortpathname(command)
if short then
command = short
end
end
if command and command ~= '' and height and height ~= '' then
command = command..' --height '..height
end
if env then
local options = os.getenv(env)
if options then
command = command..' '..options
end
end
return command
end
local function get_clink()
local clink_alias = os.getalias('clink')
if not clink_alias or clink_alias == '' then
return ''
end
return clink_alias:gsub(' $[*]', '')
end
local function replace_dir(str, line_state)
local dir = '.'
if line_state:getwordcount() > 0 then
local info = line_state:getwordinfo(line_state:getwordcount())
if info then
local word = line_state:getline():sub(info.offset, line_state:getcursor())
if word and #word > 0 then
dir = word
end
end
end
return str:gsub('$dir', dir)
end
--------------------------------------------------------------------------------
-- Functions for use with 'luafunc:' key bindings.
function fzf_complete(rl_buffer)
fzf_complete_intercept = true
rl.invokecommand('complete')
if fzf_complete_intercept then
rl_buffer:ding()
end
fzf_complete_intercept = false
rl_buffer:refreshline()
end
function fzf_history(rl_buffer)
local clink_command = get_clink()
if #clink_command == 0 then
rl_buffer:ding()
return
end
-- Build command to get history for the current Clink session.
local history = clink_command..' --session '..clink.getsession()..' history --bare'
if diag then
history = history..' --diag'
end
-- This intentionally does not use '--query' because that isn't safe:
-- Depending on what the user has typed so far, passing it as an argument
-- may cause the command to interpreted differently than expected.
-- E.g. suppose the user typed: "pgm.exe & rd /s
-- Then fzf would be invoked as: fzf.exe --query""pgm.exe & rd /s"
-- And since the & is not inside quotes, the 'rd /s' command gets actually
-- run by mistake!
local r = io.popen(history..' | '..get_fzf("FZF_CTRL_R_OPTS")..' -i --tac')
if not r then
rl_buffer:ding()
return
end
local str = r:read('*all')
str = str and str:gsub('[\r\n]', '') or ''
r:close()
-- If something was selected, insert it.
if #str > 0 then
rl_buffer:beginundogroup()
rl_buffer:remove(0, -1)
rl_buffer:insert(str)
rl_buffer:endundogroup()
end
rl_buffer:refreshline()
end
function fzf_file(rl_buffer, line_state)
local ctrl_t_command = os.getenv('FZF_CTRL_T_COMMAND')
if not ctrl_t_command then
ctrl_t_command = 'dir /b /s /a:-s $dir'
end
ctrl_t_command = replace_dir(ctrl_t_command, line_state)
print('"'..ctrl_t_command..'"')
local r = io.popen(ctrl_t_command..' | '..get_fzf('FZF_CTRL_T_OPTS')..' -i -m')
if not r then
rl_buffer:ding()
return
end
local str = r:read('*line')
str = str and str:gsub('[\r\n]+', ' ') or ''
str = str:gsub(' +$', '')
r:close()
if #str > 0 then
rl_buffer:insert(str)
end
rl_buffer:refreshline()
end
function fzf_directory(rl_buffer, line_state)
local alt_c_opts = os.getenv('FZF_ALT_C_OPTS')
if not alt_c_opts then
alt_c_opts = ""
end
local alt_c_command = os.getenv('FZF_ALT_C_COMMAND')
if not alt_c_command then
alt_c_command = 'dir /b /s /a:d-s $dir'
end
alt_c_command = replace_dir(alt_c_command, line_state)
local temp_contents = rl_buffer:getbuffer()
local r = io.popen(alt_c_command..' | '..get_fzf('FZF_ALT_C_OPTS')..' -i')
if not r then
rl_buffer:ding()
return
end
local str = r:read('*all')
str = str and str:gsub('[\r\n]', '') or ''
r:close()
if #str > 0 then
rl_buffer:beginundogroup()
rl_buffer:remove(0, -1)
rl_buffer:insert('cd /d '..str)
rl_buffer:endundogroup()
rl_buffer:refreshline()
rl.invokecommand('accept-line')
return
end
rl_buffer:refreshline()
end
function fzf_bindings(rl_buffer)
if not rl.getkeybindings then
rl_buffer:beginoutput()
print('fzf_bindings() in fzf.lua requires a newer version of Clink; please upgrade.')
return
end
local bindings = rl.getkeybindings()
if #bindings <= 0 then
rl_buffer:refreshline()
return
end
local line
local r,w = io.popenrw(get_fzf('FZF_BINDINGS_OPTS')..' -i')
if r and w then
-- Write key bindings to the write pipe.
for _,kb in ipairs(bindings) do
w:write(kb.key..' : '..kb.binding..'\n')
end
w:close()
-- Read filtered matches.
local ret = {}
line = r:read('*line')
r:close()
end
rl_buffer:refreshline()
if line and #line > 0 then
local binding = line:sub(#bindings[1].key + 3 + 1)
rl.invokecommand(binding)
end
end
--------------------------------------------------------------------------------
-- Match generator.
local function filter_matches(matches, completion_type, filename_completion_desired)
if not fzf_complete_intercept then
return
end
-- Start fzf.
local r,w = io.popenrw(get_fzf("FZF_COMPLETE_OPTS"))
if not r or not w then
return
end
-- Write matches to the write pipe.
for _,m in ipairs(matches) do
w:write(m.match..'\n')
end
w:close()
-- Read filtered matches.
local ret = {}
while (true) do
local line = r:read('*line')
if not line then
break
end
for _,m in ipairs(matches) do
if m.match == line then
table.insert(ret, m)
end
end
end
r:close()
-- Yay, successful; clear it to not ding.
fzf_complete_intercept = false
return ret
end
local interceptor = clink.generator(0)
function interceptor:generate(line_state, match_builder)
if fzf_complete_intercept then
clink.onfiltermatches(filter_matches)
end
return false
end
clink.onbeginedit(function ()
fzf_complete_intercept = false
end)

992
git.lua Normal file
View File

@ -0,0 +1,992 @@
-- preamble: common routines
local path = require('path')
local git = require('gitutil')
local matchers = require('matchers')
local w = require('tables').wrap
local clink_version = require('clink_version')
local color = require('color')
local parser = clink.arg.new_parser
if clink_version.supports_color_settings then
settings.add('color.git.star', 'bright green', 'Color for preferred branch completions')
end
---
-- Lists remote branches based on packed-refs file from git directory
-- @param string [dir] Directory where to search file for
-- @return table List of remote branches
local function list_packed_refs(dir)
local result = w()
local git_dir = dir or git.get_git_common_dir()
if not git_dir then return result end
local packed_refs_file = io.open(git_dir..'/packed-refs')
if packed_refs_file == nil then return {} end
for line in packed_refs_file:lines() do
-- SHA is 40 char length + 1 char for space
if #line > 41 then
local match = line:sub(41):match('refs/remotes/(.*)')
if match then table.insert(result, match) end
end
end
packed_refs_file:close()
return result
end
local function list_remote_branches(dir)
local git_dir = dir or git.get_git_common_dir()
if not git_dir then return w() end
return w(path.list_files(git_dir..'/refs/remotes', '/*',
--[[recursive=]]true, --[[reverse_separator=]]true))
:concat(list_packed_refs(git_dir))
:sort():dedupe()
end
---
-- Lists local branches for git repo in git_dir directory.
--
-- @param string [dir] Git directory, where to search for remote branches
-- @return table List of branches.
local function list_local_branches(dir)
local git_dir = dir or git.get_git_common_dir()
if not git_dir then return w() end
local result = w(path.list_files(git_dir..'/refs/heads', '/*',
--[[recursive=]]true, --[[reverse_separator=]]true))
return result
end
local branches = function (token)
local git_dir = git.get_git_common_dir()
if not git_dir then return w() end
return list_local_branches(git_dir)
:filter(function(branch)
return clink.is_match(token, branch)
end)
end
local function alias(token)
local res = w()
-- Try to resolve .git directory location
local git_dir = git.get_git_dir()
if git_dir == nil then return res end
local f = io.popen("git config --get-regexp alias 2>nul")
if f == nil then return {} end
for line in f:lines() do
local s = line:find(" ", 1, true)
local alias_name = line:sub(7, s - 1)
local start = alias_name:find(token, 1, true)
if start and start == 1 then
table.insert(res, alias_name)
end
end
f:close()
return res
end
local function remotes(token) -- luacheck: no unused args
local result = w()
local git_dir = git.get_git_common_dir()
if not git_dir then return result end
local git_config = io.open(git_dir..'/config')
-- if there is no gitconfig file (WAT?!), return empty list
if git_config == nil then return result end
for line in git_config:lines() do
local remote = line:match('%[remote "(.*)"%]')
if (remote) then
table.insert(result, remote)
end
end
git_config:close()
return result
end
local function local_or_remote_branches(token)
-- Try to resolve .git directory location
local git_dir = git.get_git_common_dir()
if not git_dir then return w() end
return list_local_branches(git_dir)
:concat(list_remote_branches(git_dir))
:filter(function(branch)
return clink.is_match(token, branch)
end)
end
local function checkout_spec_generator(token)
local files = matchers.files(token)
:filter(function(file)
return path.is_real_dir(file)
end)
local git_dir = git.get_git_common_dir()
local local_branches = branches(token)
local remote_branches = list_remote_branches(git_dir)
:filter(function(branch)
return clink.is_match(token, branch)
end)
local predicted_branches = list_remote_branches(git_dir)
:map(function (remote_branch)
return remote_branch:match('.-/(.+)')
end)
:filter(function(branch)
return branch
and clink.is_match(token, branch)
-- Filter out those predictions which are already exists as local branches
and not local_branches:contains(branch)
end)
if (#local_branches + #remote_branches + #predicted_branches) == 0 then return files end
-- if there is any refspec that matches token then:
-- * disable readline's filename completion, otherwise we'll get a list of these specs
-- threaten as list of files (without 'path' part), ie. 'some_branch' instead of 'my_remote/some_branch'
-- * create display filter for completion table to append path separator to each directory entry
-- since it is not added automatically by readline (see previous point)
clink.matches_are_files(0)
clink.match_display_filter = function ()
local star = '*'
if clink_version.supports_query_rl_var and rl.isvariabletrue('colored-stats') then
star = color.get_clink_color('color.git.star')..star..color.get_clink_color('color.filtered')
end
return files:map(function(file)
return clink.is_dir(file) and file..'\\' or file
end)
:concat(local_branches)
:concat(predicted_branches:map(function(branch) return star..branch end))
:concat(remote_branches)
end
return files
:concat(local_branches)
:concat(predicted_branches)
:concat(remote_branches)
end
local function push_branch_spec(token)
local git_dir = git.get_git_common_dir()
if not git_dir then return w() end
local plus_prefix = token:sub(0, 1) == '+'
-- cut out leading '+' symbol as it is a part of branch spec
local branch_spec = plus_prefix and token:sub(2) or token
-- check if there a local/remote branch separator
local s, e = branch_spec:find(':')
-- starting from here we have 2 options:
-- * if there is no branch separator complete word with local branches
if not s then
local b = branches(branch_spec)
-- setup display filter to prevent display '+' symbol in completion list
clink.match_display_filter = function ()
return b
end
return b:map(function(branch)
-- append '+' to results if it was specified
return plus_prefix and '+'..branch or branch
end)
else
-- * if there is ':' separator then we need to complete remote branch
local local_branch_spec = branch_spec:sub(1, s - 1)
local remote_branch_spec = branch_spec:sub(e + 1)
-- TODO: show remote branches only for remote that has been specified as previous argument
local b = w(clink.find_dirs(git_dir..'/refs/remotes/*'))
:filter(function(remote) return path.is_real_dir(remote) end)
:reduce({}, function(result, remote)
return w(path.list_files(git_dir..'/refs/remotes/'..remote, '/*',
--[[recursive=]]true, --[[reverse_separator=]]true))
:filter(function(remote_branch)
return clink.is_match(remote_branch_spec, remote_branch)
end)
:concat(result)
end)
-- setup display filter to prevent display '+' symbol in completion list
clink.match_display_filter = function ()
return b
end
return b:map(function(branch)
return (plus_prefix and '+'..local_branch_spec or local_branch_spec)..':'..branch
end)
end
end
local stashes = function(token) -- luacheck: no unused args
local git_dir = git.get_git_dir()
if not git_dir then return w() end
local stash_file = io.open(git_dir..'/logs/refs/stash')
-- if there is no stash file, return empty list
if stash_file == nil then return w() end
local stashes = {}
-- make a dictionary of stash time and stash comment to
-- be able to sort stashes by date/time created
for stash in stash_file:lines() do
local stash_time, stash_name = stash:match('(%d%d%d%d%d%d%d%d%d%d) [+-]%d%d%d%d%s+(.*)')
if (stash_name and stash_name) then
stashes[stash_time] = stash_name
end
end
stash_file:close()
-- get times for available stashes into separate table and sort it
-- from newest to oldest. This is required because of stash@{0}
-- represents _latest_ stash, not the last one in file
local stash_times = {}
for k in pairs(stashes) do
table.insert(stash_times, k)
end
table.sort(stash_times, function (a, b)
return a > b
end)
-- generate matches and match filter table
local ret = {}
local ret_filter = {}
for i,v in ipairs(stash_times) do
local match = "stash@{"..(i-1).."}"
table.insert(ret, match)
if clink_version.supports_display_filter_description then
-- Clink now has a richer match interface. By returning a table,
-- the script is able to provide the stash name separately from the
-- description. If the script does so, then the popup completion
-- window is able to show the stash name plus a dimmed description,
-- but only insert the stash name.
table.insert(ret_filter, { match=match, type="word", description=stashes[v] })
else
table.insert(ret_filter, match.." "..stashes[v])
end
end
local function filter()
return ret_filter
end
if clink_version.supports_display_filter_description then
clink.ondisplaymatches(filter)
else
clink.match_display_filter = filter
end
return ret
end
local color_opts = parser({"true", "false", "always"})
local git_options = {
"core.editor",
"core.pager",
"core.excludesfile",
"core.autocrlf"..parser({"true", "false", "input"}),
"core.trustctime"..parser({"true", "false"}),
"core.whitespace"..parser({
"cr-at-eol",
"-cr-at-eol",
"indent-with-non-tab",
"-indent-with-non-tab",
"space-before-tab",
"-space-before-tab",
"trailing-space",
"-trailing-space"
}),
"commit.template",
"color.ui"..color_opts, "color.*"..color_opts, "color.branch"..color_opts,
"color.diff"..color_opts, "color.interactive"..color_opts, "color.status"..color_opts,
"help.autocorrect",
"merge.tool", "mergetool.*.cmd", "mergetool.trustExitCode"..parser({"true", "false"}), "diff.external",
"user.name", "user.email", "user.signingkey",
}
local config_parser = parser(
"--system", "--global", "--local", "--file"..parser({matchers.files}),
"--int", "--bool", "--path",
"-z", "--null",
"--add",
"--replace-all",
"--get", "--get-all", "--get-regexp", "--get-urlmatch",
"--unset", "--unset-all",
"--rename-section", "--remove-section",
"-l", "--list",
"--get-color", "--get-colorbool",
"-e", "--edit",
{git_options}
)
local merge_recursive_options = parser({
"ours",
"theirs",
"renormalize",
"no-renormalize",
"diff-algorithm="..parser({
"patience",
"minimal",
"histogram",
"myers"
}),
"patience",
"ignore-space-change",
"ignore-all-space",
"ignore-space-at-eol",
"rename-threshold=",
-- "subtree="..parser(),
"subtree"
})
local merge_strategies = parser({
"resolve",
"recursive",
"ours",
"octopus",
"subtree"
})
local cleanup_options = parser({
"strip",
"whitespace",
"verbatim",
"scissors",
"default"
})
local git_parser = parser(
{
{alias},
"add" .. parser({matchers.files},
"-n", "--dry-run",
"-v", "--verbose",
"-f", "--force",
"-i", "--interactive",
"-p", "--patch",
"-e", "--edit",
"-u", "--update",
"-A", "--all",
"--no-all",
"--ignore-removal",
"--no-ignore-removal",
"-N", "--intent-to-add",
"--refresh",
"--ignore-errors",
"--ignore-missing"
),
"add--interactive",
"am",
"annotate" .. parser({matchers.files},
"-b",
"--root",
"--show-stats",
"-L",
"-l",
"-t",
"-S",
"--reverse",
"-p",
"--porcelain",
"--line-porcelain",
"--incremental",
"--encoding=",
"--contents",
"--date",
"-M",
"-C",
"-h"
),
"apply" .. parser(
"--stat",
"--numstat",
"--summary",
"--check",
"--index",
"--cached",
"-3", "--3way",
"--build-fake-ancestor=",
"-R", "--reverse",
"--reject",
"-z",
"-p",
"-C",
"--unidiff-zero",
"--apply",
"--no-add",
"--allow-binary-replacement", "--binary",
"--exclude=",
"--include=",
"--ignore-space-change", "--ignore-whitespace",
"--whitespace=",
"--inaccurate-eof",
"-v", "--verbose",
"--recount",
"--directory="
),
"archive",
"bisect",
"bisect--helper",
"blame",
"branch" .. parser(
"-v", "--verbose",
"-q", "--quiet",
"-t", "--track",
"--set-upstream",
"-u", "--set-upstream-to",
"--unset-upstream",
"--color",
"-r", "--remotes",
"--contains" ,
"--abbrev",
"-a", "--all",
"-d" .. parser({branches}):loop(1),
"--delete" .. parser({branches}):loop(1),
"-D" .. parser({branches}):loop(1),
"-m", "--move",
"-M",
"--list",
"-l", "--create-reflog",
"--edit-description",
"-f", "--force",
"--no-merged",
"--merged",
"--column"
),
"bundle",
"cat-file",
"check-attr",
"check-ignore",
"check-mailmap",
"check-ref-format",
"checkout" .. parser({checkout_spec_generator},
"-q", "--quiet",
"-b",
"-B",
"-l",
"--detach",
"-t", "--track",
"--orphan",
"-2", "--ours",
"-3", "--theirs",
"-f", "--force",
"-m", "--merge",
"--overwrite-ignore",
"--conflict",
"-p", "--patch",
"--ignore-skip-worktree-bits"
),
"checkout-index",
"cherry",
"cherry-pick"..parser(
"-e", "--edit",
"-m", "--mainline ",
"-n", "--no-commit",
"-r",
"-x",
"--ff",
"-s", "-S", "--gpg-sign",
"--allow-empty",
"--allow-empty-message",
"--keep-redundant-commits",
"--strategy"..parser({merge_strategies}),
"-X"..parser({merge_recursive_options}),
"--strategy-option"..parser({merge_recursive_options}),
"--continue",
"--quit",
"--abort"
),
"citool",
"clean",
"clone" .. parser(
"--template",
"-l", "--local",
"-s", "--shared",
"--no-hardlinks",
"-q", "--quiet",
"-n", "--no-checkout",
"--bare",
"--mirror",
"-o", "--origin",
"-b", "--branch",
"-u", "--upload-pack",
"--reference",
"--dissociate",
"--separate-git-dir",
"--depth",
"--single-branch", "--no-single-branch",
"--no-tags",
"--recurse-submodules", "--shallow-submodules", "--no-shallow-submodules",
"--jobs"
),
"column",
"commit" .. parser(
"-a", "--all",
"-p", "--patch",
"-C", "--reuse-message=",
"-c", "--reedit-message=",
"--fixup=",
"--squash=",
"--reset-author",
"--short",
"--branch",
"--porcelain",
"--long",
"-z",
"--null",
"-F", "--file=",
"--author=",
"--date=",
"-m", "--message=",
"-t", "--template=",
"-s", "--signoff",
"-n", "--no-verify",
"--allow-empty",
"--allow-empty-message",
"--cleanup"..cleanup_options,
"-e", "--edit",
"--no-edit",
"--amend",
"--no-post-rewrite",
"-i", "--include",
"-o", "--only",
"-u", "--untracked-files", "--untracked-files=", -- .. parser({"no", "normal", "all"}),
"-v", "--verbose",
"-q", "--quiet",
"--dry-run",
"--status",
"--no-status",
"-S", "--gpg-sign", "--gpg-sign=",
"--"
),
"commit-tree",
"config"..config_parser,
"count-objects",
"credential",
"credential-store",
"credential-wincred",
"daemon",
"describe",
"diff" .. parser({local_or_remote_branches, matchers.files}),
"diff-files",
"diff-index",
"diff-tree",
"difftool"..parser(
"-d", "--dir-diff",
"-y", "--no-prompt", "--prompt",
"-t", "--tool=" -- TODO: complete tool (take from config)
),
"difftool--helper",
"fast-export",
"fast-import",
"fetch" .. parser({remotes},
"--all",
"--prune",
"--tags"
),
"fetch-pack",
"filter-branch",
"fmt-merge-msg",
"for-each-ref",
"format-patch",
"fsck",
"fsck-objects",
"gc",
"get-tar-commit-id",
"grep",
"gui",
"gui--askpass",
"gui--askyesno",
"gui.tcl",
"hash-object",
"help",
"http-backend",
"http-fetch",
"http-push",
"imap-send",
"index-pack",
"init",
"init-db",
"log",
"lost-found",
"ls-files",
"ls-remote",
"ls-tree",
"mailinfo",
"mailsplit",
"merge" .. parser({branches},
"--commit", "--no-commit",
"--edit", "-e", "--no-edit",
"--ff", "--no-ff", "--ff-only",
"--log", "--no-log",
"--stat", "-n", "--no-stat",
"--squash", "--no-squash",
"-s" .. merge_strategies,
"--strategy" .. merge_strategies,
"-X" .. merge_recursive_options,
"--strategy-option" .. merge_recursive_options,
"--verify-signatures", "--no-verify-signatures",
"-q", "--quiet", "-v", "--verbose",
"--progress", "--no-progress",
"-S", "--gpg-sign",
"-m",
"--rerere-autoupdate", "--no-rerere-autoupdate",
"--abort"
),
"merge-base",
"merge-file",
"merge-index",
"merge-octopus",
"merge-one-file",
"merge-ours",
"merge-recursive",
"merge-resolve",
"merge-subtree",
"merge-tree",
"mergetool",
"mergetool--lib",
"mktag",
"mktree",
"mv",
"name-rev",
"notes",
"p4",
"pack-objects",
"pack-redundant",
"pack-refs",
"parse-remote",
"patch-id",
"peek-remote",
"prune",
"prune-packed",
"pull" .. parser(
{remotes}, {branches},
"-q", "--quiet",
"-v", "--verbose",
"--recurse-submodules", --[no-]recurse-submodules[=yes|on-demand|no]
"--no-recurse-submodules",
"--commit", "--no-commit",
"-e", "--edit", "--no-edit",
"--ff", "--no-ff", "--ff-only",
"--log", "--no-log",
"--stat", "-n", "--no-stat",
"--squash", "--no-squash",
"-s"..merge_strategies,
"--strategy"..merge_strategies,
"-X"..merge_recursive_options,
"--strategy-option"..merge_recursive_options,
"--verify-signatures", "--no-verify-signatures",
"--summary", "--no-summary",
"-r", "--rebase", "--no-rebase",
"--all",
"-a", "--append",
"--depth", "--unshallow", "--update-shallow",
"-f", "--force",
"-k", "--keep",
"--no-tags",
"-u", "--update-head-ok",
"--upload-pack",
"--progress"
),
"push" .. parser(
{remotes},
{push_branch_spec},
"-v", "--verbose",
"-q", "--quiet",
"--repo",
"--all",
"--mirror",
"--delete",
"--tags",
"-n", "--dry-run",
"--porcelain",
"-f", "--force",
"--force-with-lease",
"--recurse-submodules",
"--thin",
"--receive-pack",
"--exec",
"-u", "--set-upstream",
"--progress",
"--prune",
"--no-verify",
"--follow-tags"
),
"quiltimport",
"read-tree",
"rebase" .. parser({local_or_remote_branches}, {branches},
"-i", "--interactive",
"--onto" .. parser({branches}),
"--continue",
"--abort",
"--keep-empty",
"--skip",
"--edit-todo",
"-m", "--merge",
"-s" .. merge_strategies,
"--strategy"..merge_strategies,
"-X" .. merge_recursive_options,
"--strategy-option"..merge_recursive_options,
"-S", "--gpg-sign",
"-q", "--quiet",
"-v", "--verbose",
"--stat", "-n", "--no-stat",
"--no-verify", "--verify",
"-C",
"-f", "--force-rebase",
"--fork-point", "--no-fork-point",
"--ignore-whitespace", "--whitespace",
"--committer-date-is-author-date", "--ignore-date",
"-i", "--interactive",
"-p", "--preserve-merges",
"-x", "--exec",
"--root",
"--autosquash", "--no-autosquash",
"--autostash", "--no-autostash",
"--no-ff"
),
"receive-pack",
"reflog",
"remote"..parser({
"add" ..parser(
"-t"..parser({branches}),
"-m",
"-f",
"--mirror",
"--tags", "--no-tags"
),
"rename"..parser({remotes}),
"remove"..parser({remotes}),
"rm"..parser({remotes}),
"set-head"..parser({remotes}, {branches},
"-a", "--auto",
"-d", "--delete"
),
"set-branches"..parser("--add", {remotes}, {branches}),
"set-url"..parser(
"--add"..parser("--push", {remotes}),
"--delete"..parser("--push", {remotes})
),
"get-url"..parser({remotes}, "--push", "--all"),
"show"..parser("-n", {remotes}),
"prune"..parser("-n", "--dry-run", {remotes}),
"update"..parser({remotes}, "-p", "--prune")
}, "-v", "--verbose"),
"remote-ext",
"remote-fd",
"remote-ftp",
"remote-ftps",
"remote-hg",
"remote-http",
"remote-https",
"remote-testsvn",
"repack",
"replace",
"repo-config",
"request-pull",
"rerere",
-- TODO: Add commit completions
"reset"..parser({local_or_remote_branches},
"-q",
"-p", "--patch",
"--soft", "--mixed", "--hard",
"--merge", "--keep"
),
"restore"..parser({matchers.files},
"-s", "--source",
"-p", "--patch",
"-W", "--worktree",
"-S", "--staged",
"-q", "--quiet",
"--progress", "--no-progress",
"--ours", "--theirs",
"-m", "--merge",
"--conflict",
"--ignore-unmerged",
"--ignore-skip-worktree-bits",
"--overlay", "--no-overlay"
),
"rev-list",
"rev-parse",
"revert"..parser(
"-e", "--edit",
"-m", "--mainline",
"--no-edit",
"--cleanup"..cleanup_options,
"-n", "--no-commit",
"-S", "--gpg-sign",
"--no-gpg-sign",
"-s", "--signoff",
"--strategy"..merge_strategies,
"-X"..merge_recursive_options,
"--strategy-option"..merge_recursive_options,
"--rerere-autoupdate",
"--no-rerere-autoupdate",
"--continue",
"--skip",
"--quit",
"--abort"
),
"rm",
"send-email",
"send-pack",
"sh-i18n",
"sh-i18n--envsubst",
"sh-setup",
"shortlog",
"show",
"show-branch",
"show-index",
"show-ref",
"stage",
"stash"..parser({
"list", -- TODO: The command takes options applicable to the git log
-- command to control what is shown and how it's done
"show"..parser({stashes}),
"drop"..parser({stashes}, "-q", "--quiet"),
"pop"..parser({stashes}, "--index", "-q", "--quiet"),
"apply"..parser({stashes}, "--index", "-q", "--quiet"),
"branch"..parser({branches}, {stashes}),
"save"..parser(
"-p", "--patch",
"-k", "--no-keep-index", "--keep-index",
"-q", "--quiet",
"-u", "--include-untracked",
"-a", "--all"
),
"clear"
}),
"status",
"stripspace",
"submodule"..parser({
"add",
"init",
"deinit",
"foreach",
"status"..parser("--cached", "--recursive"),
"summary",
"sync",
"update"
}, '--quiet'),
"subtree",
"switch"..parser({local_or_remote_branches},
"-c", "-C", "--create",
"--force-create",
"-d", "--detach",
"--guess", "--no-guess",
"-f", "--force", "--discard-changes",
"-m", "--merge",
"--conflict",
"-q", "--quiet",
"--progress", "--no-progress",
"-t", "--track",
"--no-track",
"--orphan",
"--ignore-other-worktrees",
"--recurse-submodules", "--no-recurse-submodules"
),
"svn"..parser({
"init"..parser("-T", "--trunk", "-t", "--tags", "-b", "--branches", "-s", "--stdlayout",
"--no-metadata", "--use-svm-props", "--use-svnsync-props", "--rewrite-root",
"--rewrite-uuid", "--username", "--prefix"..parser({"origin"}), "--ignore-paths",
"--include-paths", "--no-minimize-url"),
"fetch"..parser({remotes}, "--localtime", "--parent", "--ignore-paths", "--include-paths",
"--log-window-size"),
"clone"..parser("-T", "--trunk", "-t", "--tags", "-b", "--branches", "-s", "--stdlayout",
"--no-metadata", "--use-svm-props", "--use-svnsync-props", "--rewrite-root",
"--rewrite-uuid", "--username", "--prefix"..parser({"origin"}), "--ignore-paths",
"--include-paths", "--no-minimize-url", "--preserve-empty-dirs",
"--placeholder-filename"),
"rebase"..parser({local_or_remote_branches}, {branches}),
"dcommit"..parser("--no-rebase", "--commit-url", "--mergeinfo", "--interactive"),
"branch"..parser("-m","--message","-t", "--tags", "-d", "--destination",
"--username", "--commit-url", "--parents"),
"log"..parser("-r", "--revision", "-v", "--verbose", "--limit",
"--incremental", "--show-commit", "--oneline"),
"find-rev"..parser("--before", "--after"),
"reset"..parser("-r", "--revision", "-p", "--parent"),
"tag",
"blame",
"set-tree",
"create-ignore",
"show-ignore",
"mkdirs",
"commit-diff",
"info",
"proplist",
"propget",
"show-externals",
"gc"
}),
"symbolic-ref",
"tag",
"tar-tree",
"unpack-file",
"unpack-objects",
"update-index",
"update-ref",
"update-server-info",
"upload-archive",
"upload-pack",
"var",
"verify-pack",
"verify-tag",
"web--browse",
"whatchanged",
"worktree"..parser({
"add"..parser(
{matchers.dirs},
{branches},
"-f", "--force",
"--detach",
"--checkout",
"--lock",
"-b"..parser({branches})
),
"list"..parser("--porcelain"),
"lock"..parser("--reason"),
"move",
"prune"..parser(
"-n", "--dry-run",
"-v", "--verbose",
"--expire"
),
"remove"..parser("-f"),
"unlock"
}),
"write-tree",
},
"--version",
"--help",
"-c",
"--exec-path",
"--html-path",
"--man-path",
"--info-path",
"-p", "--paginate", "--no-pager",
"--no-replace-objects",
"--bare",
"--git-dir=",
"--work-tree=",
"--namespace="
)
clink.arg.register_parser("git", git_parser)

1724
modules/JSON.lua Normal file

File diff suppressed because it is too large Load Diff

13
modules/clink_version.lua Normal file
View File

@ -0,0 +1,13 @@
local exports = {}
-- Busted runs these modules scripts *outside* of Clink.
-- So these Clink scripts have to work without any Clink APIs being available.
clink = clink or {}
local clink_version_encoded = clink.version_encoded or 0
exports.supports_display_filter_description = (clink_version_encoded >= 10010012)
exports.supports_color_settings = (clink_version_encoded >= 10010009)
exports.supports_query_rl_var = (clink_version_encoded >= 10010009)
return exports

41
modules/color.lua Normal file
View File

@ -0,0 +1,41 @@
local clink_version = require('clink_version')
local exports = {}
exports.BLACK = 0
exports.RED = 1
exports.GREEN = 2
exports.YELLOW = 3
exports.BLUE = 4
exports.MAGENTA = 5
exports.CYAN = 6
exports.WHITE = 7
exports.DEFAULT = 9
exports.BOLD = 1
exports.set_color = function (fore, back, bold)
local err_message = "All arguments must be either nil or numbers between 0-9"
assert(fore == nil or (type(fore) == "number" and fore >= 0 and fore <=9), err_message)
assert(back == nil or (type(back) == "number" and back >= 0 and back <=9), err_message)
fore = fore or exports.DEFAULT
back = back or exports.DEFAULT
bold = bold and exports.BOLD or 22
return "\x1b[3"..fore..";"..bold..";".."4"..back.."m"
end
exports.get_clink_color = function (setting_name)
-- Clink's settings.get() returns SGR parameters for a CSI SGR escape code.
local sgr = clink_version.supports_color_settings and settings.get(setting_name) or ""
if sgr ~= "" then
sgr = "\x1b["..sgr.."m"
end
return sgr
end
exports.color_text = function (text, fore, back, bold)
return exports.set_color(fore, back, bold)..text..exports.set_color()
end
return exports

100
modules/funclib.lua Normal file
View File

@ -0,0 +1,100 @@
local exports = {}
--- Implementation of table.filter function. Applies filter function to each
-- element of table and returns a new table with values for which filter
-- returns 'true'.
--
-- @param tbl a table to filter. Default is an empty table.
-- @param filter function that accepts an element of table, specified in the
-- first argument and returns either 'true' or 'false'. If not specified,
-- then default function is used that returns its argument.
--
-- @return a new table with values that are not filtered out by 'filter' function.
exports.filter = function (tbl, filter)
if not tbl then return {} end
if not filter then filter = function(v) return v end end
local ret = {}
for _,v in ipairs(tbl) do
if filter(v) then table.insert(ret, v) end
end
return ret
end
--- Implementation of table.map function. Applies filter function to each
-- element of table and returns a new table with values returned by mapper
-- function.
--
-- @param tbl a table to filter. Default is an empty table.
-- @param map_func function that accepts an element of table, specified in the
-- first argument and returns a new value for resultant table. If not
-- specified, then 'map' function returns it input table.
--
-- @return a new table with values produced by 'map_func'.
exports.map = function (tbl, map_func)
assert(tbl == nil or type(tbl) == "table",
"First argument must be either table or nil")
assert(map_func == nil or type(map_func) == "function",
"Second argument must be either function or nil")
if tbl == nil then return {} end
if not map_func then return tbl end
local ret = {}
for _,v in ipairs(tbl) do
table.insert(ret, map_func(v))
end
return ret
end
--- Implementation of table.reduce function. Iterates through table and calls
-- 'func' function passing an accumulator and an entry from the original
-- table. The result of table is stored in accumulator and passed to next
-- 'func' call.
--
-- @param accum an accumulator, initial value that will be passed to first
-- 'func' call.
-- @param tbl a table to reduce. Default is an empty table.
-- @param func function that accepts two params: an accumulator and an element
-- of table, specified in the first argument and returns a new value for
-- accumulator.
--
-- @return a resultant accumulator value.
exports.reduce = function (accum, tbl, func)
assert(type(func) == "function",
"Third argument must be a function")
if not tbl then return accum end
for _,v in ipairs(tbl) do
accum = func(accum, v)
end
return accum
end
--- Concatenates any number of input values into one table. If input parameter is
-- a table then its values is copied to the end of resultant table. If the
-- parameter is single value, then it is appended to the resultant table. If
-- the input value is 'nil', then it is omitted.
--
-- @return a result of concatenation. The result is always a table.
exports.concat = function (...)
local input = {...}
local ret = {}
local i = 1
while i <= #input do
local arg = input[i]
if type(arg) == 'table' then
for _,v in ipairs(arg) do
table.insert(ret, v)
end
elseif arg ~= nil then
table.insert(ret, arg)
end
i = i + 1
end
return ret
end
return exports

83
modules/gitutil.lua Normal file
View File

@ -0,0 +1,83 @@
local path = require('path')
local exports = {}
---
-- Resolves closest .git directory location.
-- Navigates subsequently up one level and tries to find .git directory
-- @param {string} path Path to directory will be checked. If not provided
-- current directory will be used
-- @return {string} Path to .git directory or nil if such dir not found
exports.get_git_dir = function (start_dir)
-- Checks if provided directory contains '.git' directory
-- and returns path to that directory
local function has_git_dir(dir)
return #clink.find_dirs(dir..'/.git') > 0 and dir..'/.git'
end
-- checks if directory contains '.git' _file_ and if it does
-- parses it and returns a path to git directory from that file
local function has_git_file(dir)
local gitfile = io.open(dir..'/.git')
if not gitfile then return false end
local git_dir = gitfile:read():match('gitdir: (.*)')
gitfile:close()
if not git_dir then return false end
-- If found path is absolute don't prepend initial
-- directory - return absolute path value
return path.is_absolute(git_dir) and git_dir
or dir..'/'..git_dir
end
-- Set default path to current directory
if not start_dir or start_dir == '.' then start_dir = clink.get_cwd() end
-- Calculate parent path now otherwise we won't be
-- able to do that inside of logical operator
local parent_path = path.pathname(start_dir)
return has_git_dir(start_dir)
or has_git_file(start_dir)
-- Otherwise go up one level and make a recursive call
or (parent_path ~= start_dir and exports.get_git_dir(parent_path) or nil)
end
exports.get_git_common_dir = function (start_dir)
local git_dir = exports.get_git_dir(start_dir)
if not git_dir then return git_dir end
local commondirfile = io.open(git_dir..'/commondir')
if commondirfile then
-- If there's a commondir file, we're in a git worktree
local commondir = commondirfile:read()
commondirfile.close()
return path.is_absolute(commondir) and commondir
or git_dir..'/'..commondir
end
return git_dir
end
---
-- Find out current branch
-- @return {nil|git branch name}
---
exports.get_git_branch = function (dir)
local git_dir = dir or exports.get_git_dir()
-- If git directory not found then we're probably outside of repo
-- or something went wrong. The same is when head_file is nil
local head_file = git_dir and io.open(git_dir..'/HEAD')
if not head_file then return end
local HEAD = head_file:read()
head_file:close()
-- if HEAD matches branch expression, then we're on named branch
-- otherwise it is a detached commit
local branch_name = HEAD:match('ref: refs/heads/(.+)')
return branch_name or 'HEAD detached at '..HEAD:sub(1, 7)
end
return exports

80
modules/matchers.lua Normal file
View File

@ -0,0 +1,80 @@
local exports = {}
local path = require('path')
local w = require('tables').wrap
exports.dirs = function(word)
-- Strip off any path components that may be on text.
local prefix = ""
local i = word:find("[\\/:][^\\/:]*$")
if i then
prefix = word:sub(1, i)
end
local include_dots = word:find("%.+$") ~= nil
-- Find matches.
local matches = w(clink.find_dirs(word.."*", true))
:filter(function (dir)
return clink.is_match(word, prefix..dir) and
(include_dots or path.is_real_dir(dir))
end)
:map(function(dir)
return prefix..dir
end)
-- If there was no matches but word is a dir then use it as the single match.
-- Otherwise tell readline that matches are files and it will do magic.
if #matches == 0 and clink.is_dir(rl_state.text) then
return {rl_state.text}
end
clink.matches_are_files()
return matches
end
exports.files = function (word)
-- Strip off any path components that may be on text.
local prefix = ""
local i = word:find("[\\/:][^\\/:]*$")
if i then
prefix = word:sub(1, i)
end
-- Find matches.
local matches = w(clink.find_files(word.."*", true))
:filter(function (file)
return clink.is_match(word, prefix..file)
end)
:map(function(file)
return prefix..file
end)
-- Tell readline that matches are files and it will do magic.
if #matches ~= 0 then
clink.matches_are_files()
end
return matches
end
exports.create_dirs_matcher = function (dir_pattern, show_dotfiles)
return function (token)
return w(clink.find_dirs(dir_pattern))
:filter(function(dir)
return clink.is_match(token, dir) and (path.is_real_dir(dir) or show_dotfiles)
end )
end
end
exports.create_files_matcher = function (file_pattern)
return function (token)
return w(clink.find_files(file_pattern))
:filter(function(file)
-- Filter out '.' and '..' entries as well
return clink.is_match(token, file) and path.is_real_dir(file)
end )
end
end
return exports

69
modules/path.lua Normal file
View File

@ -0,0 +1,69 @@
local exports = {}
local w = require('tables').wrap
exports.list_files = function (base_path, glob, recursive, reverse_separator)
local mask = glob or '/*'
local entries = w(clink.find_files(base_path..mask))
:filter(function(entry)
return exports.is_real_dir(entry)
end)
local files = entries:filter(function(entry)
return not clink.is_dir(base_path..'/'..entry)
end)
-- if 'recursive' flag is not set, we don't need to iterate
-- through directories, so just return files found
if not recursive then return files end
local sep = reverse_separator and '/' or '\\'
return entries
:filter(function(entry)
return clink.is_dir(base_path..'/'..entry)
end)
:reduce(files, function(accum, dir)
-- iterate through directories and call list_files recursively
return exports.list_files(base_path..'/'..dir, mask, recursive, reverse_separator)
:map(function(entry)
return dir..sep..entry
end)
:concat(accum)
end)
end
exports.basename = function (path)
local prefix = path
local i = path:find("[\\/:][^\\/:]*$")
if i then
prefix = path:sub(i + 1)
end
return prefix
end
exports.pathname = function (path)
local prefix = ""
local i = path:find("[\\/:][^\\/:]*$")
if i then
prefix = path:sub(1, i-1)
end
return prefix
end
exports.is_absolute = function (path)
local drive = path:find("^%s?[%l%a]:[\\/]")
if drive then return true else return false end
end
exports.is_metadir = function (dirname)
return exports.basename(dirname) == '.'
or exports.basename(dirname) == '..'
end
exports.is_real_dir = function (dirname)
return not exports.is_metadir(dirname)
end
return exports

74
modules/tables.lua Normal file
View File

@ -0,0 +1,74 @@
local concat = require('funclib').concat
local filter = require('funclib').filter
local map = require('funclib').map
local reduce = require('funclib').reduce
local exports = {}
local wrap_filter = function (tbl, filter_func)
return exports.wrap(filter(tbl, filter_func))
end
local wrap_map = function (tbl, map_func)
return exports.wrap(map(tbl, map_func))
end
local wrap_reduce = function (tbl, accum, reduce_func)
local res = reduce(accum, tbl, reduce_func)
return (type(res) == "table" and exports.wrap(res) or res)
end
local wrap_concat = function (tbl, ...)
return exports.wrap(concat(tbl, ...))
end
local wrap_print = function (tbl)
return exports.wrap(filter(tbl, function (item)
print(item)
return true
end))
end
exports.wrap = function (tbl)
if tbl == nil then tbl = {} end
if type(tbl) ~= "table" then tbl = {tbl} end
local mt = getmetatable(tbl) or {}
mt.__index = mt.__index or {}
mt.__index.filter = wrap_filter
mt.__index.map = wrap_map
mt.__index.reduce = wrap_reduce
mt.__index.concat = wrap_concat
mt.__index.print = wrap_print
mt.__index.keys = function (arg)
local res = {}
for k,_ in pairs(arg) do
table.insert(res, k)
end
return exports.wrap(res)
end
mt.__index.sort = function (arg)
table.sort(arg)
return arg
end
mt.__index.dedupe = function (arg)
local res, hash = {}, {}
for _,v in ipairs(arg) do
if not hash[v] then
hash[v] = true
table.insert(res, v)
end
end
return exports.wrap(res)
end
mt.__index.contains = function (arg, value)
for _,v in ipairs(arg) do
if v == value then return true, _ end
end
return false
end
return setmetatable(tbl, mt)
end
return exports

240
pip.lua Normal file
View File

@ -0,0 +1,240 @@
-- -*- coding: utf-8 -*-
-- preamble: common routines
local matchers = require("matchers")
local w = require("tables").wrap
local parser = clink.arg.new_parser
local function pip_libs_list(token)
local handle = io.popen('python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"')
local python_lib_path = handle:read("*a")
handle:close()
-- trim spaces
python_lib_path = python_lib_path:gsub("^%s*(.-)%s*$", "%1")
local finder = matchers.create_files_matcher(python_lib_path .. "\\*.dist-info")
local list = w(finder(token))
list =
list:map(
function(package)
package = package:gsub("-[%d%.]+dist%-info$", "")
return package
end
)
return list
end
local pip_default_flags = {
"--help",
"-h",
"--isolated",
"--verbose",
"-v",
"--version",
"-V",
"--quiet",
"-q",
"--log",
"--proxy",
"--retries",
"--timeout",
"--exists-action",
"--trusted-host",
"--cert",
"--client-cert",
"--cache-dir",
"--no-cache-dir",
"--disable-pip-version-check",
"--no-color"
}
local pip_requirement_flags = {
"--requirement" .. parser({clink.matches_are_files}),
"-r" .. parser({clink.matches_are_files})
}
local pip_index_flags = {
"--index-url",
"-i",
"--extra-index-url",
"--no-index",
"--find-links",
"-f"
}
local pip_install_download_wheel_flags = {
pip_requirement_flags,
"--no-binary",
"--only-binary",
"--prefer-binary",
"--no-build-isolation",
"--use-pep517",
"--constraint",
"-c",
"--src",
"--no-deps",
"--progress-bar" .. parser({"off", "on", "ascii", "pretty", "emoji"}),
"--global-option",
"--pre",
"--no-clean",
"--requires-hashes"
}
local pip_install_download_flags = {
pip_install_download_wheel_flags,
"--platform",
"--python-version",
"--implementation" .. parser({"pp", "jy", "cp", "ip"}),
"--abi"
}
local pip_install_parser =
parser(
{},
"--editable",
"-e",
"--target",
"-t",
"--user",
"--root",
"--prefix",
"--build",
"-b",
"--upgrade",
"-U",
"--upgrade-strategy" .. parser({"eager", "only-if-needed"}),
"--force-reinstall",
"--ignore-installed",
"-I",
"--ignore-requires-python",
"--install-option",
"--compile",
"--no-compile",
"--no-warn-script-location",
"--no-warn-conflicts"
):loop(1)
pip_install_parser:add_flags(pip_install_download_flags)
pip_install_parser:add_flags(pip_index_flags)
pip_install_parser:add_flags(pip_default_flags)
local pip_download_parser = parser({}, "--build", "-b", "--dest", "-d"):loop(1)
pip_download_parser:add_flags(pip_install_download_flags)
pip_download_parser:add_flags(pip_index_flags)
pip_download_parser:add_flags(pip_default_flags)
local pip_uninstall_parser =
parser({pip_libs_list}, "--yes", "-y"):add_flags(pip_default_flags, pip_requirement_flags):loop(1)
local pip_freeze_parser = parser({}, "--find-links", "--local", "-l", "--user", "--all", "--exclude-editable")
pip_freeze_parser:add_flags(pip_default_flags, pip_requirement_flags)
local pip_list_parser =
parser(
{},
"--outdated",
"-o",
"--uptodate",
"-u",
"--editable",
"-e",
"--local",
"-l",
"--user",
"--pre",
"--format" .. parser({"columns", "freeze", "json"}),
"--not-required",
"--exclude-editable",
"--include-editable"
)
pip_list_parser:add_flags(pip_default_flags)
local pip_config_parser =
parser(
{
"list",
"edit",
"get",
"set",
"unset"
},
"--editor",
"--global",
"--user",
"--venv",
pip_default_flags
)
pip_config_parser:add_flags(pip_default_flags)
local pip_search_parser = parser({}, "--index", "-i"):add_flags(pip_default_flags)
local pip_wheel_parser =
parser(
{},
"--wheel-dir",
"-w",
"--build-option",
"--editable",
"-e",
"--ignore-requires-python",
"--build",
"-b"
):loop(1)
pip_wheel_parser:add_flags(pip_install_download_flags)
pip_wheel_parser:add_flags(pip_index_flags)
pip_wheel_parser:add_flags(pip_default_flags)
local pip_hash_parser =
parser(
{},
"--algorithm" .. parser({"sha256", "sha384", "sha512"}),
"-a" .. parser({"sha256", "sha384", "sha512"}),
pip_default_flags
)
pip_hash_parser:add_flags(pip_default_flags)
local pip_completion_parser = parser({}, "--bash", "-b", "--zsh", "-z", "--fish", "-f"):add_flags(pip_default_flags)
local pip_help_parser =
parser(
{
"install",
"download",
"uninstall",
"freeze",
"list",
"show",
"config",
"search",
"wheel",
"hash",
"completion",
"help"
}
)
pip_help_parser:add_flags(pip_default_flags)
local pip_parser =
parser(
{
"install" .. pip_install_parser,
"download" .. pip_download_parser,
"uninstall" .. pip_uninstall_parser,
"freeze" .. pip_freeze_parser,
"list" .. pip_list_parser,
"show" .. parser({pip_libs_list}, pip_default_flags),
"config" .. pip_config_parser,
"search" .. pip_search_parser,
"wheel" .. pip_wheel_parser,
"hash" .. pip_hash_parser,
"completion" .. pip_completion_parser,
"help" .. pip_help_parser
}
)
pip_parser:add_flags(pip_default_flags)
clink.arg.register_parser("pip", pip_parser)

343
scoop.lua Normal file
View File

@ -0,0 +1,343 @@
-- -*- coding: utf-8 -*-
-- preamble: common routines
local JSON = require("JSON")
local matchers = require("matchers")
local path = require("path")
local w = require("tables").wrap
local concat = require("funclib").concat
local parser = clink.arg.new_parser
local profile = os.getenv("home") or os.getenv("USERPROFILE")
local function scoop_folder()
local folder = os.getenv("SCOOP")
if not folder then
folder = profile .. "\\scoop"
end
return folder
end
local function scoop_global_folder()
local folder = os.getenv("SCOOP_GLOBAL")
if not folder then
folder = os.getenv("ProgramData") .. "\\scoop"
end
return folder
end
local function scoop_load_config() -- luacheck: no unused args
local file = io.open(profile .. "\\.config\\scoop\\config.json")
-- If there is no such file, then close handle and return
if file == nil then
return w()
end
-- Read the whole file contents
local contents = file:read("*a")
file:close()
-- strip UTF-8-BOM
local utf8_len = contents:len()
local pat_start, _ = string.find(contents, "{")
contents = contents:sub(pat_start, utf8_len)
local data = JSON:decode(contents)
if data == nil then
return w()
end
return data
end
local function scoop_alias_list(token) -- luacheck: no unused args
local data = scoop_load_config()
return w(data.alias):keys()
end
local function scoop_config_list(token) -- luacheck: no unused args
local data = scoop_load_config()
return w(data):keys()
end
local function scoop_bucket_known_list(token) -- luacheck: no unused args
local file = io.open(scoop_folder() .. "\\apps\\scoop\\current\\buckets.json")
-- If there is no such file, then close handle and return
if file == nil then
return w()
end
-- Read the whole file contents
local contents = file:read("*a")
file:close()
local data = JSON:decode(contents)
return w(data):keys()
end
local function scoop_bucket_list(token)
local finder = matchers.create_files_matcher(scoop_folder() .. "\\buckets\\*")
local list = finder(token)
return list:filter(path.is_real_dir)
end
local function scoop_apps_list(token)
local folders = {scoop_folder(), scoop_global_folder()}
local list = w()
for _, folder in pairs(folders) do
local finder = matchers.create_files_matcher(folder .. "\\apps\\*")
local new_list = finder(token)
list = w(concat(list, new_list))
end
return list:filter(path.is_real_dir)
end
local function scoop_available_apps_list(token)
-- search in default bucket
local finder = matchers.create_files_matcher(scoop_folder() .. "\\apps\\scoop\\current\\bucket\\*.json")
local list = finder(token)
-- search in each installed bucket
local buckets = scoop_bucket_list("")
for _, bucket in pairs(buckets) do
local bucket_folder = scoop_folder() .. "\\buckets\\" .. bucket
-- check the bucket folder exists
if clink.is_dir(bucket_folder .. "\\bucket") then
bucket_folder = bucket_folder .. "\\bucket"
end
local b_finder = matchers.create_files_matcher(bucket_folder .. "\\*.json")
local b_list = b_finder(token)
list = w(concat(list, b_list))
end
-- remove ".json" of file name
for k, v in pairs(list) do
list[k] = v:gsub(".json", "")
end
return list
end
local function scoop_cache_apps_list(token)
local cache_folder = os.getenv("SCOOP_CACHE")
if not cache_folder then
cache_folder = scoop_folder() .. "\\cache"
end
local finder = matchers.create_files_matcher(cache_folder .. "\\*")
local list = finder(token)
list = w(list:filter(path.is_real_dir))
-- get name before "#" from cache list (name#version#url)
for k, v in pairs(list) do
list[k] = v:gsub("#.*$", "")
end
return list
end
local scoop_default_flags = {
"--help",
"-h"
}
local scoop_alias_parser =
parser(
{
"add",
"list" .. parser("-v", "--verbose"),
"rm" .. parser({scoop_alias_list})
}
)
local scoop_bucket_parser =
parser(
{
"add" .. parser({scoop_bucket_known_list}),
"list",
"known",
"rm" .. parser({scoop_bucket_list})
}
)
local scoop_cache_parser =
parser(
{
"show" .. parser({scoop_cache_apps_list, scoop_apps_list, "*"}),
"rm" .. parser({scoop_cache_apps_list, "*"})
}
)
local scoop_cleanup_parser =
parser(
{
scoop_apps_list,
"*"
},
"--global",
"-g",
"--cache",
"-k"
):loop(1)
local scoop_config_parser =
parser(
{
"rm" .. parser({scoop_config_list}),
scoop_config_list,
"aria2-enabled" .. parser({"true", "false"}),
"aria2-max-connection-per-server",
"aria2-min-split-size",
"aria2-options",
"aria2-retry-wait",
"aria2-split",
"debug" .. parser({"true", "false"}),
"proxy",
"show_update_log" .. parser({"true", "false"}),
"virustotal_api_key"
}
)
local scoop_uninstall_parser =
parser(
{
scoop_apps_list
},
"--global",
"-g",
"--purge",
"-p"
):loop(1)
local scoop_update_parser =
parser(
{
scoop_apps_list,
"*"
},
"--force",
"-f",
"--global",
"-g",
"--independent",
"-i",
"--no-cache",
"-k",
"--skip",
"-s",
"--quiet",
"-q"
):loop(1)
local scoop_install_parser =
parser(
{scoop_available_apps_list},
"--global",
"-g",
"--independent",
"-i",
"--no-cache",
"-k",
"--skip",
"-s",
"--arch" .. parser({"32bit", "64bit"}),
"-a" .. parser({"32bit", "64bit"})
):loop(1)
local scoop_help_parser =
parser(
{
"alias",
"bucket",
"cache",
"checkup",
"cleanup",
"config",
"create",
"depends",
"export",
"help",
"home",
"hold",
"info",
"install",
"list",
"prefix",
"reset",
"search",
"status",
"unhold",
"uninstall",
"update",
"virustotal",
"which"
},
"/?",
"--help",
"-h",
"--version"
)
local scoop_parser = parser()
scoop_parser:set_flags(scoop_default_flags)
scoop_parser:set_arguments(
{
scoop_alias_list,
"alias" .. scoop_alias_parser,
"bucket" .. scoop_bucket_parser,
"cache" .. scoop_cache_parser,
"checkup",
"cleanup" .. scoop_cleanup_parser,
"config" .. scoop_config_parser,
"create",
"depends" ..
parser(
{scoop_available_apps_list, scoop_apps_list},
"--arch" .. parser({"32bit", "64bit"}),
"-a" .. parser({"32bit", "64bit"})
),
"export",
"help" .. scoop_help_parser,
"hold" .. parser({scoop_apps_list}),
"home" .. parser({scoop_available_apps_list, scoop_apps_list}),
"info" .. parser({scoop_available_apps_list, scoop_apps_list}),
"install" .. scoop_install_parser,
"list",
"prefix" .. parser({scoop_apps_list}),
"reset" .. parser({scoop_apps_list}):loop(1),
"search",
"status",
"unhold" .. parser({scoop_apps_list}),
"uninstall" .. scoop_uninstall_parser,
"update" .. scoop_update_parser,
"virustotal" ..
parser(
{scoop_apps_list, "*"},
"--arch" .. parser({"32bit", "64bit"}),
"-a" .. parser({"32bit", "64bit"}),
"--scan",
"-s",
"--no-depends",
"-n"
):loop(1),
"which"
}
)
clink.arg.register_parser("scoop", scoop_parser)

39
ssh.lua Normal file
View File

@ -0,0 +1,39 @@
local w = require('tables').wrap
local parser = clink.arg.new_parser
local function read_lines (filename)
local lines = w({})
local f = io.open(filename)
if not f then return lines end
for line in f:lines() do table.insert(lines, line) end
f:close()
return lines
end
-- read all Host entries in the user's ssh config file
local function list_ssh_hosts()
return read_lines(clink.get_env("userprofile") .. "/.ssh/config")
:map(function (line)
return line:match('^Host%s+(.*)$')
end)
:filter()
end
local function list_known_hosts()
return read_lines(clink.get_env("userprofile") .. "/.ssh/known_hosts")
:map(function (line)
return line:match('^([%w-.]*).*')
end)
:filter()
end
local hosts = function (token) -- luacheck: no unused args
return list_ssh_hosts()
:concat(list_known_hosts())
end
local ssh_hosts_parser = parser({hosts})
clink.arg.register_parser("ssh", ssh_hosts_parser)