- Python 99.6%
- Makefile 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .github | ||
| debian | ||
| scripts | ||
| src/textual_textarea | ||
| stubs/pyperclip | ||
| tests | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| .python-version | ||
| CHANGELOG.md | ||
| LICENSE | ||
| Makefile | ||
| pyproject.toml | ||
| README.md | ||
| textarea.svg | ||
| uv.lock | ||
Textual Textarea
Note: This is NOT the official TextArea widget!
With v0.38.0, Textual added a built-in TextArea widget. You probably want to use that widget instead of this one. This project predated the official widget; versions < v0.8.0 had a completely separate implmentation.
Since v0.8.0, this project uses the built-in TextArea widget, but adds the features outlined below.
Installation
pip install textual-textarea
Features
Full-featured text editor experience with VS-Code-like bindings, in your Textual App:
- Syntax highlighting and support for Pygments themes.
- Move cursor and scroll with mouse or keys (including ctrl+arrow, PgUp/Dn, ctrl+Home/End).
- Open (ctrl+o) and save (ctrl+s) files.
- Cut (ctrl+x), copy (ctrl+c), paste (ctrl+u/v), optionally using the system clipboard. On terminals that support the Kitty keyboard protocol, the cmd equivalents work too.
- Delete a word with ctrl+backspace, alt+backspace, or alt+delete.
- Comment selections with ctrl+/.
- Indent and dedent (optionally for a multiline selection) to tab stops with Tab and shift+Tab.
- Automatic completions of quotes and brackets.
- Select text by double-, triple-, or quadruple-clicking.
- Quit with ctrl+q.
Usage
Initializing the Widget
The TextArea is a Textual Widget. You can add it to a Textual
app using compose or mount:
from textual_textarea import TextEditor
from textual.app import App, ComposeResult
class TextApp(App, inherit_bindings=False):
def compose(self) -> ComposeResult:
yield TextEditor(text="hi", language="python", theme="nord-darker", id="ta")
def on_mount(self) -> None:
editor = self.query_one("#id", expect_type=TextEditor)
editor.focus()
app = TextApp()
app.run()
In addition to the standard Widget arguments, TextArea accepts several additional, optional arguments when initializing the widget:
- language (str): Must be
Noneor the short name of a Pygments lexer, e.g.,python,sql,as3. Defaults toNone. - theme (str): Must be name of a Pygments style, e.g.,
bw,github-dark,solarized-light. Defaults tomonokai. - use_system_clipboard (bool): Set to
Falseto make the TextArea's copy and paste operations ignore the system clipboard. Defaults toTrue. Some Linux users may need to apt-installxcliporxselto enable the system clipboard features. - read_only (bool): Set to
Trueto prevent the contents from being edited from the keyboard. Defaults toFalse. - show_cursor (bool): Set to
Falseto hide the cursor (and the shading of the line that contains it), and to scroll the contents like an ordinary container. Defaults toTrue.
Read-only previews
To show text the user can read, scroll, and copy from, but not edit, pass both
read_only=True and show_cursor=False:
yield TextEditor(text=query, language="sql", read_only=True, show_cursor=False, id="preview")
A read_only editor also disables the bindings that open its footer inputs --
save (ctrl+s), open (ctrl+o), find (ctrl+f, F3),
and go to line (ctrl+g) -- and lets escape bubble, so the screen
showing the preview can bind it to dismiss the preview. To keep some of those
bindings, subclass the editor and narrow READ_ONLY_DISABLED_ACTIONS:
class SearchablePreview(TextEditor):
READ_ONLY_DISABLED_ACTIONS = frozenset({"save", "load"})
The TextArea supports many actions and key bindings. For proper binding of ctrl+c to the COPY action,
you must initialize your App with inherit_bindings=False (as shown above), so that ctrl+c does not quit the app. The TextArea implements ctrl+q as quit; you way wish to mimic that in your app so that other in-focus widgets use the same behavior.
Interacting with the Widget
Getting and Setting Text
The TextArea exposes a text property that contains the full text contained in the widget. You can retrieve or set the text by interacting with this property:
editor = self.query_one(TextEditor)
old_text = editor.text
editor.text = "New Text!\n\nMany Lines!"
Similarly, the TextEditor exposes a selected_text property (read-only):
editor = self.query_one(TextEditor)
selection = editor.selected_text
Inserting Text
You can insert text at the current selection:
editor = self.query_one(TextEditor)
editor.text = "01234"
editor.selection = Selection((0, 2), (0, 2))
editor.insert_text_at_selection("\nabc\n")
assert editor.text == "01\nabc\n234"
assert editor.selection == Selection((2, 0), (2, 0))
Getting and Setting The Cursor Position
The TextEditor exposes a selection property that returns a textual.widgets.text_area.Selection:
editor = self.query_one(TextEditor)
old_selection = editor.selection
editor.selection = Selection((999, 0),(999, 0)) # the cursor will move as close to line 999, pos 0 as possible
cursor_line_number = editor.selection.end[0]
cursor_x_position = editor.selection.end[1]
Getting and Setting The Language
Syntax highlighting and comment insertion depends on the configured language for the TextEditor.
The TextArea exposes a language property that returns None or a string that is equal to the short name of an installed tree-sitter language:
editor = self.query_one(TextEditor)
old_language = editor.language
editor.language = "python"
Getting Theme Colors
If you would like the rest of your app to match the colors from the TextArea's theme, they are exposed via the theme_colors property.
editor = self.query_one(TextEditor)
color = editor.theme_colors.contrast_text_color
bgcolor = editor.theme_colors.bgcolor
highlight = editor.theme_colors.selection_bgcolor
Adding Bindings and other Behavior
You can subclass TextEditor to add your own behavior. This snippet adds an action that posts a Submitted message containing the text of the TextEditor when the user presses ctrl+j:
from textual.message import Message
from textual_textarea import TextEditor
class CodeEditor(TextEditor):
BINDINGS = [
("ctrl+j", "submit", "Run Query"),
]
class Submitted(Message, bubble=True):
def __init__(self, text: str) -> None:
super().__init__()
self.text = text
async def action_submit(self) -> None:
self.post_message(self.Submitted(self.text))