I've just started messing with AwesomeWM to see if I'm going to like
it, and decided that I really wanted history search in the run prompt.  So I
started hacking on prompt.lua, and ended up with this.  It's not really
finished yet (lots of loose ends that need work), but it's functional enough
that I like it.

        Maybe someone else will like it, too.

                                        Schwab
---------------------------------------------------------------------------
-- @author Julien Danjou <[email protected]>
-- @copyright 2008 Julien Danjou
-- @release v3.4.5
---------------------------------------------------------------------------

-- Grab environment we need
local assert = assert
local io = io
local table = table
local math = math
local ipairs = ipairs
local capi =
{
    keygrabber = keygrabber,
    selection = selection
}
local util = require("awful.util")
local beautiful = require("beautiful")
local naughty = require("naughty")

--- Prompt module for awful
module("awful.prompt")

--- Private data
local data = {}
data.history = {}

-- Load history file in history table
-- @param id The data.history identifier which is the path to the filename
-- @param max Optional parameter, 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")

        -- Read history file
        if f then
            for line in f:lines() do
                table.insert(data.history[id].table, line)
                if #data.history[id].table > data.history[id].max then
                    -- If the history file is longer than max entries,
                    -- retain only the last max entries.
                    table.remove (data.history[id].table, 1)
                end
            end
            f:close()
        end
    end
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] then
        if command ~= ""
            and command ~= data.history[id].table[#data.history[id].table] 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)
        end
    end
end


-- Draw the prompt text with a cursor.
-- @param args The table of arguments.
-- @param text The text.
-- @param font The font.
-- @param prompt The text prefix.
-- @param text_color The text color.
-- @param cursor_color The cursor color.
-- @param cursor_pos The cursor position.
-- @param cursor_ul The cursor underline style.
-- @param 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

    ret = prompt .. text_start .. "<span background=\"" .. 
util.color_strip_alpha(args.cursor_color) .. "\" foreground=\"" .. 
util.color_strip_alpha(args.text_color) .. "\" underline=\"" .. underline .. 
"\">" .. char .. "</span>" .. text_end .. spacer
    if args.font then ret = "<span font_desc='" .. args.font .. "'>" .. ret .. 
"</span>" end
    return ret
end

--- Run a prompt in a box.
-- @param args A table with optional arguments: fg_cursor, bg_cursor, 
ul_cursor, prompt, text, selectall, font.
-- @param textbox The textbox to use for the prompt.
-- @param exe_callback The callback function to call with command as argument 
when finished.
-- @param completion_callback The callback function to call to get completion.
-- @param history_path Optional parameter: file path where the history should 
be saved, set nil to disable history
-- @param history_max Optional parameter: set the maximum entries in history 
file, 50 by default
-- @param done_callback Optional parameter: the callback function to always 
call without arguments, regardless of whether the prompt was cancelled.
function run(args, textbox, exe_callback, completion_callback, history_path, 
history_max, done_callback)
    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 argprompt = 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 searchstate = {}
    local keyfunc
    local prompt

    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 or not exe_callback then
        return
    end
    prompt = argprompt
    textbox.text = prompt_text_with_cursor{
        text = text, text_color = inv_col, cursor_color = cur_col,
        cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
        font = font, prompt = prompt }

    capi.keygrabber.run(
    function (modifiers, key, event)
        if event ~= "press" then return true end
        -- Convert index array to hash table
        local mod = {}
        for k, v in ipairs(modifiers) do mod[v] = true end

        -- Declare key processing functions; one for regular typing, one for 
searching.
        local function key_normal (mod, key, event)
            -- Get out cases
            if (mod.Control and (key == "c" or key == "g"))
                or (not mod.Control and key == "Escape") then
                return "exit"
            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
                return "accept exit"
            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 == "e" then
                    cur_pos = #command + 1
                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 == "w" then
                    local wstart = 1
                    local wend = 1
                    local cword_start = 1
                    local cword_end = 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 = wstart
                            cword_end = cur_pos - 1
                            break
                        end
                        wstart = wend + 1
                    end
                    command = command:sub(1, cword_start - 1) .. 
command:sub(cword_end + 1)
                    cur_pos = cword_start
                elseif key == "r" then
                    return "search-rev"
                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 true end
                            if ncomp == 2 then
                                command = command_before_comp
                                textbox.text = 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,
                                    font = font, prompt = prompt }
                                return true
                            end

                            ncomp = ncomp - 2
                        elseif ncomp == 1 then
                            command_before_comp = command
                            cur_pos_before_comp = cur_pos
                        end
                        command, cur_pos = 
completion_callback(command_before_comp, cur_pos_before_comp, ncomp)
                        ncomp = ncomp + 1
                        key = ""
                    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 .. selection
                        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
            return nil
        end
        local function key_search (mod, key, event)
            if mod.Control and (key == "g" or key == "c") then
                command = searchstate.save_cmd
                history_index = searchstate.save_idx
                cur_pos = searchstate.save_curpos
                return "endsearch"
            elseif mod.Control and key == 'r' then
                if history_index > 1 then
                    searchstate.idx_start = history_index
                end
            elseif key == "Escape" then
                return "endsearch"
            elseif key == "Return" or key == "KP_Enter" then
                return "accept exit"
            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
                return "endsearch"
            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
                return "endsearch"
            elseif key == "BackSpace" then
                -- Chop off last character
                searchstate.str = searchstate.str:sub(1, #searchstate.str - 1)
            elseif key:wlen() == 1 then
                -- Append normal typed character to search string
                searchstate.str = searchstate.str .. key
                cur_pos = cur_pos + #key
            end
            if searchstate.dir < 0 then
                -- Cheesy reverse incremental search
                for i = searchstate.idx_start - 1, 1, -1 do
                    local s = data.history[history_path].table[i]
                    local f = s:find (searchstate.str)
                    if f then
                        command = s
                        history_index = i
                        cur_pos = f
                        break
                    end
                end
            end
            prompt = "(reverse-i-search)`" .. searchstate.str .. "':"
            return nil
        end

        if not keyfunc then keyfunc = key_normal end
        local kfret = keyfunc (mod, key, event)
        if kfret then
            if kfret == "search-fwd" or kfret == "search-rev" then
                searchstate.save_cmd    = command
                searchstate.save_curpos = cur_pos 
                searchstate.save_idx    = history_index
                searchstate.idx_start   = history_index
                searchstate.dir         = -1
                searchstate.str         = ""
                keyfunc = key_search
                prompt = "(reverse-i-search)`'"
            end
            if kfret == "endsearch" then
                keyfunc = key_normal
                prompt = argprompt
            end
            if kfret:find ("exit") then
                local retval = false
                textbox.text = ""
                if kfret:find ("accept") then
                    history_add (history_path, command)
                    capi.keygrabber.stop ()
                    exe_callback (command)
                    -- We already unregistered ourselves so we don't want to 
return
                    -- true, otherwise we may unregister someone else.
                    retval = true
                end
                if done_callback then done_callback() end
                return retval
            end
        end


        -- Update textbox
        textbox.text = prompt_text_with_cursor{
            text = command, text_color = inv_col, cursor_color = cur_col,
            cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
            font = font, prompt = prompt }

        return true
    end)
end

-- vim: 
filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80

Reply via email to