suplemon-editor 0.3.2__tar.gz → 0.3.4__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.
- {suplemon_editor-0.3.2/suplemon_editor.egg-info → suplemon_editor-0.3.4}/PKG-INFO +49 -8
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/README.md +48 -7
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/editor.py +43 -6
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/main.py +31 -5
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/ui.py +98 -1
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4/suplemon_editor.egg-info}/PKG-INFO +49 -8
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/LICENSE +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/MANIFEST.in +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/setup.cfg +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/setup.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/__init__.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/__main__.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/cli.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/config/defaults.json +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/config/keymap.json +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/config.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/cursor.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/file.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/help.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/helpers.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/hex2xterm.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/key_mappings.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/lexer.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/line.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/__init__.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/color_map.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/css.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/diff.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/html.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/js.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/json.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/lua.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/md.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/php.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/py.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/logger.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/module_loader.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/application_state.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/autocomplete.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/autodocstring.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/battery.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/bulk_delete.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/clock.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/comment.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/config.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/date.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/diff.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/eval.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/hostname.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/input_test.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/keymap.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/linter.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/lower.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/lstrip.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/paste.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/reload.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/replace_all.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/reverse.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/rstrip.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/save.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/save_all.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/sort_lines.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/strip.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/system_clipboard.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/tabstospaces.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/toggle_whitespace.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/upper.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/prompt.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/suplemon_module.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/themes/8colors.tmTheme +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/themes/monokai.tmTheme +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/themes.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/viewer.py +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon_editor.egg-info/SOURCES.txt +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon_editor.egg-info/dependency_links.txt +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon_editor.egg-info/entry_points.txt +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon_editor.egg-info/requires.txt +0 -0
- {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon_editor.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: suplemon-editor
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Console text editor with multi cursor support. Maintained fork of Suplemon.
|
|
5
5
|
Home-page: https://github.com/leancode/suplemon/
|
|
6
6
|
Author: Richard Lewis
|
|
@@ -67,10 +67,11 @@ https://github.com/leancode/suplemon
|
|
|
67
67
|
This is an actively maintained fork of [richrd/suplemon](https://github.com/richrd/suplemon).
|
|
68
68
|
|
|
69
69
|
The original is a genuinely good editor and none of the design here is ours.
|
|
70
|
-
Upstream simply stopped
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
Upstream simply stopped. The last change of any substance to `master` was on
|
|
71
|
+
11 December 2019; the only two commits since were README image URL edits in
|
|
72
|
+
January 2021. The `dev` branch last moved in June 2020. 25 issues and 5 pull
|
|
73
|
+
requests are still open, some since 2016. Suplemon had also stopped running
|
|
74
|
+
altogether on Python 3.12 and newer, which is what prompted the fork.
|
|
74
75
|
|
|
75
76
|
Since then this fork has:
|
|
76
77
|
|
|
@@ -196,13 +197,31 @@ That works because the launcher hard-codes an absolute path to the source
|
|
|
196
197
|
tree. `$HOME` is root's under sudo, so a launcher written in terms of
|
|
197
198
|
`$HOME` would look for the interpreter in the wrong place.
|
|
198
199
|
|
|
199
|
-
If you do this often,
|
|
200
|
-
`
|
|
200
|
+
If you do this often, link the launcher somewhere sudo already looks.
|
|
201
|
+
`/usr/local/bin` is on `secure_path` on every system I know of, so this makes
|
|
202
|
+
`sudo suplemon` work by name:
|
|
203
|
+
|
|
204
|
+
sudo ln -sf ~/.local/bin/suplemon /usr/local/bin/suplemon && sudo ln -sf ~/.local/bin/suplemon /usr/local/bin/se
|
|
205
|
+
|
|
206
|
+
`install.sh` prints this command for you, and skips it if something that is
|
|
207
|
+
not ours already sits at either name — a stream editor called `se` exists, and
|
|
208
|
+
silently replacing it would be rude.
|
|
209
|
+
|
|
210
|
+
Links rather than copies, so re-running `install.sh` keeps them current; a
|
|
211
|
+
copy would quietly go stale. The launcher holds an absolute path to your own
|
|
212
|
+
source tree, so these run your checkout whoever invokes them: fine on a
|
|
213
|
+
single-user machine, probably not on a shared one. `uninstall.sh` will not
|
|
214
|
+
remove them, since it only touches `~/.local`, but it does tell you they are
|
|
215
|
+
there.
|
|
216
|
+
|
|
217
|
+
The alternative is `sudoedit`, which copies the file to a temporary location,
|
|
201
218
|
opens it as you rather than as root, and copies it back:
|
|
202
219
|
|
|
203
220
|
SUDO_EDITOR=~/.local/bin/suplemon sudoedit /etc/hosts
|
|
204
221
|
|
|
205
|
-
`sudoedit` is the
|
|
222
|
+
`sudoedit` is the safest of the three: the editor never runs as root at all,
|
|
223
|
+
so nothing it loads — modules, your config, a theme — is running with
|
|
224
|
+
privileges.
|
|
206
225
|
|
|
207
226
|
|
|
208
227
|
### Notes
|
|
@@ -535,3 +554,25 @@ have a too steep learning curve. For them (like for me) nano was the weapon of
|
|
|
535
554
|
choice. But nano feels clunky and it has its limitations. That's why
|
|
536
555
|
I wrote my own editor with built in multi cursor support to fix the situation.
|
|
537
556
|
Another reason is that developing Suplemon is simply fun to do.
|
|
557
|
+
|
|
558
|
+
### Why this fork exists
|
|
559
|
+
|
|
560
|
+
*— leancode*
|
|
561
|
+
|
|
562
|
+
I had a shared FreeBSD account with nano on it and nothing else. I normally
|
|
563
|
+
use [ne](https://ne.di.unimi.it/), but there was no binary I could install
|
|
564
|
+
without a compiler or a package manager, and a shared account gives you
|
|
565
|
+
neither. What it did have was Python 3.
|
|
566
|
+
|
|
567
|
+
That turns out to be the interesting thing about Suplemon. Most of the good
|
|
568
|
+
terminal editors — kakoune, helix, micro — ship as compiled binaries, so on
|
|
569
|
+
a locked down box you are back to whatever the host chose to install for
|
|
570
|
+
you. Anywhere Python 3 runs, `pip install suplemon-editor` gets you a real
|
|
571
|
+
editor with multiple cursors, and that covers a lot of machines where you
|
|
572
|
+
are not allowed to build anything.
|
|
573
|
+
|
|
574
|
+
I liked it enough to keep it working. It had stopped running on Python 3.12
|
|
575
|
+
when `imp` was removed, and the upstream repository had been quiet since
|
|
576
|
+
2019, so this fork picks it up rather than switching to something else.
|
|
577
|
+
Needing a feature and being able to write it, or hitting a bug and being
|
|
578
|
+
able to fix it that afternoon, is the rest of the appeal.
|
|
@@ -26,10 +26,11 @@ https://github.com/leancode/suplemon
|
|
|
26
26
|
This is an actively maintained fork of [richrd/suplemon](https://github.com/richrd/suplemon).
|
|
27
27
|
|
|
28
28
|
The original is a genuinely good editor and none of the design here is ours.
|
|
29
|
-
Upstream simply stopped
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
Upstream simply stopped. The last change of any substance to `master` was on
|
|
30
|
+
11 December 2019; the only two commits since were README image URL edits in
|
|
31
|
+
January 2021. The `dev` branch last moved in June 2020. 25 issues and 5 pull
|
|
32
|
+
requests are still open, some since 2016. Suplemon had also stopped running
|
|
33
|
+
altogether on Python 3.12 and newer, which is what prompted the fork.
|
|
33
34
|
|
|
34
35
|
Since then this fork has:
|
|
35
36
|
|
|
@@ -155,13 +156,31 @@ That works because the launcher hard-codes an absolute path to the source
|
|
|
155
156
|
tree. `$HOME` is root's under sudo, so a launcher written in terms of
|
|
156
157
|
`$HOME` would look for the interpreter in the wrong place.
|
|
157
158
|
|
|
158
|
-
If you do this often,
|
|
159
|
-
`
|
|
159
|
+
If you do this often, link the launcher somewhere sudo already looks.
|
|
160
|
+
`/usr/local/bin` is on `secure_path` on every system I know of, so this makes
|
|
161
|
+
`sudo suplemon` work by name:
|
|
162
|
+
|
|
163
|
+
sudo ln -sf ~/.local/bin/suplemon /usr/local/bin/suplemon && sudo ln -sf ~/.local/bin/suplemon /usr/local/bin/se
|
|
164
|
+
|
|
165
|
+
`install.sh` prints this command for you, and skips it if something that is
|
|
166
|
+
not ours already sits at either name — a stream editor called `se` exists, and
|
|
167
|
+
silently replacing it would be rude.
|
|
168
|
+
|
|
169
|
+
Links rather than copies, so re-running `install.sh` keeps them current; a
|
|
170
|
+
copy would quietly go stale. The launcher holds an absolute path to your own
|
|
171
|
+
source tree, so these run your checkout whoever invokes them: fine on a
|
|
172
|
+
single-user machine, probably not on a shared one. `uninstall.sh` will not
|
|
173
|
+
remove them, since it only touches `~/.local`, but it does tell you they are
|
|
174
|
+
there.
|
|
175
|
+
|
|
176
|
+
The alternative is `sudoedit`, which copies the file to a temporary location,
|
|
160
177
|
opens it as you rather than as root, and copies it back:
|
|
161
178
|
|
|
162
179
|
SUDO_EDITOR=~/.local/bin/suplemon sudoedit /etc/hosts
|
|
163
180
|
|
|
164
|
-
`sudoedit` is the
|
|
181
|
+
`sudoedit` is the safest of the three: the editor never runs as root at all,
|
|
182
|
+
so nothing it loads — modules, your config, a theme — is running with
|
|
183
|
+
privileges.
|
|
165
184
|
|
|
166
185
|
|
|
167
186
|
### Notes
|
|
@@ -494,3 +513,25 @@ have a too steep learning curve. For them (like for me) nano was the weapon of
|
|
|
494
513
|
choice. But nano feels clunky and it has its limitations. That's why
|
|
495
514
|
I wrote my own editor with built in multi cursor support to fix the situation.
|
|
496
515
|
Another reason is that developing Suplemon is simply fun to do.
|
|
516
|
+
|
|
517
|
+
### Why this fork exists
|
|
518
|
+
|
|
519
|
+
*— leancode*
|
|
520
|
+
|
|
521
|
+
I had a shared FreeBSD account with nano on it and nothing else. I normally
|
|
522
|
+
use [ne](https://ne.di.unimi.it/), but there was no binary I could install
|
|
523
|
+
without a compiler or a package manager, and a shared account gives you
|
|
524
|
+
neither. What it did have was Python 3.
|
|
525
|
+
|
|
526
|
+
That turns out to be the interesting thing about Suplemon. Most of the good
|
|
527
|
+
terminal editors — kakoune, helix, micro — ship as compiled binaries, so on
|
|
528
|
+
a locked down box you are back to whatever the host chose to install for
|
|
529
|
+
you. Anywhere Python 3 runs, `pip install suplemon-editor` gets you a real
|
|
530
|
+
editor with multiple cursors, and that covers a lot of machines where you
|
|
531
|
+
are not allowed to build anything.
|
|
532
|
+
|
|
533
|
+
I liked it enough to keep it working. It had stopped running on Python 3.12
|
|
534
|
+
when `imp` was removed, and the upstream repository had been quiet since
|
|
535
|
+
2019, so this fork picks it up rather than switching to something else.
|
|
536
|
+
Needing a feature and being able to write it, or hitting a bug and being
|
|
537
|
+
able to fix it that afternoon, is the rest of the appeal.
|
|
@@ -56,6 +56,9 @@ class Editor(Viewer):
|
|
|
56
56
|
self.current_state = 0
|
|
57
57
|
# Last editor action that was used (for undo/redo)
|
|
58
58
|
self.last_action = None
|
|
59
|
+
# True while a bracketed paste is being inserted. Auto indent
|
|
60
|
+
# and per-keystroke undo states are both wrong during one.
|
|
61
|
+
self.pasting = False
|
|
59
62
|
|
|
60
63
|
def init(self):
|
|
61
64
|
Viewer.init(self)
|
|
@@ -109,14 +112,22 @@ class Editor(Viewer):
|
|
|
109
112
|
|
|
110
113
|
def store_action_state(self, action, state=None):
|
|
111
114
|
"""Store the editor state if a new action is taken."""
|
|
115
|
+
if self.pasting:
|
|
116
|
+
# The whole paste is one action. Its state was stored before
|
|
117
|
+
# insertion began, so undo puts back what was there before.
|
|
118
|
+
return
|
|
112
119
|
if self.last_action != action:
|
|
113
120
|
self.last_action = action
|
|
114
121
|
self.store_state(state)
|
|
115
|
-
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
elif self.current_state > 0:
|
|
123
|
+
# Same action continuing, so fold it into the state this run
|
|
124
|
+
# already created rather than making a new one. The guard used
|
|
125
|
+
# to be current_state < len(history)-1, which is never true
|
|
126
|
+
# while editing forwards: store_state leaves current_state at
|
|
127
|
+
# the end of the history. So a run was only ever recorded as
|
|
128
|
+
# its first keystroke, and the next different action left an
|
|
129
|
+
# undo point holding one character of what the user had typed.
|
|
130
|
+
self.history[self.current_state].store(self)
|
|
120
131
|
|
|
121
132
|
def store_state(self, state=None, action=None):
|
|
122
133
|
"""Store the current editor state for undo/redo."""
|
|
@@ -364,7 +375,7 @@ class Editor(Viewer):
|
|
|
364
375
|
# Leave the beginning of the line
|
|
365
376
|
self.lines[cursor.y].set_data(start)
|
|
366
377
|
wspace = ""
|
|
367
|
-
if self.config["auto_indent_newline"]:
|
|
378
|
+
if self.config["auto_indent_newline"] and not self.pasting:
|
|
368
379
|
wspace = helpers.leading_whitespace(self.lines[cursor.y])
|
|
369
380
|
self.lines.insert(cursor.y+1, Line(wspace+end))
|
|
370
381
|
self.move_y_cursors(cursor.y, 1)
|
|
@@ -531,6 +542,32 @@ class Editor(Viewer):
|
|
|
531
542
|
# Add a restore point if previous action != type
|
|
532
543
|
self.store_action_state("type")
|
|
533
544
|
|
|
545
|
+
def insert_text(self, text):
|
|
546
|
+
"""Insert pasted text literally, without auto indent.
|
|
547
|
+
|
|
548
|
+
A paste arrives as ordinary keystrokes, so auto indent fires on
|
|
549
|
+
every newline and prepends the previous line's whitespace to a line
|
|
550
|
+
that already carries its own. Over a block of text that compounds
|
|
551
|
+
line by line into a staircase. The terminal tells us where a paste
|
|
552
|
+
starts and ends, so suppress it for the duration.
|
|
553
|
+
|
|
554
|
+
:param str text: Text to insert at the cursor(s).
|
|
555
|
+
"""
|
|
556
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
557
|
+
self.pasting = True
|
|
558
|
+
try:
|
|
559
|
+
for i, line in enumerate(text.split("\n")):
|
|
560
|
+
if i:
|
|
561
|
+
self.enter()
|
|
562
|
+
if line:
|
|
563
|
+
self.type(line)
|
|
564
|
+
finally:
|
|
565
|
+
self.pasting = False
|
|
566
|
+
# Stored once, after the insertion, the same way type() and enter()
|
|
567
|
+
# do it. The flag kept the per-keystroke stores from piling up, so
|
|
568
|
+
# one undo steps over the whole paste rather than part of it.
|
|
569
|
+
self.store_action_state("paste")
|
|
570
|
+
|
|
534
571
|
def type_at_cursor(self, cursor, data):
|
|
535
572
|
"""Insert data at specified cursor."""
|
|
536
573
|
line = self.lines[cursor.y]
|
|
@@ -6,6 +6,7 @@ The main class that starts and runs Suplemon.
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
|
+
import curses
|
|
9
10
|
import sys
|
|
10
11
|
|
|
11
12
|
from . import ui
|
|
@@ -18,7 +19,13 @@ from .logger import logger
|
|
|
18
19
|
from .config import Config
|
|
19
20
|
from .editor import Editor
|
|
20
21
|
|
|
21
|
-
__version__ = "0.3.
|
|
22
|
+
__version__ = "0.3.4"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ncurses only defines BUTTON5 when built with NCURSES_MOUSE_VERSION 2.
|
|
26
|
+
# Without it there is no wheel-down event to match, so scrolling down is
|
|
27
|
+
# simply unavailable rather than mismatched.
|
|
28
|
+
BUTTON5_PRESSED = getattr(curses, "BUTTON5_PRESSED", 0)
|
|
22
29
|
|
|
23
30
|
|
|
24
31
|
class App:
|
|
@@ -307,8 +314,22 @@ class App:
|
|
|
307
314
|
return self.handle_key(event)
|
|
308
315
|
elif event.type == "mouse":
|
|
309
316
|
return self.handle_mouse(event)
|
|
317
|
+
elif event.type == "paste":
|
|
318
|
+
return self.handle_paste(event)
|
|
310
319
|
return False
|
|
311
320
|
|
|
321
|
+
def handle_paste(self, event):
|
|
322
|
+
"""Insert text the terminal reported as a paste.
|
|
323
|
+
|
|
324
|
+
:param event: Event instance carrying the text in .data.
|
|
325
|
+
:return: Boolean indicating if the event was handled.
|
|
326
|
+
:rtype: boolean
|
|
327
|
+
"""
|
|
328
|
+
if not event.data:
|
|
329
|
+
return False
|
|
330
|
+
self.get_editor().insert_text(event.data)
|
|
331
|
+
return True
|
|
332
|
+
|
|
312
333
|
def handle_key(self, event):
|
|
313
334
|
"""Handle a key input event.
|
|
314
335
|
|
|
@@ -340,13 +361,18 @@ class App:
|
|
|
340
361
|
:rtype: boolean
|
|
341
362
|
"""
|
|
342
363
|
editor = self.get_editor()
|
|
343
|
-
|
|
364
|
+
code = event.mouse_code
|
|
365
|
+
# These were hardcoded integers matching NCURSES_MOUSE_VERSION 1,
|
|
366
|
+
# which gave each button six bits. Version 2 packs them into five to
|
|
367
|
+
# make room for button 5, so every value except BUTTON1_RELEASED
|
|
368
|
+
# moved and only left click still worked. Ask curses instead.
|
|
369
|
+
if code & curses.BUTTON1_RELEASED:
|
|
344
370
|
editor.set_single_cursor(event.mouse_pos)
|
|
345
|
-
elif
|
|
371
|
+
elif code & curses.BUTTON3_RELEASED:
|
|
346
372
|
editor.add_cursor(event.mouse_pos)
|
|
347
|
-
elif
|
|
373
|
+
elif code & curses.BUTTON4_PRESSED: # Wheel up
|
|
348
374
|
editor.jump_up()
|
|
349
|
-
elif
|
|
375
|
+
elif BUTTON5_PRESSED and code & BUTTON5_PRESSED:
|
|
350
376
|
editor.jump_down()
|
|
351
377
|
else:
|
|
352
378
|
return False
|
|
@@ -4,6 +4,7 @@ Curses user interface.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
import sys
|
|
7
8
|
import logging
|
|
8
9
|
from wcwidth import wcswidth
|
|
9
10
|
|
|
@@ -15,6 +16,13 @@ from .key_mappings import key_map
|
|
|
15
16
|
curses = None
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
# What the terminal wraps a paste in when bracketed paste mode is on.
|
|
20
|
+
# curses has no terminfo entry for these, so they arrive as plain
|
|
21
|
+
# characters and have to be recognised here.
|
|
22
|
+
PASTE_START = "\x1b[200~"
|
|
23
|
+
PASTE_END = "\x1b[201~"
|
|
24
|
+
|
|
25
|
+
|
|
18
26
|
class InputEvent:
|
|
19
27
|
"""Represents a keyboard or mouse event."""
|
|
20
28
|
def __init__(self):
|
|
@@ -24,6 +32,7 @@ class InputEvent:
|
|
|
24
32
|
self.is_typeable = False
|
|
25
33
|
self.curses_key_name = None
|
|
26
34
|
self.mouse_code = None
|
|
35
|
+
self.data = None # Pasted text, for events of type 'paste'
|
|
27
36
|
self.mouse_pos = (0, 0)
|
|
28
37
|
self.logger = logging.getLogger("{0}.InputEvent".format(__name__))
|
|
29
38
|
|
|
@@ -39,6 +48,12 @@ class InputEvent:
|
|
|
39
48
|
self.type = "key"
|
|
40
49
|
self.key_name = name
|
|
41
50
|
|
|
51
|
+
def set_paste_data(self, data):
|
|
52
|
+
"""Turn this into a paste event carrying the pasted text."""
|
|
53
|
+
self.type = "paste"
|
|
54
|
+
self.data = data
|
|
55
|
+
return self
|
|
56
|
+
|
|
42
57
|
def parse_mouse_state(self, state):
|
|
43
58
|
"""Parse curses mouse events."""
|
|
44
59
|
self.type = "mouse"
|
|
@@ -166,15 +181,35 @@ class UI:
|
|
|
166
181
|
|
|
167
182
|
self.current_yx = self.screen.getmaxyx() # For checking resize
|
|
168
183
|
self.setup_mouse()
|
|
184
|
+
self.setup_bracketed_paste(True)
|
|
169
185
|
self.setup_windows()
|
|
170
186
|
|
|
187
|
+
def setup_bracketed_paste(self, enable):
|
|
188
|
+
"""Turn the terminal's bracketed paste mode on or off.
|
|
189
|
+
|
|
190
|
+
With it on, a paste arrives wrapped in ESC[200~ and ESC[201~ so it
|
|
191
|
+
can be told apart from typing. Terminals that don't support it
|
|
192
|
+
ignore the sequence and nothing changes.
|
|
193
|
+
|
|
194
|
+
:param bool enable: True to turn it on, False to turn it off.
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
sys.stdout.write("\033[?2004h" if enable else "\033[?2004l")
|
|
198
|
+
sys.stdout.flush()
|
|
199
|
+
except (OSError, ValueError):
|
|
200
|
+
# Not fatal: without it, pastes are simply indistinguishable
|
|
201
|
+
# from typing, which is how it behaved before.
|
|
202
|
+
self.logger.debug("Could not set bracketed paste mode.")
|
|
203
|
+
|
|
171
204
|
def unload(self):
|
|
172
205
|
"""Unload curses."""
|
|
206
|
+
# Leave the terminal as we found it. Without this the shell keeps
|
|
207
|
+
# receiving paste markers after suplemon exits.
|
|
208
|
+
self.setup_bracketed_paste(False)
|
|
173
209
|
# Don't call curses.endwin() here. self.run() drives the app via
|
|
174
210
|
# curses.wrapper(), which always calls endwin() itself when it
|
|
175
211
|
# returns. Ending the screen twice makes modern ncurses return ERR,
|
|
176
212
|
# which Python raises as _curses.error on exit.
|
|
177
|
-
pass
|
|
178
213
|
|
|
179
214
|
def setup_mouse(self):
|
|
180
215
|
# Mouse support
|
|
@@ -623,11 +658,73 @@ class UI:
|
|
|
623
658
|
if state:
|
|
624
659
|
event.parse_mouse_state(state)
|
|
625
660
|
return event
|
|
661
|
+
elif char == "\x1b" and self.match_sequence(PASTE_START[1:]):
|
|
662
|
+
return event.set_paste_data(self.read_paste())
|
|
626
663
|
else:
|
|
627
664
|
event.parse_key_code(char)
|
|
628
665
|
return event
|
|
629
666
|
return False
|
|
630
667
|
|
|
668
|
+
def match_sequence(self, rest):
|
|
669
|
+
"""Check whether the characters after ESC are `rest`.
|
|
670
|
+
|
|
671
|
+
Reads without blocking and pushes back anything that does not
|
|
672
|
+
match, so an ESC that starts something else, or a lone ESC key
|
|
673
|
+
press, is handed on untouched.
|
|
674
|
+
|
|
675
|
+
:param str rest: Expected characters following the escape.
|
|
676
|
+
:return: True if they were consumed, False if pushed back.
|
|
677
|
+
:rtype: boolean
|
|
678
|
+
"""
|
|
679
|
+
seen = ""
|
|
680
|
+
self.screen.nodelay(1)
|
|
681
|
+
try:
|
|
682
|
+
for expected in rest:
|
|
683
|
+
try:
|
|
684
|
+
c = self.screen.get_wch()
|
|
685
|
+
except (curses.error, KeyboardInterrupt):
|
|
686
|
+
c = None
|
|
687
|
+
if c != expected:
|
|
688
|
+
if c is not None:
|
|
689
|
+
seen += str(c)
|
|
690
|
+
for ch in reversed(seen):
|
|
691
|
+
try:
|
|
692
|
+
curses.unget_wch(ch)
|
|
693
|
+
except (curses.error, TypeError):
|
|
694
|
+
pass
|
|
695
|
+
return False
|
|
696
|
+
seen += str(c)
|
|
697
|
+
finally:
|
|
698
|
+
self.screen.nodelay(0)
|
|
699
|
+
return True
|
|
700
|
+
|
|
701
|
+
def read_paste(self):
|
|
702
|
+
"""Read a paste up to the closing marker and return its text.
|
|
703
|
+
|
|
704
|
+
:return: The pasted text, without the markers.
|
|
705
|
+
:rtype: str
|
|
706
|
+
"""
|
|
707
|
+
data = []
|
|
708
|
+
while True:
|
|
709
|
+
try:
|
|
710
|
+
c = self.screen.get_wch()
|
|
711
|
+
except KeyboardInterrupt:
|
|
712
|
+
break
|
|
713
|
+
except curses.error:
|
|
714
|
+
break
|
|
715
|
+
if c == "\x1b":
|
|
716
|
+
if self.match_sequence(PASTE_END[1:]):
|
|
717
|
+
break
|
|
718
|
+
# An ESC inside the payload is content, not a marker.
|
|
719
|
+
data.append(c)
|
|
720
|
+
continue
|
|
721
|
+
if isinstance(c, int):
|
|
722
|
+
# A translated key code cannot appear in pasted text;
|
|
723
|
+
# ignore rather than corrupt the payload with a number.
|
|
724
|
+
continue
|
|
725
|
+
data.append(c)
|
|
726
|
+
return "".join(data)
|
|
727
|
+
|
|
631
728
|
def is_mouse(self, key):
|
|
632
729
|
"""Check for mouse events"""
|
|
633
730
|
return key == curses.KEY_MOUSE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: suplemon-editor
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Console text editor with multi cursor support. Maintained fork of Suplemon.
|
|
5
5
|
Home-page: https://github.com/leancode/suplemon/
|
|
6
6
|
Author: Richard Lewis
|
|
@@ -67,10 +67,11 @@ https://github.com/leancode/suplemon
|
|
|
67
67
|
This is an actively maintained fork of [richrd/suplemon](https://github.com/richrd/suplemon).
|
|
68
68
|
|
|
69
69
|
The original is a genuinely good editor and none of the design here is ours.
|
|
70
|
-
Upstream simply stopped
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
Upstream simply stopped. The last change of any substance to `master` was on
|
|
71
|
+
11 December 2019; the only two commits since were README image URL edits in
|
|
72
|
+
January 2021. The `dev` branch last moved in June 2020. 25 issues and 5 pull
|
|
73
|
+
requests are still open, some since 2016. Suplemon had also stopped running
|
|
74
|
+
altogether on Python 3.12 and newer, which is what prompted the fork.
|
|
74
75
|
|
|
75
76
|
Since then this fork has:
|
|
76
77
|
|
|
@@ -196,13 +197,31 @@ That works because the launcher hard-codes an absolute path to the source
|
|
|
196
197
|
tree. `$HOME` is root's under sudo, so a launcher written in terms of
|
|
197
198
|
`$HOME` would look for the interpreter in the wrong place.
|
|
198
199
|
|
|
199
|
-
If you do this often,
|
|
200
|
-
`
|
|
200
|
+
If you do this often, link the launcher somewhere sudo already looks.
|
|
201
|
+
`/usr/local/bin` is on `secure_path` on every system I know of, so this makes
|
|
202
|
+
`sudo suplemon` work by name:
|
|
203
|
+
|
|
204
|
+
sudo ln -sf ~/.local/bin/suplemon /usr/local/bin/suplemon && sudo ln -sf ~/.local/bin/suplemon /usr/local/bin/se
|
|
205
|
+
|
|
206
|
+
`install.sh` prints this command for you, and skips it if something that is
|
|
207
|
+
not ours already sits at either name — a stream editor called `se` exists, and
|
|
208
|
+
silently replacing it would be rude.
|
|
209
|
+
|
|
210
|
+
Links rather than copies, so re-running `install.sh` keeps them current; a
|
|
211
|
+
copy would quietly go stale. The launcher holds an absolute path to your own
|
|
212
|
+
source tree, so these run your checkout whoever invokes them: fine on a
|
|
213
|
+
single-user machine, probably not on a shared one. `uninstall.sh` will not
|
|
214
|
+
remove them, since it only touches `~/.local`, but it does tell you they are
|
|
215
|
+
there.
|
|
216
|
+
|
|
217
|
+
The alternative is `sudoedit`, which copies the file to a temporary location,
|
|
201
218
|
opens it as you rather than as root, and copies it back:
|
|
202
219
|
|
|
203
220
|
SUDO_EDITOR=~/.local/bin/suplemon sudoedit /etc/hosts
|
|
204
221
|
|
|
205
|
-
`sudoedit` is the
|
|
222
|
+
`sudoedit` is the safest of the three: the editor never runs as root at all,
|
|
223
|
+
so nothing it loads — modules, your config, a theme — is running with
|
|
224
|
+
privileges.
|
|
206
225
|
|
|
207
226
|
|
|
208
227
|
### Notes
|
|
@@ -535,3 +554,25 @@ have a too steep learning curve. For them (like for me) nano was the weapon of
|
|
|
535
554
|
choice. But nano feels clunky and it has its limitations. That's why
|
|
536
555
|
I wrote my own editor with built in multi cursor support to fix the situation.
|
|
537
556
|
Another reason is that developing Suplemon is simply fun to do.
|
|
557
|
+
|
|
558
|
+
### Why this fork exists
|
|
559
|
+
|
|
560
|
+
*— leancode*
|
|
561
|
+
|
|
562
|
+
I had a shared FreeBSD account with nano on it and nothing else. I normally
|
|
563
|
+
use [ne](https://ne.di.unimi.it/), but there was no binary I could install
|
|
564
|
+
without a compiler or a package manager, and a shared account gives you
|
|
565
|
+
neither. What it did have was Python 3.
|
|
566
|
+
|
|
567
|
+
That turns out to be the interesting thing about Suplemon. Most of the good
|
|
568
|
+
terminal editors — kakoune, helix, micro — ship as compiled binaries, so on
|
|
569
|
+
a locked down box you are back to whatever the host chose to install for
|
|
570
|
+
you. Anywhere Python 3 runs, `pip install suplemon-editor` gets you a real
|
|
571
|
+
editor with multiple cursors, and that covers a lot of machines where you
|
|
572
|
+
are not allowed to build anything.
|
|
573
|
+
|
|
574
|
+
I liked it enough to keep it working. It had stopped running on Python 3.12
|
|
575
|
+
when `imp` was removed, and the upstream repository had been quiet since
|
|
576
|
+
2019, so this fork picks it up rather than switching to something else.
|
|
577
|
+
Needing a feature and being able to write it, or hitting a bug and being
|
|
578
|
+
able to fix it that afternoon, is the rest of the appeal.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon_editor.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|