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