summaryrefslogtreecommitdiff
path: root/lib/awful/prompt.lua
blob: 9a86475fddb8fcc2f16bdb21f9e5adf8445c16ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
---------------------------------------------------------------------------
--- Prompt module for awful.
--
-- By default, `rc.lua` will create one `awful.widget.prompt` per screen called
-- `mypromptbox`. It is used for both the command execution (`mod4+r`) and
-- Lua prompt (`mod4+x`). It can be re-used for random inputs using:
--
--    -- Create a shortcut function
--    local function echo_test()
--        awful.prompt.run {
--            prompt       = "Echo: ",
--            textbox      = mouse.screen.mypromptbox.widget,
--            exe_callback = function(input)
--                if not input or #input == 0 then return end
--                naughty.notify{ text = "The input was: "..input }
--            end
--        }
--    end
--
--    -- Then **IN THE globalkeys TABLE** add a new shortcut
--    awful.key({ modkey }, "e", echo_test,
--        {description = "Echo a string", group = "custom"}),
--
-- Note that this assumes an `rc.lua` file based on the default one. The way
-- to access the screen prompt may vary.
--
-- @author Julien Danjou <julien@danjou.info>
-- @copyright 2008 Julien Danjou
-- @module awful.prompt
---------------------------------------------------------------------------

-- Grab environment we need
local assert = assert
local io = io
local table = table
local math = math
local ipairs = ipairs
local pcall = pcall
local capi =
{
    selection = selection
}
local unpack = unpack or table.unpack -- luacheck: globals unpack (compatibility with Lua 5.1)
local keygrabber = require("awful.keygrabber")
local util = require("awful.util")
local beautiful = require("beautiful")
local akey = require("awful.key")

local prompt = {}

--- Private data
local data = {}
data.history = {}

local search_term = nil
local function itera (inc,a, i)
    i = i + inc
    local v = a[i]
    if v then return i,v end
end

--- Load history file in history table
-- @param id The data.history identifier which is the path to the filename.
-- @param[opt] max The maximum number of entries in file.
local function history_check_load(id, max)
    if id and id ~= "" and not data.history[id] then
        data.history[id] = { max = 50, table = {} }

        if max then
            data.history[id].max = max
        end

        local f = io.open(id, "r")
        if not f then return end

        -- Read history file
        for line in f:lines() do
            if util.table.hasitem(data.history[id].table, line) == nil then
                table.insert(data.history[id].table, line)
                if #data.history[id].table >= data.history[id].max then
                    break
                end
            end
        end
        f:close()
    end
end

local function is_word_char(c)
    if string.find("[{[(,.:;_-+=@/ ]", c) then
        return false
    else
        return true
    end
end

local function cword_start(s, pos)
    local i = pos
    if i > 1 then
        i = i - 1
    end
    while i >= 1 and not is_word_char(s:sub(i, i)) do
        i = i - 1
    end
    while i >= 1 and is_word_char(s:sub(i, i)) do
        i = i - 1
    end
    if i <= #s then
        i = i + 1
    end
    return i
end

local function cword_end(s, pos)
    local i = pos
    while i <= #s and not is_word_char(s:sub(i, i)) do
        i = i + 1
    end
    while i <= #s and  is_word_char(s:sub(i, i)) do
        i = i + 1
    end
    return i
end

