luaa: do not replace string.len(), export wlen()
[awesome.git] / lib / awful / prompt.lua.in
blobafebeb58d470f00d3abf2cd8bcfee397b15d2126
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 capi =
14 keygrabber = keygrabber
16 local util = require("awful.util")
17 local beautiful = require("beautiful")
19 --- Prompt module for awful
20 module("awful.prompt")
22 --- Private data
23 local data = {}
24 data.history = {}
26 --- Load history file in history table
27 -- @param id The data.history identifier which is the path to the filename
28 -- @param max Optional parameter, the maximum number of entries in file
29 local function history_check_load(id, max)
30 if id and id ~= ""
31 and not data.history[id] then
32 data.history[id] = { max = 50, table = {} }
34 if max then
35 data.history[id].max = max
36 end
38 local f = io.open(id, "r")
40 -- Read history file
41 if f then
42 for line in f:lines() do
43 table.insert(data.history[id].table, line)
44 if #data.history[id].table >= data.history[id].max then
45 break
46 end
47 end
48 end
49 end
50 end
52 --- Save history table in history file
53 -- @param id The data.history identifier
54 local function history_save(id)
55 if data.history[id] then
56 local f = io.open(id, "w")
57 if not f then
58 local i = 0
59 for d in id:gmatch(".-/") do
60 i = i + #d
61 end
62 util.mkdir(id:sub(1, i - 1))
63 f = assert(io.open(id, "w"))
64 end
65 for i = 1, math.min(#data.history[id].table, data.history[id].max) do
66 f:write(data.history[id].table[i] .. "\n")
67 end
68 f:close()
69 end
70 end
72 --- Return the number of items in history table regarding the id
73 -- @param id The data.history identifier
74 -- @return the number of items in history table, -1 if history is disabled
75 local function history_items(id)
76 if data.history[id] then
77 return #data.history[id].table
78 else
79 return -1
80 end
81 end
83 --- Add an entry to the history file
84 -- @param id The data.history identifier
85 -- @param command The command to add
86 local function history_add(id, command)
87 if data.history[id] then
88 if command ~= ""
89 and command ~= data.history[id].table[#data.history[id].table] then
90 table.insert(data.history[id].table, command)
92 -- Do not exceed our max_cmd
93 if #data.history[id].table > data.history[id].max then
94 table.remove(data.history[id].table, 1)
95 end
97 history_save(id)
98 end
99 end
103 --- Draw the prompt text with a cursor.
104 -- @param text The text.
105 -- @param text_color The text color.
106 -- @param cursor_color The cursor color.
107 -- @param cursor_pos The cursor position.
108 -- @param cursor_pos The cursor underline style.
109 -- @param selectall If true cursor is rendered on the entire text.
110 local function prompt_text_with_cursor(text, text_color, cursor_color, cursor_pos, cursor_ul, selectall)
111 local char, spacer, text_start, text_end
112 if not text then text = "" end
113 if #text < cursor_pos then
114 char = " "
115 spacer = ""
116 else
117 char = util.escape(text:sub(cursor_pos, cursor_pos))
118 spacer = " "
120 text_start = util.escape(text:sub(1, cursor_pos - 1))
121 text_end = util.escape(text:sub(cursor_pos + 1))
122 if selectall then
123 char = text_start .. char .. text_end
124 text_start = ""
125 text_end = ""
127 local underline = cursor_ul or "none"
128 return text_start .. "<span background=\"" .. util.color_strip_alpha(cursor_color) .. "\" foreground=\"" .. util.color_strip_alpha(text_color) .. "\" underline=\"" .. underline .. "\">" .. char .. "</span>" .. text_end .. spacer
131 --- Run a prompt in a box.
132 -- @param args A table with optional arguments: fg_cursor, bg_cursor, ul_cursor, prompt, text, selectall .
133 -- @param textbox The textbox to use for the prompt.
134 -- @param exe_callback The callback function to call with command as argument when finished.
135 -- @param completion_callback The callback function to call to get completion.
136 -- @param history_path Optional parameter: file path where the history should be saved, set nil to disable history
137 -- @param history_max Optional parameter: set the maximum entries in history file, 50 by default
138 -- @param done_callback Optional parameter: the callback function to always call without arguments, regardless of whether the prompt was cancelled.
139 function run(args, textbox, exe_callback, completion_callback, history_path, history_max, done_callback)
140 local theme = beautiful.get()
141 if not args then args = {} end
142 local command = args.text or ""
143 local command_before_comp
144 local cur_pos_before_comp
145 local prettyprompt = args.prompt or ""
146 local inv_col = args.fg_cursor or theme.fg_focus or "black"
147 local cur_col = args.bg_cursor or theme.bg_focus or "white"
148 local cur_ul = args.ul_cursor
149 local text = args.text or ""
151 history_check_load(history_path, history_max)
152 local history_index = history_items(history_path) + 1
153 -- The cursor position
154 local cur_pos = (args.selectall and 1) or text:wlen() + 1
155 -- The completion element to use on completion request.
156 local ncomp = 1
157 if not textbox or not exe_callback then
158 return
160 textbox.text = prettyprompt .. prompt_text_with_cursor(text, inv_col, cur_col, cur_pos, cur_ul, args.selectall)
161 capi.keygrabber.run(
162 function (mod, key)
163 -- Get out cases
164 if (mod.Control and (key == "c" or key == "g"))
165 or (not mod.Control and key == "Escape") then
166 textbox.text = ""
167 if done_callback then done_callback() end
168 return false
169 elseif (mod.Control and (key == "j" or key == "m"))
170 or (not mod.Control and key == "Return")
171 or (not mod.Control and key == "KP_Enter") then
172 textbox.text = ""
173 history_add(history_path, command)
174 capi.keygrabber.stop()
175 exe_callback(command)
176 if done_callback then done_callback() end
177 -- We already unregistered ourselves so we don't want to return
178 -- true, otherwise we may unregister someone else.
179 return true
182 -- Control cases
183 if mod.Control then
184 args.selectall = nil
185 if key == "a" then
186 cur_pos = 1
187 elseif key == "b" then
188 if cur_pos > 1 then
189 cur_pos = cur_pos - 1
191 elseif key == "d" then
192 if cur_pos <= #command then
193 command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1)
195 elseif key == "e" then
196 cur_pos = #command + 1
197 elseif key == "f" then
198 if cur_pos <= #command then
199 cur_pos = cur_pos + 1
201 elseif key == "h" then
202 if cur_pos > 1 then
203 command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos)
204 cur_pos = cur_pos - 1
206 elseif key == "k" then
207 command = command:sub(1, cur_pos - 1)
208 elseif key == "u" then
209 command = command:sub(cur_pos, #command)
210 cur_pos = 1
211 elseif key == "w" then
212 local wstart = 1
213 local wend = 1
214 local cword_start = 1
215 local cword_end = 1
216 while wend < cur_pos do
217 wend = command:find(" ", wstart)
218 if not wend then wend = #command + 1 end
219 if cur_pos >= wstart and cur_pos <= wend + 1 then
220 cword_start = wstart
221 cword_end = cur_pos - 1
222 break
224 wstart = wend + 1
226 command = command:sub(1, cword_start - 1) .. command:sub(cword_end + 1)
227 cur_pos = cword_start
229 else
230 if completion_callback then
231 -- That's tab
232 if key:byte() == 9 or key == "ISO_Left_Tab" then
233 if key == "ISO_Left_Tab" then
234 if ncomp == 1 then return true end
235 if ncomp == 2 then
236 command = command_before_comp
237 textbox.text = prettyprompt .. prompt_text_with_cursor(command_before_comp, inv_col, cur_col, cur_pos, args.selectall)
238 return true
241 ncomp = ncomp - 2
242 elseif ncomp == 1 then
243 command_before_comp = command
244 cur_pos_before_comp = cur_pos
246 command, cur_pos = completion_callback(command_before_comp, cur_pos_before_comp, ncomp)
247 ncomp = ncomp + 1
248 key = ""
249 else
250 ncomp = 1
254 -- Typin cases
255 if key == "Home" then
256 cur_pos = 1
257 elseif key == "End" then
258 cur_pos = #command + 1
259 elseif key == "BackSpace" then
260 if cur_pos > 1 then
261 command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos)
262 cur_pos = cur_pos - 1
264 -- That's DEL
265 elseif key:byte() == 127 then
266 command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1)
267 elseif key == "Left" then
268 cur_pos = cur_pos - 1
269 elseif key == "Right" then
270 cur_pos = cur_pos + 1
271 elseif key == "Up" then
272 if history_index > 1 then
273 history_index = history_index - 1
275 command = data.history[history_path].table[history_index]
276 cur_pos = #command + 2
278 elseif key == "Down" then
279 if history_index < history_items(history_path) then
280 history_index = history_index + 1
282 command = data.history[history_path].table[history_index]
283 cur_pos = #command + 2
284 elseif history_index == history_items(history_path) then
285 history_index = history_index + 1
287 command = ""
288 cur_pos = 1
290 else
291 -- wlen() is UTF-8 aware but #key is not,
292 -- so check that we have one UTF-8 char but advance the cursor of # position
293 if key:wlen() == 1 then
294 if args.selectall then command = "" end
295 command = command:sub(1, cur_pos - 1) .. key .. command:sub(cur_pos)
296 cur_pos = cur_pos + #key
299 if cur_pos < 1 then
300 cur_pos = 1
301 elseif cur_pos > #command + 1 then
302 cur_pos = #command + 1
304 args.selectall = nil
307 -- Update textbox
308 textbox.text = prettyprompt .. prompt_text_with_cursor(command, inv_col, cur_col, cur_pos, cur_ul, args.selectall)
310 return true
311 end)
314 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80