commit 445d335
delthas
·
2025-09-15 16:10:29 +0000 UTC
parent 972d647
Support custom keyboard shortcuts
6 files changed,
+366,
-103
M
app.go
+225,
-102
1@@ -108,6 +108,11 @@ type pendingCompletion struct {
2 deadline time.Time
3 }
4
5+type keyMatch struct {
6+ keycode rune
7+ mods vaxis.ModifierMask
8+}
9+
10 type App struct {
11 win *ui.UI
12 sessions map[string]*irc.Session // map of network IDs to their current session
13@@ -119,6 +124,7 @@ type App struct {
14
15 cfg Config
16 highlights []string
17+ shortcuts map[keyMatch][]string
18
19 lastQuery string
20 lastQueryNet string
21@@ -171,9 +177,19 @@ func NewApp(cfg Config) (app *App, err error) {
22 sessions: map[string]*irc.Session{},
23 events: make(chan event, eventChanSize),
24 cfg: cfg,
25+ shortcuts: make(map[keyMatch][]string),
26 messageBounds: map[boundKey]bound{},
27 monitor: make(map[string]map[string]struct{}),
28 }
29+ for _, m := range []map[string][]string{defaultCommands, app.cfg.Shortcuts} {
30+ for name, actions := range m {
31+ k := keyNameMatch(name)
32+ if k == nil {
33+ return nil, fmt.Errorf("unknown key name: %v", name)
34+ }
35+ app.shortcuts[*k] = actions
36+ }
37+ }
38
39 if cfg.Highlights != nil {
40 app.highlights = make([]string, len(cfg.Highlights))
41@@ -813,119 +829,86 @@ func (app *App) handleMouseEvent(ev vaxis.Mouse) {
42 }
43 }
44
45-func (app *App) handleKeyEvent(ev vaxis.Key) {
46- switch ev.EventType {
47- case vaxis.EventPress, vaxis.EventRepeat, vaxis.EventPaste:
48- default:
49- return
50- }
51- if ev.Text != "" {
52- for _, r := range ev.Text {
53- app.win.InputRune(r)
54- }
55- app.typing()
56- return
57- }
58-
59- if keyMatches(ev, 'c', vaxis.ModCtrl) {
60+func (app *App) handleAction(action string, args ...string) {
61+ switch action {
62+ case "quit":
63 if app.win.InputClear() {
64 app.typing()
65 } else {
66 app.win.InputSet("/quit")
67 }
68- } else if keyMatches(ev, 'f', vaxis.ModCtrl) {
69- if len(app.win.InputContent()) == 0 {
70- app.win.InputSet("/search ")
71- }
72- } else if keyMatches(ev, 'k', vaxis.ModCtrl) {
73+ case "set-editor":
74 if len(app.win.InputContent()) == 0 {
75- app.win.InputSet("/buffer ")
76+ app.win.InputSet(strings.Join(args, " "))
77 }
78- } else if keyMatches(ev, 'a', vaxis.ModCtrl) {
79+ case "cursor-start":
80 app.win.InputHome()
81- } else if keyMatches(ev, 'e', vaxis.ModCtrl) {
82+ case "cursor-end":
83 app.win.InputEnd()
84- } else if keyMatches(ev, 'l', vaxis.ModCtrl) {
85+ case "redraw":
86 app.win.Resize()
87- } else if keyMatches(ev, 'u', vaxis.ModCtrl) || keyMatches(ev, vaxis.KeyPgUp, 0) {
88+ case "scroll-up":
89 app.win.ScrollUp()
90- } else if keyMatches(ev, 'd', vaxis.ModCtrl) || keyMatches(ev, vaxis.KeyPgDown, 0) {
91+ case "scroll-down":
92 app.win.ScrollDown()
93- } else if keyMatches(ev, 'n', vaxis.ModCtrl) {
94+ case "buffer-next":
95 app.win.NextBuffer()
96 app.win.ScrollToBuffer()
97- } else if keyMatches(ev, 'p', vaxis.ModCtrl) {
98+ case "buffer-previous":
99 app.win.PreviousBuffer()
100 app.win.ScrollToBuffer()
101- } else if keyMatches(ev, vaxis.KeyRight, vaxis.ModAlt) {
102- app.win.NextBuffer()
103- app.win.ScrollToBuffer()
104- } else if keyMatches(ev, vaxis.KeyRight, vaxis.ModShift) {
105+ case "buffer-next-unread":
106 app.win.NextUnreadBuffer()
107 app.win.ScrollToBuffer()
108- } else if keyMatches(ev, vaxis.KeyRight, vaxis.ModCtrl) {
109- app.win.InputRightWord()
110- } else if keyMatches(ev, vaxis.KeyRight, 0) {
111- app.win.InputRight()
112- } else if keyMatches(ev, vaxis.KeyLeft, vaxis.ModAlt) {
113- app.win.PreviousBuffer()
114- app.win.ScrollToBuffer()
115- } else if keyMatches(ev, vaxis.KeyLeft, vaxis.ModShift) {
116+ case "buffer-previous-unread":
117 app.win.PreviousUnreadBuffer()
118 app.win.ScrollToBuffer()
119- } else if keyMatches(ev, vaxis.KeyLeft, vaxis.ModCtrl) {
120+ case "cursor-right-word":
121+ app.win.InputRightWord()
122+ case "cursor-left-word":
123 app.win.InputLeftWord()
124- } else if keyMatches(ev, vaxis.KeyLeft, 0) {
125+ case "cursor-right":
126+ app.win.InputRight()
127+ case "cursor-left":
128 app.win.InputLeft()
129- } else if keyMatches(ev, vaxis.KeyUp, vaxis.ModAlt) {
130- app.win.PreviousBuffer()
131- } else if keyMatches(ev, vaxis.KeyUp, 0) {
132+ case "cursor-up":
133 app.win.InputUp()
134- } else if keyMatches(ev, vaxis.KeyDown, vaxis.ModAlt) {
135- app.win.NextBuffer()
136- } else if keyMatches(ev, vaxis.KeyDown, 0) {
137+ case "cursor-down":
138 app.win.InputDown()
139- } else if keyMatches(ev, vaxis.KeyHome, vaxis.ModAlt) {
140- app.win.GoToBufferNo(0)
141- } else if keyMatches(ev, vaxis.KeyHome, 0) {
142- app.win.InputHome()
143- } else if keyMatches(ev, vaxis.KeyEnd, vaxis.ModAlt) {
144- maxInt := int(^uint(0) >> 1)
145- app.win.GoToBufferNo(maxInt)
146- } else if keyMatches(ev, vaxis.KeyEnd, 0) {
147- app.win.InputEnd()
148- } else if keyMatches(ev, vaxis.KeyBackspace, vaxis.ModAlt) {
149+ case "cursor-delete-previous-word":
150 if app.win.InputDeleteWord() {
151 app.typing()
152 }
153- } else if keyMatches(ev, vaxis.KeyBackspace, 0) || keyMatches(ev, vaxis.KeyBackspace, vaxis.ModShift) {
154+ case "cursor-delete-previous":
155 if app.win.InputBackspace() {
156 app.typing()
157 }
158- } else if keyMatches(ev, vaxis.KeyDelete, 0) {
159+ case "cursor-delete-next":
160 if app.win.InputDelete() {
161 app.typing()
162 }
163- } else if keyMatches(ev, 'w', vaxis.ModCtrl) {
164- if app.win.InputDeleteWord() {
165+ case "cursor-delete-before":
166+ if app.win.InputDeleteBefore() {
167 app.typing()
168 }
169- } else if keyMatches(ev, 'r', vaxis.ModCtrl) {
170+ case "cursor-delete-after":
171+ if app.win.InputDeleteAfter() {
172+ app.typing()
173+ }
174+ case "search-editor":
175 app.win.InputBackSearch()
176- } else if keyMatches(ev, vaxis.KeyTab, 0) {
177+ case "auto-complete":
178 if app.win.InputAutoComplete() {
179 app.typing()
180 }
181- } else if keyMatches(ev, vaxis.KeyEsc, 0) {
182+ case "close-overlay":
183 app.win.CloseOverlay()
184- } else if keyMatches(ev, vaxis.KeyF07, 0) {
185+ case "toggle-channel-list":
186 app.win.ToggleChannelList()
187- } else if keyMatches(ev, vaxis.KeyF08, 0) {
188+ case "toggle-member-list":
189 app.win.ToggleMemberList()
190- } else if keyMatches(ev, '\n', 0) || keyMatches(ev, '\r', 0) || keyMatches(ev, 'j', vaxis.ModCtrl) || keyMatches(ev, vaxis.KeyKeyPadEnter, 0) {
191- if ev.EventType == vaxis.EventPaste {
192- app.win.InputRune('\n')
193- } else if !app.win.InputEnter() {
194+ case "send":
195+ if !app.win.InputEnter() {
196 netID, buffer := app.win.CurrentBuffer()
197 input := string(app.win.InputContent())
198 var err error
199@@ -945,28 +928,130 @@ func (app *App) handleKeyEvent(ev vaxis.Key) {
200 app.win.InputFlush()
201 }
202 }
203- } else if keyMatches(ev, 'n', vaxis.ModAlt) {
204+ case "scroll-next-highlight":
205 app.win.ScrollDownHighlight()
206- } else if keyMatches(ev, 'p', vaxis.ModAlt) {
207+ case "scroll-previous-highlight":
208 app.win.ScrollUpHighlight()
209- } else if keyMatches(ev, '1', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad1, vaxis.ModAlt) {
210- app.win.GoToBufferNo(0)
211- } else if keyMatches(ev, '2', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad2, vaxis.ModAlt) {
212- app.win.GoToBufferNo(1)
213- } else if keyMatches(ev, '3', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad3, vaxis.ModAlt) {
214- app.win.GoToBufferNo(2)
215- } else if keyMatches(ev, '4', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad4, vaxis.ModAlt) {
216- app.win.GoToBufferNo(3)
217- } else if keyMatches(ev, '5', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad5, vaxis.ModAlt) {
218- app.win.GoToBufferNo(4)
219- } else if keyMatches(ev, '6', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad6, vaxis.ModAlt) {
220- app.win.GoToBufferNo(5)
221- } else if keyMatches(ev, '7', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad7, vaxis.ModAlt) {
222- app.win.GoToBufferNo(6)
223- } else if keyMatches(ev, '8', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad8, vaxis.ModAlt) {
224- app.win.GoToBufferNo(7)
225- } else if keyMatches(ev, '9', vaxis.ModAlt) || keyMatches(ev, vaxis.KeyKeyPad9, vaxis.ModAlt) {
226- app.win.GoToBufferNo(8)
227+ case "buffer":
228+ if len(args) > 0 {
229+ if n, err := strconv.Atoi(args[0]); err == nil && n >= 0 {
230+ app.win.GoToBufferNo(n)
231+ } else if args[0] == "last" {
232+ maxInt := int(^uint(0) >> 1)
233+ app.win.GoToBufferNo(maxInt)
234+ }
235+ }
236+ case "none":
237+ default:
238+ netID, buffer := app.win.CurrentBuffer()
239+ app.win.AddLine(netID, buffer, ui.Line{
240+ At: time.Now(),
241+ Head: "!!",
242+ HeadColor: ui.ColorRed,
243+ Notify: ui.NotifyUnread,
244+ Body: ui.PlainSprintf("shortcut: action %q does not exist", action),
245+ })
246+ }
247+}
248+
249+var defaultCommands = map[string][]string{
250+ "Control+c": {"quit"},
251+ "Control+f": {"set-editor", "/search "},
252+ "Control+k": {"set-editor", "/buffer "},
253+ "Control+a": {"cursor-start"},
254+ "Control+e": {"cursor-end"},
255+ "Control+l": {"redraw"},
256+ "Control+u": {"scroll-up"},
257+ "Page_Up": {"scroll-up"},
258+ "Control+d": {"scroll-down"},
259+ "Page_Down": {"scroll-down"},
260+ "Control+n": {"buffer-next"},
261+ "Control+p": {"buffer-previous"},
262+ "Alt+Right": {"buffer-next"},
263+ "Shift+Right": {"buffer-next-unread"},
264+ "Control+Right": {"cursor-right-word"},
265+ "Right": {"cursor-right"},
266+ "Alt+Left": {"buffer-previous"},
267+ "Shift+Left": {"buffer-previous-unread"},
268+ "Control+Left": {"cursor-left-word"},
269+ "Left": {"cursor-left"},
270+ "Alt+Up": {"buffer-previous"},
271+ "Up": {"cursor-up"},
272+ "Alt+Down": {"buffer-next"},
273+ "Down": {"cursor-down"},
274+ "Alt+Home": {"buffer", "0"},
275+ "Home": {"cursor-start"},
276+ "Alt+End": {"buffer", "last"},
277+ "End": {"cursor-end"},
278+ "Alt+BackSpace": {"cursor-delete-previous-word"},
279+ "BackSpace": {"cursor-delete-previous"},
280+ "Shift+BackSpace": {"cursor-delete-previous"},
281+ "Delete": {"cursor-delete-next"},
282+ "Control+w": {"cursor-delete-previous-word"},
283+ "Control+r": {"search-editor"},
284+ "Tab": {"auto-complete"},
285+ "Escape": {"close-overlay"},
286+ "F7": {"toggle-channel-list"},
287+ "F8": {"toggle-member-list"},
288+ "\n": {"send"},
289+ "\r": {"send"},
290+ "Control+j": {"send"},
291+ "KP_Enter": {"send"},
292+ "Alt+n": {"scroll-next-highlight"},
293+ "Alt+p": {"scroll-previous-highlight"},
294+ "Alt+1": {"buffer", "0"},
295+ "Alt+KP_1": {"buffer", "0"},
296+ "Alt+2": {"buffer", "1"},
297+ "Alt+KP_2": {"buffer", "1"},
298+ "Alt+3": {"buffer", "2"},
299+ "Alt+KP_3": {"buffer", "2"},
300+ "Alt+4": {"buffer", "3"},
301+ "Alt+KP_4": {"buffer", "3"},
302+ "Alt+5": {"buffer", "4"},
303+ "Alt+KP_5": {"buffer", "4"},
304+ "Alt+6": {"buffer", "5"},
305+ "Alt+KP_6": {"buffer", "5"},
306+ "Alt+7": {"buffer", "6"},
307+ "Alt+KP_7": {"buffer", "6"},
308+ "Alt+8": {"buffer", "7"},
309+ "Alt+KP_8": {"buffer", "7"},
310+ "Alt+9": {"buffer", "8"},
311+ "Alt+KP_9": {"buffer", "8"},
312+}
313+
314+func (app *App) handleKeyEvent(ev vaxis.Key) {
315+ switch ev.EventType {
316+ case vaxis.EventPress, vaxis.EventRepeat, vaxis.EventPaste:
317+ default:
318+ return
319+ }
320+ if ev.Text != "" {
321+ for _, r := range ev.Text {
322+ app.win.InputRune(r)
323+ }
324+ app.typing()
325+ return
326+ }
327+
328+ if ev.EventType == vaxis.EventPaste {
329+ for _, keycode := range []rune{'\n', '\r', vaxis.KeyKeyPadEnter} {
330+ k := keyMatch{
331+ keycode: keycode,
332+ }
333+ for _, km := range keyMatches(ev) {
334+ if km == k {
335+ app.win.InputRune('\n')
336+ return
337+ }
338+ }
339+ }
340+ }
341+
342+ for _, km := range keyMatches(ev) {
343+ if d := app.shortcuts[km]; len(d) != 0 {
344+ app.handleAction(d[0], d[1:]...)
345+ return
346+ }
347 }
348 }
349
350@@ -2338,23 +2423,61 @@ func (app *App) addUserBuffer(netID, buffer string, t time.Time) (i int, added b
351 return
352 }
353
354-func keyMatches(k vaxis.Key, r rune, mods vaxis.ModifierMask) bool {
355+func keyNameMatch(name string) *keyMatch {
356+ parts := strings.Split(name, "+")
357+ mods := parts[:len(parts)-1]
358+ key := parts[len(parts)-1]
359+
360+ var m vaxis.ModifierMask
361+ for _, mod := range mods {
362+ switch mod {
363+ case "Control":
364+ m |= vaxis.ModCtrl
365+ case "Shift":
366+ m |= vaxis.ModShift
367+ case "Alt":
368+ m |= vaxis.ModAlt
369+ case "Super":
370+ m |= vaxis.ModSuper
371+ default:
372+ return nil
373+ }
374+ }
375+ if r, n := utf8.DecodeRuneInString(key); n == len(key) {
376+ return &keyMatch{
377+ keycode: r,
378+ mods: m,
379+ }
380+ }
381+ if r := ui.KeyNames[key]; r > 0 {
382+ return &keyMatch{
383+ keycode: r,
384+ mods: m,
385+ }
386+ }
387+ return nil
388+}
389+
390+func keyMatches(k vaxis.Key) []keyMatch {
391 m := k.Modifiers
392 m &^= vaxis.ModCapsLock
393 m &^= vaxis.ModNumLock
394- if k.Keycode == r && mods == m {
395- // ctrl+a and user pressed ctrl+a
396- // ctrl+. and user pressed ctrl+. on a US keyboard
397- return true
398+
399+ keys := []keyMatch{
400+ {
401+ keycode: k.Keycode,
402+ mods: m,
403+ },
404 }
405- if m&vaxis.ModShift != 0 {
406+ if m&vaxis.ModShift != 0 && k.ShiftedCode != 0 {
407+ // ctrl+. and user pressed ctrl+shift+; on a French keyboard
408 m &^= vaxis.ModShift
409- if k.ShiftedCode == r && mods == m {
410- // ctrl+. and user pressed ctrl+shift+; on a French keyboard
411- return true
412- }
413+ keys = append(keys, keyMatch{
414+ keycode: k.ShiftedCode,
415+ mods: m &^ vaxis.ModShift,
416+ })
417 }
418- return false
419+ return keys
420 }
421
422 func formatSize(v int64) string {
+9,
-0
1@@ -111,6 +111,7 @@ type Config struct {
2 MemberColEnabled bool
3 TextMaxWidth int
4 StatusEnabled bool
5+ Shortcuts map[string][]string
6
7 Colors ui.ConfigColors
8
9@@ -161,6 +162,7 @@ func Defaults() Config {
10 Others: ui.ColorDefault,
11 },
12 },
13+ Shortcuts: make(map[string][]string),
14 Debug: false,
15 Transient: false,
16 LocalIntegrations: true,
17@@ -442,6 +444,13 @@ func unmarshal(filename string, cfg *Config) (err error) {
18 return fmt.Errorf("unknown colors directive %q", child.Name)
19 }
20 }
21+ case "shortcuts":
22+ for _, child := range d.Children {
23+ if err := child.ParseParams(nil); err != nil {
24+ return err
25+ }
26+ cfg.Shortcuts[child.Name] = child.Params
27+ }
28 case "debug":
29 var debug string
30 if err := d.ParseParams(&debug); err != nil {
+3,
-1
1@@ -91,6 +91,8 @@ specified above, instead of an unmodified left click.
2
3 # KEYBOARD SHORTCUTS
4
5+These shortcuts can be customized in the configuration, see senpai(5).
6+
7 *CTRL-A*
8 Move the cursor to the beginning of the input field.
9
10@@ -98,7 +100,7 @@ specified above, instead of an unmodified left click.
11 Move the cursor to the end of the input field.
12
13 *CTRL-C*
14- Clear input line, or prepare for exit by adding /exit to input line.
15+ Clear input line, or prepare for exit by adding /quit to input line.
16
17 *CTRL-F*
18 Prepare for search: add /search to input line.
+91,
-0
1@@ -166,6 +166,94 @@ colors {
2 | nicks self <self>
3 : show self nick with a fixed specified color (can be added along other directives)
4
5+*shortcuts* { ... }
6+ Settings for custom keyboard shortcuts.
7+
8+ Shortcuts are defined by `*<key> <action> [<params>...]*` subdirectives:
9+
10+```
11+shortcuts {
12+ Alt+k cursor-delete-after
13+ Shift+Alt+Page_Up set-editor "/me slaps OP with a large trout"
14+}
15+```
16+
17+ Keys are optional modifiers (*Control*/*Shift*/*Alt*/*Super*) and a key name,
18+ which can either be a literal character (e.g. *k*) or a name for that key,
19+ taken from the XKB key names (on Linux, see *wev* for finding key names,
20+ e.g. *Page_Up*, *XF86AudioPlay*).
21+
22+ A custom shortcut will override any default shortcut for that key.
23+
24+[[ *action*
25+:< *Description*
26+| none
27+: *clears the shortcut, no action*
28+| quit
29+: clear the editor, or type `/quit`
30+| set-editor <text>
31+: set the editor to the passed text, if empty
32+| cursor-start
33+: move the cursor to the beginning of the editor
34+| cursor-end
35+: move the cursor to the end of the editor
36+| redraw
37+: refresh the window
38+| scroll-up
39+: go up in the timeline
40+| scroll-down
41+: go down in the timeline
42+| scroll-next-highlight
43+: go down to the next highlight
44+| scroll-previous-highlight
45+: go up to the next highlight
46+| buffer-next
47+: go to the next buffer
48+| buffer-previous
49+: go to the previous buffer
50+| buffer-next-unread
51+: go to the next unread buffer
52+| buffer-previous-unread
53+: go to the previous unread buffer
54+| cursor-right-word
55+: move the cursor to the next word
56+| cursor-left-word
57+: move the cursor to the previous word
58+| cursor-right
59+: move the cursor to the next character
60+| cursor-left
61+: move the cursor to the previous character
62+| cursor-up
63+: move the cursor to the previous character
64+| cursor-up
65+: scroll back one line in the editor
66+| cursor-down
67+: scroll forward one line in the editor
68+| cursor-delete-previous-word
69+: delete the previous word in the editor
70+| cursor-delete-previous
71+: delete the previous character in the editor
72+| cursor-delete-next
73+: delete the next character in the editor
74+| cursor-delete-before
75+: delete from the cursor to the beginning of the line
76+| cursor-delete-after
77+: delete from the cursor to the end of the line
78+| search-editor
79+: reverse-search in the editor history
80+| auto-complete
81+: open/select the auto-completion dialog/item
82+| close-overlay
83+: close any open overlay buffer (e.g. search results)
84+| toggle-channel-list
85+: show/hide the vertical channel list
86+| toggle-member-list
87+: show/hide the vertical member list
88+| send
89+: send the contents of the editor
90+| buffer <number>|_last_
91+: go the 0-indexed numbered buffer, or the last one
92+
93 *debug*
94 Advanced.
95 Dump all sent and received data to the home buffer, useful for debugging.
96@@ -254,6 +342,9 @@ pane-widths {
97 colors {
98 nicks extended
99 }
100+shortcuts {
101+ Alt+k cursor-delete-after
102+}
103 ```
104
105 # SEE ALSO
+30,
-0
1@@ -211,6 +211,36 @@ func (e *Editor) RemClusterForward() (ok bool) {
2 return
3 }
4
5+func (e *Editor) RemBefore() (ok bool) {
6+ ok = e.cursorIdx > 0
7+ if !ok {
8+ return
9+ }
10+ e.text[e.lineIdx].runes = e.text[e.lineIdx].runes[e.text[e.lineIdx].clusters[e.cursorIdx]:]
11+ e.cursorIdx = 0
12+ e.offsetIdx = 0
13+
14+ e.recompute()
15+ e.bumpOldestChange()
16+ e.autoCache = nil
17+ e.backsearchEnd()
18+ return
19+}
20+
21+func (e *Editor) RemAfter() (ok bool) {
22+ ok = e.cursorIdx < len(e.text[e.lineIdx].clusters)-1
23+ if !ok {
24+ return
25+ }
26+ e.text[e.lineIdx].runes = e.text[e.lineIdx].runes[:e.text[e.lineIdx].clusters[e.cursorIdx]]
27+
28+ e.recompute()
29+ e.bumpOldestChange()
30+ e.autoCache = nil
31+ e.backsearchEnd()
32+ return
33+}
34+
35 func (e *Editor) remClusterAt(idx int) {
36 rs := e.text[e.lineIdx].clusters[idx]
37 re := e.text[e.lineIdx].clusters[idx+1]
M
ui/ui.go
+8,
-0
1@@ -652,6 +652,14 @@ func (ui *UI) InputDelete() (ok bool) {
2 return ui.e.RemClusterForward()
3 }
4
5+func (ui *UI) InputDeleteBefore() (ok bool) {
6+ return ui.e.RemBefore()
7+}
8+
9+func (ui *UI) InputDeleteAfter() (ok bool) {
10+ return ui.e.RemAfter()
11+}
12+
13 func (ui *UI) InputDeleteWord() (ok bool) {
14 return ui.e.RemWord()
15 }