1--- Add readline bindings to the input bar.
2--
3-- This module adds a set of readline-inspired bindings to the input bar. These
4-- bindings are not bound to any specific mode, but are automatically activated
5-- whenever the input bar has focus.
6--
7-- @module readline
8-- @copyright 2017 Aidan Holm <aidanholm@gmail.com>
9
10local window = require "window"
11local lousy = require "lousy"
12local prev_glyph = lousy.util.string.prev_glyph
13local next_glyph = lousy.util.string.next_glyph
14
15local _M = {}
16
17local yank_ring = ""
18
19local actions =  {
20    paste = {
21        func = function (w)
22            local str = luakit.selection.primary
23            if not str then return end
24            local i = w.ibar.input
25            local text = i.text
26            local pos = i.position
27            local left, right = string.sub(text, 1, pos), string.sub(text, pos+1)
28            i.text = left .. str .. right
29            i.position = pos + #str
30        end,
31        desc = "Insert contents of primary selection at cursor position.",
32    },
33    del_word = {
34        func = function (w)
35            local i = w.ibar.input
36            local text = i.text
37            local pos = i.position
38            if text and utf8.len(text) > 1 and pos > 1 then
39                local left = string.sub(text, 2, utf8.offset(text, pos))
40                local right = string.sub(text, utf8.offset(text, pos + 1))
41                if not string.find(left, "%s") then
42                    left = ""
43                elseif string.find(left, "%S+%s*$") then
44                    left = string.sub(left, 0, string.find(left, "%S+%s*$") - 1)
45                elseif string.find(left, "%W+%s*$") then
46                    left = string.sub(left, 0, string.find(left, "%W+%s*$") - 1)
47                end
48                i.text =  string.sub(text, 1, 1) .. left .. right
49                i.position = utf8.len(left) + 1
50            end
51        end,
52        desc = "Delete previous word.",
53    },
54    del_word_backward = {
55        func = function (w)
56            local i = w.ibar.input
57            local text = i.text
58            local pos = i.position
59            if text and utf8.len(text) > 1 and pos > 1 then
60                local right = string.sub(text, utf8.offset(text, pos + 1))
61                pos = utf8.offset(text, pos) - 1
62                while true
63                do
64                    local new_pos, glyph = prev_glyph(text, pos)
65                    if not new_pos or (glyph:len() == 1 and not glyph:find("%w")) then
66                        break
67                    end
68                    pos = new_pos
69                end
70                local left = ""
71                if pos then
72                    left = text:sub(2, pos)
73                end
74                i.text =  text:sub(1, 1) .. left .. right
75                i.position = utf8.len(left) + 1
76            end
77        end,
78        desc = "Delete word backward.",
79    },
80    del_word_forward = {
81        func = function (w)
82            local i = w.ibar.input
83            local text = i.text
84            local pos = i.position
85            if text and utf8.len(text) > 1 and pos < utf8.len(text) then
86                -- include current character
87                local left = text:sub(1, utf8.offset(text, pos + 1) - 1)
88                -- at least delete one character
89                pos = utf8.offset(text, pos + 2)
90                while true
91                do
92                    local new_pos, glyph = next_glyph(text, pos)
93                    if not new_pos or (glyph:len() == 1 and not glyph:find("%w")) then
94                        break
95                    end
96                    pos = new_pos
97                end
98                local right
99                if pos then
100                    right = text:sub(pos)
101                else
102                    right = ""
103                end
104                i.text = left .. right
105                i.position = utf8.len(left)
106            end
107        end,
108        desc = "Delete word forward.",
109    },
110    del_line = {
111        func = function (w)
112            local i = w.ibar.input
113            if not string.match(i.text, "^[:/?]$") then
114                yank_ring = string.sub(i.text, 2)
115                i.text = string.sub(i.text, 1, 1)
116                i.position = -1
117            end
118        end,
119        desc = "Delete until beginning of current line.",
120    },
121    del_to_eol = {
122        func = function (w)
123            local i = w.ibar.input
124            local text = i.text
125            local pos = i.position
126            if not string.match(text, "^[:/?]$") then
127                i.text = string.sub(text, 1, pos)
128                i.position = pos
129            end
130        end,
131        desc = "Delete to the end of current line.",
132    },
133    del_backward_char = {
134        func = function (w)
135            local i = w.ibar.input
136            local text = i.text
137            local pos = i.position
138
139            if pos > 1 then
140                i.text = string.sub(text, 0, pos - 1) .. string.sub(text, pos + 1)
141                i.position = pos - 1
142            end
143        end,
144        desc = "Delete character to the left.",
145    },
146    del_forward_char = {
147        func = function (w)
148            local i = w.ibar.input
149            local text = i.text
150            local pos = i.position
151
152            i.text = string.sub(text, 0, pos) .. string.sub(text, pos + 2)
153            i.position = pos
154        end,
155        desc = "Delete character to the right.",
156    },
157    beg_line = {
158        func = function (w)
159            local i = w.ibar.input
160            i.position = 1
161        end,
162        desc = "Move cursor to beginning of current line.",
163    },
164    end_line = {
165        func = function (w)
166            local i = w.ibar.input
167            i.position = -1
168        end,
169        desc = "Move cursor to end of current line.",
170    },
171    forward_char = {
172        func = function (w)
173            local i = w.ibar.input
174            i.position = i.position + 1
175        end,
176        desc = "Move cursor forward one character.",
177    },
178    backward_char = {
179        func = function (w)
180            local i = w.ibar.input
181            local pos = i.position
182            if pos > 1 then
183                i.position = pos - 1
184            end
185        end,
186        desc = "Move cursor backward one character.",
187    },
188    forward_word = {
189        func = function (w)
190            local i = w.ibar.input
191            local text = i.text
192            local pos = i.position
193            if text and utf8.len(text) > 1 then
194                pos = pos + 1
195                local raw_pos = utf8.offset(text, pos + 1)
196                while true
197                do
198                    local glyph
199                    raw_pos, glyph = next_glyph(text, raw_pos)
200                    if not raw_pos or (glyph:len() == 1 and not glyph:find("%w")) then
201                        break
202                    end
203                    pos = pos + 1
204                end
205                i.position = pos
206            end
207        end,
208        desc = "Move cursor forward one word.",
209    },
210    backward_word = {
211        func = function (w)
212            local i = w.ibar.input
213            local text = i.text
214            local pos = i.position
215            if text and utf8.len(text) > 1 and pos > 1 then
216                local raw_pos = utf8.offset(text, pos) - 1
217                while true
218                do
219                    local glyph
220                    raw_pos, glyph = prev_glyph(text, raw_pos)
221                    pos = pos - 1
222                    if not raw_pos or (glyph:len() == 1 and not glyph:find("%w")) then
223                        break
224                    end
225                end
226                if not pos then
227                    i.position = 1
228                else
229                    i.position = pos
230                end
231            end
232        end,
233        desc = "Move cursor backward one word.",
234    },
235
236    yank_text = {
237        func = function (w)
238            local i = w.ibar.input
239            local text = i.text
240            local pos = i.position
241            local left, right = string.sub(text, 1, pos), string.sub(text, pos+1)
242            i.text = left .. yank_ring .. right
243            i.position = pos + #yank_ring
244        end,
245        desc = "Yank the most recently killed text into the input bar, at the cursor.",
246    },
247}
248
249--- Table of bindings that are added to the input bar.
250-- @readwrite
251-- @type table
252_M.bindings = {
253    { "<Shift-Insert>",       actions.paste                , {} },
254    { "<Control-w>",          actions.del_word             , {} },
255    { "<Mod1-BackSpace>",     actions.del_word_backward    , {} },
256    { "<Mod1-d>",             actions.del_word_forward     , {} },
257    { "<Control-u>",          actions.del_line             , {} },
258    { "<Control-o>",          actions.del_to_eol           , {} },
259    { "<Control-h>",          actions.del_backward_char    , {} },
260    { "<Control-d>",          actions.del_forward_char     , {} },
261    { "<Control-a>",          actions.beg_line             , {} },
262    { "<Control-e>",          actions.end_line             , {} },
263    { "<Control-f>",          actions.forward_char         , {} },
264    { "<Control-b>",          actions.backward_char        , {} },
265    { "<Mod1-f>",             actions.forward_word         , {} },
266    { "<Mod1-b>",             actions.backward_word        , {} },
267    { "<Control-y>",          actions.yank_text            , {} },
268}
269
270window.add_signal("init", function (w)
271    w.ibar.input:add_signal("key-press", function (input, mods, key)
272        local ww = assert(window.ancestor(input)) -- Unlikely, but just in case
273        local success, match = xpcall(
274            function () return lousy.bind.hit(ww, _M.bindings, mods, key, {}) end,
275            function (err) w:error(debug.traceback(err, 2)) end)
276        if success and match then
277            return true
278        end
279    end)
280end)
281
282-- Check for old config/window.lua
283for k in pairs(actions) do
284    k = k == "paste" and "insert_cmd" or k
285    for wm in pairs(window.methods) do
286        if k == wm then
287            msg.warn("detected old window.lua: method '%s'", wm)
288            msg.warn("  readline bindings have been moved to readline.lua")
289            msg.warn("  you should remove this method from your config/window.lua")
290        end
291    end
292end
293
294return _M
295
296-- vim: et:sw=4:ts=8:sts=4:tw=80
297