Skip to content

Instantly share code, notes, and snippets.

@Zaloog
Last active December 26, 2025 06:18
Show Gist options
  • Select an option

  • Save Zaloog/afba1bb7a8172b5385ef9ff83b0e2e92 to your computer and use it in GitHub Desktop.

Select an option

Save Zaloog/afba1bb7a8172b5385ef9ff83b0e2e92 to your computer and use it in GitHub Desktop.
textual-textarea with some vim bindings
# /// 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