Line Editor

The blessed.line_editor module provides LineEditor, a headless single-line editor with readline-style keybindings, grapheme-aware cursor movement, auto-suggest from history, password masking, and horizontal scrolling.

Note

The editor never writes to the terminal directly, your application controls when and where output appears.

Use the built-in render methods to produce ready-to-print escape sequences, or read display for raw display state if you need fully custom rendering.

Overview

Feed keystrokes (from inkey() or async_inkey()) into feed_key(). It returns a LineEditResult with these fields:

  • line the accepted string when Enter is pressed, otherwise None.

  • interrupt, eof: True on Ctrl+C / Ctrl+D respectively.

  • changed True when the display needs redrawing.

  • bell a bell string to emit (empty when silent).

For bracketed paste, feed the pasted text through insert_text() instead of feed_key().

For async usage, simply replace term.inkey() with await term.async_inkey(). See Async Input in the keyboard documentation for more on async_inkey.

Example

A line editor with history, auto-suggest, password mode, and styled rendering:

 1#!/usr/bin/env python3
 2"""Line editor demo with clipboard, bracketed paste, history, and scrolling."""
 3from functools import partial
 4
 5from blessed import Terminal
 6from blessed.line_editor import LineEditor, LineHistory, LineEditResult
 7
 8term = Terminal()
 9history = LineHistory()
10echo = partial(print, end='\r\n', flush=True)
11
12has_clipboard = term.does_osc52_clipboard()
13
14
15def copy_line(ed):
16    term.clipboard_copy(ed.line)
17    return LineEditResult()
18
19
20def paste_line(ed):
21    text = term.clipboard_paste()
22    return ed.insert_text(text) if text else LineEditResult()
23
24
25with term.raw(), term.cursor_shape(term.CursorShape.BLINKING_BLOCK), term.bracketed_paste():
26    if has_clipboard:
27        echo("press ^C and ^V for OS clipboard, type 'quit' to exit")
28    else:
29        echo("type 'quit' to exit")
30    echo()
31    while True:
32        margin = max(1, term.width // 5)
33        width = term.width - margin * 2
34        prompt = "Prompt> "
35        col = margin
36        ed_width = width - len(prompt)
37        keymap = {}
38        if has_clipboard:
39            keymap['KEY_CTRL_C'] = copy_line
40            keymap['KEY_CTRL_V'] = paste_line
41        ed = LineEditor(history=history, max_width=ed_width, limit=200,
42                        bg_sgr=term.on_brown,
43                        keymap=keymap or None)
44        echo(term.move_x(col) + prompt, end='')
45        row = term.get_location()[0]
46        ed_col = col + len(prompt)
47        echo(ed.render(term, row, ed_width, col=ed_col), end='')
48        while True:
49            key = term.inkey()
50            if key.name == 'BRACKETED_PASTE':
51                result = ed.insert_text(key.text)
52            else:
53                result = ed.feed_key(key)
54            if result.bell:
55                echo(result.bell, end='')
56            if result.changed:
57                key_str = str(key)
58                out = None
59                if key_str and key_str.isprintable() and len(key_str) == 1:
60                    out = ed.render_insert(term, row, key_str)
61                elif getattr(key, "name", None) == "KEY_BACKSPACE":
62                    out = ed.render_backspace(term, row)
63                if out is None:
64                    out = ed.render(term, row, ed_width, col=ed_col)
65                echo(out, end='')
66            if result.line is not None:
67                echo()
68                if result.line:
69                    echo(term.move_x(col) + f"  => {result.line!r}")
70                break
71        if (result.line or '').strip() == 'quit':
72            break

History

LineHistory stores command history in memory. History navigation (Up/Down) and auto-suggest (type a prefix, press Right to accept) are enabled automatically when a history instance is attached to the editor.

For on-disk persistence, read/write the entries list directly (most recent entry last).

Display State & Styling

Each call to display returns a DisplayState with the visible text, cursor position, suggestion suffix, and clipping indicators.

The editor ships with default SGR styling (light cream text, dark suggestion). Override with text_sgr, suggestion_sgr, bg_sgr, or ellipsis_sgr:

editor = LineEditor(bg_sgr=term.on_brown, max_width=term.width)

When max_width is set and text overflows, overflow_left and overflow_right indicate which edges are truncated. Use ellipsis_sgr to style the overflow indicator.

Rendering Helpers

LineEditor provides three render methods that build complete escape-sequence strings from the current display state:

render()

Full redraw, always produces correct output.

render_insert()

Fast-path after a character insert at end of buffer. Returns None when a full redraw is needed instead.

render_backspace()

Fast-path after a backspace at end of buffer. Returns None when a full redraw is needed instead.

Try the fast-path first and fall back to render() when it returns None. See the example above for the complete pattern.

Other Methods

clear()

Reset the buffer, cursor, and undo history.

set_password_mode()

Toggle password masking on or off mid-session.

Constructor Options

Beyond the styling and keybinding options shown above, LineEditor accepts:

password

If True, start in password mode (characters are masked). Toggle at runtime with set_password_mode().

password_char

Replacement character shown in password mode (default "⚻").

limit

Maximum buffer length in characters (default 65536).

limit_bell

Bell string emitted when the limit is reached (default "\\a").

scroll_jump

Fraction of max_width to scroll when the cursor overflows (default 0.5).

Custom Keybindings

Pass a keymap dict to override or extend the default emacs/readline bindings. Keys are Keystroke .name strings (e.g. "KEY_CTRL_K"), values are callables accepting a LineEditor and returning a LineEditResult.

Override an existing binding:

def my_enter(editor):
    # custom accept logic
    return LineEditResult(line=editor.line, changed=True)

editor = LineEditor(keymap={"KEY_ENTER": my_enter})

Add a new binding:

def handle_f1(editor):
    editor.insert_text("help!")
    return LineEditResult(changed=True)

editor = LineEditor(keymap={"KEY_F1": handle_f1})

Disable a binding by setting it to None:

# Ctrl+C becomes a silent no-op instead of raising interrupt
editor = LineEditor(keymap={"KEY_CTRL_C": None})

Default Keybindings

These are the default emacs/readline bindings. All can be overridden via keymap.

Key

Action

Enter

Accept line

Ctrl+C

Cancel (interrupt)

Ctrl+D

EOF on empty line, delete at cursor otherwise

Left, Ctrl+B

Move cursor left

Right

Accept auto-suggest at end, otherwise move right

Ctrl+F

Move cursor right

Home, Ctrl+A

Move to start of line

End, Ctrl+E

Move to end of line

Shift+Left, Ctrl+Left

Move word left

Shift+Right, Ctrl+Right

Move word right

Backspace

Delete character before cursor

Delete

Delete character at cursor

Ctrl+K

Kill to end of line

Ctrl+U

Kill to start of line

Ctrl+W

Kill word backward

Ctrl+Y

Yank (paste from kill ring)

Up, Ctrl+P

Previous history entry

Down, Ctrl+N

Next history entry

Ctrl+Z

Undo