Last active
December 26, 2025 06:18
-
-
Save Zaloog/afba1bb7a8172b5385ef9ff83b0e2e92 to your computer and use it in GitHub Desktop.
textual-textarea with some vim bindings
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # /// script | |
| # requires-python = ">=3.13" | |
| # dependencies = [ | |
| # "textual", | |
| # ] | |
| # /// | |
| from enum import Enum | |
| from textual.events import Key | |
| from textual.app import App | |
| from textual.reactive import reactive | |
| from textual.widgets import TextArea | |
| # from textual.document._document import Location | |
| TEXT = """\ | |
| def hello(name): | |
| print("hello" + name) | |
| def goodbye(name): | |
| print("goodbye" + name) | |
| """ | |
| class MODES(str, Enum): | |
| NORMAL = 'normal' | |
| VISUAL = 'visual' | |
| INSERT = 'insert' | |
| class VimText(TextArea): | |
| mode: reactive(MODES) = reactive(MODES.NORMAL) | |
| last_keys: reactive(str) = reactive('') | |
| def on_mount(self): | |
| ... | |
| def watch_mode(self): | |
| self.border_title = self.mode | |
| match self.mode: | |
| case MODES.NORMAL: | |
| self.read_only = True | |
| case MODES.INSERT: | |
| self.read_only = False | |
| def _on_key(self, event:Key): | |
| ## Switch Modes | |
| if self.mode == MODES.NORMAL: | |
| # Go to Insert Mode | |
| if event.character in ['i', 'I', 'a', 'A', 's', 'S', 'o', 'O']: | |
| self._move_to_insert_mode(character_pressed=event.character) | |
| event.prevent_default() | |
| # Navigate | |
| if event.character in ['h', 'j', 'k', 'l', 'w', 'b']: | |
| self._navigate_cursor(character_pressed=event.character) | |
| event.prevent_default() | |
| elif self.mode == MODES.INSERT: | |
| if event.key in ["escape", "ctrl+c"]: | |
| self.mode = MODES.NORMAL | |
| event.prevent_default() | |
| if event.character in ["(", "[", "{", "'", '"', "`"]: | |
| self._auto_close_brackets_and_quotes(character_pressed=event.character) | |
| event.prevent_default() | |
| def _move_to_insert_mode(self, character_pressed): | |
| match character_pressed: | |
| case "i": | |
| self.move_cursor_relative(columns=0) | |
| case "I": | |
| self.move_cursor(location=self.get_cursor_line_start_location()) | |
| case "a": | |
| self.move_cursor_relative(columns=+1) | |
| case "A": | |
| self.move_cursor(location=self.get_cursor_line_end_location()) | |
| case "s": | |
| self.replace("", *self.selection) | |
| case "S": | |
| # doesnt work | |
| self._delete_cursor_line() | |
| case "o": | |
| self.move_cursor(location=self.get_cursor_line_end_location()) | |
| self.insert("\n") | |
| case "O": | |
| self.move_cursor_relative(rows=-1) | |
| self.move_cursor(location=self.get_cursor_line_end_location()) | |
| self.insert("\n") | |
| self.mode = MODES.INSERT | |
| def _navigate_cursor(self, character_pressed): | |
| self.cursor_blink = False | |
| match character_pressed: | |
| case "h": | |
| self.move_cursor_relative(columns=-1) | |
| case "j": | |
| self.move_cursor_relative(rows=+1) | |
| case "k": | |
| self.move_cursor_relative(rows=-1) | |
| case "l": | |
| self.move_cursor_relative(columns=+1) | |
| case "w": | |
| self.action_cursor_word_right() | |
| self.move_cursor_relative(columns=+1) | |
| case "b": | |
| self.action_cursor_word_left() | |
| self.cursor_blink = True | |
| def _auto_close_brackets_and_quotes(self, character_pressed): | |
| match character_pressed: | |
| case "(": | |
| self.insert("()") | |
| case "[": | |
| self.insert("[]") | |
| case "{": | |
| self.insert("{}") | |
| case "'": | |
| self.insert("''") | |
| case '"': | |
| self.insert('""') | |
| case '`': | |
| self.insert('``') | |
| self.move_cursor_relative(columns=-1) | |
| class VimApp(App): | |
| CSS = """ | |
| VimText { | |
| border:orange; | |
| } | |
| """ | |
| def compose(self): | |
| yield VimText(TEXT) | |
| def main(): | |
| app = VimApp() | |
| app.run() | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment