textual-vim-textarea 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- textual_vim_textarea-1.1.0/.gitignore +3 -0
- textual_vim_textarea-1.1.0/.python-version +1 -0
- textual_vim_textarea-1.1.0/LICENSE +21 -0
- textual_vim_textarea-1.1.0/PKG-INFO +157 -0
- textual_vim_textarea-1.1.0/README.md +134 -0
- textual_vim_textarea-1.1.0/examples/dummy_app.py +100 -0
- textual_vim_textarea-1.1.0/examples/dummy_app_plus.py +82 -0
- textual_vim_textarea-1.1.0/examples/vim_text_editor.py +44 -0
- textual_vim_textarea-1.1.0/pyproject.toml +49 -0
- textual_vim_textarea-1.1.0/src/textual_vim_textarea/__init__.py +11 -0
- textual_vim_textarea-1.1.0/src/textual_vim_textarea/_modal.py +605 -0
- textual_vim_textarea-1.1.0/src/textual_vim_textarea/textarea_plus.py +154 -0
- textual_vim_textarea-1.1.0/src/textual_vim_textarea/vim_textarea.py +103 -0
- textual_vim_textarea-1.1.0/tests/_vim_text_editor_for_tests.py +44 -0
- textual_vim_textarea-1.1.0/tests/test_textarea_plus.py +246 -0
- textual_vim_textarea-1.1.0/tests/test_vim_textarea.py +361 -0
- textual_vim_textarea-1.1.0/uv.lock +621 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.10
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dastageer Siddiqui
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: textual-vim-textarea
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: A Vim-style textarea widget for Textual
|
|
5
|
+
Project-URL: Homepage, https://github.com/Dking08/textual-vim-textarea
|
|
6
|
+
Project-URL: Repository, https://github.com/Dking08/textual-vim-textarea
|
|
7
|
+
Project-URL: Issues, https://github.com/Dking08/textual-vim-textarea/issues
|
|
8
|
+
Author-email: Dking08 <dastageer_foss@yahoo.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: editor,textarea,textual,tui,vim
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Terminals
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: textual>=6.4.0
|
|
20
|
+
Provides-Extra: textarea-plus
|
|
21
|
+
Requires-Dist: textual-textarea<0.18,>=0.17; extra == 'textarea-plus'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# textual-vim-textarea
|
|
25
|
+
|
|
26
|
+
vim-style editing built on top of Textual's built-in `TextArea`. This is just a subclass that intercepts keys before they hit the default "every keypress inserts a character" behavior.
|
|
27
|
+
|
|
28
|
+
Built this to eventually wire vim keybindings into a TUI app, but split it out as its own module first so it could be tested properly on its own before touching real app code.
|
|
29
|
+
|
|
30
|
+
## Two flavors
|
|
31
|
+
|
|
32
|
+
- **`VimTextArea`** - for plain Textual `TextArea`. Zero extra dependencies beyond `textual` itself.
|
|
33
|
+
- **`VimTextAreaPlus`** - for apps built on [`textual-textarea`](https://github.com/tconbeer/textual-textarea)'s `TextEditor`/`TextAreaPlus` instead of plain `TextArea`.
|
|
34
|
+
|
|
35
|
+
Both share the same underlying vim logic (`_modal.VimModalMixin`) - same motions, operators, counts, everything. They only differ in how they hook into their respective base widget's key handling, because the two bases genuinely work differently under the hood (see `textarea_plus.py`'s module docstring if you're curious exactly why).
|
|
36
|
+
|
|
37
|
+
## What you get
|
|
38
|
+
|
|
39
|
+
- Modes: `NORMAL`, `INSERT`, `VISUAL`, `VISUAL LINE`, `COMMAND`
|
|
40
|
+
- Motions: `h j k l`, `0`, `^`, `$`, `w`, `b`, `e`, `gg`, `G` (arrow keys work too)
|
|
41
|
+
- Insert entry: `i a I A o O`
|
|
42
|
+
- Editing: `x X`, `dd dw db de`, `d$ / D`, `cc cw cb ce`, `c$ / C`,
|
|
43
|
+
`yy yw yb ye`, `y$`, `p P`, `u`, `ctrl+r`
|
|
44
|
+
- Visual mode: `v` (charwise) / `V` (linewise), then `d x c y` act on the selection
|
|
45
|
+
- Counts: `3j`, `3dd`, `2dw`, even `2d3w` (counts multiply, like real vim)
|
|
46
|
+
- Command line: `:w`, `:q`, `:wq`, `:n` to jump to line `n` - anything else
|
|
47
|
+
gets posted as a message so your app can handle its own commands
|
|
48
|
+
- Line numbers - this one's actually just `TextArea`'s built-in
|
|
49
|
+
`show_line_numbers=True`, not something this module adds
|
|
50
|
+
|
|
51
|
+
**Not** currently implemented: registers beyond the default one, macros, marks, `:s///` regex substitution, folds, splits.
|
|
52
|
+
|
|
53
|
+
> If you want to more vim features, you can contribute!
|
|
54
|
+
|
|
55
|
+
## Project layout
|
|
56
|
+
|
|
57
|
+
- `src/textual_vim_textarea/_modal.py` - shared vim logic, not meant to be used directly
|
|
58
|
+
- `src/textual_vim_textarea/vim_textarea.py` - `VimTextArea`, built on plain `TextArea`. Primary file.
|
|
59
|
+
- `src/textual_vim_textarea/textarea_plus.py` - `VimTextAreaPlus`, built on `textual-textarea`'s `TextAreaPlus`.
|
|
60
|
+
|
|
61
|
+
- `examples/dummy_app.py` - standalone playground for `VimTextArea`
|
|
62
|
+
- `examples/dummy_app_plus.py` + `examples/vim_text_editor.py` - standalone playground for `VimTextAreaPlus`, and the `TextEditor` subclass pattern to copy into a real app
|
|
63
|
+
|
|
64
|
+
- `tests/test_vim_textarea.py`, `tests/test_textarea_plus.py` - headless tests using Textual's `Pilot`
|
|
65
|
+
|
|
66
|
+
## Try it
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv sync
|
|
70
|
+
python examples/dummy_app.py
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> Bottom bar shows the mode/pending-command line, same idea as vim's own.
|
|
74
|
+
> `:q` quits, `:w` just fires a notification since there's no real file
|
|
75
|
+
> backing the dummy app.
|
|
76
|
+
|
|
77
|
+
For the `TextAreaPlus` flavor (needs the extra) - OPTIONAL:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv sync --extra textarea-plus
|
|
81
|
+
python examples/dummy_app_plus.py
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Using it in your own TUI
|
|
85
|
+
|
|
86
|
+
It's a drop-in replacement for `TextArea`. Use it exactly like you'd use `TextArea`, and listen for a couple of extra messages to keep a status bar in sync and to hook up `:w` / `:q`:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from textual_vim_textarea import VimTextArea
|
|
90
|
+
|
|
91
|
+
class MyApp(App):
|
|
92
|
+
def compose(self):
|
|
93
|
+
yield VimTextArea("some text", language="python", show_line_numbers=True, id="editor")
|
|
94
|
+
|
|
95
|
+
def on_vim_text_area_status_changed(self, message: VimTextArea.StatusChanged):
|
|
96
|
+
# fires on every NORMAL/VISUAL/COMMAND keystroke it handles -
|
|
97
|
+
# pending counts, pending operators, the command buffer, etc.
|
|
98
|
+
self.query_one("#status").update(message.status)
|
|
99
|
+
|
|
100
|
+
def on_vim_text_area_mode_changed(self, message: VimTextArea.ModeChanged):
|
|
101
|
+
# fires only when the mode itself actually changes
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
def on_vim_text_area_save_requested(self, message: VimTextArea.SaveRequested):
|
|
105
|
+
# ':w' or ':wq' - do your actual file save here
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
def on_vim_text_area_quit_requested(self, message: VimTextArea.QuitRequested):
|
|
109
|
+
# ':q' or ':wq'
|
|
110
|
+
self.exit()
|
|
111
|
+
|
|
112
|
+
def on_vim_text_area_command_entered(self, message: VimTextArea.CommandEntered):
|
|
113
|
+
# any ':' command that isn't w/q/wq/n - bring your own command palette
|
|
114
|
+
...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Everything else - `.text`, `.document`, `.selection`, themes, syntax highlighting, `language=`, etc. - is untouched `TextArea` API, works exactly as it does normally. This class only takes over key handling while you're not in INSERT mode.**
|
|
118
|
+
|
|
119
|
+
### One gotcha if you're subclassing further
|
|
120
|
+
|
|
121
|
+
`VimTextArea` uses `_on_key` as its extension point. If you need to add your own key handling on top, don't fight the binding system - override `_on_key`, check mode, and either fall through to `super()._on_key(event)` or handle it yourself and call `event.stop()` + `event.prevent_default()`. Same pattern this module already uses internally.
|
|
122
|
+
|
|
123
|
+
`VimTextAreaPlus` is different on purpose - it hooks `on_key`, not `_on_key`, and in INSERT mode does **nothing at all** (not even calling `super()`) so the event cascades naturally through `TextAreaPlus`'s own `on_key` and then base `TextArea`'s `_on_key`. If you're subclassing `VimTextAreaPlus` further, match that: don't call `super().on_key()` manually, just decide whether to `prevent_default()`+`stop()` or leave the event alone entirely. See `textarea_plus.py`'s module docstring for the actual Textual dispatch mechanics this depends on - it's worth reading before you touch it, the ordering isn't obvious from the outside.
|
|
124
|
+
|
|
125
|
+
### Using VimTextAreaPlus
|
|
126
|
+
|
|
127
|
+
Same idea, but you also get to decide what `:w`/`:q` actually do, since `TextAreaPlus`'s real save flow lives on an ancestor `TextEditor` widget, not on the text area itself:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from textual_vim_textarea.textarea_plus import VimTextAreaPlus
|
|
131
|
+
|
|
132
|
+
class MyCodeEditor(TextEditor): # from textual_textarea
|
|
133
|
+
def compose(self):
|
|
134
|
+
self.text_input = VimTextAreaPlus(language="sql", text=self._initial_text)
|
|
135
|
+
... # rest of TextEditor's own compose() body, unchanged
|
|
136
|
+
|
|
137
|
+
def on_vim_text_area_plus_quit_requested(self, message):
|
|
138
|
+
# ':q' - deliberately just a message. Closing a buffer/tab vs.
|
|
139
|
+
# quitting the whole app is your call, not this widget's.
|
|
140
|
+
...
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`:w` already works out of the box - it walks up to whatever ancestor has `action_save` (the real `TextEditor` action ctrl+s also triggers) and calls it directly, since Textual's own `run_action()` won't resolve an un-prefixed action name against an ancestor by default (verified this the hard way, see the integration guide).
|
|
144
|
+
|
|
145
|
+
## Known rough edges
|
|
146
|
+
|
|
147
|
+
- Word motions (`w b e`) use a simplified vim "word" definition (keyword run vs punctuation run vs whitespace). Covers the common case, isn't byte-for-byte identical to vim in every corner case.
|
|
148
|
+
- `cw` intentionally behaves like `ce` (doesn't eat trailing whitespace).
|
|
149
|
+
- Only one register (the unnamed one). `dd` then `yy` then `p` pastes whatever you yanked/deleted most recently, same register for everything.
|
|
150
|
+
|
|
151
|
+
## Running the tests
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
uv run pytest tests -v --asyncio-mode=auto
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
39 tests, all green as of this writing - 25 for `VimTextArea`, 14 for `VimTextAreaPlus`
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# textual-vim-textarea
|
|
2
|
+
|
|
3
|
+
vim-style editing built on top of Textual's built-in `TextArea`. This is just a subclass that intercepts keys before they hit the default "every keypress inserts a character" behavior.
|
|
4
|
+
|
|
5
|
+
Built this to eventually wire vim keybindings into a TUI app, but split it out as its own module first so it could be tested properly on its own before touching real app code.
|
|
6
|
+
|
|
7
|
+
## Two flavors
|
|
8
|
+
|
|
9
|
+
- **`VimTextArea`** - for plain Textual `TextArea`. Zero extra dependencies beyond `textual` itself.
|
|
10
|
+
- **`VimTextAreaPlus`** - for apps built on [`textual-textarea`](https://github.com/tconbeer/textual-textarea)'s `TextEditor`/`TextAreaPlus` instead of plain `TextArea`.
|
|
11
|
+
|
|
12
|
+
Both share the same underlying vim logic (`_modal.VimModalMixin`) - same motions, operators, counts, everything. They only differ in how they hook into their respective base widget's key handling, because the two bases genuinely work differently under the hood (see `textarea_plus.py`'s module docstring if you're curious exactly why).
|
|
13
|
+
|
|
14
|
+
## What you get
|
|
15
|
+
|
|
16
|
+
- Modes: `NORMAL`, `INSERT`, `VISUAL`, `VISUAL LINE`, `COMMAND`
|
|
17
|
+
- Motions: `h j k l`, `0`, `^`, `$`, `w`, `b`, `e`, `gg`, `G` (arrow keys work too)
|
|
18
|
+
- Insert entry: `i a I A o O`
|
|
19
|
+
- Editing: `x X`, `dd dw db de`, `d$ / D`, `cc cw cb ce`, `c$ / C`,
|
|
20
|
+
`yy yw yb ye`, `y$`, `p P`, `u`, `ctrl+r`
|
|
21
|
+
- Visual mode: `v` (charwise) / `V` (linewise), then `d x c y` act on the selection
|
|
22
|
+
- Counts: `3j`, `3dd`, `2dw`, even `2d3w` (counts multiply, like real vim)
|
|
23
|
+
- Command line: `:w`, `:q`, `:wq`, `:n` to jump to line `n` - anything else
|
|
24
|
+
gets posted as a message so your app can handle its own commands
|
|
25
|
+
- Line numbers - this one's actually just `TextArea`'s built-in
|
|
26
|
+
`show_line_numbers=True`, not something this module adds
|
|
27
|
+
|
|
28
|
+
**Not** currently implemented: registers beyond the default one, macros, marks, `:s///` regex substitution, folds, splits.
|
|
29
|
+
|
|
30
|
+
> If you want to more vim features, you can contribute!
|
|
31
|
+
|
|
32
|
+
## Project layout
|
|
33
|
+
|
|
34
|
+
- `src/textual_vim_textarea/_modal.py` - shared vim logic, not meant to be used directly
|
|
35
|
+
- `src/textual_vim_textarea/vim_textarea.py` - `VimTextArea`, built on plain `TextArea`. Primary file.
|
|
36
|
+
- `src/textual_vim_textarea/textarea_plus.py` - `VimTextAreaPlus`, built on `textual-textarea`'s `TextAreaPlus`.
|
|
37
|
+
|
|
38
|
+
- `examples/dummy_app.py` - standalone playground for `VimTextArea`
|
|
39
|
+
- `examples/dummy_app_plus.py` + `examples/vim_text_editor.py` - standalone playground for `VimTextAreaPlus`, and the `TextEditor` subclass pattern to copy into a real app
|
|
40
|
+
|
|
41
|
+
- `tests/test_vim_textarea.py`, `tests/test_textarea_plus.py` - headless tests using Textual's `Pilot`
|
|
42
|
+
|
|
43
|
+
## Try it
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv sync
|
|
47
|
+
python examples/dummy_app.py
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> Bottom bar shows the mode/pending-command line, same idea as vim's own.
|
|
51
|
+
> `:q` quits, `:w` just fires a notification since there's no real file
|
|
52
|
+
> backing the dummy app.
|
|
53
|
+
|
|
54
|
+
For the `TextAreaPlus` flavor (needs the extra) - OPTIONAL:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv sync --extra textarea-plus
|
|
58
|
+
python examples/dummy_app_plus.py
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Using it in your own TUI
|
|
62
|
+
|
|
63
|
+
It's a drop-in replacement for `TextArea`. Use it exactly like you'd use `TextArea`, and listen for a couple of extra messages to keep a status bar in sync and to hook up `:w` / `:q`:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from textual_vim_textarea import VimTextArea
|
|
67
|
+
|
|
68
|
+
class MyApp(App):
|
|
69
|
+
def compose(self):
|
|
70
|
+
yield VimTextArea("some text", language="python", show_line_numbers=True, id="editor")
|
|
71
|
+
|
|
72
|
+
def on_vim_text_area_status_changed(self, message: VimTextArea.StatusChanged):
|
|
73
|
+
# fires on every NORMAL/VISUAL/COMMAND keystroke it handles -
|
|
74
|
+
# pending counts, pending operators, the command buffer, etc.
|
|
75
|
+
self.query_one("#status").update(message.status)
|
|
76
|
+
|
|
77
|
+
def on_vim_text_area_mode_changed(self, message: VimTextArea.ModeChanged):
|
|
78
|
+
# fires only when the mode itself actually changes
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
def on_vim_text_area_save_requested(self, message: VimTextArea.SaveRequested):
|
|
82
|
+
# ':w' or ':wq' - do your actual file save here
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
def on_vim_text_area_quit_requested(self, message: VimTextArea.QuitRequested):
|
|
86
|
+
# ':q' or ':wq'
|
|
87
|
+
self.exit()
|
|
88
|
+
|
|
89
|
+
def on_vim_text_area_command_entered(self, message: VimTextArea.CommandEntered):
|
|
90
|
+
# any ':' command that isn't w/q/wq/n - bring your own command palette
|
|
91
|
+
...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Everything else - `.text`, `.document`, `.selection`, themes, syntax highlighting, `language=`, etc. - is untouched `TextArea` API, works exactly as it does normally. This class only takes over key handling while you're not in INSERT mode.**
|
|
95
|
+
|
|
96
|
+
### One gotcha if you're subclassing further
|
|
97
|
+
|
|
98
|
+
`VimTextArea` uses `_on_key` as its extension point. If you need to add your own key handling on top, don't fight the binding system - override `_on_key`, check mode, and either fall through to `super()._on_key(event)` or handle it yourself and call `event.stop()` + `event.prevent_default()`. Same pattern this module already uses internally.
|
|
99
|
+
|
|
100
|
+
`VimTextAreaPlus` is different on purpose - it hooks `on_key`, not `_on_key`, and in INSERT mode does **nothing at all** (not even calling `super()`) so the event cascades naturally through `TextAreaPlus`'s own `on_key` and then base `TextArea`'s `_on_key`. If you're subclassing `VimTextAreaPlus` further, match that: don't call `super().on_key()` manually, just decide whether to `prevent_default()`+`stop()` or leave the event alone entirely. See `textarea_plus.py`'s module docstring for the actual Textual dispatch mechanics this depends on - it's worth reading before you touch it, the ordering isn't obvious from the outside.
|
|
101
|
+
|
|
102
|
+
### Using VimTextAreaPlus
|
|
103
|
+
|
|
104
|
+
Same idea, but you also get to decide what `:w`/`:q` actually do, since `TextAreaPlus`'s real save flow lives on an ancestor `TextEditor` widget, not on the text area itself:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from textual_vim_textarea.textarea_plus import VimTextAreaPlus
|
|
108
|
+
|
|
109
|
+
class MyCodeEditor(TextEditor): # from textual_textarea
|
|
110
|
+
def compose(self):
|
|
111
|
+
self.text_input = VimTextAreaPlus(language="sql", text=self._initial_text)
|
|
112
|
+
... # rest of TextEditor's own compose() body, unchanged
|
|
113
|
+
|
|
114
|
+
def on_vim_text_area_plus_quit_requested(self, message):
|
|
115
|
+
# ':q' - deliberately just a message. Closing a buffer/tab vs.
|
|
116
|
+
# quitting the whole app is your call, not this widget's.
|
|
117
|
+
...
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`:w` already works out of the box - it walks up to whatever ancestor has `action_save` (the real `TextEditor` action ctrl+s also triggers) and calls it directly, since Textual's own `run_action()` won't resolve an un-prefixed action name against an ancestor by default (verified this the hard way, see the integration guide).
|
|
121
|
+
|
|
122
|
+
## Known rough edges
|
|
123
|
+
|
|
124
|
+
- Word motions (`w b e`) use a simplified vim "word" definition (keyword run vs punctuation run vs whitespace). Covers the common case, isn't byte-for-byte identical to vim in every corner case.
|
|
125
|
+
- `cw` intentionally behaves like `ce` (doesn't eat trailing whitespace).
|
|
126
|
+
- Only one register (the unnamed one). `dd` then `yy` then `p` pastes whatever you yanked/deleted most recently, same register for everything.
|
|
127
|
+
|
|
128
|
+
## Running the tests
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
uv run pytest tests -v --asyncio-mode=auto
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
39 tests, all green as of this writing - 25 for `VimTextArea`, 14 for `VimTextAreaPlus`
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dummy_app.py
|
|
3
|
+
|
|
4
|
+
A minimal standalone Textual app for interactively trying out VimTextArea
|
|
5
|
+
before it gets wired into a real TUI. Run it with:
|
|
6
|
+
|
|
7
|
+
python3 dummy_app.py
|
|
8
|
+
|
|
9
|
+
Bottom status bar mirrors vim's own mode/command line: shows NORMAL,
|
|
10
|
+
-- INSERT --, -- VISUAL --, -- VISUAL LINE --, pending counts/operators,
|
|
11
|
+
or ":<command>" while typing a command.
|
|
12
|
+
|
|
13
|
+
:w -> just shows a notification (no real file I/O in this dummy)
|
|
14
|
+
:q -> quits the app
|
|
15
|
+
:wq -> both
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from textual.app import App, ComposeResult
|
|
19
|
+
from textual.containers import Vertical
|
|
20
|
+
from textual.widgets import Footer, Header, Static
|
|
21
|
+
|
|
22
|
+
from textual_vim_textarea import VimTextArea
|
|
23
|
+
|
|
24
|
+
SAMPLE_TEXT = """\
|
|
25
|
+
def greet(name):
|
|
26
|
+
print("hello", name)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main():
|
|
30
|
+
greet("world")
|
|
31
|
+
numbers = [1, 2, 3, 4, 5]
|
|
32
|
+
total = sum(numbers)
|
|
33
|
+
print(total)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class StatusBar(Static):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DummyVimApp(App):
|
|
46
|
+
CSS = """
|
|
47
|
+
Screen {
|
|
48
|
+
background: #0d1117;
|
|
49
|
+
}
|
|
50
|
+
VimTextArea {
|
|
51
|
+
border: solid #30363d;
|
|
52
|
+
height: 1fr;
|
|
53
|
+
}
|
|
54
|
+
StatusBar {
|
|
55
|
+
dock: bottom;
|
|
56
|
+
height: 1;
|
|
57
|
+
background: #161b22;
|
|
58
|
+
color: #58a6ff;
|
|
59
|
+
padding: 0 1;
|
|
60
|
+
}
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def compose(self) -> ComposeResult:
|
|
64
|
+
yield Header()
|
|
65
|
+
with Vertical():
|
|
66
|
+
yield VimTextArea(
|
|
67
|
+
SAMPLE_TEXT,
|
|
68
|
+
language="python",
|
|
69
|
+
id="editor",
|
|
70
|
+
show_line_numbers=True,
|
|
71
|
+
)
|
|
72
|
+
yield StatusBar("NORMAL", id="status")
|
|
73
|
+
yield Footer()
|
|
74
|
+
|
|
75
|
+
def on_mount(self) -> None:
|
|
76
|
+
self.query_one("#editor", VimTextArea).focus()
|
|
77
|
+
self._refresh_status()
|
|
78
|
+
|
|
79
|
+
def _refresh_status(self) -> None:
|
|
80
|
+
editor = self.query_one("#editor", VimTextArea)
|
|
81
|
+
self.query_one("#status", StatusBar).update(editor.status_text)
|
|
82
|
+
|
|
83
|
+
def on_vim_text_area_mode_changed(self, message: VimTextArea.ModeChanged) -> None:
|
|
84
|
+
self._refresh_status()
|
|
85
|
+
|
|
86
|
+
def on_vim_text_area_status_changed(self, message: VimTextArea.StatusChanged) -> None:
|
|
87
|
+
self.query_one("#status", StatusBar).update(message.status)
|
|
88
|
+
|
|
89
|
+
def on_vim_text_area_save_requested(self, message: VimTextArea.SaveRequested) -> None:
|
|
90
|
+
self.notify("Saved (dummy -- no real file I/O here)")
|
|
91
|
+
|
|
92
|
+
def on_vim_text_area_quit_requested(self, message: VimTextArea.QuitRequested) -> None:
|
|
93
|
+
self.exit()
|
|
94
|
+
|
|
95
|
+
def on_vim_text_area_command_entered(self, message: VimTextArea.CommandEntered) -> None:
|
|
96
|
+
self.notify(f"Unhandled command: :{message.command}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
DummyVimApp().run()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dummy_app_plus.py
|
|
3
|
+
|
|
4
|
+
Standalone playground for VimTextAreaPlus -- the variant for apps built
|
|
5
|
+
on textual-textarea's TextEditor/TextAreaPlus rather
|
|
6
|
+
than plain Textual TextArea. Requires the 'textarea-plus' extra:
|
|
7
|
+
|
|
8
|
+
pip install textual-vim-textarea[textarea-plus]
|
|
9
|
+
python3 dummy_app_plus.py
|
|
10
|
+
|
|
11
|
+
Uses vim_text_editor.VimTextEditor (in this same examples/ folder) --
|
|
12
|
+
a TextEditor subclass that swaps in VimTextAreaPlus. That's the exact
|
|
13
|
+
pattern to copy into a real app's own CodeEditor-equivalent class;
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from textual.app import App, ComposeResult
|
|
17
|
+
from textual.widgets import Footer, Header, Static
|
|
18
|
+
|
|
19
|
+
from textual_vim_textarea.textarea_plus import VimTextAreaPlus
|
|
20
|
+
from vim_text_editor import VimTextEditor
|
|
21
|
+
|
|
22
|
+
SAMPLE_TEXT = """\
|
|
23
|
+
select
|
|
24
|
+
customer_id,
|
|
25
|
+
sum(amount) as total_amount
|
|
26
|
+
from orders
|
|
27
|
+
where status = 'completed'
|
|
28
|
+
group by customer_id
|
|
29
|
+
order by total_amount desc
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StatusBar(Static):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DummyVimPlusApp(App):
|
|
38
|
+
CSS = """
|
|
39
|
+
Screen {
|
|
40
|
+
background: #0d1117;
|
|
41
|
+
}
|
|
42
|
+
StatusBar {
|
|
43
|
+
dock: bottom;
|
|
44
|
+
height: 1;
|
|
45
|
+
background: #161b22;
|
|
46
|
+
color: #58a6ff;
|
|
47
|
+
padding: 0 1;
|
|
48
|
+
}
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def compose(self) -> ComposeResult:
|
|
52
|
+
yield Header()
|
|
53
|
+
self.editor = VimTextEditor(language="sql", text=SAMPLE_TEXT)
|
|
54
|
+
yield self.editor
|
|
55
|
+
yield StatusBar("NORMAL", id="status")
|
|
56
|
+
yield Footer()
|
|
57
|
+
|
|
58
|
+
def on_mount(self) -> None:
|
|
59
|
+
self.editor.text_input.focus()
|
|
60
|
+
self._refresh_status()
|
|
61
|
+
|
|
62
|
+
def _refresh_status(self) -> None:
|
|
63
|
+
self.query_one("#status", StatusBar).update(self.editor.text_input.status_text)
|
|
64
|
+
|
|
65
|
+
def on_vim_text_area_plus_mode_changed(
|
|
66
|
+
self, message: VimTextAreaPlus.ModeChanged
|
|
67
|
+
) -> None:
|
|
68
|
+
self._refresh_status()
|
|
69
|
+
|
|
70
|
+
def on_vim_text_area_plus_status_changed(
|
|
71
|
+
self, message: VimTextAreaPlus.StatusChanged
|
|
72
|
+
) -> None:
|
|
73
|
+
self.query_one("#status", StatusBar).update(message.status)
|
|
74
|
+
|
|
75
|
+
def on_vim_text_area_plus_quit_requested(
|
|
76
|
+
self, message: VimTextAreaPlus.QuitRequested
|
|
77
|
+
) -> None:
|
|
78
|
+
self.exit()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
DummyVimPlusApp().run()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
vim_text_editor.py
|
|
3
|
+
|
|
4
|
+
A drop-in TextEditor subclass that substitutes VimTextAreaPlus for
|
|
5
|
+
TextAreaPlus.
|
|
6
|
+
Kept as its own small file/class so it can be tested against a *real* TextEditor container in
|
|
7
|
+
isolation first, the same "build it standalone, test it, then integrate"
|
|
8
|
+
approach used for the original vim_textarea.py.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from textual.app import ComposeResult
|
|
14
|
+
from textual.widgets import Label
|
|
15
|
+
from textual_textarea import TextEditor
|
|
16
|
+
from textual_textarea.autocomplete import CompletionList
|
|
17
|
+
from textual_textarea.containers import FooterContainer, TextContainer
|
|
18
|
+
|
|
19
|
+
from textual_vim_textarea.textarea_plus import VimTextAreaPlus
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VimTextEditor(TextEditor):
|
|
23
|
+
"""Identical to textual_textarea.TextEditor, except `compose()`
|
|
24
|
+
mounts VimTextAreaPlus instead of the stock TextAreaPlus as
|
|
25
|
+
`self.text_input`. Every other TextEditor feature (save/load/find/
|
|
26
|
+
goto-line footer inputs, autocomplete dropdown wiring, theming) is
|
|
27
|
+
inherited completely unchanged, since none of it depends on the
|
|
28
|
+
concrete class of `text_input` -- it only calls TextArea-level
|
|
29
|
+
methods on it.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def compose(self) -> ComposeResult:
|
|
33
|
+
self.text_container = TextContainer()
|
|
34
|
+
self.text_input = VimTextAreaPlus(
|
|
35
|
+
language=self._language, text=self._initial_text, read_only=self.read_only
|
|
36
|
+
)
|
|
37
|
+
self.completion_list = CompletionList()
|
|
38
|
+
self.footer = FooterContainer(classes="hide")
|
|
39
|
+
self.footer_label = Label("", id="textarea__save_open_input_label")
|
|
40
|
+
with self.text_container:
|
|
41
|
+
yield self.text_input
|
|
42
|
+
yield self.completion_list
|
|
43
|
+
with self.footer:
|
|
44
|
+
yield self.footer_label
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "textual-vim-textarea"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "A Vim-style textarea widget for Textual"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Dking08", email = "dastageer_foss@yahoo.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["textual", "vim", "tui", "textarea", "editor"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Framework :: AsyncIO",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
"Topic :: Terminals",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"textual>=6.4.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
# Only needed for textual_vim_textarea.textarea_plus.VimTextAreaPlus --
|
|
30
|
+
# vim mode for apps built on textual-textarea's TextEditor/TextAreaPlus
|
|
31
|
+
# plain Textual TextArea.
|
|
32
|
+
textarea-plus = [
|
|
33
|
+
"textual-textarea>=0.17,<0.18",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/Dking08/textual-vim-textarea"
|
|
38
|
+
Repository = "https://github.com/Dking08/textual-vim-textarea"
|
|
39
|
+
Issues = "https://github.com/Dking08/textual-vim-textarea/issues"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"pytest>=9.1.1",
|
|
44
|
+
"pytest-asyncio>=1.4.0",
|
|
45
|
+
"textual-textarea>=0.17,<0.18",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/textual_vim_textarea"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .vim_textarea import VimTextArea, Mode
|
|
2
|
+
|
|
3
|
+
__all__ = ["VimTextArea", "Mode"]
|
|
4
|
+
|
|
5
|
+
# VimTextAreaPlus is intentionally NOT imported here. It requires
|
|
6
|
+
# textual-textarea (the 'textarea-plus' extra), and importing it
|
|
7
|
+
# unconditionally would force that dependency on every install of this
|
|
8
|
+
# package, even for people only using plain VimTextArea. Import it
|
|
9
|
+
# explicitly if you need it:
|
|
10
|
+
#
|
|
11
|
+
# from textual_vim_textarea.textarea_plus import VimTextAreaPlus
|