diff options
Diffstat (limited to 'awesome/lib/awful/prompt.lua')
-rw-r--r-- | awesome/lib/awful/prompt.lua | 777 |
1 files changed, 777 insertions, 0 deletions
diff --git a/awesome/lib/awful/prompt.lua b/awesome/lib/awful/prompt.lua new file mode 100644 index 0000000..9a86475 --- /dev/null +++ b/awesome/lib/awful/prompt.lua @@ -0,0 +1,777 @@ +--------------------------------------------------------------------------- +--- Prompt module for awful. +-- +-- By default, `rc.lua` will create one `awful.widget.prompt` per screen called +-- `mypromptbox`. It is used for both the command execution (`mod4+r`) and +-- Lua prompt (`mod4+x`). It can be re-used for random inputs using: +-- +-- -- Create a shortcut function +-- local function echo_test() +-- awful.prompt.run { +-- prompt = "Echo: ", +-- textbox = mouse.screen.mypromptbox.widget, +-- exe_callback = function(input) +-- if not input or #input == 0 then return end +-- naughty.notify{ text = "The input was: "..input } +-- end +-- } +-- end +-- +-- -- Then **IN THE globalkeys TABLE** add a new shortcut +-- awful.key({ modkey }, "e", echo_test, +-- {description = "Echo a string", group = "custom"}), +-- +-- Note that this assumes an `rc.lua` file based on the default one. The way +-- to access the screen prompt may vary. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2008 Julien Danjou +-- @module awful.prompt +--------------------------------------------------------------------------- + +-- Grab environment we need +local assert = assert +local io = io +local table = table +local math = math +local ipairs = ipairs +local pcall = pcall +local capi = +{ + selection = selection +} +local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1) +local keygrabber = require("awful.keygrabber") +local util = require("awful.util") +local beautiful = require("beautiful") +local akey = require("awful.key") + +local prompt = {} + +--- Private data +local data = {} +data.history = {} + +local search_term = nil +local function itera (inc,a, i) + i = i + inc + local v = a[i] + if v then return i,v end +end + +--- Load history file in history table +-- @param id The data.history identifier which is the path to the filename. +-- @param[opt] max The maximum number of entries in file. +local function history_check_load(id, max) + if id and id ~= "" and not data.history[id] then + data.history[id] = { max = 50, table = {} } + + if max then + data.history[id].max = max + end + + local f = io.open(id, "r") + if not f then return end + + -- Read history file + for line in f:lines() do + if util.table.hasitem(data.history[id].table, line) == nil then + table.insert(data.history[id].table, line) + if #data.history[id].table >= data.history[id].max then + break + end + end + end + f:close() + end +end + +local function is_word_char(c) + if string.find("[{[(,.:;_-+=@/ ]", c) then + return false + else + return true + end +end + +local function cword_start(s, pos) + local i = pos + if i > 1 then + i = i - 1 + end + while i >= 1 and not is_word_char(s:sub(i, i)) do + i = i - 1 + end + while i >= 1 and is_word_char(s:sub(i, i)) do + i = i - 1 + end + if i <= #s then + i = i + 1 + end + return i +end + +local function cword_end(s, pos) + local i = pos + while i <= #s and not is_word_char(s:sub(i, i)) do + i = i + 1 + end + while i <= #s and is_word_char(s:sub(i, i)) do + i = i + 1 + end + return i +end + +--- Save history table in history file +-- @param id The data.history identifier +local function history_save(id) + if data.history[id] then + local f = io.open(id, "w") + if not f then + local i = 0 + for d in id:gmatch(".-/") do + i = i + #d + end + util.mkdir(id:sub(1, i - 1)) + f = assert(io.open(id, "w")) + end + for i = 1, math.min(#data.history[id].table, data.history[id].max) do + f:write(data.history[id].table[i] .. "\n") + end + f:close() + end +end + +--- Return the number of items in history table regarding the id +-- @param id The data.history identifier +-- @return the number of items in history table, -1 if history is disabled +local function history_items(id) + if data.history[id] then + return #data.history[id].table + else + return -1 + end +end + +--- Add an entry to the history file +-- @param id The data.history identifier +-- @param command The command to add +local function history_add(id, command) + if data.history[id] and command ~= "" then + local index = util.table.hasitem(data.history[id].table, command) + if index == nil then + table.insert(data.history[id].table, command) + + -- Do not exceed our max_cmd + if #data.history[id].table > data.history[id].max then + table.remove(data.history[id].table, 1) + end + + history_save(id) + else + -- Bump this command to the end of history + table.remove(data.history[id].table, index) + table.insert(data.history[id].table, command) + history_save(id) + end + end +end + + +--- Draw the prompt text with a cursor. +-- @tparam table args The table of arguments. +-- @field text The text. +-- @field font The font. +-- @field prompt The text prefix. +-- @field text_color The text color. +-- @field cursor_color The cursor color. +-- @field cursor_pos The cursor position. +-- @field cursor_ul The cursor underline style. +-- @field selectall If true cursor is rendered on the entire text. +local function prompt_text_with_cursor(args) + local char, spacer, text_start, text_end, ret + local text = args.text or "" + local _prompt = args.prompt or "" + local underline = args.cursor_ul or "none" + + if args.selectall then + if #text == 0 then char = " " else char = util.escape(text) end + spacer = " " + text_start = "" + text_end = "" + elseif #text < args.cursor_pos then + char = " " + spacer = "" + text_start = util.escape(text) + text_end = "" + else + char = util.escape(text:sub(args.cursor_pos, args.cursor_pos)) + spacer = " " + text_start = util.escape(text:sub(1, args.cursor_pos - 1)) + text_end = util.escape(text:sub(args.cursor_pos + 1)) + end + + local cursor_color = util.ensure_pango_color(args.cursor_color) + local text_color = util.ensure_pango_color(args.text_color) + + ret = _prompt .. text_start .. "<span background=\"" .. cursor_color .. + "\" foreground=\"" .. text_color .. "\" underline=\"" .. underline .. + "\">" .. char .. "</span>" .. text_end .. spacer + return ret +end + +--- Run a prompt in a box. +-- +-- The following readline keyboard shortcuts are implemented as expected: +-- <kbd>CTRL+A</kbd>, <kbd>CTRL+B</kbd>, <kbd>CTRL+C</kbd>, <kbd>CTRL+D</kbd>, +-- <kbd>CTRL+E</kbd>, <kbd>CTRL+J</kbd>, <kbd>CTRL+M</kbd>, <kbd>CTRL+F</kbd>, +-- <kbd>CTRL+H</kbd>, <kbd>CTRL+K</kbd>, <kbd>CTRL+U</kbd>, <kbd>CTRL+W</kbd>, +-- <kbd>CTRL+BACKSPACE</kbd>, <kbd>SHIFT+INSERT</kbd>, <kbd>HOME</kbd>, +-- <kbd>END</kbd> and arrow keys. +-- +-- The following shortcuts implement additional history manipulation commands +-- where the search term is defined as the substring of the command from first +-- character to cursor position. +-- +-- * <kbd>CTRL+R</kbd>: reverse history search, matches any history entry +-- containing search term. +-- * <kbd>CTRL+S</kbd>: forward history search, matches any history entry +-- containing search term. +-- * <kbd>CTRL+UP</kbd>: ZSH up line or search, matches any history entry +-- starting with search term. +-- * <kbd>CTRL+DOWN</kbd>: ZSH down line or search, matches any history +-- entry starting with search term. +-- * <kbd>CTRL+DELETE</kbd>: delete the currently visible history entry from +-- history file. This does not delete new commands or history entries under +-- user editing. +-- +-- @tparam[opt={}] table args A table with optional arguments +-- @tparam[opt] gears.color args.fg_cursor +-- @tparam[opt] gears.color args.bg_cursor +-- @tparam[opt] gears.color args.ul_cursor +-- @tparam[opt] widget args.prompt +-- @tparam[opt] string args.text +-- @tparam[opt] boolean args.selectall +-- @tparam[opt] string args.font +-- @tparam[opt] boolean args.autoexec +-- @tparam widget args.textbox The textbox to use for the prompt. +-- @tparam function args.exe_callback The callback function to call with command as argument +-- when finished. +-- @tparam function args.completion_callback The callback function to call to get completion. +-- @tparam[opt] string args.history_path File path where the history should be +-- saved, set nil to disable history +-- @tparam[opt] function args.history_max Set the maximum entries in history +-- file, 50 by default +-- @tparam[opt] function args.done_callback The callback function to always call +-- without arguments, regardless of whether the prompt was cancelled. +-- @tparam[opt] function args.changed_callback The callback function to call +-- with command as argument when a command was changed. +-- @tparam[opt] function args.keypressed_callback The callback function to call +-- with mod table, key and command as arguments when a key was pressed. +-- @tparam[opt] function args.keyreleased_callback The callback function to call +-- with mod table, key and command as arguments when a key was pressed. +-- @tparam[opt] table args.hooks The "hooks" argument uses a syntax similar to +-- `awful.key`. It will call a function for the matching modifiers + key. +-- It receives the command (widget text/input) as an argument. +-- If the callback returns a command, this will be passed to the +-- `exe_callback`, otherwise nothing gets executed by default, and the hook +-- needs to handle it. +-- hooks = { +-- -- Apply startup notification properties with Shift-Return. +-- {{"Shift" }, "Return", function(command) +-- awful.screen.focused().mypromptbox:spawn_and_handle_error( +-- command, {floating=true}) +-- end}, +-- -- Override default behavior of "Return": launch commands prefixed +-- -- with ":" in a terminal. +-- {{}, "Return", function(command) +-- if command:sub(1,1) == ":" then +-- return terminal .. ' -e ' .. command:sub(2) +-- end +-- return command +-- end} +-- } +-- @param textbox The textbox to use for the prompt. [**DEPRECATED**] +-- @param exe_callback The callback function to call with command as argument +-- when finished. [**DEPRECATED**] +-- @param completion_callback The callback function to call to get completion. +-- [**DEPRECATED**] +-- @param[opt] history_path File path where the history should be +-- saved, set nil to disable history [**DEPRECATED**] +-- @param[opt] history_max Set the maximum entries in history +-- file, 50 by default [**DEPRECATED**] +-- @param[opt] done_callback The callback function to always call +-- without arguments, regardless of whether the prompt was cancelled. +-- [**DEPRECATED**] +-- @param[opt] changed_callback The callback function to call +-- with command as argument when a command was changed. [**DEPRECATED**] +-- @param[opt] keypressed_callback The callback function to call +-- with mod table, key and command as arguments when a key was pressed. +-- [**DEPRECATED**] +-- @see gears.color +function prompt.run(args, textbox, exe_callback, completion_callback, + history_path, history_max, done_callback, + changed_callback, keypressed_callback) + local grabber + local theme = beautiful.get() + if not args then args = {} end + local command = args.text or "" + local command_before_comp + local cur_pos_before_comp + local prettyprompt = args.prompt or "" + local inv_col = args.fg_cursor or theme.fg_focus or "black" + local cur_col = args.bg_cursor or theme.bg_focus or "white" + local cur_ul = args.ul_cursor + local text = args.text or "" + local font = args.font or theme.font + local selectall = args.selectall + local hooks = {} + + -- A function with 9 parameters deserve to die + if textbox then + util.deprecate("Use args.textbox instead of the textbox parameter") + end + if exe_callback then + util.deprecate( + "Use args.exe_callback instead of the exe_callback parameter" + ) + end + if completion_callback then + util.deprecate( + "Use args.completion_callback instead of the completion_callback parameter" + ) + end + if history_path then + util.deprecate( + "Use args.history_path instead of the history_path parameter" + ) + end + if history_max then + util.deprecate( + "Use args.history_max instead of the history_max parameter" + ) + end + if done_callback then + util.deprecate( + "Use args.done_callback instead of the done_callback parameter" + ) + end + if changed_callback then + util.deprecate( + "Use args.changed_callback instead of the changed_callback parameter" + ) + end + if keypressed_callback then + util.deprecate( + "Use args.keypressed_callback instead of the keypressed_callback parameter" + ) + end + + -- This function has already an absurd number of parameters, allow them + -- to be set using the args to avoid a "nil, nil, nil, nil, foo" scenario + keypressed_callback = keypressed_callback or args.keypressed_callback + changed_callback = changed_callback or args.changed_callback + done_callback = done_callback or args.done_callback + history_max = history_max or args.history_max + history_path = history_path or args.history_path + completion_callback = completion_callback or args.completion_callback + exe_callback = exe_callback or args.exe_callback + textbox = textbox or args.textbox + + search_term=nil + + history_check_load(history_path, history_max) + local history_index = history_items(history_path) + 1 + -- The cursor position + local cur_pos = (selectall and 1) or text:wlen() + 1 + -- The completion element to use on completion request. + local ncomp = 1 + if not textbox then + return + end + + -- Build the hook map + for _,v in ipairs(args.hooks or {}) do + if #v == 3 then + local _,key,callback = unpack(v) + if type(callback) == "function" then + hooks[key] = hooks[key] or {} + hooks[key][#hooks[key]+1] = v + else + assert("The hook's 3rd parameter has to be a function.") + end + else + assert("The hook has to have 3 parameters.") + end + end + + textbox:set_font(font) + textbox:set_markup(prompt_text_with_cursor{ + text = text, text_color = inv_col, cursor_color = cur_col, + cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall, + prompt = prettyprompt }) + + local function exec(cb, command_to_history) + textbox:set_markup("") + history_add(history_path, command_to_history) + keygrabber.stop(grabber) + if cb then cb(command) end + if done_callback then done_callback() end + end + + -- Update textbox + local function update() + textbox:set_font(font) + textbox:set_markup(prompt_text_with_cursor{ + text = command, text_color = inv_col, cursor_color = cur_col, + cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall, + prompt = prettyprompt }) + end + + grabber = keygrabber.run( + function (modifiers, key, event) + -- Convert index array to hash table + local mod = {} + for _, v in ipairs(modifiers) do mod[v] = true end + + if event ~= "press" then + if args.keyreleased_callback then + args.keyreleased_callback(mod, key, command) + end + + return + end + + -- Call the user specified callback. If it returns true as + -- the first result then return from the function. Treat the + -- second and third results as a new command and new prompt + -- to be set (if provided) + if keypressed_callback then + local user_catched, new_command, new_prompt = + keypressed_callback(mod, key, command) + if new_command or new_prompt then + if new_command then + command = new_command + end + if new_prompt then + prettyprompt = new_prompt + end + update() + end + if user_catched then + if changed_callback then + changed_callback(command) + end + return + end + end + + local filtered_modifiers = {} + + -- User defined cases + if hooks[key] then + -- Remove caps and num lock + for _, m in ipairs(modifiers) do + if not util.table.hasitem(akey.ignore_modifiers, m) then + table.insert(filtered_modifiers, m) + end + end + + for _,v in ipairs(hooks[key]) do + if #filtered_modifiers == #v[1] then + local match = true + for _,v2 in ipairs(v[1]) do + match = match and mod[v2] + end + if match then + local cb + local ret = v[3](command) + local original_command = command + if ret then + command = ret + cb = exe_callback + else + -- No callback. + cb = function() end + end + exec(cb, original_command) + return + end + end + end + end + + -- Get out cases + if (mod.Control and (key == "c" or key == "g")) + or (not mod.Control and key == "Escape") then + keygrabber.stop(grabber) + textbox:set_markup("") + history_save(history_path) + if done_callback then done_callback() end + return false + elseif (mod.Control and (key == "j" or key == "m")) + or (not mod.Control and key == "Return") + or (not mod.Control and key == "KP_Enter") then + exec(exe_callback, command) + -- We already unregistered ourselves so we don't want to return + -- true, otherwise we may unregister someone else. + return + end + + -- Control cases + if mod.Control then + selectall = nil + if key == "a" then + cur_pos = 1 + elseif key == "b" then + if cur_pos > 1 then + cur_pos = cur_pos - 1 + end + elseif key == "d" then + if cur_pos <= #command then + command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1) + end + elseif key == "p" then + if history_index > 1 then + history_index = history_index - 1 + + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + end + elseif key == "n" then + if history_index < history_items(history_path) then + history_index = history_index + 1 + + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + elseif history_index == history_items(history_path) then + history_index = history_index + 1 + + command = "" + cur_pos = 1 + end + elseif key == "e" then + cur_pos = #command + 1 + elseif key == "r" then + search_term = search_term or command:sub(1, cur_pos - 1) + for i,v in (function(a,i) return itera(-1,a,i) end), data.history[history_path].table, history_index do + if v:find(search_term,1,true) ~= nil then + command=v + history_index=i + cur_pos=#command+1 + break + end + end + elseif key == "s" then + search_term = search_term or command:sub(1, cur_pos - 1) + for i,v in (function(a,i) return itera(1,a,i) end), data.history[history_path].table, history_index do + if v:find(search_term,1,true) ~= nil then + command=v + history_index=i + cur_pos=#command+1 + break + end + end + elseif key == "f" then + if cur_pos <= #command then + cur_pos = cur_pos + 1 + end + elseif key == "h" then + if cur_pos > 1 then + command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) + cur_pos = cur_pos - 1 + end + elseif key == "k" then + command = command:sub(1, cur_pos - 1) + elseif key == "u" then + command = command:sub(cur_pos, #command) + cur_pos = 1 + elseif key == "Up" then + search_term = command:sub(1, cur_pos - 1) or "" + for i,v in (function(a,i) return itera(-1,a,i) end), data.history[history_path].table, history_index do + if v:find(search_term,1,true) == 1 then + command=v + history_index=i + break + end + end + elseif key == "Down" then + search_term = command:sub(1, cur_pos - 1) or "" + for i,v in (function(a,i) return itera(1,a,i) end), data.history[history_path].table, history_index do + if v:find(search_term,1,true) == 1 then + command=v + history_index=i + break + end + end + elseif key == "w" or key == "BackSpace" then + local wstart = 1 + local wend = 1 + local cword_start_pos = 1 + local cword_end_pos = 1 + while wend < cur_pos do + wend = command:find("[{[(,.:;_-+=@/ ]", wstart) + if not wend then wend = #command + 1 end + if cur_pos >= wstart and cur_pos <= wend + 1 then + cword_start_pos = wstart + cword_end_pos = cur_pos - 1 + break + end + wstart = wend + 1 + end + command = command:sub(1, cword_start_pos - 1) .. command:sub(cword_end_pos + 1) + cur_pos = cword_start_pos + elseif key == "Delete" then + -- delete from history only if: + -- we are not dealing with a new command + -- the user has not edited an existing entry + if command == data.history[history_path].table[history_index] then + table.remove(data.history[history_path].table, history_index) + if history_index <= history_items(history_path) then + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + elseif history_index > 1 then + history_index = history_index - 1 + + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + else + command = "" + cur_pos = 1 + end + end + end + elseif mod.Mod1 or mod.Mod3 then + if key == "b" then + cur_pos = cword_start(command, cur_pos) + elseif key == "f" then + cur_pos = cword_end(command, cur_pos) + elseif key == "d" then + command = command:sub(1, cur_pos - 1) .. command:sub(cword_end(command, cur_pos)) + elseif key == "BackSpace" then + local wstart = cword_start(command, cur_pos) + command = command:sub(1, wstart - 1) .. command:sub(cur_pos) + cur_pos = wstart + end + else + if completion_callback then + if key == "Tab" or key == "ISO_Left_Tab" then + if key == "ISO_Left_Tab" then + if ncomp == 1 then return end + if ncomp == 2 then + command = command_before_comp + textbox:set_font(font) + textbox:set_markup(prompt_text_with_cursor{ + text = command_before_comp, text_color = inv_col, cursor_color = cur_col, + cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall, + prompt = prettyprompt }) + return + end + + ncomp = ncomp - 2 + elseif ncomp == 1 then + command_before_comp = command + cur_pos_before_comp = cur_pos + end + local matches + command, cur_pos, matches = completion_callback(command_before_comp, cur_pos_before_comp, ncomp) + ncomp = ncomp + 1 + key = "" + -- execute if only one match found and autoexec flag set + if matches and #matches == 1 and args.autoexec then + exec(exe_callback) + return + end + else + ncomp = 1 + end + end + + -- Typin cases + if mod.Shift and key == "Insert" then + local selection = capi.selection() + if selection then + -- Remove \n + local n = selection:find("\n") + if n then + selection = selection:sub(1, n - 1) + end + command = command:sub(1, cur_pos - 1) .. selection .. command:sub(cur_pos) + cur_pos = cur_pos + #selection + end + elseif key == "Home" then + cur_pos = 1 + elseif key == "End" then + cur_pos = #command + 1 + elseif key == "BackSpace" then + if cur_pos > 1 then + command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) + cur_pos = cur_pos - 1 + end + elseif key == "Delete" then + command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1) + elseif key == "Left" then + cur_pos = cur_pos - 1 + elseif key == "Right" then + cur_pos = cur_pos + 1 + elseif key == "Up" then + if history_index > 1 then + history_index = history_index - 1 + + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + end + elseif key == "Down" then + if history_index < history_items(history_path) then + history_index = history_index + 1 + + command = data.history[history_path].table[history_index] + cur_pos = #command + 2 + elseif history_index == history_items(history_path) then + history_index = history_index + 1 + + command = "" + cur_pos = 1 + end + else + -- wlen() is UTF-8 aware but #key is not, + -- so check that we have one UTF-8 char but advance the cursor of # position + if key:wlen() == 1 then + if selectall then command = "" end + command = command:sub(1, cur_pos - 1) .. key .. command:sub(cur_pos) + cur_pos = cur_pos + #key + end + end + if cur_pos < 1 then + cur_pos = 1 + elseif cur_pos > #command + 1 then + cur_pos = #command + 1 + end + selectall = nil + end + + local success = pcall(update) + while not success do + -- TODO UGLY HACK TODO + -- Setting the text failed. Most likely reason is that the user + -- entered a multibyte character and pressed backspace which only + -- removed the last byte. Let's remove another byte. + if cur_pos <= 1 then + -- No text left?! + break + end + + command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos) + cur_pos = cur_pos - 1 + success = pcall(update) + end + + if changed_callback then + changed_callback(command) + end + end) +end + +return prompt + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 |