--- Save history table in history file
-- @param id The data.history identifier
local function history_save(id)
    if data.history[id] then
        local f = io.open(id, "w")
        if not f then
            local i = 0
            for d in id:gmatch(".-/") do
                i = i + #d
            end
            util.mkdir(id:sub(1, i - 1))
            f = assert(io.open(id, "w"))
        end
        for i = 1, math.min(#data.history[id].table, data.history[id].max) do
            f:write(data.history[id].table[i] .. "\n")
        end
       f:close()
    end
end

--- Return the number of items in history table regarding the id
-- @param id The data.history identifier
-- @return the number of items in history table, -1 if history is disabled
local function history_items(id)
    if data.history[id] then
        return #data.history[id].table
    else
        return -1
    end
end

--- Add an entry to the history file
-- @param id The data.history identifier
-- @param command The command to add
local function history_add(id, command)
    if data.history[id] and command ~= "" then
        local index = util.table.hasitem(data.history[id].table, command)
        if index == nil then
            table.insert(data.history[id].table, command)

            -- Do not exceed our max_cmd
            if #data.history[id].table > data.history[id].max then
                table.remove(data.history[id].table, 1)
            end

            history_save(id)
        else
            -- Bump this command to the end of history
            table.remove(data.history[id].table, index)
            table.insert(data.history[id].table, command)
            history_save(id)
        end
    end
end


--- Draw the prompt text with a cursor.
-- @tparam table args The table of arguments.
-- @field text The text.
-- @field font The font.
-- @field prompt The text prefix.
-- @field text_color The text color.
-- @field cursor_color The cursor color.
-- @field cursor_pos The cursor position.
-- @field cursor_ul The cursor underline style.
-- @field selectall If true cursor is rendered on the entire text.
local function prompt_text_with_cursor(args)
    local char, spacer, text_start, text_end, ret
    local text = args.text or ""
    local _prompt = args.prompt or ""
    local underline = args.cursor_ul or "none"

    if args.selectall then
        if #text == 0 then char = " " else char = util.escape(text) end
        spacer = " "
        text_start = ""
        text_end = ""
    elseif #text < args.cursor_pos then
        char = " "
        spacer = ""
        text_start = util.escape(text)
        text_end = ""
    else
        char = util.escape(text:sub(args.cursor_pos, args.cursor_pos))
        spacer = " "
        text_start = util.escape(text:sub(1, args.cursor_pos - 1))
        text_end = util.escape(text:sub(args.cursor_pos + 1))
    end

    local cursor_color = util.ensure_pango_color(args.cursor_color)
    local text_color = util.ensure_pango_color(args.text_color)

    ret = _prompt .. text_start .. "<span background=\"" .. cursor_color ..
        "\" foreground=\"" .. text_color .. "\" underline=\"" .. underline ..
        "\">" .. char .. "</span>" .. text_end .. spacer
    return ret
end

--- Run a prompt in a box.
--
-- The following readline keyboard shortcuts are implemented as expected:
-- <kbd>CTRL+A</kbd>, <kbd>CTRL+B</kbd>, <kbd>CTRL+C</kbd>, <kbd>CTRL+D</kbd>,
-- <kbd>CTRL+E</kbd>, <kbd>CTRL+J</kbd>, <kbd>CTRL+M</kbd>, <kbd>CTRL+F</kbd>,
-- <kbd>CTRL+H</kbd>, <kbd>CTRL+K</kbd>, <kbd>CTRL+U</kbd>, <kbd>CTRL+W</kbd>,
-- <kbd>CTRL+BACKSPACE</kbd>, <kbd>SHIFT+INSERT</kbd>, <kbd>HOME</kbd>,
-- <kbd>END</kbd> and arrow keys.
--
-- The following shortcuts implement additional history manipulation commands
-- where the search term is defined as the substring of the command from first
-- character to cursor position.
--
-- * <kbd>CTRL+R</kbd>: reverse history search, matches any history entry
-- containing search term.
-- * <kbd>CTRL+S</kbd>: forward history search, matches any history entry
-- containing search term.
-- * <kbd>CTRL+UP</kbd>: ZSH up line or search, matches any history entry
-- starting with search term.
-- * <kbd>CTRL+DOWN</kbd>: ZSH down line or search, matches any history
-- entry starting with search term.
-- * <kbd>CTRL+DELETE</kbd>: delete the currently visible history entry from
-- history file. This does not delete new commands or history entries under
-- user editing.
--
-- @tparam[opt={}] table args A table with optional arguments
-- @tparam[opt] gears.color args.fg_cursor
-- @tparam[opt] gears.color args.bg_cursor
-- @tparam[opt] gears.color args.ul_cursor
-- @tparam[opt] widget args.prompt
-- @tparam[opt] string args.text
-- @tparam[opt] boolean args.selectall
-- @tparam[opt] string args.font
-- @tparam[opt] boolean args.autoexec
-- @tparam widget args.textbox The textbox to use for the prompt.
-- @tparam function args.exe_callback The callback function to call with command as argument
-- when finished.
-- @tparam function args.completion_callback The callback function to call to get completion.
-- @tparam[opt] string args.history_path File path where the history should be
-- saved, set nil to disable history
-- @tparam[opt] function args.history_max Set the maximum entries in history
-- file, 50 by default
-- @tparam[opt] function args.done_callback The callback function to always call
-- without arguments, regardless of whether the prompt was cancelled.
-- @tparam[opt] function args.changed_callback The callback function to call
-- with command as argument when a command was changed.
-- @tparam[opt] function args.keypressed_callback The callback function to call
--   with mod table, key and command as arguments when a key was pressed.
-- @tparam[opt] function args.keyreleased_callback The callback function to call
--   with mod table, key and command as arguments when a key was pressed.
-- @tparam[opt] table args.hooks The "hooks" argument uses a syntax similar to
--   `awful.key`.  It will call a function for the matching modifiers + key.
--   It receives the command (widget text/input) as an argument.
--   If the callback returns a command, this will be passed to the
--   `exe_callback`, otherwise nothing gets executed by default, and the hook
--   needs to handle it.
--     hooks = {
--       -- Apply startup notification properties with Shift-Return.
--       {{"Shift"  }, "Return", function(command)
--         awful.screen.focused().mypromptbox:spawn_and_handle_error(
--           command, {floating=true})
--       end},
--       -- Override default behavior of "Return": launch commands prefixed
--       -- with ":" in a terminal.
--       {{}, "Return", function(command)
--         if command:sub(1,1) == ":" then
--           return terminal .. ' -e ' .. command:sub(2)
--         end
--         return command
--       end}
--     }
-- @param textbox The textbox to use for the prompt. [**DEPRECATED**]
-- @param exe_callback The callback function to call with command as argument
-- when finished. [**DEPRECATED**]
-- @param completion_callback The callback function to call to get completion.
--   [**DEPRECATED**]
-- @param[opt] history_path File path where the history should be
-- saved, set nil to disable history [**DEPRECATED**]
-- @param[opt] history_max Set the maximum entries in history
-- file, 50 by default [**DEPRECATED**]
-- @param[opt] done_callback The callback function to always call
-- without arguments, regardless of whether the prompt was cancelled.
--  [**DEPRECATED**]
-- @param[opt] changed_callback The callback function to call
-- with command as argument when a command was changed. [**DEPRECATED**]
-- @param[opt] keypressed_callback The callback function to call
--   with mod table, key and command as arguments when a key was pressed.
--   [**DEPRECATED**]
-- @see gears.color
function prompt.run(args, textbox, exe_callback, completion_callback,
                    history_path, history_max, done_callback,
                    changed_callback, keypressed_callback)
    local grabber
    local theme = beautiful.get()
    if not args then args = {} end
    local command = args.text or ""
    local command_before_comp
    local cur_pos_before_comp
    local prettyprompt = args.prompt or ""
    local inv_col = args.fg_cursor or theme.fg_focus or "black"
    local cur_col = args.bg_cursor or theme.bg_focus or "white"
    local cur_ul = args.ul_cursor
    local text = args.text or ""
    local font = args.font or theme.font
    local selectall = args.selectall
    local hooks = {}

    -- A function with 9 parameters deserve to die
    if textbox then
        util.deprecate("Use args.textbox instead of the textbox parameter")
    end
    if exe_callback then
        util.deprecate(
            "Use args.exe_callback instead of the exe_callback parameter"
        )
    end
    if completion_callback then
        util.deprecate(
            "Use args.completion_callback instead of the completion_callback parameter"
        )
    end
    if history_path then
        util.deprecate(
            "Use args.history_path instead of the history_path parameter"
        )
    end
    if history_max then
        util.deprecate(
            "Use args.history_max instead of the history_max parameter"
        )
    end
    if done_callback then
        util.deprecate(
            "Use args.done_callback instead of the done_callback parameter"
        )
    end
    if changed_callback then
        util.deprecate(
            "Use args.changed_callback instead of the changed_callback parameter"
        )
    end
    if keypressed_callback then
        util.deprecate(
            "Use args.keypressed_callback instead of the keypressed_callback parameter"
        )
    end

    -- This function has already an absurd number of parameters, allow them
    -- to be set using the args to avoid a "nil, nil, nil, nil, foo" scenario
    keypressed_callback = keypressed_callback or args.keypressed_callback
    changed_callback    = changed_callback    or args.changed_callback
    done_callback       = done_callback       or args.done_callback
    history_max         = history_max         or args.history_max
    history_path        = history_path        or args.history_path
    completion_callback = completion_callback or args.completion_callback
    exe_callback        = exe_callback        or args.exe_callback
    textbox             = textbox             or args.textbox

    search_term=nil

    history_check_load(history_path, history_max)
    local history_index = history_items(history_path) + 1
    -- The cursor position
    local cur_pos = (selectall and 1) or text:wlen() + 1
    -- The completion element to use on completion request.
    local ncomp = 1
    if not textbox then
        return
    end

    -- Build the hook map
    for _,v in ipairs(args.hooks or {}) do
        if #v == 3 then
            local _,key,callback = unpack(v)
            if type(callback) == "function" then
                hooks[key] = hooks[key] or {}
                hooks[key][#hooks[key]+1] = v
            else
                assert("The hook's 3rd parameter has to be a function.")
            end
        else
            assert("The hook has to have 3 parameters.")
        end
    end

    textbox:set_font(font)
    textbox:set_markup(prompt_text_with_cursor{
        text = text, text_color = inv_col, cursor_color = cur_col,
        cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
        prompt = prettyprompt })

    local function exec(cb, command_to_history)
        textbox:set_markup("")
        history_add(history_path, command_to_history)
        keygrabber.stop(grabber)
        if cb then cb(command) end
        if done_callback then done_callback() end
    end

    -- Update textbox
    local function update()
        textbox:set_font(font)
        textbox:set_markup(prompt_text_with_cursor{
                               text = command, text_color = inv_col, cursor_color = cur_col,
                               cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
                               prompt = prettyprompt })
    end

    grabber = keygrabber.run(
    function (modifiers, key, event)
        -- Convert index array to hash table
        local mod = {}
        for _, v in ipairs(modifiers) do mod[v] = true end

        if event ~= "press" then
            if args.keyreleased_callback then
                args.keyreleased_callback(mod, key, command)
            end

            return
        end

        -- Call the user specified callback. If it returns true as
        -- the first result then return from the function. Treat the
        -- second and third results as a new command and new prompt
        -- to be set (if provided)
        if keypressed_callback then
            local user_catched, new_command, new_prompt =
                keypressed_callback(mod, key, command)
            if new_command or new_prompt then
                if new_command then
                    command = new_command
                end
                if new_prompt then
                    prettyprompt = new_prompt
                end
                update()
            end
            if user_catched then
                if changed_callback then
                    changed_callback(command)
                end
                return
            end
        end

        local filtered_modifiers = {}

        -- User defined cases
        if hooks[key] then
            -- Remove caps and num lock
            for _, m in ipairs(modifiers) do
                if not util.table.hasitem(akey.ignore_modifiers, m) then
                    table.insert(filtered_modifiers, m)
                end
            end

            for _,v in ipairs(hooks[key]) do
                if #filtered_modifiers == #v[1] then
                    local match = true
                    for _,v2 in ipairs(v[1]) do
                        match = match and mod[v2]
                    end
                    if match then
                        local cb
                        local ret = v[3](command)
                        local original_command = command
                        if ret then
                            command = ret
                            cb = exe_callback
                        else
                            -- No callback.
                            cb = function() end
                        end
                        exec(cb, original_command)
                        return
                    end
                end
            end
        end

        -- Get out cases
        if (mod.Control and (key == "c" or key == "g"))
            or (not mod.Control and key == "Escape") then
            keygrabber.stop(grabber)
            textbox:set_markup("")
            history_save(history_path)
            if done_callback then done_callback() end
            return false
        elseif (mod.Control and (key == "j" or key == "m"))
            or (not mod.Control and key == "Return")
            or (not mod.Control and key == "KP_Enter") then
            exec(exe_callback, command)
            -- We already unregistered ourselves so we don't want to return
            -- true, otherwise we may unregister someone else.
            return
        end

        -- Control cases
        if mod.Control then
            selectall = nil
            if key == "a" then
                cur_pos = 1
            elseif key == "b" then
                if cur_pos > 1 then
                    cur_pos = cur_pos - 1
                end
            elseif key == "d" then
                if cur_pos <= #command then
                    command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1)
                end
            elseif key == "p" then
                if history_index > 1 then
                    history_index = history_index - 1

                    command = data.history[history_path].table[history_index]
                    cur_pos = #command + 2
                end
            elseif key == "n" then
                if history_index < history_items(history_path) then
                    history_index = history_index + 1

                    command = data.history[history_path].table[history_index]
                    cur_pos = #command + 2
                elseif history_index == history_items(history_path) then
                    history_index = history_index + 1

                    command = ""
                    cur_pos = 1
                end
            elseif key == "e" then
                cur_pos = #command + 1
            elseif key == "r" then
                search_term = search_term or command:sub(1, cur_pos - 1)
                for i,v in (function(a,i) return itera(-1,a,i) end), data.history[history_path].table, history_index do
                    if v:find(search_term,1,true) ~= nil then
                        command=v
                        history_index=i
                        cur_pos=#command+1
                        break
                    end
                end
            elseif key == "s" then
                search_term = search_term or command:sub(1, cur_pos - 1)
                for i,v in (function(a,i) return itera(1,a,i) end), data.history[history_path].table, history_index do
                    if v:find(search_term,1,true) ~= nil then
                        command=v
                        history_index=i
                        cur_pos=#command+1
                        break
                    end
                end
            elseif key == "f" then
                if cur_pos <= #command then
                    cur_pos = cur_pos + 1
                end
            elseif key == "h" then
                if cur_pos > 1 then
                    command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos)
                    cur_pos = cur_pos - 1
                end
            elseif key == "k" then
                command = command:sub(1, cur_pos - 1)
            elseif key == "u" then
                command = command:sub(cur_pos, #command)
                cur_pos = 1
            elseif key == "Up" then
                search_term = command:sub(1, cur_pos - 1) or ""
                for i,v in (function(a,i) return itera(-1,a,i) end), data.history[history_path].table, history_index do
                    if v:find(search_term,1,true) == 1 then
                        command=v
                        history_index=i
                        break
                    end
                end
            elseif key == "Down" then
                search_term = command:sub(1, cur_pos - 1) or ""
                for i,v in (function(a,i) return itera(1,a,i) end), data.history[history_path].table, history_index do
                    if v:find(search_term,1,true) == 1 then
                        command=v
                        history_index=i
                        break
                    end
                end
            elseif key == "w" or key == "BackSpace" then
                local wstart = 1
                local wend = 1
                local cword_start_pos = 1
                local cword_end_pos = 1
                while wend < cur_pos do
                    wend = command:find("[{[(,.:;_-+=@/ ]", wstart)
                    if not wend then wend = #command + 1 end
                    if cur_pos >= wstart and cur_pos <= wend + 1 then
                        cword_start_pos = wstart
                        cword_end_pos = cur_pos - 1
                        break
                    end
                    wstart = wend + 1
                end
                command = command:sub(1, cword_start_pos - 1) .. command:sub(cword_end_pos + 1)
                cur_pos = cword_start_pos
            elseif key == "Delete" then
                -- delete from history only if:
                --  we are not dealing with a new command
                --  the user has not edited an existing entry
                if command == data.history[history_path].table[history_index] then
                    table.remove(data.history[history_path].table, history_index)
                    if history_index <= history_items(history_path) then
                        command = data.history[history_path].table[history_index]
                        cur_pos = #command + 2
                    elseif history_index > 1 then
                        history_index = history_index - 1

                        command = data.history[history_path].table[history_index]
                        cur_pos = #command + 2
                    else
                        command = ""
                        cur_pos = 1
                    end
                end
            end
        elseif mod.Mod1 or mod.Mod3 then
            if key == "b" then
                cur_pos = cword_start(command, cur_pos)
            elseif key == "f" then
                cur_pos = cword_end(command, cur_pos)
            elseif key == "d" then
                command = command:sub(1, cur_pos - 1) .. command:sub(cword_end(command, cur_pos))
            elseif key == "BackSpace" then
                local wstart = cword_start(command, cur_pos)
                command = command:sub(1, wstart - 1) .. command:sub(cur_pos)
                cur_pos = wstart
            end
        else
            if completion_callback then
                if key == "Tab" or key == "ISO_Left_Tab" then
                    if key == "ISO_Left_Tab" then
                        if ncomp == 1 then return end
                        if ncomp == 2 then
                            command = command_before_comp
                            textbox:set_font(font)
                            textbox:set_markup(prompt_text_with_cursor{
                                text = command_before_comp, text_color = inv_col, cursor_color = cur_col,
                                cursor_pos = cur_pos, cursor_ul = cur_ul, selectall = selectall,
                                prompt = prettyprompt })
                            return
                        end

                        ncomp = ncomp - 2
                    elseif ncomp == 1 then
                        command_before_comp = command
                        cur_pos_before_comp = cur_pos
                    end
                    local matches
                    command, cur_pos, matches = completion_callback(command_before_comp, cur_pos_before_comp, ncomp)
                    ncomp = ncomp + 1
                    key = ""
                    -- execute if only one match found and autoexec flag set
                    if matches and #matches == 1 and args.autoexec then
                        exec(exe_callback)
                        return
                    end
                else
                    ncomp = 1
                end
            end

            -- Typin cases
            if mod.Shift and key == "Insert" then
                local selection = capi.selection()
                if selection then
                    -- Remove \n
                    local n = selection:find("\n")
                    if n then
                        selection = selection:sub(1, n - 1)
                    end
                    command = command:sub(1, cur_pos - 1) .. selection .. command:sub(cur_pos)
                    cur_pos = cur_pos + #selection
                end
            elseif key == "Home" then
                cur_pos = 1
            elseif key == "End" then
                cur_pos = #command + 1
            elseif key == "BackSpace" then
                if cur_pos > 1 then
                    command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos)
                    cur_pos = cur_pos - 1
                end
            elseif key == "Delete" then
                command = command:sub(1, cur_pos - 1) .. command:sub(cur_pos + 1)
            elseif key == "Left" then
                cur_pos = cur_pos - 1
            elseif key == "Right" then
                cur_pos = cur_pos + 1
            elseif key == "Up" then
                if history_index > 1 then
                    history_index = history_index - 1

                    command = data.history[history_path].table[history_index]
                    cur_pos = #command + 2
                end
            elseif key == "Down" then
               if history_index < history_items(history_path) then
                    history_index = history_index + 1

                    command = data.history[history_path].table[history_index]
                    cur_pos = #command + 2
                elseif history_index == history_items(history_path) then
                    history_index = history_index + 1

                    command = ""
                    cur_pos = 1
                end
            else
                -- wlen() is UTF-8 aware but #key is not,
                -- so check that we have one UTF-8 char but advance the cursor of # position
                if key:wlen() == 1 then
                    if selectall then command = "" end
                    command = command:sub(1, cur_pos - 1) .. key .. command:sub(cur_pos)
                    cur_pos = cur_pos + #key
                end
            end
            if cur_pos < 1 then
                cur_pos = 1
            elseif cur_pos > #command + 1 then
                cur_pos = #command + 1
            end
            selectall = nil
        end

        local success = pcall(update)
        while not success do
            -- TODO UGLY HACK TODO
            -- Setting the text failed. Most likely reason is that the user
            -- entered a multibyte character and pressed backspace which only
            -- removed the last byte. Let's remove another byte.
            if cur_pos <= 1 then
                -- No text left?!
                break
            end

            command = command:sub(1, cur_pos - 2) .. command:sub(cur_pos)
            cur_pos = cur_pos - 1
            success = pcall(update)
        end

        if changed_callback then
            changed_callback(command)
        end
    end)
end

return prompt

-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80