2008-09-29 16:49:18 +02:00
---------------------------------------------------------------------------
-- @author Julien Danjou <julien@danjou.info>
-- @copyright 2008 Julien Danjou
-- @release @AWESOME_VERSION@
---------------------------------------------------------------------------
-- Grab environment we need
local assert = assert
local io = io
local table = table
2008-09-30 09:36:40 +02:00
local math = math
2009-04-27 21:04:15 +02:00
local ipairs = ipairs
2010-08-26 20:42:22 +02:00
local pcall = pcall
2008-09-29 16:49:18 +02:00
local capi =
{
2009-01-04 17:44:37 +01:00
selection = selection
2008-09-29 16:49:18 +02:00
}
2012-04-30 17:47:37 +02:00
local keygrabber = require ( " awful.keygrabber " )
2008-09-29 16:49:18 +02:00
local util = require ( " awful.util " )
2008-11-13 11:53:41 +01:00
local beautiful = require ( " beautiful " )
2008-09-29 16:49:18 +02:00
--- Prompt module for awful
2012-06-12 20:13:09 +02:00
-- awful.prompt
local prompt = { }
2008-09-29 16:49:18 +02:00
--- Private data
local data = { }
data.history = { }
2011-10-24 18:54:55 +02:00
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
2009-01-08 12:23:31 +01:00
-- Load history file in history table
2008-09-29 16:49:18 +02:00
-- @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
2011-10-22 15:56:30 +02:00
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
2008-09-29 16:49:18 +02:00
end
2009-04-02 13:39:52 +02:00
f : close ( )
2008-09-29 16:49:18 +02:00
end
end
end
2009-01-08 12:23:31 +01:00
-- Save history table in history file
2008-09-29 16:49:18 +02:00
-- @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
2009-01-08 12:23:31 +01:00
-- Return the number of items in history table regarding the id
2008-09-29 16:49:18 +02:00
-- @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
2009-01-08 12:23:31 +01:00
-- Add an entry to the history file
2008-09-29 16:49:18 +02:00
-- @param id The data.history identifier
-- @param command The command to add
local function history_add ( id , command )
2011-12-04 02:03:40 +01:00
if data.history [ id ] and command ~= " " then
local index = util.table . hasitem ( data.history [ id ] . table , command )
if index == nil then
2008-09-29 16:49:18 +02:00
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
2011-12-04 02:03:40 +01:00
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 )
2008-09-29 16:49:18 +02:00
history_save ( id )
end
end
end
2009-01-08 12:23:31 +01:00
-- Draw the prompt text with a cursor.
2009-08-31 21:34:46 +02:00
-- @param args The table of arguments.
2008-09-29 16:49:18 +02:00
-- @param text The text.
2009-08-31 21:34:46 +02:00
-- @param font The font.
-- @param prompt The text prefix.
2008-09-29 16:49:18 +02:00
-- @param text_color The text color.
-- @param cursor_color The cursor color.
-- @param cursor_pos The cursor position.
2009-08-31 21:34:46 +02:00
-- @param cursor_ul The cursor underline style.
2008-12-03 22:34:41 +01:00
-- @param selectall If true cursor is rendered on the entire text.
2009-08-31 21:34:46 +02:00
local function prompt_text_with_cursor ( args )
local char , spacer , text_start , text_end , ret
local text = args.text or " "
2012-06-12 20:13:09 +02:00
local _prompt = args.prompt or " "
2009-08-31 21:34:46 +02:00
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
2008-09-29 16:49:18 +02:00
char = " "
2008-11-19 13:40:13 +01:00
spacer = " "
2009-08-31 21:34:46 +02:00
text_start = util.escape ( text )
text_end = " "
2008-09-29 16:49:18 +02:00
else
2009-08-31 21:34:46 +02:00
char = util.escape ( text : sub ( args.cursor_pos , args.cursor_pos ) )
2008-11-19 13:40:13 +01:00
spacer = " "
2009-08-31 21:34:46 +02:00
text_start = util.escape ( text : sub ( 1 , args.cursor_pos - 1 ) )
text_end = util.escape ( text : sub ( args.cursor_pos + 1 ) )
2008-09-29 16:49:18 +02:00
end
2009-08-31 21:34:46 +02:00
2012-06-12 20:13:09 +02:00
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
2009-08-31 21:34:46 +02:00
return ret
2008-09-29 16:49:18 +02:00
end
--- Run a prompt in a box.
2010-10-11 12:33:38 +02:00
-- @param args A table with optional arguments: fg_cursor, bg_cursor, ul_cursor, prompt, text, selectall, font, autoexec.
2008-09-29 16:49:18 +02:00
-- @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.
2012-03-01 18:33:22 +01:00
-- @param changed_callback Optional parameter: the callback function to call with command as argument when a command was changed.
-- @param keypressed_callback Optional parameter: the callback function to call with mod table, key and command as arguments when a key was pressed.
2014-03-09 15:18:56 +01:00
-- @usage The following readline keyboard shortcuts are implemented as expected:
-- <ul>
-- <li><code>CTRL+A</code></li>
-- <li><code>CTRL+B</code></li>
-- <li><code>CTRL+C</code></li>
-- <li><code>CTRL+D</code></li>
-- <li><code>CTRL+E</code></li>
-- <li><code>CTRL+J</code></li>
-- <li><code>CTRL+M</code></li>
-- <li><code>CTRL+F</code></li>
-- <li><code>CTRL+H</code></li>
-- <li><code>CTRL+K</code></li>
-- <li><code>CTRL+U</code></li>
-- <li><code>CTRL+W</code></li>
-- <li><code>CTRL+BASKPACE</code></li>
-- <li><code>SHIFT+INSERT</code></li>
-- <li><code>HOME</code></li>
-- <li><code>END</code></li>
-- <li>arrow keys</li>
-- </ul>
-- <br/>
-- The following shortcuts implement additional history manipulation commands where the search term is defined as the substring of command from first character to cursor position
-- <ul>
-- <li><code>CTRL+R</code>: reverse history search, matches any history entry containing search term</li>
-- <li><code>CTRL+S</code>: forward history search, matches any history entry containing search term</li>
-- <li><code>CTRL+UP</code>: ZSH up line or search, matches any history entry starting with search term</li>
-- <li><code>CTRL+DOWN</code>: ZSH down line or search, matches any history entry starting with search term</li>
-- <li><code>CTRL+DELETE</code>: delete the currently visible history entry from history file. <br/>Does not delete new commands or history entries under user editing</li>
-- </ul>
2012-06-12 20:13:09 +02:00
function prompt . run ( args , textbox , exe_callback , completion_callback , history_path , history_max , done_callback , changed_callback , keypressed_callback )
2012-04-30 17:47:37 +02:00
local grabber
2008-09-29 16:49:18 +02:00
local theme = beautiful.get ( )
if not args then args = { } end
2008-11-17 19:25:11 +01:00
local command = args.text or " "
2008-09-29 16:49:18 +02:00
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 "
2008-11-06 18:07:12 +01:00
local cur_ul = args.ul_cursor
2008-11-17 19:25:11 +01:00
local text = args.text or " "
2009-08-31 21:34:46 +02:00
local font = args.font or theme.font
2009-08-31 21:37:18 +02:00
local selectall = args.selectall
2008-09-29 16:49:18 +02:00
2011-10-24 18:54:55 +02:00
search_term = nil
2008-09-29 16:49:18 +02:00
history_check_load ( history_path , history_max )
local history_index = history_items ( history_path ) + 1
-- The cursor position
2009-08-31 21:37:18 +02:00
local cur_pos = ( selectall and 1 ) or text : wlen ( ) + 1
2008-09-29 16:49:18 +02:00
-- The completion element to use on completion request.
local ncomp = 1
if not textbox or not exe_callback then
return
end
2011-04-14 07:04:48 +02:00
textbox : set_font ( font )
2010-10-06 21:27:42 +02:00
textbox : set_markup ( prompt_text_with_cursor {
2009-08-31 21:34:46 +02:00
text = text , text_color = inv_col , cursor_color = cur_col ,
2009-08-31 21:37:18 +02:00
cursor_pos = cur_pos , cursor_ul = cur_ul , selectall = selectall ,
2011-04-14 07:04:48 +02:00
prompt = prettyprompt } )
2009-08-31 21:34:46 +02:00
2010-10-11 12:33:38 +02:00
local exec = function ( )
textbox : set_markup ( " " )
history_add ( history_path , command )
2012-04-30 17:47:37 +02:00
keygrabber.stop ( grabber )
2010-10-11 12:33:38 +02:00
exe_callback ( command )
if done_callback then done_callback ( ) end
end
2012-03-01 18:01:23 +01:00
-- 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
2012-04-30 17:47:37 +02:00
grabber = keygrabber.run (
2009-04-27 21:04:15 +02:00
function ( modifiers , key , event )
2012-03-04 21:53:49 +01:00
if event ~= " press " then return end
2009-04-27 21:04:15 +02:00
-- Convert index array to hash table
local mod = { }
for k , v in ipairs ( modifiers ) do mod [ v ] = true end
2012-03-01 18:33:22 +01:00
-- 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
2008-09-29 16:49:18 +02:00
-- Get out cases
2008-11-13 14:06:22 +01:00
if ( mod.Control and ( key == " c " or key == " g " ) )
or ( not mod.Control and key == " Escape " ) then
2012-04-30 17:47:37 +02:00
keygrabber.stop ( grabber )
2010-10-06 21:27:42 +02:00
textbox : set_markup ( " " )
2014-03-09 15:18:56 +01:00
history_save ( history_path )
2008-11-13 14:06:22 +01:00
if done_callback then done_callback ( ) end
return false
elseif ( mod.Control and ( key == " j " or key == " m " ) )
2008-11-29 02:07:06 +01:00
or ( not mod.Control and key == " Return " )
or ( not mod.Control and key == " KP_Enter " ) then
2010-10-11 12:33:38 +02:00
exec ( )
2008-11-24 11:56:42 +01:00
-- We already unregistered ourselves so we don't want to return
-- true, otherwise we may unregister someone else.
2012-03-04 21:53:49 +01:00
return
2008-09-29 16:49:18 +02:00
end
-- Control cases
if mod.Control then
2009-08-31 21:37:18 +02:00
selectall = nil
2008-09-29 16:49:18 +02:00
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
2011-10-24 18:54:55 +02:00
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
2014-02-22 12:24:59 +01:00
if v : find ( search_term , 1 , true ) ~= nil then
2011-10-24 18:54:55 +02:00
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
2014-02-22 12:24:59 +01:00
if v : find ( search_term , 1 , true ) ~= nil then
2011-10-24 18:54:55 +02:00
command = v
history_index = i
cur_pos =# command + 1
break
end
end
2008-09-29 16:49:18 +02:00
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
2011-10-24 18:54:55 +02:00
elseif key == " Up " then
2014-02-22 12:21:09 +01:00
search_term = command : sub ( 1 , cur_pos - 1 ) or " "
2011-10-24 18:54:55 +02:00
for i , v in ( function ( a , i ) return itera ( - 1 , a , i ) end ) , data.history [ history_path ] . table , history_index do
2014-02-22 12:24:59 +01:00
if v : find ( search_term , 1 , true ) == 1 then
2011-10-24 18:54:55 +02:00
command = v
history_index = i
break
end
end
elseif key == " Down " then
2014-02-22 12:21:09 +01:00
search_term = command : sub ( 1 , cur_pos - 1 ) or " "
2011-10-24 18:54:55 +02:00
for i , v in ( function ( a , i ) return itera ( 1 , a , i ) end ) , data.history [ history_path ] . table , history_index do
2014-02-22 12:24:59 +01:00
if v : find ( search_term , 1 , true ) == 1 then
2011-10-24 18:54:55 +02:00
command = v
history_index = i
break
end
end
2010-10-21 09:05:40 +02:00
elseif key == " w " or key == " BackSpace " then
2008-09-29 16:49:18 +02:00
local wstart = 1
local wend = 1
local cword_start = 1
local cword_end = 1
while wend < cur_pos do
2009-05-10 08:10:35 +02:00
wend = command : find ( " [{[(,.:;_-+=@/ ] " , wstart )
2008-09-29 16:49:18 +02:00
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
2014-03-09 15:18:56 +01:00
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
2008-09-29 16:49:18 +02:00
end
else
if completion_callback then
2009-01-05 11:16:08 +01:00
if key == " Tab " or key == " ISO_Left_Tab " then
2008-09-29 16:49:18 +02:00
if key == " ISO_Left_Tab " then
2012-03-04 21:53:49 +01:00
if ncomp == 1 then return end
2008-09-29 16:49:18 +02:00
if ncomp == 2 then
command = command_before_comp
2011-04-14 07:04:48 +02:00
textbox : set_font ( font )
2010-10-06 21:27:42 +02:00
textbox : set_markup ( prompt_text_with_cursor {
2009-08-31 21:34:46 +02:00
text = command_before_comp , text_color = inv_col , cursor_color = cur_col ,
2009-08-31 21:37:18 +02:00
cursor_pos = cur_pos , cursor_ul = cur_ul , selectall = selectall ,
2011-04-14 07:04:48 +02:00
prompt = prettyprompt } )
2012-03-04 21:53:49 +01:00
return
2008-09-29 16:49:18 +02:00
end
ncomp = ncomp - 2
elseif ncomp == 1 then
command_before_comp = command
cur_pos_before_comp = cur_pos
end
2010-10-11 12:33:38 +02:00
local matches
command , cur_pos , matches = completion_callback ( command_before_comp , cur_pos_before_comp , ncomp )
2008-09-29 16:49:18 +02:00
ncomp = ncomp + 1
key = " "
2010-10-11 12:33:38 +02:00
-- execute if only one match found and autoexec flag set
if matches and # matches == 1 and args.autoexec then
exec ( )
2012-03-04 21:53:49 +01:00
return
2010-10-11 12:33:38 +02:00
end
2008-09-29 16:49:18 +02:00
else
ncomp = 1
end
end
-- Typin cases
2009-01-04 17:44:37 +01:00
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
2010-08-31 13:48:59 +02:00
command = command : sub ( 1 , cur_pos - 1 ) .. selection .. command : sub ( cur_pos )
2009-01-04 17:44:37 +01:00
cur_pos = cur_pos + # selection
end
elseif key == " Home " then
2008-09-29 16:49:18 +02:00
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
2009-05-07 22:03:04 +02:00
elseif key == " Delete " then
2008-09-29 16:49:18 +02:00
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
2008-12-15 14:06:22 +01:00
-- wlen() is UTF-8 aware but #key is not,
2008-09-29 16:49:18 +02:00
-- so check that we have one UTF-8 char but advance the cursor of # position
2008-12-15 14:06:22 +01:00
if key : wlen ( ) == 1 then
2009-08-31 21:37:18 +02:00
if selectall then command = " " end
2008-09-29 16:49:18 +02:00
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
2009-08-31 21:37:18 +02:00
selectall = nil
2008-09-29 16:49:18 +02:00
end
2010-08-26 20:42:22 +02:00
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
2012-03-01 18:33:22 +01:00
if changed_callback then
changed_callback ( command )
end
2008-09-29 16:49:18 +02:00
end )
end
2012-06-12 20:13:09 +02:00
return prompt
2011-09-11 16:50:01 +02:00
-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80