1 ---------------------------------------------------------------------------
2 -- @author Julien Danjou <julien@danjou.info>
3 -- @copyright 2008 Julien Danjou
4 -- @release @AWESOME_VERSION@
5 ---------------------------------------------------------------------------
7 -- Grab environment we need
18 local keygrabber
= require("awful.keygrabber")
19 local util
= require("awful.util")
20 local beautiful
= require("beautiful")
22 --- Prompt module for awful
30 local search_term
= nil
31 local function itera (inc
,a
, i
)
34 if v
then return i
,v
end
37 -- Load history file in history table
38 -- @param id The data.history identifier which is the path to the filename
39 -- @param max Optional parameter, the maximum number of entries in file
40 local function history_check_load(id
, max)
42 and not data
.history
[id
] then
43 data
.history
[id
] = { max = 50, table = {} }
46 data
.history
[id
].max = max
49 local f
= io
.open(id
, "r")
53 for line
in f
:lines() do
54 if util
.table.hasitem(data
.history
[id
].table, line
) == nil then
55 table.insert(data
.history
[id
].table, line
)
56 if #data
.history
[id
].table >= data
.history
[id
].max then
66 -- Save history table in history file
67 -- @param id The data.history identifier
68 local function history_save(id
)
69 if data
.history
[id
] then
70 local f
= io
.open(id
, "w")
73 for d
in id
:gmatch(".-/") do
76 util
.mkdir(id
:sub(1, i
- 1))
77 f
= assert(io
.open(id
, "w"))
79 for i
= 1, math
.min(#data
.history
[id
].table, data
.history
[id
].max) do
80 f
:write(data
.history
[id
].table[i
] .. "\n")
86 -- Return the number of items in history table regarding the id
87 -- @param id The data.history identifier
88 -- @return the number of items in history table, -1 if history is disabled
89 local function history_items(id
)
90 if data
.history
[id
] then
91 return #data
.history
[id
].table
97 -- Add an entry to the history file
98 -- @param id The data.history identifier
99 -- @param command The command to add
100 local function history_add(id
, command
)
101 if data
.history
[id
] and command
~= "" then
102 local index
= util
.table.hasitem(data
.history
[id
].table, command
)
104 table.insert(data
.history
[id
].table, command
)
106 -- Do not exceed our max_cmd
107 if #data
.history
[id
].table > data
.history
[id
].max then
108 table.remove(data
.history
[id
].table, 1)
113 -- Bump this command to the end of history
114 table.remove(data
.history
[id
].table, index
)
115 table.insert(data
.history
[id
].table, command
)
122 -- Draw the prompt text with a cursor.
123 -- @param args The table of arguments.
124 -- @param text The text.
125 -- @param font The font.
126 -- @param prompt The text prefix.
127 -- @param text_color The text color.
128 -- @param cursor_color The cursor color.
129 -- @param cursor_pos The cursor position.
130 -- @param cursor_ul The cursor underline style.
131 -- @param selectall If true cursor is rendered on the entire text.
132 local function prompt_text_with_cursor(args
)
133 local char
, spacer
, text_start
, text_end
, ret
134 local text
= args
.text
or ""
135 local _prompt
= args
.prompt
or ""
136 local underline
= args
.cursor_ul
or "none"
138 if args
.selectall
then
139 if #text
== 0 then char
= " " else char
= util
.escape(text
) end
143 elseif #text
< args
.cursor_pos
then
146 text_start
= util
.escape(text
)
149 char
= util
.escape(text
:sub(args
.cursor_pos
, args
.cursor_pos
))
151 text_start
= util
.escape(text
:sub(1, args
.cursor_pos
- 1))
152 text_end
= util
.escape(text
:sub(args
.cursor_pos
+ 1))
155 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
159 --- Run a prompt in a box.
160 -- @param args A table with optional arguments: fg_cursor, bg_cursor, ul_cursor, prompt, text, selectall, font, autoexec.
161 -- @param textbox The textbox to use for the prompt.
162 -- @param exe_callback The callback function to call with command as argument when finished.
163 -- @param completion_callback The callback function to call to get completion.
164 -- @param history_path Optional parameter: file path where the history should be saved, set nil to disable history
165 -- @param history_max Optional parameter: set the maximum entries in history file, 50 by default
166 -- @param done_callback Optional parameter: the callback function to always call without arguments, regardless of whether the prompt was cancelled.
167 -- @param changed_callback Optional parameter: the callback function to call with command as argument when a command was changed.
168 -- @param keypressed_callback Optional parameter: the callback function to call with mod table, key and command as arguments when a key was pressed.
169 function prompt
.run(args
, textbox
, exe_callback
, completion_callback
, history_path
, history_max
, done_callback
, changed_callback
, keypressed_callback
)
171 local theme
= beautiful
.get()
172 if not args
then args
= {} end
173 local command
= args
.text
or ""
174 local command_before_comp
175 local cur_pos_before_comp
176 local prettyprompt
= args
.prompt
or ""
177 local inv_col
= args
.fg_cursor
or theme
.fg_focus
or "black"
178 local cur_col
= args
.bg_cursor
or theme
.bg_focus
or "white"
179 local cur_ul
= args
.ul_cursor
180 local text
= args
.text
or ""
181 local font
= args
.font
or theme
.font
182 local selectall
= args
.selectall
186 history_check_load(history_path
, history_max
)
187 local history_index
= history_items(history_path
) + 1
188 -- The cursor position
189 local cur_pos
= (selectall
and 1) or text
:wlen() + 1
190 -- The completion element to use on completion request.
192 if not textbox
or not exe_callback
then
195 textbox
:set_font(font
)
196 textbox
:set_markup(prompt_text_with_cursor
{
197 text
= text
, text_color
= inv_col
, cursor_color
= cur_col
,
198 cursor_pos
= cur_pos
, cursor_ul
= cur_ul
, selectall
= selectall
,
199 prompt
= prettyprompt
})
201 local exec
= function()
202 textbox
:set_markup("")
203 history_add(history_path
, command
)
204 keygrabber
.stop(grabber
)
205 exe_callback(command
)
206 if done_callback
then done_callback() end
210 local function update()
211 textbox
:set_font(font
)
212 textbox
:set_markup(prompt_text_with_cursor
{
213 text
= command
, text_color
= inv_col
, cursor_color
= cur_col
,
214 cursor_pos
= cur_pos
, cursor_ul
= cur_ul
, selectall
= selectall
,
215 prompt
= prettyprompt
})
218 grabber
= keygrabber
.run(
219 function (modifiers
, key
, event
)
220 if event
~= "press" then return end
221 -- Convert index array to hash table
223 for k
, v
in ipairs(modifiers
) do mod[v
] = true end
225 -- Call the user specified callback. If it returns true as
226 -- the first result then return from the function. Treat the
227 -- second and third results as a new command and new prompt
228 -- to be set (if provided)
229 if keypressed_callback
then
230 local user_catched
, new_command
, new_prompt
=
231 keypressed_callback(mod, key
, command
)
232 if new_command
or new_prompt
then
234 command
= new_command
237 prettyprompt
= new_prompt
242 if changed_callback
then
243 changed_callback(command
)
250 if (mod.Control
and (key
== "c" or key
== "g"))
251 or (not mod.Control
and key
== "Escape") then
252 keygrabber
.stop(grabber
)
253 textbox
:set_markup("")
254 if done_callback
then done_callback() end
256 elseif (mod.Control
and (key
== "j" or key
== "m"))
257 or (not mod.Control
and key
== "Return")
258 or (not mod.Control
and key
== "KP_Enter") then
260 -- We already unregistered ourselves so we don't want to return
261 -- true, otherwise we may unregister someone else.
270 elseif key
== "b" then
272 cur_pos
= cur_pos
- 1
274 elseif key
== "d" then
275 if cur_pos
<= #command
then
276 command
= command
:sub(1, cur_pos
- 1) .. command
:sub(cur_pos
+ 1)
278 elseif key
== "e" then
279 cur_pos
= #command
+ 1
280 elseif key
== "r" then
281 search_term
= search_term
or command
:sub(1, cur_pos
- 1)
282 for i
,v
in (function(a
,i
) return itera(-1,a
,i
) end), data
.history
[history_path
].table, history_index
do
283 if v
:find(search_term
,1,true) ~= nil then
290 elseif key
== "s" then
291 search_term
= search_term
or command
:sub(1, cur_pos
- 1)
292 for i
,v
in (function(a
,i
) return itera(1,a
,i
) end), data
.history
[history_path
].table, history_index
do
293 if v
:find(search_term
,1,true) ~= nil then
300 elseif key
== "f" then
301 if cur_pos
<= #command
then
302 cur_pos
= cur_pos
+ 1
304 elseif key
== "h" then
306 command
= command
:sub(1, cur_pos
- 2) .. command
:sub(cur_pos
)
307 cur_pos
= cur_pos
- 1
309 elseif key
== "k" then
310 command
= command
:sub(1, cur_pos
- 1)
311 elseif key
== "u" then
312 command
= command
:sub(cur_pos
, #command
)
314 elseif key
== "Up" then
315 search_term
= command
:sub(1, cur_pos
- 1) or ""
316 for i
,v
in (function(a
,i
) return itera(-1,a
,i
) end), data
.history
[history_path
].table, history_index
do
317 if v
:find(search_term
,1,true) == 1 then
323 elseif key
== "Down" then
324 search_term
= command
:sub(1, cur_pos
- 1) or ""
325 for i
,v
in (function(a
,i
) return itera(1,a
,i
) end), data
.history
[history_path
].table, history_index
do
326 if v
:find(search_term
,1,true) == 1 then
332 elseif key
== "w" or key
== "BackSpace" then
335 local cword_start
= 1
337 while wend
< cur_pos
do
338 wend
= command
:find("[{[(,.:;_-+=@/ ]", wstart
)
339 if not wend
then wend
= #command
+ 1 end
340 if cur_pos
>= wstart
and cur_pos
<= wend
+ 1 then
342 cword_end
= cur_pos
- 1
347 command
= command
:sub(1, cword_start
- 1) .. command
:sub(cword_end
+ 1)
348 cur_pos
= cword_start
351 if completion_callback
then
352 if key
== "Tab" or key
== "ISO_Left_Tab" then
353 if key
== "ISO_Left_Tab" then
354 if ncomp
== 1 then return end
356 command
= command_before_comp
357 textbox
:set_font(font
)
358 textbox
:set_markup(prompt_text_with_cursor
{
359 text
= command_before_comp
, text_color
= inv_col
, cursor_color
= cur_col
,
360 cursor_pos
= cur_pos
, cursor_ul
= cur_ul
, selectall
= selectall
,
361 prompt
= prettyprompt
})
366 elseif ncomp
== 1 then
367 command_before_comp
= command
368 cur_pos_before_comp
= cur_pos
371 command
, cur_pos
, matches
= completion_callback(command_before_comp
, cur_pos_before_comp
, ncomp
)
374 -- execute if only one match found and autoexec flag set
375 if matches
and #matches
== 1 and args
.autoexec
then
385 if mod.Shift
and key
== "Insert" then
386 local selection
= capi
.selection()
389 local n
= selection
:find("\n")
391 selection
= selection
:sub(1, n
- 1)
393 command
= command
:sub(1, cur_pos
- 1) .. selection
.. command
:sub(cur_pos
)
394 cur_pos
= cur_pos
+ #selection
396 elseif key
== "Home" then
398 elseif key
== "End" then
399 cur_pos
= #command
+ 1
400 elseif key
== "BackSpace" then
402 command
= command
:sub(1, cur_pos
- 2) .. command
:sub(cur_pos
)
403 cur_pos
= cur_pos
- 1
405 elseif key
== "Delete" then
406 command
= command
:sub(1, cur_pos
- 1) .. command
:sub(cur_pos
+ 1)
407 elseif key
== "Left" then
408 cur_pos
= cur_pos
- 1
409 elseif key
== "Right" then
410 cur_pos
= cur_pos
+ 1
411 elseif key
== "Up" then
412 if history_index
> 1 then
413 history_index
= history_index
- 1
415 command
= data
.history
[history_path
].table[history_index
]
416 cur_pos
= #command
+ 2
418 elseif key
== "Down" then
419 if history_index
< history_items(history_path
) then
420 history_index
= history_index
+ 1
422 command
= data
.history
[history_path
].table[history_index
]
423 cur_pos
= #command
+ 2
424 elseif history_index
== history_items(history_path
) then
425 history_index
= history_index
+ 1
431 -- wlen() is UTF-8 aware but #key is not,
432 -- so check that we have one UTF-8 char but advance the cursor of # position
433 if key
:wlen() == 1 then
434 if selectall
then command
= "" end
435 command
= command
:sub(1, cur_pos
- 1) .. key
.. command
:sub(cur_pos
)
436 cur_pos
= cur_pos
+ #key
441 elseif cur_pos
> #command
+ 1 then
442 cur_pos
= #command
+ 1
447 local success
= pcall(update
)
449 -- TODO UGLY HACK TODO
450 -- Setting the text failed. Most likely reason is that the user
451 -- entered a multibyte character and pressed backspace which only
452 -- removed the last byte. Let's remove another byte.
458 command
= command
:sub(1, cur_pos
- 2) .. command
:sub(cur_pos
)
459 cur_pos
= cur_pos
- 1
460 success
= pcall(update
)
463 if changed_callback
then
464 changed_callback(command
)
471 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80