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.
@@ -0,0 +1,3 @@
1
+ __pycache__
2
+ .venv/
3
+ *.egg-info/
@@ -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