1"""
2Key bindings which are also known by GNU Readline by the given names.
3
4See: http://www.delorie.com/gnu/docs/readline/rlman_13.html
5"""
6
7from __future__ import annotations
8
9from typing import Callable, TypeVar, Union, cast
10
11from prompt_toolkit.document import Document
12from prompt_toolkit.enums import EditingMode
13from prompt_toolkit.key_binding.key_bindings import Binding, key_binding
14from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
15from prompt_toolkit.keys import Keys
16from prompt_toolkit.layout.controls import BufferControl
17from prompt_toolkit.search import SearchDirection
18from prompt_toolkit.selection import PasteMode
19
20from .completion import display_completions_like_readline, generate_completions
21
22__all__ = [
23 "get_by_name",
24]
25
26
27# Typing.
28_Handler = Callable[[KeyPressEvent], None]
29_HandlerOrBinding = Union[_Handler, Binding]
30_T = TypeVar("_T", bound=_HandlerOrBinding)
31E = KeyPressEvent
32
33
34# Registry that maps the Readline command names to their handlers.
35_readline_commands: dict[str, Binding] = {}
36
37
38def register(name: str) -> Callable[[_T], _T]:
39 """
40 Store handler in the `_readline_commands` dictionary.
41 """
42
43 def decorator(handler: _T) -> _T:
44 "`handler` is a callable or Binding."
45 if isinstance(handler, Binding):
46 _readline_commands[name] = handler
47 else:
48 _readline_commands[name] = key_binding()(cast(_Handler, handler))
49
50 return handler
51
52 return decorator
53
54
55def get_by_name(name: str) -> Binding:
56 """
57 Return the handler for the (Readline) command with the given name.
58 """
59 try:
60 return _readline_commands[name]
61 except KeyError as e:
62 raise KeyError(f"Unknown Readline command: {name!r}") from e
63
64
65#
66# Commands for moving
67# See: http://www.delorie.com/gnu/docs/readline/rlman_14.html
68#
69
70
71@register("beginning-of-buffer")
72def beginning_of_buffer(event: E) -> None:
73 """
74 Move to the start of the buffer.
75 """
76 buff = event.current_buffer
77 buff.cursor_position = 0
78
79
80@register("end-of-buffer")
81def end_of_buffer(event: E) -> None:
82 """
83 Move to the end of the buffer.
84 """
85 buff = event.current_buffer
86 buff.cursor_position = len(buff.text)
87
88
89@register("beginning-of-line")
90def beginning_of_line(event: E) -> None:
91 """
92 Move to the start of the current line.
93 """
94 buff = event.current_buffer
95 buff.cursor_position += buff.document.get_start_of_line_position(
96 after_whitespace=False
97 )
98
99
100@register("end-of-line")
101def end_of_line(event: E) -> None:
102 """
103 Move to the end of the line.
104 """
105 buff = event.current_buffer
106 buff.cursor_position += buff.document.get_end_of_line_position()
107
108
109@register("forward-char")
110def forward_char(event: E) -> None:
111 """
112 Move forward a character.
113 """
114 buff = event.current_buffer
115 buff.cursor_position += buff.document.get_cursor_right_position(count=event.arg)
116
117
118@register("backward-char")
119def backward_char(event: E) -> None:
120 "Move back a character."
121 buff = event.current_buffer
122 buff.cursor_position += buff.document.get_cursor_left_position(count=event.arg)
123
124
125@register("forward-word")
126def forward_word(event: E) -> None:
127 """
128 Move forward to the end of the next word. Words are composed of letters and
129 digits.
130 """
131 buff = event.current_buffer
132 pos = buff.document.find_next_word_ending(count=event.arg)
133
134 if pos:
135 buff.cursor_position += pos
136
137
138@register("backward-word")
139def backward_word(event: E) -> None:
140 """
141 Move back to the start of the current or previous word. Words are composed
142 of letters and digits.
143 """
144 buff = event.current_buffer
145 pos = buff.document.find_previous_word_beginning(count=event.arg)
146
147 if pos:
148 buff.cursor_position += pos
149
150
151@register("clear-screen")
152def clear_screen(event: E) -> None:
153 """
154 Clear the screen and redraw everything at the top of the screen.
155 """
156 event.app.renderer.clear()
157
158
159@register("redraw-current-line")
160def redraw_current_line(event: E) -> None:
161 """
162 Refresh the current line.
163 (Readline defines this command, but prompt-toolkit doesn't have it.)
164 """
165 pass
166
167
168#
169# Commands for manipulating the history.
170# See: http://www.delorie.com/gnu/docs/readline/rlman_15.html
171#
172
173
174@register("accept-line")
175def accept_line(event: E) -> None:
176 """
177 Accept the line regardless of where the cursor is.
178 """
179 event.current_buffer.validate_and_handle()
180
181
182@register("previous-history")
183def previous_history(event: E) -> None:
184 """
185 Move `back` through the history list, fetching the previous command.
186 """
187 event.current_buffer.history_backward(count=event.arg)
188
189
190@register("next-history")
191def next_history(event: E) -> None:
192 """
193 Move `forward` through the history list, fetching the next command.
194 """
195 event.current_buffer.history_forward(count=event.arg)
196
197
198@register("beginning-of-history")
199def beginning_of_history(event: E) -> None:
200 """
201 Move to the first line in the history.
202 """
203 event.current_buffer.go_to_history(0)
204
205
206@register("end-of-history")
207def end_of_history(event: E) -> None:
208 """
209 Move to the end of the input history, i.e., the line currently being entered.
210 """
211 event.current_buffer.history_forward(count=10**100)
212 buff = event.current_buffer
213 buff.go_to_history(len(buff._working_lines) - 1)
214
215
216@register("reverse-search-history")
217def reverse_search_history(event: E) -> None:
218 """
219 Search backward starting at the current line and moving `up` through
220 the history as necessary. This is an incremental search.
221 """
222 control = event.app.layout.current_control
223
224 if isinstance(control, BufferControl) and control.search_buffer_control:
225 event.app.current_search_state.direction = SearchDirection.BACKWARD
226 event.app.layout.current_control = control.search_buffer_control
227
228
229#
230# Commands for changing text
231#
232
233
234@register("end-of-file")
235def end_of_file(event: E) -> None:
236 """
237 Exit.
238 """
239 event.app.exit()
240
241
242@register("delete-char")
243def delete_char(event: E) -> None:
244 """
245 Delete character before the cursor.
246 """
247 deleted = event.current_buffer.delete(count=event.arg)
248 if not deleted:
249 event.app.output.bell()
250
251
252@register("backward-delete-char")
253def backward_delete_char(event: E) -> None:
254 """
255 Delete the character behind the cursor.
256 """
257 if event.arg < 0:
258 # When a negative argument has been given, this should delete in front
259 # of the cursor.
260 deleted = event.current_buffer.delete(count=-event.arg)
261 else:
262 deleted = event.current_buffer.delete_before_cursor(count=event.arg)
263
264 if not deleted:
265 event.app.output.bell()
266
267
268@register("self-insert")
269def self_insert(event: E) -> None:
270 """
271 Insert yourself.
272 """
273 event.current_buffer.insert_text(event.data * event.arg)
274
275
276@register("transpose-chars")
277def transpose_chars(event: E) -> None:
278 """
279 Emulate Emacs transpose-char behavior: at the beginning of the buffer,
280 do nothing. At the end of a line or buffer, swap the characters before
281 the cursor. Otherwise, move the cursor right, and then swap the
282 characters before the cursor.
283 """
284 b = event.current_buffer
285 p = b.cursor_position
286 if p == 0:
287 return
288 elif p == len(b.text) or b.text[p] == "\n":
289 b.swap_characters_before_cursor()
290 else:
291 b.cursor_position += b.document.get_cursor_right_position()
292 b.swap_characters_before_cursor()
293
294
295@register("uppercase-word")
296def uppercase_word(event: E) -> None:
297 """
298 Uppercase the current (or following) word.
299 """
300 buff = event.current_buffer
301
302 for i in range(event.arg):
303 pos = buff.document.find_next_word_ending()
304 words = buff.document.text_after_cursor[:pos]
305 buff.insert_text(words.upper(), overwrite=True)
306
307
308@register("downcase-word")
309def downcase_word(event: E) -> None:
310 """
311 Lowercase the current (or following) word.
312 """
313 buff = event.current_buffer
314
315 for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!!
316 pos = buff.document.find_next_word_ending()
317 words = buff.document.text_after_cursor[:pos]
318 buff.insert_text(words.lower(), overwrite=True)
319
320
321@register("capitalize-word")
322def capitalize_word(event: E) -> None:
323 """
324 Capitalize the current (or following) word.
325 """
326 buff = event.current_buffer
327
328 for i in range(event.arg):
329 pos = buff.document.find_next_word_ending()
330 words = buff.document.text_after_cursor[:pos]
331 buff.insert_text(words.title(), overwrite=True)
332
333
334@register("quoted-insert")
335def quoted_insert(event: E) -> None:
336 """
337 Add the next character typed to the line verbatim. This is how to insert
338 key sequences like C-q, for example.
339 """
340 event.app.quoted_insert = True
341
342
343#
344# Killing and yanking.
345#
346
347
348@register("kill-line")
349def kill_line(event: E) -> None:
350 """
351 Kill the text from the cursor to the end of the line.
352
353 If we are at the end of the line, this should remove the newline.
354 (That way, it is possible to delete multiple lines by executing this
355 command multiple times.)
356 """
357 buff = event.current_buffer
358 if event.arg < 0:
359 deleted = buff.delete_before_cursor(
360 count=-buff.document.get_start_of_line_position()
361 )
362 else:
363 if buff.document.current_char == "\n":
364 deleted = buff.delete(1)
365 else:
366 deleted = buff.delete(count=buff.document.get_end_of_line_position())
367 event.app.clipboard.set_text(deleted)
368
369
370@register("kill-word")
371def kill_word(event: E) -> None:
372 """
373 Kill from point to the end of the current word, or if between words, to the
374 end of the next word. Word boundaries are the same as forward-word.
375 """
376 buff = event.current_buffer
377 pos = buff.document.find_next_word_ending(count=event.arg)
378
379 if pos:
380 deleted = buff.delete(count=pos)
381
382 if event.is_repeat:
383 deleted = event.app.clipboard.get_data().text + deleted
384
385 event.app.clipboard.set_text(deleted)
386
387
388@register("unix-word-rubout")
389def unix_word_rubout(event: E, WORD: bool = True) -> None:
390 """
391 Kill the word behind point, using whitespace as a word boundary.
392 Usually bound to ControlW.
393 """
394 buff = event.current_buffer
395 pos = buff.document.find_start_of_previous_word(count=event.arg, WORD=WORD)
396
397 if pos is None:
398 # Nothing found? delete until the start of the document. (The
399 # input starts with whitespace and no words were found before the
400 # cursor.)
401 pos = -buff.cursor_position
402
403 if pos:
404 deleted = buff.delete_before_cursor(count=-pos)
405
406 # If the previous key press was also Control-W, concatenate deleted
407 # text.
408 if event.is_repeat:
409 deleted += event.app.clipboard.get_data().text
410
411 event.app.clipboard.set_text(deleted)
412 else:
413 # Nothing to delete. Bell.
414 event.app.output.bell()
415
416
417@register("backward-kill-word")
418def backward_kill_word(event: E) -> None:
419 """
420 Kills the word before point, using "not a letter nor a digit" as a word boundary.
421 Usually bound to M-Del or M-Backspace.
422 """
423 unix_word_rubout(event, WORD=False)
424
425
426@register("delete-horizontal-space")
427def delete_horizontal_space(event: E) -> None:
428 """
429 Delete all spaces and tabs around point.
430 """
431 buff = event.current_buffer
432 text_before_cursor = buff.document.text_before_cursor
433 text_after_cursor = buff.document.text_after_cursor
434
435 delete_before = len(text_before_cursor) - len(text_before_cursor.rstrip("\t "))
436 delete_after = len(text_after_cursor) - len(text_after_cursor.lstrip("\t "))
437
438 buff.delete_before_cursor(count=delete_before)
439 buff.delete(count=delete_after)
440
441
442@register("unix-line-discard")
443def unix_line_discard(event: E) -> None:
444 """
445 Kill backward from the cursor to the beginning of the current line.
446 """
447 buff = event.current_buffer
448
449 if buff.document.cursor_position_col == 0 and buff.document.cursor_position > 0:
450 buff.delete_before_cursor(count=1)
451 else:
452 deleted = buff.delete_before_cursor(
453 count=-buff.document.get_start_of_line_position()
454 )
455 event.app.clipboard.set_text(deleted)
456
457
458@register("yank")
459def yank(event: E) -> None:
460 """
461 Paste before cursor.
462 """
463 event.current_buffer.paste_clipboard_data(
464 event.app.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS
465 )
466
467
468@register("yank-nth-arg")
469def yank_nth_arg(event: E) -> None:
470 """
471 Insert the first argument of the previous command. With an argument, insert
472 the nth word from the previous command (start counting at 0).
473 """
474 n = event.arg if event.arg_present else None
475 event.current_buffer.yank_nth_arg(n)
476
477
478@register("yank-last-arg")
479def yank_last_arg(event: E) -> None:
480 """
481 Like `yank_nth_arg`, but if no argument has been given, yank the last word
482 of each line.
483 """
484 n = event.arg if event.arg_present else None
485 event.current_buffer.yank_last_arg(n)
486
487
488@register("yank-pop")
489def yank_pop(event: E) -> None:
490 """
491 Rotate the kill ring, and yank the new top. Only works following yank or
492 yank-pop.
493 """
494 buff = event.current_buffer
495 doc_before_paste = buff.document_before_paste
496 clipboard = event.app.clipboard
497
498 if doc_before_paste is not None:
499 buff.document = doc_before_paste
500 clipboard.rotate()
501 buff.paste_clipboard_data(clipboard.get_data(), paste_mode=PasteMode.EMACS)
502
503
504#
505# Completion.
506#
507
508
509@register("complete")
510def complete(event: E) -> None:
511 """
512 Attempt to perform completion.
513 """
514 display_completions_like_readline(event)
515
516
517@register("menu-complete")
518def menu_complete(event: E) -> None:
519 """
520 Generate completions, or go to the next completion. (This is the default
521 way of completing input in prompt_toolkit.)
522 """
523 generate_completions(event)
524
525
526@register("menu-complete-backward")
527def menu_complete_backward(event: E) -> None:
528 """
529 Move backward through the list of possible completions.
530 """
531 event.current_buffer.complete_previous()
532
533
534#
535# Keyboard macros.
536#
537
538
539@register("start-kbd-macro")
540def start_kbd_macro(event: E) -> None:
541 """
542 Begin saving the characters typed into the current keyboard macro.
543 """
544 event.app.emacs_state.start_macro()
545
546
547@register("end-kbd-macro")
548def end_kbd_macro(event: E) -> None:
549 """
550 Stop saving the characters typed into the current keyboard macro and save
551 the definition.
552 """
553 event.app.emacs_state.end_macro()
554
555
556@register("call-last-kbd-macro")
557@key_binding(record_in_macro=False)
558def call_last_kbd_macro(event: E) -> None:
559 """
560 Re-execute the last keyboard macro defined, by making the characters in the
561 macro appear as if typed at the keyboard.
562
563 Notice that we pass `record_in_macro=False`. This ensures that the 'c-x e'
564 key sequence doesn't appear in the recording itself. This function inserts
565 the body of the called macro back into the KeyProcessor, so these keys will
566 be added later on to the macro of their handlers have `record_in_macro=True`.
567 """
568 # Insert the macro.
569 macro = event.app.emacs_state.macro
570
571 if macro:
572 event.app.key_processor.feed_multiple(macro, first=True)
573
574
575@register("print-last-kbd-macro")
576def print_last_kbd_macro(event: E) -> None:
577 """
578 Print the last keyboard macro.
579 """
580
581 # TODO: Make the format suitable for the inputrc file.
582 def print_macro() -> None:
583 macro = event.app.emacs_state.macro
584 if macro:
585 for k in macro:
586 print(k)
587
588 from prompt_toolkit.application.run_in_terminal import run_in_terminal
589
590 run_in_terminal(print_macro)
591
592
593#
594# Miscellaneous Commands.
595#
596
597
598@register("undo")
599def undo(event: E) -> None:
600 """
601 Incremental undo.
602 """
603 event.current_buffer.undo()
604
605
606@register("insert-comment")
607def insert_comment(event: E) -> None:
608 """
609 Without numeric argument, comment all lines.
610 With numeric argument, uncomment all lines.
611 In any case accept the input.
612 """
613 buff = event.current_buffer
614
615 # Transform all lines.
616 if event.arg != 1:
617
618 def change(line: str) -> str:
619 return line[1:] if line.startswith("#") else line
620
621 else:
622
623 def change(line: str) -> str:
624 return "#" + line
625
626 buff.document = Document(
627 text="\n".join(map(change, buff.text.splitlines())), cursor_position=0
628 )
629
630 # Accept input.
631 buff.validate_and_handle()
632
633
634@register("vi-editing-mode")
635def vi_editing_mode(event: E) -> None:
636 """
637 Switch to Vi editing mode.
638 """
639 event.app.editing_mode = EditingMode.VI
640
641
642@register("emacs-editing-mode")
643def emacs_editing_mode(event: E) -> None:
644 """
645 Switch to Emacs editing mode.
646 """
647 event.app.editing_mode = EditingMode.EMACS
648
649
650@register("prefix-meta")
651def prefix_meta(event: E) -> None:
652 """
653 Metafy the next character typed. This is for keyboards without a meta key.
654
655 Sometimes people also want to bind other keys to Meta, e.g. 'jj'::
656
657 key_bindings.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta)
658 """
659 # ('first' should be true, because we want to insert it at the current
660 # position in the queue.)
661 event.app.key_processor.feed(KeyPress(Keys.Escape), first=True)
662
663
664@register("operate-and-get-next")
665def operate_and_get_next(event: E) -> None:
666 """
667 Accept the current line for execution and fetch the next line relative to
668 the current line from the history for editing.
669 """
670 buff = event.current_buffer
671 new_index = buff.working_index + 1
672
673 # Accept the current input. (This will also redraw the interface in the
674 # 'done' state.)
675 buff.validate_and_handle()
676
677 # Set the new index at the start of the next run.
678 def set_working_index() -> None:
679 if new_index < len(buff._working_lines):
680 buff.working_index = new_index
681
682 event.app.pre_run_callables.append(set_working_index)
683
684
685@register("edit-and-execute-command")
686def edit_and_execute(event: E) -> None:
687 """
688 Invoke an editor on the current command line, and accept the result.
689 """
690 buff = event.current_buffer
691 buff.open_in_editor(validate_and_handle=True)