232 lines
7.8 KiB
Lua
232 lines
7.8 KiB
Lua
---------------------------------------------------------------------------
|
|
--- Completion module.
|
|
--
|
|
-- This module store a set of function using shell to complete commands name.
|
|
--
|
|
-- @author Julien Danjou <julien@danjou.info>
|
|
-- @author Sébastien Gross <seb-awesome@chezwam.org>
|
|
-- @copyright 2008 Julien Danjou, Sébastien Gross
|
|
-- @module awful.completion
|
|
---------------------------------------------------------------------------
|
|
|
|
local gfs = require("gears.filesystem")
|
|
|
|
-- Grab environment we need
|
|
local io = io
|
|
local os = os
|
|
local table = table
|
|
local math = math
|
|
local print = print
|
|
local pairs = pairs
|
|
local string = string
|
|
|
|
local gears_debug = require("gears.debug")
|
|
|
|
local completion = {}
|
|
|
|
-- mapping of command/completion function
|
|
local bashcomp_funcs = {}
|
|
local bashcomp_src = "@SYSCONFDIR@/bash_completion"
|
|
|
|
--- Enable programmable bash completion in awful.completion.bash at the price of
|
|
-- a slight overhead.
|
|
-- @param src The bash completion source file, /etc/bash_completion by default.
|
|
function completion.bashcomp_load(src)
|
|
if src then bashcomp_src = src end
|
|
local c, err = io.popen("/usr/bin/env bash -c 'source " .. bashcomp_src .. "; complete -p'")
|
|
if c then
|
|
while true do
|
|
local line = c:read("*line")
|
|
if not line then break end
|
|
-- if a bash function is used for completion, register it
|
|
if line:match(".* -F .*") then
|
|
bashcomp_funcs[line:gsub(".* (%S+)$","%1")] = line:gsub(".*-F +(%S+) .*$", "%1")
|
|
end
|
|
end
|
|
c:close()
|
|
else
|
|
print(err)
|
|
end
|
|
end
|
|
|
|
local function bash_escape(str)
|
|
str = str:gsub(" ", "\\ ")
|
|
str = str:gsub("%[", "\\[")
|
|
str = str:gsub("%]", "\\]")
|
|
str = str:gsub("%(", "\\(")
|
|
str = str:gsub("%)", "\\)")
|
|
return str
|
|
end
|
|
|
|
completion.default_shell = nil
|
|
|
|
--- Use shell completion system to complete commands and filenames.
|
|
-- @tparam string command The command line.
|
|
-- @tparam number cur_pos The cursor position.
|
|
-- @tparam number ncomp The element number to complete.
|
|
-- @tparam[opt=based on SHELL] string shell The shell to use for completion.
|
|
-- Supports "bash" and "zsh".
|
|
-- @treturn string The new command.
|
|
-- @treturn number The new cursor position.
|
|
-- @treturn table The table with all matches.
|
|
function completion.shell(command, cur_pos, ncomp, shell)
|
|
local wstart = 1
|
|
local wend = 1
|
|
local words = {}
|
|
local cword_index = 0
|
|
local cword_start = 0
|
|
local cword_end = 0
|
|
local i = 1
|
|
local comptype = "file"
|
|
|
|
local function str_starts(str, start)
|
|
return string.sub(str, 1, string.len(start)) == start
|
|
end
|
|
|
|
-- do nothing if we are on a letter, i.e. not at len + 1 or on a space
|
|
if cur_pos ~= #command + 1 and command:sub(cur_pos, cur_pos) ~= " " then
|
|
return command, cur_pos
|
|
elseif #command == 0 then
|
|
return command, cur_pos
|
|
end
|
|
|
|
while wend <= #command do
|
|
wend = command:find(" ", wstart)
|
|
if not wend then wend = #command + 1 end
|
|
table.insert(words, command:sub(wstart, wend - 1))
|
|
if cur_pos >= wstart and cur_pos <= wend + 1 then
|
|
cword_start = wstart
|
|
cword_end = wend
|
|
cword_index = i
|
|
end
|
|
wstart = wend + 1
|
|
i = i + 1
|
|
end
|
|
|
|
if cword_index == 1 then
|
|
comptype = "command"
|
|
end
|
|
|
|
local shell_cmd
|
|
if not shell then
|
|
if not completion.default_shell then
|
|
local env_shell = os.getenv('SHELL')
|
|
if not env_shell then
|
|
gears_debug.print_warning('SHELL not set in environment, falling back to bash.')
|
|
completion.default_shell = 'bash'
|
|
elseif env_shell:match('zsh$') then
|
|
completion.default_shell = 'zsh'
|
|
else
|
|
completion.default_shell = 'bash'
|
|
end
|
|
end
|
|
shell = completion.default_shell
|
|
end
|
|
if shell == 'zsh' then
|
|
if comptype == "file" then
|
|
-- NOTE: ${~:-"..."} turns on GLOB_SUBST, useful for expansion of
|
|
-- "~/" ($HOME). ${:-"foo"} is the string "foo" as var.
|
|
shell_cmd = "/usr/bin/env zsh -c 'local -a res; res=( ${~:-"
|
|
.. string.format('%q', words[cword_index]) .. "}*(N) ); "
|
|
.. "print -ln -- ${res[@]}'"
|
|
else
|
|
-- Check commands, aliases, builtins, functions and reswords.
|
|
-- Adds executables and non-empty dirs from $PWD (pwd_exe).
|
|
shell_cmd = "/usr/bin/env zsh -c 'local -a res pwd_exe; "..
|
|
"pwd_exe=(*(N*:t) *(NF:t)); "..
|
|
"res=( "..
|
|
"\"${(k)commands[@]}\" \"${(k)aliases[@]}\" \"${(k)builtins[@]}\" \"${(k)functions[@]}\" "..
|
|
"\"${(k)reswords[@]}\" "..
|
|
"./${^${pwd_exe}} "..
|
|
"); "..
|
|
"print -ln -- ${(M)res[@]:#" .. string.format('%q', words[cword_index]) .. "*}'"
|
|
end
|
|
else
|
|
if bashcomp_funcs[words[1]] then
|
|
-- fairly complex command with inline bash script to get the possible completions
|
|
shell_cmd = "/usr/bin/env bash -c 'source " .. bashcomp_src .. "; " ..
|
|
"__print_completions() { for ((i=0;i<${#COMPREPLY[*]};i++)); do echo ${COMPREPLY[i]}; done }; " ..
|
|
"COMP_WORDS=(" .. command .."); COMP_LINE=\"" .. command .. "\"; " ..
|
|
"COMP_COUNT=" .. cur_pos .. "; COMP_CWORD=" .. cword_index-1 .. "; " ..
|
|
bashcomp_funcs[words[1]] .. "; __print_completions'"
|
|
else
|
|
shell_cmd = "/usr/bin/env bash -c 'compgen -A " .. comptype .. " "
|
|
.. string.format('%q', words[cword_index]) .. "'"
|
|
end
|
|
end
|
|
local c, err = io.popen(shell_cmd .. " | sort -u")
|
|
local output = {}
|
|
if c then
|
|
while true do
|
|
local line = c:read("*line")
|
|
if not line then break end
|
|
if gfs.is_dir(line) then
|
|
line = line .. "/"
|
|
end
|
|
table.insert(output, bash_escape(line))
|
|
end
|
|
|
|
c:close()
|
|
else
|
|
print(err)
|
|
end
|
|
|
|
-- no completion, return
|
|
if #output == 0 then
|
|
return command, cur_pos
|
|
end
|
|
|
|
-- cycle
|
|
while ncomp > #output do
|
|
ncomp = ncomp - #output
|
|
end
|
|
|
|
local str = command:sub(1, cword_start - 1) .. output[ncomp] .. command:sub(cword_end)
|
|
cur_pos = cword_start + #output[ncomp]
|
|
|
|
return str, cur_pos, output
|
|
end
|
|
|
|
--- Run a generic completion.
|
|
-- For this function to run properly the awful.completion.keyword table should
|
|
-- be fed up with all keywords. The completion is run against these keywords.
|
|
-- @param text The current text the user had typed yet.
|
|
-- @param cur_pos The current cursor position.
|
|
-- @param ncomp The number of yet requested completion using current text.
|
|
-- @param keywords The keywords table uised for completion.
|
|
-- @return The new match, the new cursor position, the table of all matches.
|
|
function completion.generic(text, cur_pos, ncomp, keywords) -- luacheck: no unused args
|
|
-- The keywords table may be empty
|
|
if #keywords == 0 then
|
|
return text, #text + 1
|
|
end
|
|
|
|
-- if no text had been typed yet, then we could start cycling around all
|
|
-- keywords with out filtering and move the cursor at the end of keyword
|
|
if text == nil or #text == 0 then
|
|
ncomp = math.fmod(ncomp - 1, #keywords) + 1
|
|
return keywords[ncomp], #keywords[ncomp] + 2
|
|
end
|
|
|
|
-- Filter out only keywords starting with text
|
|
local matches = {}
|
|
for _, x in pairs(keywords) do
|
|
if x:sub(1, #text) == text then
|
|
table.insert(matches, x)
|
|
end
|
|
end
|
|
|
|
-- if there are no matches just leave out with the current text and position
|
|
if #matches == 0 then
|
|
return text, #text + 1, matches
|
|
end
|
|
|
|
-- cycle around all matches
|
|
ncomp = math.fmod(ncomp - 1, #matches) + 1
|
|
return matches[ncomp], #matches[ncomp] + 1, matches
|
|
end
|
|
|
|
return completion
|
|
|
|
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
|