suplemon-editor 0.3.0__py3-none-any.whl
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/__init__.py +0 -0
- suplemon/__main__.py +3 -0
- suplemon/cli.py +65 -0
- suplemon/config/defaults.json +153 -0
- suplemon/config/keymap.json +63 -0
- suplemon/config.py +324 -0
- suplemon/cursor.py +135 -0
- suplemon/editor.py +568 -0
- suplemon/file.py +193 -0
- suplemon/help.py +327 -0
- suplemon/helpers.py +104 -0
- suplemon/hex2xterm.py +23 -0
- suplemon/key_mappings.py +239 -0
- suplemon/lexer.py +88 -0
- suplemon/line.py +51 -0
- suplemon/linelight/__init__.py +0 -0
- suplemon/linelight/color_map.py +9 -0
- suplemon/linelight/css.py +21 -0
- suplemon/linelight/diff.py +17 -0
- suplemon/linelight/html.py +19 -0
- suplemon/linelight/js.py +21 -0
- suplemon/linelight/json.py +15 -0
- suplemon/linelight/lua.py +10 -0
- suplemon/linelight/md.py +19 -0
- suplemon/linelight/php.py +27 -0
- suplemon/linelight/py.py +27 -0
- suplemon/logger.py +90 -0
- suplemon/main.py +771 -0
- suplemon/module_loader.py +98 -0
- suplemon/modules/application_state.py +84 -0
- suplemon/modules/autocomplete.py +114 -0
- suplemon/modules/autodocstring.py +142 -0
- suplemon/modules/battery.py +118 -0
- suplemon/modules/bulk_delete.py +75 -0
- suplemon/modules/clock.py +22 -0
- suplemon/modules/comment.py +59 -0
- suplemon/modules/config.py +20 -0
- suplemon/modules/date.py +22 -0
- suplemon/modules/diff.py +42 -0
- suplemon/modules/eval.py +43 -0
- suplemon/modules/hostname.py +34 -0
- suplemon/modules/input_test.py +23 -0
- suplemon/modules/keymap.py +20 -0
- suplemon/modules/linter.py +274 -0
- suplemon/modules/lower.py +21 -0
- suplemon/modules/lstrip.py +20 -0
- suplemon/modules/paste.py +49 -0
- suplemon/modules/reload.py +16 -0
- suplemon/modules/replace_all.py +23 -0
- suplemon/modules/reverse.py +22 -0
- suplemon/modules/rstrip.py +19 -0
- suplemon/modules/save.py +16 -0
- suplemon/modules/save_all.py +19 -0
- suplemon/modules/sort_lines.py +41 -0
- suplemon/modules/strip.py +19 -0
- suplemon/modules/system_clipboard.py +121 -0
- suplemon/modules/tabstospaces.py +18 -0
- suplemon/modules/toggle_whitespace.py +18 -0
- suplemon/modules/upper.py +21 -0
- suplemon/prompt.py +319 -0
- suplemon/suplemon_module.py +210 -0
- suplemon/themes/8colors.tmTheme +179 -0
- suplemon/themes/monokai.tmTheme +284 -0
- suplemon/themes.py +201 -0
- suplemon/ui.py +656 -0
- suplemon/viewer.py +1004 -0
- suplemon_editor-0.3.0.dist-info/METADATA +515 -0
- suplemon_editor-0.3.0.dist-info/RECORD +72 -0
- suplemon_editor-0.3.0.dist-info/WHEEL +5 -0
- suplemon_editor-0.3.0.dist-info/entry_points.txt +3 -0
- suplemon_editor-0.3.0.dist-info/licenses/LICENSE +21 -0
- suplemon_editor-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
from suplemon.suplemon_module import Module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Reverse(Module):
|
|
7
|
+
"""Reverse text on current line(s)."""
|
|
8
|
+
|
|
9
|
+
def run(self, app, editor, args):
|
|
10
|
+
line_nums = []
|
|
11
|
+
for cursor in editor.cursors:
|
|
12
|
+
if cursor.y not in line_nums:
|
|
13
|
+
line_nums.append(cursor.y)
|
|
14
|
+
# Reverse string
|
|
15
|
+
data = editor.lines[cursor.y].data[::-1]
|
|
16
|
+
editor.lines[cursor.y].set_data(data)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
module = {
|
|
20
|
+
"class": Reverse,
|
|
21
|
+
"name": "upper",
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
from suplemon.suplemon_module import Module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RStrip(Module):
|
|
7
|
+
"""Trim whitespace from the end of lines."""
|
|
8
|
+
|
|
9
|
+
def run(self, app, editor, args):
|
|
10
|
+
line_nums = editor.get_lines_with_cursors()
|
|
11
|
+
for n in line_nums:
|
|
12
|
+
line = editor.lines[n]
|
|
13
|
+
line.set_data(line.data.rstrip())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
module = {
|
|
17
|
+
"class": RStrip,
|
|
18
|
+
"name": "rstrip",
|
|
19
|
+
}
|
suplemon/modules/save.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
from suplemon.suplemon_module import Module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Save(Module):
|
|
7
|
+
"""Save the current file."""
|
|
8
|
+
|
|
9
|
+
def run(self, app, editor, args):
|
|
10
|
+
return app.save_file()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
module = {
|
|
14
|
+
"class": Save,
|
|
15
|
+
"name": "save",
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
from suplemon.suplemon_module import Module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SaveAll(Module):
|
|
7
|
+
"""Save all currently open files. Asks for confirmation."""
|
|
8
|
+
|
|
9
|
+
def run(self, app, editor, args):
|
|
10
|
+
if not self.app.ui.query_bool("Save all files?", False):
|
|
11
|
+
return False
|
|
12
|
+
for file in app.get_files():
|
|
13
|
+
file.save()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
module = {
|
|
17
|
+
"class": SaveAll,
|
|
18
|
+
"name": "save_all",
|
|
19
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
from suplemon.suplemon_module import Module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SortLines(Module):
|
|
7
|
+
"""
|
|
8
|
+
Sort current lines.
|
|
9
|
+
|
|
10
|
+
Sorts alphabetically by default.
|
|
11
|
+
Add 'length' to sort by length.
|
|
12
|
+
Add 'reverse' to reverse the sorting.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def sort_normal(self, line):
|
|
16
|
+
return line.data
|
|
17
|
+
|
|
18
|
+
def sort_length(self, line):
|
|
19
|
+
return len(line.data)
|
|
20
|
+
|
|
21
|
+
def run(self, app, editor, args):
|
|
22
|
+
args = args.lower()
|
|
23
|
+
|
|
24
|
+
sorter = self.sort_normal
|
|
25
|
+
reverse = True if "reverse" in args else False
|
|
26
|
+
if "length" in args:
|
|
27
|
+
sorter = self.sort_length
|
|
28
|
+
|
|
29
|
+
indices = editor.get_lines_with_cursors()
|
|
30
|
+
lines = [editor.get_line(i) for i in indices]
|
|
31
|
+
|
|
32
|
+
sorted_lines = sorted(lines, key=sorter, reverse=reverse)
|
|
33
|
+
|
|
34
|
+
for i, line in enumerate(sorted_lines):
|
|
35
|
+
editor.lines[indices[i]] = line
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
module = {
|
|
39
|
+
"class": SortLines,
|
|
40
|
+
"name": "sort_lines",
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
from suplemon.suplemon_module import Module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Strip(Module):
|
|
7
|
+
"""Trim whitespace from start and end of lines."""
|
|
8
|
+
|
|
9
|
+
def run(self, app, editor, args):
|
|
10
|
+
line_nums = editor.get_lines_with_cursors()
|
|
11
|
+
for n in line_nums:
|
|
12
|
+
line = editor.lines[n]
|
|
13
|
+
line.set_data(line.data.strip())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
module = {
|
|
17
|
+
"class": Strip,
|
|
18
|
+
"name": "strip",
|
|
19
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from platform import system
|
|
7
|
+
|
|
8
|
+
from suplemon.suplemon_module import Module
|
|
9
|
+
|
|
10
|
+
# Windows and WSL both reach the Windows clipboard through powershell
|
|
11
|
+
POWERSHELL = ["powershell.exe", "-noprofile", "-command"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_wsl():
|
|
15
|
+
"""Detect WSL, where the Windows clipboard is reachable via powershell.exe."""
|
|
16
|
+
if system() != "Linux" or not os.path.isfile("/proc/version"):
|
|
17
|
+
return False
|
|
18
|
+
try:
|
|
19
|
+
with open("/proc/version") as f:
|
|
20
|
+
return "microsoft" in f.read().lower()
|
|
21
|
+
except OSError:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SystemClipboard(Module):
|
|
26
|
+
"""Integrates the system clipboard with suplemon."""
|
|
27
|
+
|
|
28
|
+
def init(self):
|
|
29
|
+
self.init_logging(__name__)
|
|
30
|
+
self.clipboard = self.detect_clipboard()
|
|
31
|
+
if not self.clipboard:
|
|
32
|
+
# No clipboard tool is normal on a headless or minimal system, and
|
|
33
|
+
# suplemon's own copy/paste works regardless, so this isn't a
|
|
34
|
+
# warning. Kept at debug level so it stays out of the way.
|
|
35
|
+
self.logger.debug(
|
|
36
|
+
"No system clipboard tool found. Install 'xsel', 'xclip', 'wl-clipboard', "
|
|
37
|
+
"'pbcopy' or 'termux-api' to share the clipboard with other applications.")
|
|
38
|
+
return False
|
|
39
|
+
self.bind_event_before("insert", self.insert)
|
|
40
|
+
self.bind_event_after("copy", self.copy)
|
|
41
|
+
self.bind_event_after("cut", self.copy)
|
|
42
|
+
|
|
43
|
+
def detect_clipboard(self):
|
|
44
|
+
"""Find the commands for the first usable clipboard tool.
|
|
45
|
+
|
|
46
|
+
Ordered so the native mechanism wins where more than one is present:
|
|
47
|
+
Windows and WSL first, then Wayland, then the X11 tools, then Termux.
|
|
48
|
+
|
|
49
|
+
:return: Dict with "get" and "set" commands, or False if none found.
|
|
50
|
+
"""
|
|
51
|
+
if (system() == "Windows" or is_wsl()) and shutil.which("powershell.exe"):
|
|
52
|
+
return {
|
|
53
|
+
"get": POWERSHELL + ["Get-Clipboard"],
|
|
54
|
+
"set": POWERSHELL + ["Set-Clipboard"],
|
|
55
|
+
}
|
|
56
|
+
if os.environ.get("WAYLAND_DISPLAY") and shutil.which("wl-copy"):
|
|
57
|
+
return {"get": ["wl-paste", "-n"], "set": ["wl-copy"]}
|
|
58
|
+
# xsel and xclip are X11 clients. Being installed isn't enough: with no
|
|
59
|
+
# DISPLAY they exit with "Can't open display" on every copy and paste.
|
|
60
|
+
if os.environ.get("DISPLAY"):
|
|
61
|
+
if shutil.which("xsel"):
|
|
62
|
+
return {"get": ["xsel", "-b"], "set": ["xsel", "-i", "-b"]}
|
|
63
|
+
if shutil.which("xclip"):
|
|
64
|
+
return {
|
|
65
|
+
"get": ["xclip", "-selection", "clipboard", "-out"],
|
|
66
|
+
"set": ["xclip", "-selection", "clipboard", "-in"],
|
|
67
|
+
}
|
|
68
|
+
if shutil.which("pbcopy"):
|
|
69
|
+
return {"get": ["pbpaste", "-Prefer", "txt"], "set": ["pbcopy"]}
|
|
70
|
+
if shutil.which("termux-clipboard-get"):
|
|
71
|
+
return {"get": ["termux-clipboard-get"], "set": ["termux-clipboard-set"]}
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def copy(self, event):
|
|
75
|
+
lines = self.app.get_editor().get_buffer()
|
|
76
|
+
data = "\n".join([str(line) for line in lines])
|
|
77
|
+
self.set_clipboard(data)
|
|
78
|
+
|
|
79
|
+
def insert(self, event):
|
|
80
|
+
data = self.get_clipboard()
|
|
81
|
+
if data is False:
|
|
82
|
+
# Reading the clipboard failed, leave suplemon's own buffer alone
|
|
83
|
+
return
|
|
84
|
+
lines = data.split("\n")
|
|
85
|
+
self.app.get_editor().set_buffer(lines)
|
|
86
|
+
|
|
87
|
+
def get_clipboard(self):
|
|
88
|
+
try:
|
|
89
|
+
# stderr is discarded on purpose. curses owns the screen, so
|
|
90
|
+
# anything a clipboard tool prints lands on top of the editor and
|
|
91
|
+
# corrupts the display.
|
|
92
|
+
return subprocess.check_output(
|
|
93
|
+
self.clipboard["get"],
|
|
94
|
+
stderr=subprocess.DEVNULL,
|
|
95
|
+
universal_newlines=True,
|
|
96
|
+
)
|
|
97
|
+
except (OSError, subprocess.CalledProcessError):
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def set_clipboard(self, data):
|
|
101
|
+
try:
|
|
102
|
+
# Both streams go to /dev/null rather than being inherited, for
|
|
103
|
+
# the same reason as in get_clipboard. Not PIPE: xsel forks a
|
|
104
|
+
# background process to own the selection, which keeps a piped
|
|
105
|
+
# stdout open, and communicate() would wait for it forever.
|
|
106
|
+
p = subprocess.Popen(
|
|
107
|
+
self.clipboard["set"],
|
|
108
|
+
stdin=subprocess.PIPE,
|
|
109
|
+
stdout=subprocess.DEVNULL,
|
|
110
|
+
stderr=subprocess.DEVNULL,
|
|
111
|
+
)
|
|
112
|
+
out, err = p.communicate(input=bytes(data, "utf-8"))
|
|
113
|
+
return out
|
|
114
|
+
except (OSError, subprocess.CalledProcessError):
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
module = {
|
|
119
|
+
"class": SystemClipboard,
|
|
120
|
+
"name": "system_clipboard",
|
|
121
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
from suplemon.suplemon_module import Module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TabsToSpaces(Module):
|
|
7
|
+
"""Convert tab characters to spaces in the entire file."""
|
|
8
|
+
|
|
9
|
+
def run(self, app, editor, args):
|
|
10
|
+
for i, line in enumerate(editor.lines):
|
|
11
|
+
new = line.data.replace("\t", " "*editor.config["tab_width"])
|
|
12
|
+
editor.lines[i].set_data(new)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
module = {
|
|
16
|
+
"class": TabsToSpaces,
|
|
17
|
+
"name": "tabstospaces",
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
from suplemon.suplemon_module import Module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ToggleWhitespace(Module):
|
|
7
|
+
"""Toggle visually showing whitespace."""
|
|
8
|
+
|
|
9
|
+
def run(self, app, editor, args):
|
|
10
|
+
# Toggle the boolean
|
|
11
|
+
new_value = not self.app.config["editor"]["show_white_space"]
|
|
12
|
+
self.app.config["editor"]["show_white_space"] = new_value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
module = {
|
|
16
|
+
"class": ToggleWhitespace,
|
|
17
|
+
"name": "toggle_whitespace",
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
|
|
3
|
+
from suplemon.suplemon_module import Module
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Upper(Module):
|
|
7
|
+
"""Transform current lines to upper case."""
|
|
8
|
+
|
|
9
|
+
def run(self, app, editor, args):
|
|
10
|
+
line_nums = []
|
|
11
|
+
for cursor in editor.cursors:
|
|
12
|
+
if cursor.y not in line_nums:
|
|
13
|
+
line_nums.append(cursor.y)
|
|
14
|
+
data = editor.lines[cursor.y].get_data().upper()
|
|
15
|
+
editor.lines[cursor.y].set_data(data)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
module = {
|
|
19
|
+
"class": Upper,
|
|
20
|
+
"name": "upper",
|
|
21
|
+
}
|
suplemon/prompt.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# -*- encoding: utf-8
|
|
2
|
+
"""
|
|
3
|
+
Promts based on the Editor class for querying user input.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from .editor import Editor
|
|
9
|
+
from .line import Line
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Prompt(Editor):
|
|
13
|
+
"""An input prompt based on the Editor."""
|
|
14
|
+
def __init__(self, app, window):
|
|
15
|
+
Editor.__init__(self, app, window)
|
|
16
|
+
self.ready = False
|
|
17
|
+
self.canceled = False
|
|
18
|
+
self.input_func = lambda: False
|
|
19
|
+
self.caption = ""
|
|
20
|
+
|
|
21
|
+
def init(self):
|
|
22
|
+
Editor.init(self)
|
|
23
|
+
# Remove the find feature, otherwise it can be invoked recursively
|
|
24
|
+
del self.operations["find"]
|
|
25
|
+
|
|
26
|
+
def set_config(self, config):
|
|
27
|
+
"""Set the configuration for the editor."""
|
|
28
|
+
# Override showing line numbers
|
|
29
|
+
config["show_line_nums"] = False
|
|
30
|
+
Editor.set_config(self, config)
|
|
31
|
+
|
|
32
|
+
def set_input_source(self, input_func):
|
|
33
|
+
# Set the input function to use while looping for input
|
|
34
|
+
self.input_func = input_func
|
|
35
|
+
|
|
36
|
+
def on_ready(self):
|
|
37
|
+
"""Accepts the current input."""
|
|
38
|
+
self.ready = True
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
def on_cancel(self):
|
|
42
|
+
"""Cancels the input prompt."""
|
|
43
|
+
self.set_data("")
|
|
44
|
+
self.ready = True
|
|
45
|
+
self.canceled = True
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
def line_offset(self):
|
|
49
|
+
"""Get the x coordinate of beginning of line."""
|
|
50
|
+
return len(self.caption)+1
|
|
51
|
+
|
|
52
|
+
def render_line_contents(self, line, pos, x_offset, max_len):
|
|
53
|
+
"""Render the prompt line."""
|
|
54
|
+
x_offset = self.line_offset()
|
|
55
|
+
# Render the caption
|
|
56
|
+
self.window.addstr(pos[1], 0, self.caption)
|
|
57
|
+
# Render input
|
|
58
|
+
self.render_line_normal(line, pos, x_offset, max_len)
|
|
59
|
+
|
|
60
|
+
def handle_input(self, event):
|
|
61
|
+
"""Handle special bindings for the prompt."""
|
|
62
|
+
if event.key_name in ["ctrl+c", "escape"]:
|
|
63
|
+
self.on_cancel()
|
|
64
|
+
return False
|
|
65
|
+
if event.key_name == "enter":
|
|
66
|
+
self.on_ready()
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
return Editor.handle_input(self, event)
|
|
70
|
+
|
|
71
|
+
def get_input(self, caption="", initial=""):
|
|
72
|
+
"""Get text input from the user via the prompt."""
|
|
73
|
+
self.caption = caption
|
|
74
|
+
self.set_data(initial)
|
|
75
|
+
self.end() # Move to the end of the initial text
|
|
76
|
+
|
|
77
|
+
self.refresh()
|
|
78
|
+
success = self.input_loop()
|
|
79
|
+
if success:
|
|
80
|
+
return self.get_data()
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
def input_loop(self):
|
|
84
|
+
# Run the input loop until ready
|
|
85
|
+
while not self.ready:
|
|
86
|
+
event = self.input_func(True) # blocking
|
|
87
|
+
if event:
|
|
88
|
+
self.handle_input(event)
|
|
89
|
+
self.refresh()
|
|
90
|
+
if not self.canceled:
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PromptBool(Prompt):
|
|
95
|
+
"""An input prompt for booleans based on Prompt."""
|
|
96
|
+
|
|
97
|
+
def set_value(self, value):
|
|
98
|
+
self.value = value
|
|
99
|
+
|
|
100
|
+
def handle_input(self, event):
|
|
101
|
+
"""Handle special bindings for the prompt."""
|
|
102
|
+
name = event.key_name
|
|
103
|
+
if name.lower() == "y":
|
|
104
|
+
self.set_value(True)
|
|
105
|
+
self.on_ready()
|
|
106
|
+
return False
|
|
107
|
+
if name.lower() == "n":
|
|
108
|
+
self.set_value(False)
|
|
109
|
+
self.on_ready()
|
|
110
|
+
return False
|
|
111
|
+
Prompt.handle_input(self, event)
|
|
112
|
+
|
|
113
|
+
def get_input(self, caption="", initial=False):
|
|
114
|
+
"""Get a boolean value from the user via the prompt."""
|
|
115
|
+
|
|
116
|
+
indicator = "[y/N]"
|
|
117
|
+
if initial:
|
|
118
|
+
indicator = "[Y/n]"
|
|
119
|
+
|
|
120
|
+
self.caption = caption + " " + indicator
|
|
121
|
+
self.set_value(initial)
|
|
122
|
+
self.end() # Move to the end of the initial text
|
|
123
|
+
|
|
124
|
+
self.refresh()
|
|
125
|
+
|
|
126
|
+
# Run the input loop until ready
|
|
127
|
+
success = self.input_loop()
|
|
128
|
+
if success:
|
|
129
|
+
return self.value
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class PromptPassword(Prompt):
|
|
134
|
+
"""An input prompt for passwords based on Prompt."""
|
|
135
|
+
|
|
136
|
+
def render_line_contents(self, line, pos, x_offset, max_len):
|
|
137
|
+
obscured = Line("*" * len(line))
|
|
138
|
+
Prompt.render_line_contents(self, obscured, pos, x_offset, max_len)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class PromptFiltered(Prompt):
|
|
142
|
+
"""An input prompt that allows intercepting and filtering input events."""
|
|
143
|
+
|
|
144
|
+
def __init__(self, app, window, handler=None):
|
|
145
|
+
Prompt.__init__(self, app, window)
|
|
146
|
+
self.prompt_handler = handler
|
|
147
|
+
|
|
148
|
+
def handle_input(self, event):
|
|
149
|
+
"""Handle special bindings for the prompt."""
|
|
150
|
+
# The cancel and accept keys are kept for concistency
|
|
151
|
+
if event.key_name in ["ctrl+c", "escape"]:
|
|
152
|
+
self.on_cancel()
|
|
153
|
+
return False
|
|
154
|
+
if event.key_name == "enter":
|
|
155
|
+
self.on_ready()
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
if self.prompt_handler and self.prompt_handler(self, event):
|
|
159
|
+
# If the prompt handler returns True the default action is skipped
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
return Editor.handle_input(self, event)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class PromptAutocmp(Prompt):
|
|
166
|
+
"""An input prompt with basic autocompletion."""
|
|
167
|
+
|
|
168
|
+
def __init__(self, app, window, initial_items=None):
|
|
169
|
+
Prompt.__init__(self, app, window)
|
|
170
|
+
# Whether the autocomplete feature is active
|
|
171
|
+
self.complete_active = False
|
|
172
|
+
# Index of last item that was autocompleted
|
|
173
|
+
self.complete_index = 0
|
|
174
|
+
# Input data to use for autocompletion (stored when autocompletion is activated)
|
|
175
|
+
self.complete_data = ""
|
|
176
|
+
# Default autocompletable items
|
|
177
|
+
self.complete_items = [] if initial_items is None else initial_items
|
|
178
|
+
|
|
179
|
+
def handle_input(self, event):
|
|
180
|
+
"""Handle special bindings for the prompt."""
|
|
181
|
+
name = event.key_name
|
|
182
|
+
if self.complete_active:
|
|
183
|
+
# Allow accepting completed directories with enter
|
|
184
|
+
if name == "enter":
|
|
185
|
+
if self.has_match():
|
|
186
|
+
self.deactivate_autocomplete()
|
|
187
|
+
return False
|
|
188
|
+
# Revert autocompletion with esc
|
|
189
|
+
if name == "escape":
|
|
190
|
+
self.revert_autocomplete()
|
|
191
|
+
self.deactivate_autocomplete()
|
|
192
|
+
return False
|
|
193
|
+
if name == "tab":
|
|
194
|
+
# Run autocompletion when tab is pressed
|
|
195
|
+
self.autocomplete()
|
|
196
|
+
# Don't pass the event to the parent class
|
|
197
|
+
return False
|
|
198
|
+
elif name == "shift+tab":
|
|
199
|
+
# Go to previous item when shift+tab is pressed
|
|
200
|
+
self.autocomplete(previous=True)
|
|
201
|
+
# Don't pass the event to the parent class
|
|
202
|
+
return False
|
|
203
|
+
else:
|
|
204
|
+
# If any key other than tab is pressed deactivate the autocompleter
|
|
205
|
+
self.deactivate_autocomplete()
|
|
206
|
+
Prompt.handle_input(self, event)
|
|
207
|
+
|
|
208
|
+
def autocomplete(self, previous=False):
|
|
209
|
+
data = self.get_data() # Use the current input by default
|
|
210
|
+
if self.complete_active: # If the completer is active use the its initial input value
|
|
211
|
+
data = self.complete_data
|
|
212
|
+
|
|
213
|
+
name = self.get_completable_name(data)
|
|
214
|
+
items = self.get_completable_items(data)
|
|
215
|
+
|
|
216
|
+
# Filter the items by name if the input path contains a name
|
|
217
|
+
if name:
|
|
218
|
+
items = self.filter_items(items, name)
|
|
219
|
+
if not items:
|
|
220
|
+
# Deactivate completion if there's nothing to complete
|
|
221
|
+
self.deactivate_autocomplete()
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
if not self.complete_active:
|
|
225
|
+
# Initialize the autocompletor
|
|
226
|
+
self.complete_active = True
|
|
227
|
+
self.complete_data = data
|
|
228
|
+
self.complete_index = 0
|
|
229
|
+
else:
|
|
230
|
+
# Increment the selected item countor
|
|
231
|
+
if previous:
|
|
232
|
+
# Go back
|
|
233
|
+
self.complete_index -= 1
|
|
234
|
+
if self.complete_index < 0:
|
|
235
|
+
self.complete_index = len(items)-1
|
|
236
|
+
else:
|
|
237
|
+
# Go forward
|
|
238
|
+
self.complete_index += 1
|
|
239
|
+
if self.complete_index > len(items)-1:
|
|
240
|
+
self.complete_index = 0
|
|
241
|
+
|
|
242
|
+
item = items[self.complete_index]
|
|
243
|
+
new_data = self.get_full_completion(data, item)
|
|
244
|
+
if len(items) == 1:
|
|
245
|
+
self.deactivate_autocomplete()
|
|
246
|
+
# Set the input data to the completion and move cursor to the end
|
|
247
|
+
self.set_data(new_data)
|
|
248
|
+
self.end()
|
|
249
|
+
|
|
250
|
+
def get_completable_name(self, data=""):
|
|
251
|
+
return data
|
|
252
|
+
|
|
253
|
+
def get_completable_items(self, data=""):
|
|
254
|
+
return self.complete_items
|
|
255
|
+
|
|
256
|
+
def get_full_completion(self, data, item):
|
|
257
|
+
return item
|
|
258
|
+
|
|
259
|
+
def has_match(self):
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
def deactivate_autocomplete(self):
|
|
263
|
+
self.complete_active = False
|
|
264
|
+
self.complete_index = 0
|
|
265
|
+
self.complete_data = ""
|
|
266
|
+
|
|
267
|
+
def revert_autocomplete(self):
|
|
268
|
+
if self.complete_data:
|
|
269
|
+
self.set_data(self.complete_data)
|
|
270
|
+
|
|
271
|
+
def filter_items(self, items, name):
|
|
272
|
+
"""Remove items that don't begin with name. Not case sensitive."""
|
|
273
|
+
if not name:
|
|
274
|
+
return items
|
|
275
|
+
name = name.lower()
|
|
276
|
+
return [item for item in items if item.lower().startswith(name)]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class PromptFile(PromptAutocmp):
|
|
280
|
+
"""An input prompt with path autocompletion based on PromptAutocmp."""
|
|
281
|
+
|
|
282
|
+
def __init__(self, app, window):
|
|
283
|
+
PromptAutocmp.__init__(self, app, window)
|
|
284
|
+
|
|
285
|
+
def has_match(self):
|
|
286
|
+
return os.path.isdir(os.path.expanduser(self.get_data()))
|
|
287
|
+
|
|
288
|
+
def get_completable_name(self, data=""):
|
|
289
|
+
return os.path.basename(data)
|
|
290
|
+
|
|
291
|
+
def get_completable_items(self, data=""):
|
|
292
|
+
return self.get_path_contents(data) # Get directory listing of input path
|
|
293
|
+
|
|
294
|
+
def get_full_completion(self, data, item):
|
|
295
|
+
return os.path.join(os.path.dirname(data), item)
|
|
296
|
+
|
|
297
|
+
def get_path_contents(self, path):
|
|
298
|
+
path = os.path.dirname(os.path.expanduser(path))
|
|
299
|
+
# If we get an empty path use the current directory
|
|
300
|
+
if not path:
|
|
301
|
+
try:
|
|
302
|
+
path = os.getcwd()
|
|
303
|
+
except FileNotFoundError:
|
|
304
|
+
# This might happen if the cwd has been
|
|
305
|
+
# removed after starting suplemon.
|
|
306
|
+
return []
|
|
307
|
+
# In case we don't have sufficent permissions
|
|
308
|
+
try:
|
|
309
|
+
contents = os.listdir(path)
|
|
310
|
+
except OSError:
|
|
311
|
+
return []
|
|
312
|
+
contents.sort()
|
|
313
|
+
items = []
|
|
314
|
+
# Append directory separator to directories
|
|
315
|
+
for item in contents:
|
|
316
|
+
if os.path.isdir(os.path.join(path, item)):
|
|
317
|
+
item = item + os.sep
|
|
318
|
+
items.append(item)
|
|
319
|
+
return items
|