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.
Files changed (78) hide show
  1. {suplemon_editor-0.3.2/suplemon_editor.egg-info → suplemon_editor-0.3.4}/PKG-INFO +49 -8
  2. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/README.md +48 -7
  3. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/editor.py +43 -6
  4. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/main.py +31 -5
  5. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/ui.py +98 -1
  6. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4/suplemon_editor.egg-info}/PKG-INFO +49 -8
  7. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/LICENSE +0 -0
  8. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/MANIFEST.in +0 -0
  9. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/setup.cfg +0 -0
  10. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/setup.py +0 -0
  11. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/__init__.py +0 -0
  12. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/__main__.py +0 -0
  13. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/cli.py +0 -0
  14. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/config/defaults.json +0 -0
  15. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/config/keymap.json +0 -0
  16. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/config.py +0 -0
  17. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/cursor.py +0 -0
  18. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/file.py +0 -0
  19. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/help.py +0 -0
  20. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/helpers.py +0 -0
  21. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/hex2xterm.py +0 -0
  22. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/key_mappings.py +0 -0
  23. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/lexer.py +0 -0
  24. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/line.py +0 -0
  25. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/__init__.py +0 -0
  26. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/color_map.py +0 -0
  27. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/css.py +0 -0
  28. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/diff.py +0 -0
  29. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/html.py +0 -0
  30. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/js.py +0 -0
  31. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/json.py +0 -0
  32. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/lua.py +0 -0
  33. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/md.py +0 -0
  34. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/php.py +0 -0
  35. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/linelight/py.py +0 -0
  36. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/logger.py +0 -0
  37. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/module_loader.py +0 -0
  38. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/application_state.py +0 -0
  39. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/autocomplete.py +0 -0
  40. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/autodocstring.py +0 -0
  41. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/battery.py +0 -0
  42. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/bulk_delete.py +0 -0
  43. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/clock.py +0 -0
  44. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/comment.py +0 -0
  45. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/config.py +0 -0
  46. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/date.py +0 -0
  47. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/diff.py +0 -0
  48. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/eval.py +0 -0
  49. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/hostname.py +0 -0
  50. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/input_test.py +0 -0
  51. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/keymap.py +0 -0
  52. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/linter.py +0 -0
  53. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/lower.py +0 -0
  54. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/lstrip.py +0 -0
  55. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/paste.py +0 -0
  56. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/reload.py +0 -0
  57. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/replace_all.py +0 -0
  58. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/reverse.py +0 -0
  59. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/rstrip.py +0 -0
  60. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/save.py +0 -0
  61. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/save_all.py +0 -0
  62. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/sort_lines.py +0 -0
  63. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/strip.py +0 -0
  64. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/system_clipboard.py +0 -0
  65. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/tabstospaces.py +0 -0
  66. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/toggle_whitespace.py +0 -0
  67. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/modules/upper.py +0 -0
  68. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/prompt.py +0 -0
  69. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/suplemon_module.py +0 -0
  70. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/themes/8colors.tmTheme +0 -0
  71. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/themes/monokai.tmTheme +0 -0
  72. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/themes.py +0 -0
  73. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon/viewer.py +0 -0
  74. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon_editor.egg-info/SOURCES.txt +0 -0
  75. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon_editor.egg-info/dependency_links.txt +0 -0
  76. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon_editor.egg-info/entry_points.txt +0 -0
  77. {suplemon_editor-0.3.2 → suplemon_editor-0.3.4}/suplemon_editor.egg-info/requires.txt +0 -0
  78. {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.2
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: the last commit to `master` landed in January 2021,
71
- the `dev` branch in June 2020, and 25 issues and 5 pull requests are still
72
- open, some since 2016. Suplemon had also stopped running altogether on
73
- Python 3.12 and newer, which is what prompted the fork.
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, either add `~/.local/bin` to `secure_path` in
200
- `visudo`, or use `sudoedit`, which copies the file to a temporary location,
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 safer of the two: the editor never runs as root.
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: the last commit to `master` landed in January 2021,
30
- the `dev` branch in June 2020, and 25 issues and 5 pull requests are still
31
- open, some since 2016. Suplemon had also stopped running altogether on
32
- Python 3.12 and newer, which is what prompted the fork.
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, either add `~/.local/bin` to `secure_path` in
159
- `visudo`, or use `sudoedit`, which copies the file to a temporary location,
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 safer of the two: the editor never runs as root.
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
- else:
116
- # FIXME: This if is here just for safety.
117
- # FIXME: current_state might be wrong ;.<
118
- if self.current_state < len(self.history)-1:
119
- self.history[self.current_state].store(self)
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.2"
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
- if event.mouse_code == 1: # Left mouse button release
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 event.mouse_code == 4096: # Right mouse button release
371
+ elif code & curses.BUTTON3_RELEASED:
346
372
  editor.add_cursor(event.mouse_pos)
347
- elif event.mouse_code == 524288: # Wheel up
373
+ elif code & curses.BUTTON4_PRESSED: # Wheel up
348
374
  editor.jump_up()
349
- elif event.mouse_code == 134217728: # Wheel down(and unfortunately left button drag)
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.2
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: the last commit to `master` landed in January 2021,
71
- the `dev` branch in June 2020, and 25 issues and 5 pull requests are still
72
- open, some since 2016. Suplemon had also stopped running altogether on
73
- Python 3.12 and newer, which is what prompted the fork.
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, either add `~/.local/bin` to `secure_path` in
200
- `visudo`, or use `sudoedit`, which copies the file to a temporary location,
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 safer of the two: the editor never runs as root.
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