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 -- <p>The following readline keyboard shortcuts are implemented as expected:</p>
161 -- <kbd>CTRL+A</kbd>, <kbd>CTRL+B</kbd>, <kbd>CTRL+C</kbd>, <kbd>CTRL+D</kbd>,
162 -- <kbd>CTRL+E</kbd>, <kbd>CTRL+J</kbd>, <kbd>CTRL+M</kbd>, <kbd>CTRL+F</kbd>,
163 -- <kbd>CTRL+H</kbd>, <kbd>CTRL+K</kbd>, <kbd>CTRL+U</kbd>, <kbd>CTRL+W</kbd>,
164 -- <kbd>CTRL+BACKSPACE</kbd>, <kbd>SHIFT+INSERT</kbd>, <kbd>HOME</kbd>,
165 -- <kbd>END</kbd> and arrow keys.
166 -- <p>The following shortcuts implement additional history manipulation commands
167 -- where the search term is defined as the substring of the command from first
168 -- character to cursor position.</p>
170 -- <li><code>CTRL+R</code>: reverse history search, matches any history entry
171 -- containing search term.</li>
172 -- <li><code>CTRL+S</code>: forward history search, matches any history entry
173 -- containing search term.</li>
174 -- <li><code>CTRL+UP</code>: ZSH up line or search, matches any history entry
175 -- starting with search term.</li>
176 -- <li><code>CTRL+DOWN</code>: ZSH down line or search, matches any history
177 -- entry starting with search term.</li>
178 -- <li><code>CTRL+DELETE</code>: delete the currently visible history entry from
180 -- This does not delete new commands or history entries under user editing.</li>
182 -- @param args A table with optional arguments: <code>fg_cursor</code>,
183 -- <code>bg_cursor</code>, <code>ul_cursor</code>, <code>prompt</code>,
184 -- <code>text</code>, <code>selectall</code>, <code>font</code>,
185 -- <code>autoexec</code>.
186 -- @param textbox The textbox to use for the prompt.
187 -- @param exe_callback The callback function to call with command as argument
189 -- @param completion_callback The callback function to call to get completion.
190 -- @param history_path Optional parameter: file path where the history should be
191 -- saved, set nil to disable history
192 -- @param history_max Optional parameter: set the maximum entries in history
193 -- file, 50 by default
194 -- @param done_callback Optional parameter: the callback function to always call
195 -- without arguments, regardless of whether the prompt was cancelled.
196 -- @param changed_callback Optional parameter: the callback function to call
197 -- with command as argument when a command was changed.
198 -- @param keypressed_callback Optional parameter: the callback function to call
199 -- with mod table, key and command as arguments when a key was pressed.
200 function prompt
.run(args
, textbox
, exe_callback
, completion_callback
, history_path
, history_max
, done_callback
, changed_callback
, keypressed_callback
)
202 local theme
= beautiful
.get()
203 if not args
then args
= {} end
204 local command
= args
.text
or ""
205 local command_before_comp
206 local cur_pos_before_comp
207 local prettyprompt
= args
.prompt
or ""
208 local inv_col
= args
.fg_cursor
or theme
.fg_focus
or "black"
209 local cur_col
= args
.bg_cursor
or theme
.bg_focus
or "white"
210 local cur_ul
= args
.ul_cursor
211 local text
= args
.text
or ""
212 local font
= args
.font
or theme
.font
213 local selectall
= args
.selectall
217 history_check_load(history_path
, history_max
)
218 local history_index
= history_items(history_path
) + 1
219 -- The cursor position
220 local cur_pos
= (selectall
and 1) or text
:wlen() + 1
221 -- The completion element to use on completion request.
223 if not textbox
or not exe_callback
then
226 textbox
:set_font(font
)
227 textbox
:set_markup(prompt_text_with_cursor
{
228 text
= text
, text_color
= inv_col
, cursor_color
= cur_col
,
229 cursor_pos
= cur_pos
, cursor_ul
= cur_ul
, selectall
= selectall
,
230 prompt
= prettyprompt
})
232 local exec
= function()
233 textbox
:set_markup("")
234 history_add(history_path
, command
)
235 keygrabber
.stop(grabber
)
236 exe_callback(command
)
237 if done_callback
then done_callback() end
241 local function update()
242 textbox
:set_font(font
)
243 textbox
:set_markup(prompt_text_with_cursor
{
244 text
= command
, text_color
= inv_col
, cursor_color
= cur_col
,
245 cursor_pos
= cur_pos
, cursor_ul
= cur_ul
, selectall
= selectall
,
246 prompt
= prettyprompt
})
249 grabber
= keygrabber
.run(
250 function (modifiers
, key
, event
)
251 if event
~= "press" then return end
252 -- Convert index array to hash table
254 for k
, v
in ipairs(modifiers
) do mod[v
] = true end
256 -- Call the user specified callback. If it returns true as
257 -- the first result then return from the function. Treat the
258 -- second and third results as a new command and new prompt
259 -- to be set (if provided)
260 if keypressed_callback
then
261 local user_catched
, new_command
, new_prompt
=
262 keypressed_callback(mod, key
, command
)
263 if new_command
or new_prompt
then
265 command
= new_command
268 prettyprompt
= new_prompt
273 if changed_callback
then
274 changed_callback(command
)
281 if (mod.Control
and (key
== "c" or key
== "g"))
282 or (not mod.Control
and key
== "Escape") then
283 keygrabber
.stop(grabber
)
284 textbox
:set_markup("")
285 history_save(history_path
)
286 if done_callback
then done_callback() end
288 elseif (mod.Control
and (key
== "j" or key
== "m"))
289 or (not mod.Control
and key
== "Return")
290 or (not mod.Control
and key
== "KP_Enter") then
292 -- We already unregistered ourselves so we don't want to return
293 -- true, otherwise we may unregister someone else.
302 elseif key
== "b" then
304 cur_pos
= cur_pos
- 1
306 elseif key
== "d" then
307 if cur_pos
<= #command
then
308 command
= command
:sub(1, cur_pos
- 1) .. command
:sub(cur_pos
+ 1)
310 elseif key
== "e" then
311 cur_pos
= #command
+ 1
312 elseif key
== "r" then
313 search_term
= search_term
or command
:sub(1, cur_pos
- 1)
314 for i
,v
in (function(a
,i
) return itera(-1,a
,i
) end), data
.history
[history_path
].table, history_index
do
315 if v
:find(search_term
,1,true) ~= nil then
322 elseif key
== "s" then
323 search_term
= search_term
or command
:sub(1, cur_pos
- 1)
324 for i
,v
in (function(a
,i
) return itera(1,a
,i
) end), data
.history
[history_path
].table, history_index
do
325 if v
:find(search_term
,1,true) ~= nil then
332 elseif key
== "f" then
333 if cur_pos
<= #command
then
334 cur_pos
= cur_pos
+ 1
336 elseif key
== "h" then
338 command
= command
:sub(1, cur_pos
- 2) .. command
:sub(cur_pos
)
339 cur_pos
= cur_pos
- 1
341 elseif key
== "k" then
342 command
= command
:sub(1, cur_pos
- 1)
343 elseif key
== "u" then
344 command
= command
:sub(cur_pos
, #command
)
346 elseif key
== "Up" then
347 search_term
= command
:sub(1, cur_pos
- 1) or ""
348 for i
,v
in (function(a
,i
) return itera(-1,a
,i
) end), data
.history
[history_path
].table, history_index
do
349 if v
:find(search_term
,1,true) == 1 then
355 elseif key
== "Down" then
356 search_term
= command
:sub(1, cur_pos
- 1) or ""
357 for i
,v
in (function(a
,i
) return itera(1,a
,i
) end), data
.history
[history_path
].table, history_index
do
358 if v
:find(search_term
,1,true) == 1 then
364 elseif key
== "w" or key
== "BackSpace" then
367 local cword_start
= 1
369 while wend
< cur_pos
do
370 wend
= command
:find("[{[(,.:;_-+=@/ ]", wstart
)
371 if not wend
then wend
= #command
+ 1 end
372 if cur_pos
>= wstart
and cur_pos
<= wend
+ 1 then
374 cword_end
= cur_pos
- 1
379 command
= command
:sub(1, cword_start
- 1) .. command
:sub(cword_end
+ 1)
380 cur_pos
= cword_start
381 elseif key
== "Delete" then
382 -- delete from history only if:
383 -- we are not dealing with a new command
384 -- the user has not edited an existing entry
385 if command
== data
.history
[history_path
].table[history_index
] then
386 table.remove(data
.history
[history_path
].table, history_index
)
387 if history_index
<= history_items(history_path
) then
388 command
= data
.history
[history_path
].table[history_index
]
389 cur_pos
= #command
+ 2
390 elseif history_index
> 1 then
391 history_index
= history_index
- 1
393 command
= data
.history
[history_path
].table[history_index
]
394 cur_pos
= #command
+ 2
402 if completion_callback
then
403 if key
== "Tab" or key
== "ISO_Left_Tab" then
404 if key
== "ISO_Left_Tab" then
405 if ncomp
== 1 then return end
407 command
= command_before_comp
408 textbox
:set_font(font
)
409 textbox
:set_markup(prompt_text_with_cursor
{
410 text
= command_before_comp
, text_color
= inv_col
, cursor_color
= cur_col
,
411 cursor_pos
= cur_pos
, cursor_ul
= cur_ul
, selectall
= selectall
,
412 prompt
= prettyprompt
})
417 elseif ncomp
== 1 then
418 command_before_comp
= command
419 cur_pos_before_comp
= cur_pos
422 command
, cur_pos
, matches
= completion_callback(command_before_comp
, cur_pos_before_comp
, ncomp
)
425 -- execute if only one match found and autoexec flag set
426 if matches
and #matches
== 1 and args
.autoexec
then
436 if mod.Shift
and key
== "Insert" then
437 local selection
= capi
.selection()
440 local n
= selection
:find("\n")
442 selection
= selection
:sub(1, n
- 1)
444 command
= command
:sub(1, cur_pos
- 1) .. selection
.. command
:sub(cur_pos
)
445 cur_pos
= cur_pos
+ #selection
447 elseif key
== "Home" then
449 elseif key
== "End" then
450 cur_pos
= #command
+ 1
451 elseif key
== "BackSpace" then
453 command
= command
:sub(1, cur_pos
- 2) .. command
:sub(cur_pos
)
454 cur_pos
= cur_pos
- 1
456 elseif key
== "Delete" then
457 command
= command
:sub(1, cur_pos
- 1) .. command
:sub(cur_pos
+ 1)
458 elseif key
== "Left" then
459 cur_pos
= cur_pos
- 1
460 elseif key
== "Right" then
461 cur_pos
= cur_pos
+ 1
462 elseif key
== "Up" then
463 if history_index
> 1 then
464 history_index
= history_index
- 1
466 command
= data
.history
[history_path
].table[history_index
]
467 cur_pos
= #command
+ 2
469 elseif key
== "Down" then
470 if history_index
< history_items(history_path
) then
471 history_index
= history_index
+ 1
473 command
= data
.history
[history_path
].table[history_index
]
474 cur_pos
= #command
+ 2
475 elseif history_index
== history_items(history_path
) then
476 history_index
= history_index
+ 1
482 -- wlen() is UTF-8 aware but #key is not,
483 -- so check that we have one UTF-8 char but advance the cursor of # position
484 if key
:wlen() == 1 then
485 if selectall
then command
= "" end
486 command
= command
:sub(1, cur_pos
- 1) .. key
.. command
:sub(cur_pos
)
487 cur_pos
= cur_pos
+ #key
492 elseif cur_pos
> #command
+ 1 then
493 cur_pos
= #command
+ 1
498 local success
= pcall(update
)
500 -- TODO UGLY HACK TODO
501 -- Setting the text failed. Most likely reason is that the user
502 -- entered a multibyte character and pressed backspace which only
503 -- removed the last byte. Let's remove another byte.
509 command
= command
:sub(1, cur_pos
- 2) .. command
:sub(cur_pos
)
510 cur_pos
= cur_pos
- 1
511 success
= pcall(update
)
514 if changed_callback
then
515 changed_callback(command
)
522 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80