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 -- @usage The following readline keyboard shortcuts are implemented as expected:
171 -- <li><code>CTRL+A</code></li>
172 -- <li><code>CTRL+B</code></li>
173 -- <li><code>CTRL+C</code></li>
174 -- <li><code>CTRL+D</code></li>
175 -- <li><code>CTRL+E</code></li>
176 -- <li><code>CTRL+J</code></li>
177 -- <li><code>CTRL+M</code></li>
178 -- <li><code>CTRL+F</code></li>
179 -- <li><code>CTRL+H</code></li>
180 -- <li><code>CTRL+K</code></li>
181 -- <li><code>CTRL+U</code></li>
182 -- <li><code>CTRL+W</code></li>
183 -- <li><code>CTRL+BASKPACE</code></li>
184 -- <li><code>SHIFT+INSERT</code></li>
185 -- <li><code>HOME</code></li>
186 -- <li><code>END</code></li>
187 -- <li>arrow keys</li>
190 -- 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
192 -- <li><code>CTRL+R</code>: reverse history search, matches any history entry containing search term</li>
193 -- <li><code>CTRL+S</code>: forward history search, matches any history entry containing search term</li>
194 -- <li><code>CTRL+UP</code>: ZSH up line or search, matches any history entry starting with search term</li>
195 -- <li><code>CTRL+DOWN</code>: ZSH down line or search, matches any history entry starting with search term</li>
196 -- <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>
198 function prompt
.run(args
, textbox
, exe_callback
, completion_callback
, history_path
, history_max
, done_callback
, changed_callback
, keypressed_callback
)
200 local theme
= beautiful
.get()
201 if not args
then args
= {} end
202 local command
= args
.text
or ""
203 local command_before_comp
204 local cur_pos_before_comp
205 local prettyprompt
= args
.prompt
or ""
206 local inv_col
= args
.fg_cursor
or theme
.fg_focus
or "black"
207 local cur_col
= args
.bg_cursor
or theme
.bg_focus
or "white"
208 local cur_ul
= args
.ul_cursor
209 local text
= args
.text
or ""
210 local font
= args
.font
or theme
.font
211 local selectall
= args
.selectall
215 history_check_load(history_path
, history_max
)
216 local history_index
= history_items(history_path
) + 1
217 -- The cursor position
218 local cur_pos
= (selectall
and 1) or text
:wlen() + 1
219 -- The completion element to use on completion request.
221 if not textbox
or not exe_callback
then
224 textbox
:set_font(font
)
225 textbox
:set_markup(prompt_text_with_cursor
{
226 text
= text
, text_color
= inv_col
, cursor_color
= cur_col
,
227 cursor_pos
= cur_pos
, cursor_ul
= cur_ul
, selectall
= selectall
,
228 prompt
= prettyprompt
})
230 local exec
= function()
231 textbox
:set_markup("")
232 history_add(history_path
, command
)
233 keygrabber
.stop(grabber
)
234 exe_callback(command
)
235 if done_callback
then done_callback() end
239 local function update()
240 textbox
:set_font(font
)
241 textbox
:set_markup(prompt_text_with_cursor
{
242 text
= command
, text_color
= inv_col
, cursor_color
= cur_col
,
243 cursor_pos
= cur_pos
, cursor_ul
= cur_ul
, selectall
= selectall
,
244 prompt
= prettyprompt
})
247 grabber
= keygrabber
.run(
248 function (modifiers
, key
, event
)
249 if event
~= "press" then return end
250 -- Convert index array to hash table
252 for k
, v
in ipairs(modifiers
) do mod[v
] = true end
254 -- Call the user specified callback. If it returns true as
255 -- the first result then return from the function. Treat the
256 -- second and third results as a new command and new prompt
257 -- to be set (if provided)
258 if keypressed_callback
then
259 local user_catched
, new_command
, new_prompt
=
260 keypressed_callback(mod, key
, command
)
261 if new_command
or new_prompt
then
263 command
= new_command
266 prettyprompt
= new_prompt
271 if changed_callback
then
272 changed_callback(command
)
279 if (mod.Control
and (key
== "c" or key
== "g"))
280 or (not mod.Control
and key
== "Escape") then
281 keygrabber
.stop(grabber
)
282 textbox
:set_markup("")
283 history_save(history_path
)
284 if done_callback
then done_callback() end
286 elseif (mod.Control
and (key
== "j" or key
== "m"))
287 or (not mod.Control
and key
== "Return")
288 or (not mod.Control
and key
== "KP_Enter") then
290 -- We already unregistered ourselves so we don't want to return
291 -- true, otherwise we may unregister someone else.
300 elseif key
== "b" then
302 cur_pos
= cur_pos
- 1
304 elseif key
== "d" then
305 if cur_pos
<= #command
then
306 command
= command
:sub(1, cur_pos
- 1) .. command
:sub(cur_pos
+ 1)
308 elseif key
== "e" then
309 cur_pos
= #command
+ 1
310 elseif key
== "r" then
311 search_term
= search_term
or command
:sub(1, cur_pos
- 1)
312 for i
,v
in (function(a
,i
) return itera(-1,a
,i
) end), data
.history
[history_path
].table, history_index
do
313 if v
:find(search_term
,1,true) ~= nil then
320 elseif key
== "s" then
321 search_term
= search_term
or command
:sub(1, cur_pos
- 1)
322 for i
,v
in (function(a
,i
) return itera(1,a
,i
) end), data
.history
[history_path
].table, history_index
do
323 if v
:find(search_term
,1,true) ~= nil then
330 elseif key
== "f" then
331 if cur_pos
<= #command
then
332 cur_pos
= cur_pos
+ 1
334 elseif key
== "h" then
336 command
= command
:sub(1, cur_pos
- 2) .. command
:sub(cur_pos
)
337 cur_pos
= cur_pos
- 1
339 elseif key
== "k" then
340 command
= command
:sub(1, cur_pos
- 1)
341 elseif key
== "u" then
342 command
= command
:sub(cur_pos
, #command
)
344 elseif key
== "Up" then
345 search_term
= command
:sub(1, cur_pos
- 1) or ""
346 for i
,v
in (function(a
,i
) return itera(-1,a
,i
) end), data
.history
[history_path
].table, history_index
do
347 if v
:find(search_term
,1,true) == 1 then
353 elseif key
== "Down" then
354 search_term
= command
:sub(1, cur_pos
- 1) or ""
355 for i
,v
in (function(a
,i
) return itera(1,a
,i
) end), data
.history
[history_path
].table, history_index
do
356 if v
:find(search_term
,1,true) == 1 then
362 elseif key
== "w" or key
== "BackSpace" then
365 local cword_start
= 1
367 while wend
< cur_pos
do
368 wend
= command
:find("[{[(,.:;_-+=@/ ]", wstart
)
369 if not wend
then wend
= #command
+ 1 end
370 if cur_pos
>= wstart
and cur_pos
<= wend
+ 1 then
372 cword_end
= cur_pos
- 1
377 command
= command
:sub(1, cword_start
- 1) .. command
:sub(cword_end
+ 1)
378 cur_pos
= cword_start
379 elseif key
== "Delete" then
380 -- delete from history only if:
381 -- we are not dealing with a new command
382 -- the user has not edited an existing entry
383 if command
== data
.history
[history_path
].table[history_index
] then
384 table.remove(data
.history
[history_path
].table, history_index
)
385 if history_index
<= history_items(history_path
) then
386 command
= data
.history
[history_path
].table[history_index
]
387 cur_pos
= #command
+ 2
388 elseif history_index
> 1 then
389 history_index
= history_index
- 1
391 command
= data
.history
[history_path
].table[history_index
]
392 cur_pos
= #command
+ 2
400 if completion_callback
then
401 if key
== "Tab" or key
== "ISO_Left_Tab" then
402 if key
== "ISO_Left_Tab" then
403 if ncomp
== 1 then return end
405 command
= command_before_comp
406 textbox
:set_font(font
)
407 textbox
:set_markup(prompt_text_with_cursor
{
408 text
= command_before_comp
, text_color
= inv_col
, cursor_color
= cur_col
,
409 cursor_pos
= cur_pos
, cursor_ul
= cur_ul
, selectall
= selectall
,
410 prompt
= prettyprompt
})
415 elseif ncomp
== 1 then
416 command_before_comp
= command
417 cur_pos_before_comp
= cur_pos
420 command
, cur_pos
, matches
= completion_callback(command_before_comp
, cur_pos_before_comp
, ncomp
)
423 -- execute if only one match found and autoexec flag set
424 if matches
and #matches
== 1 and args
.autoexec
then
434 if mod.Shift
and key
== "Insert" then
435 local selection
= capi
.selection()
438 local n
= selection
:find("\n")
440 selection
= selection
:sub(1, n
- 1)
442 command
= command
:sub(1, cur_pos
- 1) .. selection
.. command
:sub(cur_pos
)
443 cur_pos
= cur_pos
+ #selection
445 elseif key
== "Home" then
447 elseif key
== "End" then
448 cur_pos
= #command
+ 1
449 elseif key
== "BackSpace" then
451 command
= command
:sub(1, cur_pos
- 2) .. command
:sub(cur_pos
)
452 cur_pos
= cur_pos
- 1
454 elseif key
== "Delete" then
455 command
= command
:sub(1, cur_pos
- 1) .. command
:sub(cur_pos
+ 1)
456 elseif key
== "Left" then
457 cur_pos
= cur_pos
- 1
458 elseif key
== "Right" then
459 cur_pos
= cur_pos
+ 1
460 elseif key
== "Up" then
461 if history_index
> 1 then
462 history_index
= history_index
- 1
464 command
= data
.history
[history_path
].table[history_index
]
465 cur_pos
= #command
+ 2
467 elseif key
== "Down" then
468 if history_index
< history_items(history_path
) then
469 history_index
= history_index
+ 1
471 command
= data
.history
[history_path
].table[history_index
]
472 cur_pos
= #command
+ 2
473 elseif history_index
== history_items(history_path
) then
474 history_index
= history_index
+ 1
480 -- wlen() is UTF-8 aware but #key is not,
481 -- so check that we have one UTF-8 char but advance the cursor of # position
482 if key
:wlen() == 1 then
483 if selectall
then command
= "" end
484 command
= command
:sub(1, cur_pos
- 1) .. key
.. command
:sub(cur_pos
)
485 cur_pos
= cur_pos
+ #key
490 elseif cur_pos
> #command
+ 1 then
491 cur_pos
= #command
+ 1
496 local success
= pcall(update
)
498 -- TODO UGLY HACK TODO
499 -- Setting the text failed. Most likely reason is that the user
500 -- entered a multibyte character and pressed backspace which only
501 -- removed the last byte. Let's remove another byte.
507 command
= command
:sub(1, cur_pos
- 2) .. command
:sub(cur_pos
)
508 cur_pos
= cur_pos
- 1
509 success
= pcall(update
)
512 if changed_callback
then
513 changed_callback(command
)
520 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80