prompt: CTRL+DELETE deletes history entries
[awesome.git] / lib / awful / prompt.lua.in
blob8f5e28aa7f8eb4ffdca94e9708e8f0fda2d20d64
1 ---------------------------------------------------------------------------
2 -- @author Julien Danjou <julien@danjou.info>
3 -- @copyright 2008 Julien Danjou
4 -- @release @AWESOME_VERSION@
5 ---------------------------------------------------------------------------
7 -- Grab environment we need
8 local assert = assert
9 local io = io
10 local table = table
11 local math = math
12 local ipairs = ipairs
13 local pcall = pcall
14 local capi =
16 selection = selection
18 local keygrabber = require("awful.keygrabber")
19 local util = require("awful.util")
20 local beautiful = require("beautiful")
22 --- Prompt module for awful
23 -- awful.prompt
24 local prompt = {}
26 --- Private data
27 local data = {}
28 data.history = {}
30 local search_term = nil
31 local function itera (inc,a, i)
32 i = i + inc
33 local v = a[i]
34 if v then return i,v end
35 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)
41 if id and id ~= ""
42 and not data.history[id] then
43 data.history[id] = { max = 50, table = {} }
45 if max then
46 data.history[id].max = max
47 end
49 local f = io.open(id, "r")
51 -- Read history file
52 if f then
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
57 break
58 end
59 end
60 end
61 f:close()
62 end
63 end
64 end
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")
71 if not f then
72 local i = 0
73 for d in id:gmatch(".-/") do
74 i = i + #d
75 end
76 util.mkdir(id:sub(1, i - 1))
77 f = assert(io.open(id, "w"))
78 end
79 for i = 1, math.min(#data.history[id].table, data.history[id].max) do
80 f:write(data.history[id].table[i] .. "\n")
81 end
82 f:close()
83 end
84 end
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
92 else
93 return -1
94 end
95 end
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)
103 if index == nil then
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)
111 history_save(id)
112 else
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)
116 history_save(id)
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
140 spacer = " "
141 text_start = ""
142 text_end = ""
143 elseif #text < args.cursor_pos then
144 char = " "
145 spacer = ""
146 text_start = util.escape(text)
147 text_end = ""
148 else
149 char = util.escape(text:sub(args.cursor_pos, args.cursor_pos))
150 spacer = " "
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
156 return ret
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:
170 -- <ul>
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>
188 -- </ul>
189 -- <br/>
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
191 -- <ul>
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>
197 -- </ul>
198 function prompt.run(args, textbox, exe_callback, completion_callback, history_path, history_max, done_callback, changed_callback, keypressed_callback)
199 local grabber
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
213 search_term=nil
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.
220 local ncomp = 1
221 if not textbox or not exe_callback then
222 return
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
238 -- Update textbox
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
251 local mod = {}
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
262 if new_command then
263 command = new_command
265 if new_prompt then
266 prettyprompt = new_prompt
268 update()
270 if user_catched then
271 if changed_callback then
272 changed_callback(command)
274 return
278 -- Get out cases
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
285 return false
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
289 exec()
290 -- We already unregistered ourselves so we don't want to return
291 -- true, otherwise we may unregister someone else.
292 return
295 -- Control cases
296 if mod.Control then
297 selectall = nil
298 if key == "a" then
299 cur_pos = 1
300 elseif key == "b" then
301 if cur_pos > 1 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
314 command=v
315 history_index=i
316 cur_pos=#command+1
317 break
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
324 command=v
325 history_index=i
326 cur_pos=#command+1
327 break
330 elseif key == "f" then
331 if cur_pos <= #command then
332 cur_pos = cur_pos + 1
334 elseif key == "h" then
335 if cur_pos > 1 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)
343 cur_pos = 1
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
348 command=v
349 history_index=i
350 break
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
357 command=v
358 history_index=i
359 break
362 elseif key == "w" or key == "BackSpace" then
363 local wstart = 1
364 local wend = 1
365 local cword_start = 1
366 local cword_end = 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
371 cword_start = wstart
372 cword_end = cur_pos - 1
373 break
375 wstart = wend + 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
393 else
394 command = ""
395 cur_pos = 1
399 else
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
404 if ncomp == 2 then
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 })
411 return
414 ncomp = ncomp - 2
415 elseif ncomp == 1 then
416 command_before_comp = command
417 cur_pos_before_comp = cur_pos
419 local matches
420 command, cur_pos, matches = completion_callback(command_before_comp, cur_pos_before_comp, ncomp)
421 ncomp = ncomp + 1
422 key = ""
423 -- execute if only one match found and autoexec flag set
424 if matches and #matches == 1 and args.autoexec then
425 exec()
426 return
428 else
429 ncomp = 1
433 -- Typin cases
434 if mod.Shift and key == "Insert" then
435 local selection = capi.selection()
436 if selection then
437 -- Remove \n
438 local n = selection:find("\n")
439 if n then
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
446 cur_pos = 1
447 elseif key == "End" then
448 cur_pos = #command + 1
449 elseif key == "BackSpace" then
450 if cur_pos > 1 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
476 command = ""
477 cur_pos = 1
479 else
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
488 if cur_pos < 1 then
489 cur_pos = 1
490 elseif cur_pos > #command + 1 then
491 cur_pos = #command + 1
493 selectall = nil
496 local success = pcall(update)
497 while not success do
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.
502 if cur_pos <= 1 then
503 -- No text left?!
504 break
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)
515 end)
518 return prompt
520 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80