awful.prompt: "Fix" for multi-byte characters
[awesome.git] / lib / awful / prompt.lua.in
blobf97665f888145cd7662f754d7650bab424ac4cc8
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 keygrabber = keygrabber,
17 selection = selection
19 local util = require("awful.util")
20 local beautiful = require("beautiful")
22 --- Prompt module for awful
23 module("awful.prompt")
25 --- Private data
26 local data = {}
27 data.history = {}
29 -- Load history file in history table
30 -- @param id The data.history identifier which is the path to the filename
31 -- @param max Optional parameter, the maximum number of entries in file
32 local function history_check_load(id, max)
33 if id and id ~= ""
34 and not data.history[id] then
35 data.history[id] = { max = 50, table = {} }
37 if max then
38 data.history[id].max = max
39 end
41 local f = io.open(id, "r")
43 -- Read history file
44 if f then
45 for line in f:lines() do
46 table.insert(data.history[id].table, line)
47 if #data.history[id].table >= data.history[id].max then
48 break
49 end
50 end
51 f:close()
52 end
53 end
54 end
56 -- Save history table in history file
57 -- @param id The data.history identifier
58 local function history_save(id)
59 if data.history[id] then
60 local f = io.open(id, "w")
61 if not f then
62 local i = 0
63 for d in id:gmatch(".-/") do
64 i = i + #d
65 end
66 util.mkdir(id:sub(1, i - 1))
67 f = assert(io.open(id, "w"))
68 end
69 for i = 1, math.min(#data.history[id].table, data.history[id].max) do
70 f:write(data.history[id].table[i] .. "\n")
71 end
72 f:close()
73 end
74 end
76 -- Return the number of items in history table regarding the id
77 -- @param id The data.history identifier
78 -- @return the number of items in history table, -1 if history is disabled
79 local function history_items(id)
80 if data.history[id] then
81 return #data.history[id].table
82 else
83 return -1
84 end
85 end
87 -- Add an entry to the history file
88 -- @param id The data.history identifier
89 -- @param command The command to add
90 local function history_add(id, command)
91 if data.history[id] then
92 if command ~= ""
93 and command ~= data.history[id].table[#data.history[id].table] then
94 table.insert(data.history[id].table, command)
96 -- Do not exceed our max_cmd
97 if #data.history[id].table > data.history[id].max then
98 table.remove(data.history[id].table, 1)
99 end
101 history_save(id)
107 -- Draw the prompt text with a cursor.
108 -- @param args The table of arguments.
109 -- @param text The text.
110 -- @param font The font.
111 -- @param prompt The text prefix.
112 -- @param text_color The text color.
113 -- @param cursor_color The cursor color.
114 -- @param cursor_pos The cursor position.
115 -- @param cursor_ul The cursor underline style.
116 -- @param selectall If true cursor is rendered on the entire text.
117 local function prompt_text_with_cursor(args)
118 local char, spacer, text_start, text_end, ret
119 local text = args.text or ""
120 local prompt = args.prompt or ""
121 local underline = args.cursor_ul or "none"
123 if args.selectall then
124 if #text == 0 then char = " " else char = util.escape(text) end
125 spacer = " "
126 text_start = ""
127 text_end = ""
128 elseif #text < args.cursor_pos then
129 char = " "
130 spacer = ""
131 text_start = util.escape(text)
132 text_end = ""
133 else
134 char = util.escape(text:sub(args.cursor_pos, args.cursor_pos))
135 spacer = " "
136 text_start = util.escape(text:sub(1, args.cursor_pos - 1))
137 text_end = util.escape(text:sub(args.cursor_pos + 1))
140 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
141 if args.font then ret = "<span font_desc='" .. args.font .. "'>" .. ret .. "</span>" end
142 return ret
145 --- Run a prompt in a box.
146 -- @param args A table with optional arguments: fg_cursor, bg_cursor, ul_cursor, prompt, text, selectall, font.
147 -- @param textbox The textbox to use for the prompt.
148 -- @param exe_callback The callback function to call with command as argument when finished.
149 -- @param completion_callback The callback function to call to get completion.
150 -- @param history_path Optional parameter: file path where the history should be saved, set nil to disable history
151 -- @param history_max Optional parameter: set the maximum entries in history file, 50 by default
152 -- @param done_callback Optional parameter: the callback function to always call without arguments, regardless of whether the prompt was cancelled.
153 function run(args, textbox, exe_callback, completion_callback, history_path, history_max, done_callback)
154 local theme = beautiful.get()
155 if not args then args = {} end
156 local command = args.text or ""
157 local command_before_comp
158 local cur_pos_before_comp
159 local prettyprompt = args.prompt or ""
160 local inv_col = args.fg_cursor or theme.fg_focus or "black"
161 local cur_col = args.bg_cursor or theme.bg_focus or "white"
162 local cur_ul = args.ul_cursor
163 local text = args.text or ""
164 local font = args.font or theme.font
165 local selectall = args.selectall
167 history_check_load(history_path, history_max)
168 local history_index = history_items(history_path) + 1
169 -- The cursor position
170 local cur_pos = (selectall and 1) or text:wlen() + 1
171 -- The completion element to use on completion request.
172 local ncomp = 1
173 if not textbox or not exe_callback then
174 return
176 textbox.text = prompt_text_with_cursor{
177 text = text, text_color = inv_col, cursor_color = cur_col,
178 cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
179 font = font, prompt = prettyprompt }
181 capi.keygrabber.run(
182 function (modifiers, key, event)
183 if event ~= "press" then return true end
184 -- Convert index array to hash table
185 local mod = {}
186 for k, v in ipairs(modifiers) do mod[v] = true end
187 -- Get out cases
188 if (mod.Control and (key == "c" or key == "g"))
189 or (not mod.Control and key == "Escape") then
190 textbox.text = ""
191 if done_callback then done_callback() end
192 return false
193 elseif (mod.Control and (key == "j" or key == "m"))
194 or (not mod.Control and key == "Return")
195 or (not mod.Control and key == "KP_Enter") then
196 textbox.text = ""
197 history_add(history_path, command)
198 capi.keygrabber.stop()
199 exe_callback(command)
200 if done_callback then done_callback() end
201 -- We already unregistered ourselves so we don't want to return
202 -- true, otherwise we may unregister someone else.
203 return true
206 -- Control cases
207 if mod.Control then
208 selectall = nil
209 if key == "a" then
210 cur_pos = 1
211 elseif key == "b" then
212 if cur_pos > 1 then
213 cur_pos = cur_pos - 1
215 elseif key == "d" then
216 if cur_pos <= #command then
217 command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1)
219 elseif key == "e" then
220 cur_pos = #command + 1
221 elseif key == "f" then
222 if cur_pos <= #command then
223 cur_pos = cur_pos + 1
225 elseif key == "h" then
226 if cur_pos > 1 then
227 command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos)
228 cur_pos = cur_pos - 1
230 elseif key == "k" then
231 command = command:sub(1, cur_pos - 1)
232 elseif key == "u" then
233 command = command:sub(cur_pos, #command)
234 cur_pos = 1
235 elseif key == "w" then
236 local wstart = 1
237 local wend = 1
238 local cword_start = 1
239 local cword_end = 1
240 while wend < cur_pos do
241 wend = command:find("[{[(,.:;_-+=@/ ]", wstart)
242 if not wend then wend = #command + 1 end
243 if cur_pos >= wstart and cur_pos <= wend + 1 then
244 cword_start = wstart
245 cword_end = cur_pos - 1
246 break
248 wstart = wend + 1
250 command = command:sub(1, cword_start - 1) .. command:sub(cword_end + 1)
251 cur_pos = cword_start
253 else
254 if completion_callback then
255 if key == "Tab" or key == "ISO_Left_Tab" then
256 if key == "ISO_Left_Tab" then
257 if ncomp == 1 then return true end
258 if ncomp == 2 then
259 command = command_before_comp
260 textbox.text = prompt_text_with_cursor{
261 text = command_before_comp, text_color = inv_col, cursor_color = cur_col,
262 cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
263 font = font, prompt = prettyprompt }
264 return true
267 ncomp = ncomp - 2
268 elseif ncomp == 1 then
269 command_before_comp = command
270 cur_pos_before_comp = cur_pos
272 command, cur_pos = completion_callback(command_before_comp, cur_pos_before_comp, ncomp)
273 ncomp = ncomp + 1
274 key = ""
275 else
276 ncomp = 1
280 -- Typin cases
281 if mod.Shift and key == "Insert" then
282 local selection = capi.selection()
283 if selection then
284 -- Remove \n
285 local n = selection:find("\n")
286 if n then
287 selection = selection:sub(1, n - 1)
289 command = command .. selection
290 cur_pos = cur_pos + #selection
292 elseif key == "Home" then
293 cur_pos = 1
294 elseif key == "End" then
295 cur_pos = #command + 1
296 elseif key == "BackSpace" then
297 if cur_pos > 1 then
298 command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos)
299 cur_pos = cur_pos - 1
301 elseif key == "Delete" then
302 command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1)
303 elseif key == "Left" then
304 cur_pos = cur_pos - 1
305 elseif key == "Right" then
306 cur_pos = cur_pos + 1
307 elseif key == "Up" then
308 if history_index > 1 then
309 history_index = history_index - 1
311 command = data.history[history_path].table[history_index]
312 cur_pos = #command + 2
314 elseif key == "Down" then
315 if history_index < history_items(history_path) then
316 history_index = history_index + 1
318 command = data.history[history_path].table[history_index]
319 cur_pos = #command + 2
320 elseif history_index == history_items(history_path) then
321 history_index = history_index + 1
323 command = ""
324 cur_pos = 1
326 else
327 -- wlen() is UTF-8 aware but #key is not,
328 -- so check that we have one UTF-8 char but advance the cursor of # position
329 if key:wlen() == 1 then
330 if selectall then command = "" end
331 command = command:sub(1, cur_pos - 1) .. key .. command:sub(cur_pos)
332 cur_pos = cur_pos + #key
335 if cur_pos < 1 then
336 cur_pos = 1
337 elseif cur_pos > #command + 1 then
338 cur_pos = #command + 1
340 selectall = nil
343 -- Update textbox
344 local function update()
345 textbox.text = prompt_text_with_cursor{
346 text = command, text_color = inv_col, cursor_color = cur_col,
347 cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
348 font = font, prompt = prettyprompt }
351 local success = pcall(update)
352 while not success do
353 -- TODO UGLY HACK TODO
354 -- Setting the text failed. Most likely reason is that the user
355 -- entered a multibyte character and pressed backspace which only
356 -- removed the last byte. Let's remove another byte.
357 if cur_pos <= 1 then
358 -- No text left?!
359 break
362 command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos)
363 cur_pos = cur_pos - 1
364 success = pcall(update)
367 return true
368 end)
371 -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=80