wizlib 2.0.19__tar.gz → 3.0.1__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.
Potentially problematic release.
This version of wizlib might be problematic. Click here for more details.
- {wizlib-2.0.19 → wizlib-3.0.1}/PKG-INFO +1 -1
- {wizlib-2.0.19 → wizlib-3.0.1}/pyproject.toml +1 -1
- wizlib-3.0.1/wizlib/app.py +98 -0
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/class_family.py +3 -2
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/command.py +16 -10
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/config_handler.py +10 -14
- wizlib-3.0.1/wizlib/handler.py +24 -0
- wizlib-2.0.19/wizlib/input_handler.py → wizlib-3.0.1/wizlib/stream_handler.py +4 -3
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/ui/__init__.py +13 -67
- wizlib-3.0.1/wizlib/ui/shell/__init__.py +40 -0
- wizlib-2.0.19/wizlib/ui/shell_line_editor.py → wizlib-3.0.1/wizlib/ui/shell/line_editor.py +30 -53
- wizlib-3.0.1/wizlib/ui/shell_ui.py +67 -0
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/ui_handler.py +7 -6
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/util.py +1 -1
- wizlib-2.0.19/wizlib/app.py +0 -88
- wizlib-2.0.19/wizlib/handler.py +0 -25
- wizlib-2.0.19/wizlib/ui/shell_ui.py +0 -42
- {wizlib-2.0.19 → wizlib-3.0.1}/README.md +0 -0
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/__init__.py +0 -0
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/error.py +0 -0
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/parser.py +0 -0
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/rlinput.py +0 -0
- {wizlib-2.0.19 → wizlib-3.0.1}/wizlib/super_wrapper.py +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from wizlib.class_family import ClassFamily
|
|
7
|
+
from wizlib.command import WizHelpCommand
|
|
8
|
+
from wizlib.super_wrapper import SuperWrapper
|
|
9
|
+
from wizlib.parser import WizParser
|
|
10
|
+
from wizlib.ui import UI
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
RED = '\033[91m'
|
|
14
|
+
RESET = '\033[0m'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AppCancellation(BaseException):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WizApp:
|
|
22
|
+
"""Root of all WizLib-based CLI applications. Subclass it. Can be
|
|
23
|
+
instantiated and then run multiple commands."""
|
|
24
|
+
|
|
25
|
+
base_command = None
|
|
26
|
+
name = ''
|
|
27
|
+
|
|
28
|
+
# Set some default types so linting works
|
|
29
|
+
ui: UI
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def main(cls): # pragma: nocover
|
|
33
|
+
"""Call this from a __main__ entrypoint"""
|
|
34
|
+
cls.start(*sys.argv[1:], debug=os.getenv('DEBUG'))
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def start(cls, *args, debug=False):
|
|
38
|
+
"""Call this from a Python entrypoint"""
|
|
39
|
+
try:
|
|
40
|
+
parser = WizParser(prog=cls.name)
|
|
41
|
+
for handler in cls.base_command.handlers:
|
|
42
|
+
handler.add_args(parser)
|
|
43
|
+
ns, more = parser.parse_known_args(args)
|
|
44
|
+
app = cls.initialize(**vars(ns))
|
|
45
|
+
more = more if more else [cls.base_command.default]
|
|
46
|
+
app.run(*more)
|
|
47
|
+
except AppCancellation as cancellation:
|
|
48
|
+
if str(cancellation):
|
|
49
|
+
print(str(cancellation), file=sys.stderr)
|
|
50
|
+
except BaseException as error:
|
|
51
|
+
if debug:
|
|
52
|
+
raise error
|
|
53
|
+
else:
|
|
54
|
+
name = type(error).__name__
|
|
55
|
+
print(f"\n{RED}{name}{': ' if str(error) else ''}" +
|
|
56
|
+
f"{error}{RESET}", file=sys.stderr)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def initialize(cls, **vals):
|
|
61
|
+
"""Converts argparse values (strings) into actual handlers and
|
|
62
|
+
instantiates the app"""
|
|
63
|
+
handlers = {}
|
|
64
|
+
for handler in cls.base_command.handlers:
|
|
65
|
+
val = vals[handler.name] if (
|
|
66
|
+
handler.name in vals) else handler.default
|
|
67
|
+
handlers[handler.name] = handler.setup(val)
|
|
68
|
+
return cls(**handlers)
|
|
69
|
+
|
|
70
|
+
def __init__(self, **handlers):
|
|
71
|
+
for name, handler in handlers.items():
|
|
72
|
+
handler.app = self
|
|
73
|
+
setattr(self, name, handler)
|
|
74
|
+
self.parser = WizParser()
|
|
75
|
+
subparsers = self.parser.add_subparsers(dest='command')
|
|
76
|
+
for command in self.base_command.family_members('name'):
|
|
77
|
+
key = command.get_member_attr('key')
|
|
78
|
+
aliases = [key] if key else []
|
|
79
|
+
subparser = subparsers.add_parser(command.name, aliases=aliases)
|
|
80
|
+
command.add_args(subparser)
|
|
81
|
+
|
|
82
|
+
def run(self, *args):
|
|
83
|
+
vals = vars(self.parser.parse_args(args))
|
|
84
|
+
if 'help' in vals:
|
|
85
|
+
return WizHelpCommand(**vals)
|
|
86
|
+
command_name = vals.pop('command')
|
|
87
|
+
command_class = self.base_command.family_member(
|
|
88
|
+
'name', command_name)
|
|
89
|
+
if not command_class:
|
|
90
|
+
raise Exception(f"Unknown command {command_name}")
|
|
91
|
+
command = command_class(self, **vals)
|
|
92
|
+
result = command.execute()
|
|
93
|
+
if result:
|
|
94
|
+
print(result, end='')
|
|
95
|
+
if sys.stdout.isatty(): # pragma: nocover
|
|
96
|
+
print()
|
|
97
|
+
if command.status:
|
|
98
|
+
print(command.status, file=sys.stderr)
|
|
@@ -48,10 +48,11 @@ class ClassFamily:
|
|
|
48
48
|
return hits
|
|
49
49
|
|
|
50
50
|
@classmethod
|
|
51
|
-
def family_attrs(self, attribute):
|
|
51
|
+
def family_attrs(self, attribute): # pragma: nocover
|
|
52
52
|
"""
|
|
53
53
|
Return a set of all the values of a specific attribute that exist in
|
|
54
|
-
the family. The set avoids repetition of values in the result.
|
|
54
|
+
the family. The set avoids repetition of values in the result. This is
|
|
55
|
+
a legacy method, not currently tested or used.
|
|
55
56
|
"""
|
|
56
57
|
# Put myself into a set if I qualify
|
|
57
58
|
if self.has_member_attrs(attribute):
|
|
@@ -1,19 +1,24 @@
|
|
|
1
|
+
# Note that commands once used dataclass, but no longer.
|
|
2
|
+
|
|
1
3
|
from argparse import ArgumentParser
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
|
|
4
6
|
from wizlib.class_family import ClassFamily
|
|
5
7
|
from wizlib.config_handler import ConfigHandler
|
|
6
|
-
from wizlib.
|
|
8
|
+
from wizlib.stream_handler import StreamHandler
|
|
7
9
|
from wizlib.super_wrapper import SuperWrapper
|
|
8
10
|
from wizlib.ui import UI
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
class CommandCancellation(BaseException):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
11
17
|
class WizCommand(ClassFamily, SuperWrapper):
|
|
12
18
|
"""Define all the args you want, but stdin always works."""
|
|
13
19
|
|
|
14
20
|
status = ''
|
|
15
|
-
handlers = []
|
|
16
|
-
ui: UI = None
|
|
21
|
+
handlers = [] # TODO: Move to app
|
|
17
22
|
|
|
18
23
|
@classmethod
|
|
19
24
|
def add_args(self, parser):
|
|
@@ -21,12 +26,10 @@ class WizCommand(ClassFamily, SuperWrapper):
|
|
|
21
26
|
Add global arguments in the base class. Not wrapped."""
|
|
22
27
|
pass
|
|
23
28
|
|
|
24
|
-
def __init__(self, **vals):
|
|
29
|
+
def __init__(self, app=None, **vals):
|
|
30
|
+
self.app = app
|
|
25
31
|
for key in vals:
|
|
26
32
|
setattr(self, key, vals[key])
|
|
27
|
-
for handler in self.handlers:
|
|
28
|
-
if handler.name not in vals:
|
|
29
|
-
setattr(self, handler.name, handler.setup()())
|
|
30
33
|
|
|
31
34
|
def handle_vals(self):
|
|
32
35
|
"""Clean up vals, calculate any, ask through UI, etc. - override
|
|
@@ -43,9 +46,12 @@ class WizCommand(ClassFamily, SuperWrapper):
|
|
|
43
46
|
def execute(self, method, *args, **kwargs):
|
|
44
47
|
"""Actually perform the command - override and wrap this via
|
|
45
48
|
SuperWrapper"""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
try:
|
|
50
|
+
self.handle_vals()
|
|
51
|
+
result = method(self, *args, **kwargs)
|
|
52
|
+
return result
|
|
53
|
+
except CommandCancellation as cancellation:
|
|
54
|
+
self.status = str(cancellation) if str(cancellation) else None
|
|
49
55
|
|
|
50
56
|
|
|
51
57
|
class WizHelpCommand(WizCommand):
|
|
@@ -9,6 +9,7 @@ from yaml import Loader
|
|
|
9
9
|
from wizlib.handler import Handler
|
|
10
10
|
|
|
11
11
|
from wizlib.error import ConfigHandlerError
|
|
12
|
+
from wizlib.parser import WizParser
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class ConfigHandler(Handler):
|
|
@@ -23,8 +24,8 @@ class ConfigHandler(Handler):
|
|
|
23
24
|
|
|
24
25
|
name = 'config'
|
|
25
26
|
|
|
26
|
-
def __init__(self,
|
|
27
|
-
self.file =
|
|
27
|
+
def __init__(self, file=None):
|
|
28
|
+
self.file = file
|
|
28
29
|
self.cache = {}
|
|
29
30
|
|
|
30
31
|
@property
|
|
@@ -34,10 +35,10 @@ class ConfigHandler(Handler):
|
|
|
34
35
|
path = None
|
|
35
36
|
if self.file:
|
|
36
37
|
path = Path(self.file)
|
|
37
|
-
elif self.
|
|
38
|
-
localpath = Path.cwd() / f".{self.
|
|
39
|
-
homepath = Path.home() / f".{self.
|
|
40
|
-
if (envvar := self.env(self.
|
|
38
|
+
elif self.app and self.app.name:
|
|
39
|
+
localpath = Path.cwd() / f".{self.app.name}.yml"
|
|
40
|
+
homepath = Path.home() / f".{self.app.name}.yml"
|
|
41
|
+
if (envvar := self.env(self.app.name + '-config')):
|
|
41
42
|
path = Path(envvar)
|
|
42
43
|
elif (localpath.is_file()):
|
|
43
44
|
path = localpath
|
|
@@ -77,11 +78,6 @@ class ConfigHandler(Handler):
|
|
|
77
78
|
@classmethod
|
|
78
79
|
def fake(cls, **vals):
|
|
79
80
|
"""Return a fake ConfigHandler with forced values, for testing"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
key = name.replace('-', '_')
|
|
84
|
-
if key in vals:
|
|
85
|
-
return vals[key]
|
|
86
|
-
handler.env = fake_env
|
|
87
|
-
return handler
|
|
81
|
+
self = cls()
|
|
82
|
+
self.cache = {k.replace('_', '-'): vals[k] for k in vals}
|
|
83
|
+
return self
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from argparse import Action
|
|
2
|
+
|
|
3
|
+
from wizlib.parser import WizParser
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Handler:
|
|
7
|
+
"""Base class for handlers"""
|
|
8
|
+
|
|
9
|
+
default = ''
|
|
10
|
+
app = None
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def add_args(cls, parser: WizParser):
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
f"--{cls.name}",
|
|
16
|
+
f"-{cls.name[0]}",
|
|
17
|
+
default=cls.default)
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def setup(cls, val):
|
|
21
|
+
return cls(val)
|
|
22
|
+
|
|
23
|
+
def __init__(self, val=None):
|
|
24
|
+
pass
|
|
@@ -2,13 +2,14 @@ from pathlib import Path
|
|
|
2
2
|
import sys
|
|
3
3
|
|
|
4
4
|
from wizlib.handler import Handler
|
|
5
|
+
from wizlib.parser import WizParser
|
|
5
6
|
|
|
6
7
|
INTERACTIVE = sys.stdin.isatty()
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
class
|
|
10
|
+
class StreamHandler(Handler):
|
|
10
11
|
|
|
11
|
-
name = '
|
|
12
|
+
name = 'stream'
|
|
12
13
|
text: str = ''
|
|
13
14
|
|
|
14
15
|
def __init__(self, file=None, stdin=True):
|
|
@@ -22,7 +23,7 @@ class InputHandler(Handler):
|
|
|
22
23
|
|
|
23
24
|
@classmethod
|
|
24
25
|
def fake(cls, value):
|
|
25
|
-
"""Return a fake
|
|
26
|
+
"""Return a fake StreamHandler with forced values, for testing"""
|
|
26
27
|
handler = cls(stdin=False)
|
|
27
28
|
handler.text = value
|
|
28
29
|
return handler
|
|
@@ -37,6 +37,14 @@ from tempfile import NamedTemporaryFile
|
|
|
37
37
|
from wizlib.class_family import ClassFamily
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class Emphasis(Enum):
|
|
41
|
+
"""Semantic style"""
|
|
42
|
+
INFO = 1
|
|
43
|
+
GENERAL = 2
|
|
44
|
+
PRINCIPAL = 3
|
|
45
|
+
ERROR = 4
|
|
46
|
+
|
|
47
|
+
|
|
40
48
|
@dataclass
|
|
41
49
|
class Choice:
|
|
42
50
|
"""A single option, equivalent to a single radio button or select option in
|
|
@@ -119,81 +127,19 @@ class Chooser(Prompt):
|
|
|
119
127
|
self.choices.append(choice)
|
|
120
128
|
|
|
121
129
|
def choice_by_key(self, key):
|
|
122
|
-
if key == '\n':
|
|
123
|
-
choice = self.
|
|
130
|
+
if key == '\n' and self.default:
|
|
131
|
+
choice = self.choices[0].value
|
|
124
132
|
else:
|
|
125
133
|
choice = next(
|
|
126
134
|
(o.value for o in self.choices if key in o.keys), None)
|
|
127
|
-
return choice
|
|
135
|
+
return choice
|
|
128
136
|
|
|
129
137
|
def choices_by_text(self, text):
|
|
130
138
|
return [o.value for o in self.choices if o.hit_text(text)]
|
|
131
139
|
|
|
132
|
-
# def choice_by_word(self, word):
|
|
133
|
-
# chosenlist = [o.value for o in self.choices if o.hit_word(word)]
|
|
134
|
-
# return self._choose(chosenlist)
|
|
135
|
-
|
|
136
|
-
# def _choose(self, chosenlist):
|
|
137
|
-
# choice = next(iter(chosenlist), None)
|
|
138
|
-
# if callable(choice):
|
|
139
|
-
# return choice()
|
|
140
|
-
# else:
|
|
141
|
-
# return choice
|
|
142
|
-
|
|
143
140
|
|
|
144
141
|
@dataclass
|
|
145
142
|
class UI(ClassFamily):
|
|
146
|
-
pass
|
|
147
|
-
|
|
148
|
-
# handler: object
|
|
149
|
-
|
|
150
|
-
# # TODO: Make start an abstract method
|
|
151
|
-
# def start(self):
|
|
152
|
-
# pass
|
|
153
|
-
|
|
154
|
-
# # TODO: Make get_option_mini an abstract method
|
|
155
|
-
# def get_option_mini(self, chooser: Chooser):
|
|
156
|
-
# """Get an option from a list from the user"""
|
|
157
|
-
# pass
|
|
158
|
-
|
|
159
|
-
# # TODO: Make get_string an abstract method
|
|
160
|
-
# def get_string(self, intro, default=""):
|
|
161
|
-
# """Get a string value from the user"""
|
|
162
|
-
# pass
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
# # Terminal UIs include Shell and Curses, since both will rely on a
|
|
166
|
-
# # terminal-based editor for the e.g. Manage command.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
# class TerminalUI(UI):
|
|
170
|
-
|
|
171
|
-
# # A convenience method to get a full textual prompt for a string input
|
|
172
|
-
|
|
173
|
-
# def full_prompt(self, prompt, default=None):
|
|
174
|
-
# if default:
|
|
175
|
-
# return f'{prompt} [{default}]: '
|
|
176
|
-
# else:
|
|
177
|
-
# return f'{prompt}: '
|
|
178
|
-
|
|
179
|
-
# # @staticmethod
|
|
180
|
-
# # def edit_text(text):
|
|
181
|
-
# # commands = [['sensible-editor'], ['open', '-W']]
|
|
182
|
-
# # with NamedTemporaryFile(mode="w+") as tempfile:
|
|
183
|
-
# # tempfile.write(text)
|
|
184
|
-
# # tempfile.seek(0)
|
|
185
|
-
# # command = [os.environ.get('EDITOR')]
|
|
186
|
-
# # if not command[0] or not shutil.which(command[0]):
|
|
187
|
-
# # iterator = (c for c in commands if shutil.which(c[0]))
|
|
188
|
-
# # command = next(filter(None, iterator), None)
|
|
189
|
-
# # if not command:
|
|
190
|
-
# # raise RuntimeError(
|
|
191
|
-
# # "A text editor at the $EDITOR " +
|
|
192
|
-
# # "environment variable is required")
|
|
193
|
-
# # subprocess.run(command + [tempfile.name])
|
|
194
|
-
# # tempfile.seek(0)
|
|
195
|
-
# # return tempfile.read()
|
|
196
|
-
|
|
197
143
|
|
|
198
|
-
|
|
199
|
-
|
|
144
|
+
def send(self, value: str = '', emphasis: Emphasis = Emphasis.GENERAL):
|
|
145
|
+
pass
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# We use an odd misc of nonprintable ASCII, ANSI escape sequences, with some
|
|
2
|
+
# custom preferences. So define them all here.
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def sequence(hexes: str) -> str:
|
|
9
|
+
return bytes.fromhex(hexes).decode()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ESC = sequence("1b")
|
|
13
|
+
|
|
14
|
+
# TODO: Is any of this available in the standard library?
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class S(StrEnum):
|
|
18
|
+
LEFT = sequence("1b5b44")
|
|
19
|
+
RIGHT = sequence("1b5b43")
|
|
20
|
+
BACKSPACE = sequence("7f")
|
|
21
|
+
BEGINNING = sequence("01")
|
|
22
|
+
END = sequence("05")
|
|
23
|
+
RETURN = sequence("0a")
|
|
24
|
+
TAB = sequence("09")
|
|
25
|
+
SHIFT_TAB = sequence("1b5b5a")
|
|
26
|
+
LEFT_WORD = sequence("1b62")
|
|
27
|
+
RIGHT_WORD = sequence("1b66")
|
|
28
|
+
KILL = sequence("0b")
|
|
29
|
+
RESET = ESC + "[0m"
|
|
30
|
+
FAINT = ESC + "[2m"
|
|
31
|
+
BOLD = ESC + "[1m"
|
|
32
|
+
SEPARATORS = ' -_.,'
|
|
33
|
+
SPACE = ' '
|
|
34
|
+
RED = ESC + '[31m'
|
|
35
|
+
GREEN = ESC + '[32m'
|
|
36
|
+
YELLOW = ESC + '[33m'
|
|
37
|
+
BLUE = ESC + '[34m'
|
|
38
|
+
MAGENTA = ESC + '[35m'
|
|
39
|
+
CYAN = ESC + '[36m'
|
|
40
|
+
CLEAR = ESC + '[2J'
|
|
@@ -4,6 +4,8 @@ import re
|
|
|
4
4
|
|
|
5
5
|
from readchar import readkey
|
|
6
6
|
|
|
7
|
+
from wizlib.ui.shell import S
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
if (sys.platform == "win32"):
|
|
9
11
|
import ctypes
|
|
@@ -32,8 +34,8 @@ def cursorPos(): # pragma: nocover
|
|
|
32
34
|
termios.tcsetattr(sys.stdin, termios.TCSAFLUSH, _)
|
|
33
35
|
try:
|
|
34
36
|
_ = ""
|
|
35
|
-
sys.
|
|
36
|
-
sys.
|
|
37
|
+
sys.stdouS.write("\x1b[6n")
|
|
38
|
+
sys.stdouS.flush()
|
|
37
39
|
while not (_ := _ + sys.stdin.read(1)).endswith('R'):
|
|
38
40
|
True
|
|
39
41
|
res = re.match(r".*\[(?P<y>\d*);(?P<x>\d*)R", _)
|
|
@@ -48,38 +50,13 @@ def cursorPos(): # pragma: nocover
|
|
|
48
50
|
return (-1, -1)
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
def sequence(hexes: str) -> str:
|
|
52
|
-
return bytes.fromhex(hexes).decode()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
LEFT = sequence("1b5b44")
|
|
56
|
-
RIGHT = sequence("1b5b43")
|
|
57
|
-
BACKSPACE = sequence("7f")
|
|
58
|
-
BEGINNING = sequence("01")
|
|
59
|
-
END = sequence("05")
|
|
60
|
-
RETURN = sequence("0a")
|
|
61
|
-
ESC = sequence("1b")
|
|
62
|
-
TAB = sequence("09")
|
|
63
|
-
SHIFT_TAB = sequence("1b5b5a")
|
|
64
|
-
LEFT_WORD = sequence("1b62")
|
|
65
|
-
RIGHT_WORD = sequence("1b66")
|
|
66
|
-
KILL = sequence("0b")
|
|
67
|
-
|
|
68
|
-
RESET = ESC + "[0m"
|
|
69
|
-
FAINT = ESC + "[2m"
|
|
70
|
-
BOLD = ESC + "[1m"
|
|
71
|
-
|
|
72
|
-
WORD_SEPARATORS = ' -_.,'
|
|
73
|
-
SPACE = ' '
|
|
74
|
-
|
|
75
|
-
|
|
76
53
|
def write(key):
|
|
77
54
|
sys.stderr.write(key)
|
|
78
55
|
sys.stderr.flush()
|
|
79
56
|
|
|
80
57
|
|
|
81
58
|
# "Fill" refers to the lighter text in the editor, to the right of (or instead
|
|
82
|
-
# of) user-typed
|
|
59
|
+
# of) user-typed texS.
|
|
83
60
|
#
|
|
84
61
|
# States:
|
|
85
62
|
# - USER: User has typed something, so no fill
|
|
@@ -109,63 +86,63 @@ class ShellLineEditor: # pragma: nocover
|
|
|
109
86
|
self.fillstate = FillState.DEFAULT
|
|
110
87
|
|
|
111
88
|
def edit(self):
|
|
112
|
-
write(RESET)
|
|
89
|
+
write(S.RESET)
|
|
113
90
|
while True:
|
|
114
91
|
self.write_fill()
|
|
115
92
|
key = readkey()
|
|
116
93
|
self.clear_fill()
|
|
117
|
-
if key == RETURN:
|
|
94
|
+
if key == S.RETURN:
|
|
118
95
|
break
|
|
119
96
|
if key.isprintable():
|
|
120
|
-
write(BOLD + key + self.buf[self.pos:] +
|
|
121
|
-
('\b' * (len(self.buf) - self.pos)) + RESET)
|
|
97
|
+
write(S.BOLD + key + self.buf[self.pos:] +
|
|
98
|
+
('\b' * (len(self.buf) - self.pos)) + S.RESET)
|
|
122
99
|
self.buf = self.buf[:self.pos] + key + self.buf[self.pos:]
|
|
123
100
|
self.pos += 1
|
|
124
101
|
self.fillstate = FillState.USER
|
|
125
|
-
elif (key in [BACKSPACE, KILL]) and self.has_fill:
|
|
102
|
+
elif (key in [S.BACKSPACE, S.KILL]) and self.has_fill:
|
|
126
103
|
# Backspace clears the fill
|
|
127
104
|
self.fillstate = FillState.BLANK
|
|
128
|
-
elif (key == BACKSPACE) and (self.pos > 0):
|
|
129
|
-
write(BOLD + '\b' + self.buf[self.pos:] + ' ' +
|
|
130
|
-
('\b' * (1 + len(self.buf) - self.pos)) + RESET)
|
|
105
|
+
elif (key == S.BACKSPACE) and (self.pos > 0):
|
|
106
|
+
write(S.BOLD + '\b' + self.buf[self.pos:] + ' ' +
|
|
107
|
+
('\b' * (1 + len(self.buf) - self.pos)) + S.RESET)
|
|
131
108
|
self.buf = self.buf[:self.pos-1] + self.buf[self.pos:]
|
|
132
109
|
self.pos -= 1
|
|
133
110
|
self.fillstate = FillState.USER if (
|
|
134
111
|
self.pos > 0) else FillState.DEFAULT
|
|
135
|
-
elif key == LEFT and self.pos > 0:
|
|
112
|
+
elif key == S.LEFT and self.pos > 0:
|
|
136
113
|
self.move_left()
|
|
137
114
|
self.fillstate = FillState.USER
|
|
138
|
-
elif key == RIGHT and self.pos < len(self.buf):
|
|
115
|
+
elif key == S.RIGHT and self.pos < len(self.buf):
|
|
139
116
|
self.move_right()
|
|
140
117
|
self.fillstate = FillState.USER
|
|
141
|
-
elif key == BEGINNING and self.pos > 0:
|
|
118
|
+
elif key == S.BEGINNING and self.pos > 0:
|
|
142
119
|
self.move_beginning()
|
|
143
120
|
self.fillstate = FillState.USER
|
|
144
|
-
elif key == END and self.has_fill:
|
|
121
|
+
elif key == S.END and self.has_fill:
|
|
145
122
|
self.accept_fill()
|
|
146
123
|
self.fillstate = FillState.USER
|
|
147
|
-
elif key == END and self.pos < len(self.buf):
|
|
124
|
+
elif key == S.END and self.pos < len(self.buf):
|
|
148
125
|
self.move_end_buf()
|
|
149
126
|
self.fillstate = FillState.USER
|
|
150
|
-
elif key == TAB and (choices := self.valid_choices):
|
|
127
|
+
elif key == S.TAB and (choices := self.valid_choices):
|
|
151
128
|
self.index = (self.index + 1) % len(choices)
|
|
152
129
|
self.fillstate = FillState.TAB
|
|
153
|
-
elif key == SHIFT_TAB and self.index > -1:
|
|
130
|
+
elif key == S.SHIFT_TAB and self.index > -1:
|
|
154
131
|
self.index = (self.index - 1) % len(self.valid_choices)
|
|
155
132
|
self.fillstate = FillState.TAB
|
|
156
|
-
elif key == LEFT_WORD and self.pos > 0:
|
|
133
|
+
elif key == S.LEFT_WORD and self.pos > 0:
|
|
157
134
|
while (self.pos > 0) and self.is_sep(self.pos - 1):
|
|
158
135
|
self.move_left()
|
|
159
136
|
while (self.pos > 0) and not self.is_sep(self.pos - 1):
|
|
160
137
|
self.move_left()
|
|
161
138
|
self.fillstate = FillState.USER
|
|
162
|
-
elif key == RIGHT_WORD and self.pos < len(self.buf):
|
|
139
|
+
elif key == S.RIGHT_WORD and self.pos < len(self.buf):
|
|
163
140
|
while (self.pos < len(self.buf)) and self.is_sep(self.pos):
|
|
164
141
|
self.move_right()
|
|
165
142
|
while (self.pos < len(self.buf)) and not self.is_sep(self.pos):
|
|
166
143
|
self.move_right()
|
|
167
144
|
self.fillstate = FillState.USER
|
|
168
|
-
elif key == KILL:
|
|
145
|
+
elif key == S.KILL:
|
|
169
146
|
chars = len(self.buf) - self.pos
|
|
170
147
|
write(' ' * chars + '\b' * chars)
|
|
171
148
|
self.buf = self.buf[:self.pos]
|
|
@@ -173,11 +150,11 @@ class ShellLineEditor: # pragma: nocover
|
|
|
173
150
|
else:
|
|
174
151
|
pass
|
|
175
152
|
self.accept_fill()
|
|
176
|
-
write(RETURN)
|
|
153
|
+
write(S.RETURN)
|
|
177
154
|
return self.buf
|
|
178
155
|
|
|
179
156
|
def is_sep(self, pos):
|
|
180
|
-
return (self.buf[pos] in
|
|
157
|
+
return (self.buf[pos] in S.SEPARATORS)
|
|
181
158
|
|
|
182
159
|
@property
|
|
183
160
|
def has_fill(self):
|
|
@@ -185,16 +162,16 @@ class ShellLineEditor: # pragma: nocover
|
|
|
185
162
|
|
|
186
163
|
def accept_fill(self):
|
|
187
164
|
if self.has_fill:
|
|
188
|
-
write(BOLD + self.fill + RESET)
|
|
165
|
+
write(S.BOLD + self.fill + S.RESET)
|
|
189
166
|
self.buf += self.fill
|
|
190
167
|
self.pos = len(self.buf)
|
|
191
168
|
|
|
192
169
|
def move_left(self):
|
|
193
|
-
write(LEFT)
|
|
170
|
+
write(S.LEFT)
|
|
194
171
|
self.pos -= 1
|
|
195
172
|
|
|
196
173
|
def move_right(self):
|
|
197
|
-
write(RIGHT)
|
|
174
|
+
write(S.RIGHT)
|
|
198
175
|
self.pos += 1
|
|
199
176
|
|
|
200
177
|
def move_beginning(self):
|
|
@@ -208,7 +185,7 @@ class ShellLineEditor: # pragma: nocover
|
|
|
208
185
|
def write_fill(self):
|
|
209
186
|
if self.fillstate in [FillState.DEFAULT, FillState.TAB]:
|
|
210
187
|
self.move_end_buf()
|
|
211
|
-
write(FAINT + self.fill + '\b' * len(self.fill) + RESET)
|
|
188
|
+
write(S.FAINT + self.fill + '\b' * len(self.fill) + S.RESET)
|
|
212
189
|
|
|
213
190
|
def clear_fill(self):
|
|
214
191
|
if self.has_fill:
|
|
@@ -228,7 +205,7 @@ class ShellLineEditor: # pragma: nocover
|
|
|
228
205
|
@property
|
|
229
206
|
def last_word(self):
|
|
230
207
|
index = next((c for c in reversed(range(len(self.buf)))
|
|
231
|
-
if self.buf[c] == SPACE), -1)
|
|
208
|
+
if self.buf[c] == S.SPACE), -1)
|
|
232
209
|
return self.buf[index+1:]
|
|
233
210
|
|
|
234
211
|
@property
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from readchar import readkey
|
|
5
|
+
|
|
6
|
+
from wizlib.rlinput import rlinput
|
|
7
|
+
from wizlib.ui import UI, Chooser, Emphasis
|
|
8
|
+
from wizlib.ui.shell.line_editor import ShellLineEditor
|
|
9
|
+
from wizlib.ui.shell import S
|
|
10
|
+
|
|
11
|
+
INTERACTIVE = sys.stdin.isatty()
|
|
12
|
+
|
|
13
|
+
COLOR = {
|
|
14
|
+
Emphasis.INFO: S.BLUE,
|
|
15
|
+
Emphasis.GENERAL: S.CYAN,
|
|
16
|
+
Emphasis.PRINCIPAL: S.YELLOW,
|
|
17
|
+
Emphasis.ERROR: S.RED
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ShellUI(UI):
|
|
22
|
+
|
|
23
|
+
"""The UI to execute one command passed in through the shell. There will be
|
|
24
|
+
limited interactivity, if the user omits an argument on the command line,
|
|
25
|
+
but otherwise this is a run and done situation.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
name = "shell"
|
|
29
|
+
|
|
30
|
+
def send(self, value: str = '', emphasis: Emphasis = Emphasis.GENERAL):
|
|
31
|
+
"""Output some text"""
|
|
32
|
+
sys.stderr.write(COLOR[emphasis] + value + S.RESET + '\n')
|
|
33
|
+
sys.stderr.flush()
|
|
34
|
+
|
|
35
|
+
def ask(self, value: str):
|
|
36
|
+
"""Prompt for input"""
|
|
37
|
+
if value:
|
|
38
|
+
sys.stderr.write(S.GREEN + value + S.RESET)
|
|
39
|
+
sys.stderr.flush()
|
|
40
|
+
|
|
41
|
+
def get_option(self, chooser: Chooser):
|
|
42
|
+
"""Get a choice from the user with a single keystroke"""
|
|
43
|
+
while True:
|
|
44
|
+
self.ask(chooser.prompt_string)
|
|
45
|
+
if INTERACTIVE:
|
|
46
|
+
key = readkey()
|
|
47
|
+
else:
|
|
48
|
+
key = sys.stdin.read(1)
|
|
49
|
+
out = chooser.default if key == '\n' else \
|
|
50
|
+
key if key.isprintable() else ''
|
|
51
|
+
choice = chooser.choice_by_key(key)
|
|
52
|
+
emphasis = Emphasis.ERROR if (
|
|
53
|
+
choice is None) else Emphasis.PRINCIPAL
|
|
54
|
+
self.send(out, emphasis=emphasis)
|
|
55
|
+
if choice is not None:
|
|
56
|
+
break
|
|
57
|
+
return choice() if callable(choice) else choice
|
|
58
|
+
|
|
59
|
+
def get_text(self, prompt='', choices=[], default=''):
|
|
60
|
+
"""Allow the user to input an arbitrary line of text, with possible tab
|
|
61
|
+
completion"""
|
|
62
|
+
self.ask(prompt)
|
|
63
|
+
if INTERACTIVE:
|
|
64
|
+
value = ShellLineEditor(choices, default).edit()
|
|
65
|
+
else:
|
|
66
|
+
value = input()
|
|
67
|
+
return value
|
|
@@ -15,9 +15,10 @@ class UIHandler(Handler):
|
|
|
15
15
|
default = 'shell'
|
|
16
16
|
|
|
17
17
|
@classmethod
|
|
18
|
-
def setup(cls,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
def setup(cls, uitype):
|
|
19
|
+
return UI.family_member('name', uitype)()
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def fake(cls, val=None):
|
|
23
|
+
"""Use mock_keys"""
|
|
24
|
+
return
|
wizlib-2.0.19/wizlib/app.py
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from wizlib.class_family import ClassFamily
|
|
7
|
-
from wizlib.command import WizHelpCommand
|
|
8
|
-
from wizlib.super_wrapper import SuperWrapper
|
|
9
|
-
from wizlib.parser import WizParser
|
|
10
|
-
from wizlib.ui import UI
|
|
11
|
-
from wizlib.ui.shell_ui import ShellUI
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
RED = '\033[91m'
|
|
15
|
-
RESET = '\033[0m'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class WizApp:
|
|
19
|
-
"""Root of all WizLib-based CLI applications. Subclass it. Can be
|
|
20
|
-
instantiated and then run multiple commands."""
|
|
21
|
-
|
|
22
|
-
base_command = None
|
|
23
|
-
name = ''
|
|
24
|
-
|
|
25
|
-
@classmethod
|
|
26
|
-
def main(cls): # pragma: nocover
|
|
27
|
-
"""Call this from a __main__ entrypoint"""
|
|
28
|
-
cls.run(*sys.argv[1:], debug=os.getenv('DEBUG'))
|
|
29
|
-
|
|
30
|
-
@classmethod
|
|
31
|
-
def run(cls, *args, debug=False):
|
|
32
|
-
"""Call this from a Python entrypoint"""
|
|
33
|
-
try:
|
|
34
|
-
cls.initialize()
|
|
35
|
-
app = cls(*args)
|
|
36
|
-
command = app.first_command
|
|
37
|
-
result = command.execute()
|
|
38
|
-
if result:
|
|
39
|
-
print(result, file=sys.stdout, end='')
|
|
40
|
-
if sys.stdout.isatty(): # pragma: nocover
|
|
41
|
-
print()
|
|
42
|
-
if command.status:
|
|
43
|
-
print(command.status, file=sys.stderr)
|
|
44
|
-
except Exception as error:
|
|
45
|
-
if debug:
|
|
46
|
-
raise error
|
|
47
|
-
else:
|
|
48
|
-
print(f"\n{RED}{type(error).__name__}: " +
|
|
49
|
-
f"{error}{RESET}\n", file=sys.stderr)
|
|
50
|
-
sys.exit(1)
|
|
51
|
-
|
|
52
|
-
@classmethod
|
|
53
|
-
def initialize(cls):
|
|
54
|
-
"""Set up the app class to parse arguments"""
|
|
55
|
-
cls.parser = WizParser(
|
|
56
|
-
prog=cls.name,
|
|
57
|
-
exit_on_error=False)
|
|
58
|
-
for handler in cls.base_command.handlers:
|
|
59
|
-
cls.parser.add_argument(
|
|
60
|
-
f"--{handler.name}",
|
|
61
|
-
f"-{handler.name[0]}",
|
|
62
|
-
**handler.option_properties(cls))
|
|
63
|
-
|
|
64
|
-
subparsers = cls.parser.add_subparsers(dest='command')
|
|
65
|
-
for command in cls.base_command.family_members('name'):
|
|
66
|
-
key = command.get_member_attr('key')
|
|
67
|
-
aliases = [key] if key else []
|
|
68
|
-
subparser = subparsers.add_parser(command.name, aliases=aliases)
|
|
69
|
-
command.add_args(subparser)
|
|
70
|
-
|
|
71
|
-
def __init__(self, *args):
|
|
72
|
-
args = args if args else [self.base_command.default]
|
|
73
|
-
if not hasattr(self, 'parser'):
|
|
74
|
-
self.__class__.initialize()
|
|
75
|
-
self.vals = vars(self.parser.parse_args(args))
|
|
76
|
-
self.first_command = self.get_command(**self.vals)
|
|
77
|
-
|
|
78
|
-
def get_command(self, **vals):
|
|
79
|
-
"""Returns a single command"""
|
|
80
|
-
if 'help' in vals:
|
|
81
|
-
return WizHelpCommand(**vals)
|
|
82
|
-
else:
|
|
83
|
-
command_name = vals.pop('command')
|
|
84
|
-
command_class = self.base_command.family_member(
|
|
85
|
-
'name', command_name)
|
|
86
|
-
if not command_class:
|
|
87
|
-
raise Exception(f"Unknown command {command_name}")
|
|
88
|
-
return command_class(**vals)
|
wizlib-2.0.19/wizlib/handler.py
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from argparse import Action
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Handler:
|
|
5
|
-
"""Base class for handlers"""
|
|
6
|
-
|
|
7
|
-
appname = None
|
|
8
|
-
default = ''
|
|
9
|
-
|
|
10
|
-
@classmethod
|
|
11
|
-
def option_properties(cls, app):
|
|
12
|
-
"""Argparse keyword arguments for this optional arg"""
|
|
13
|
-
return {
|
|
14
|
-
'type': cls.setup(app.name),
|
|
15
|
-
'default': cls.default
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
@classmethod
|
|
19
|
-
def setup(cls, name=None):
|
|
20
|
-
"""Return a callable that returns an instance of the handler object
|
|
21
|
-
that's referenced by commands. In the default case, an instance of the
|
|
22
|
-
handler class itself that knows the name of the app."""
|
|
23
|
-
class NamedHandler(cls):
|
|
24
|
-
appname = name
|
|
25
|
-
return NamedHandler
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
|
|
3
|
-
from readchar import readkey
|
|
4
|
-
from wizlib.rlinput import rlinput
|
|
5
|
-
from wizlib.ui import UI
|
|
6
|
-
from wizlib.ui.shell_line_editor import ShellLineEditor
|
|
7
|
-
|
|
8
|
-
INTERACTIVE = sys.stdin.isatty()
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ShellUI(UI):
|
|
12
|
-
|
|
13
|
-
"""The UI to execute one command passed in through the shell. There will be
|
|
14
|
-
limited interactivity, if the user omits an argument on the command line,
|
|
15
|
-
but otherwise this is a run and done situation.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
name = "shell"
|
|
19
|
-
|
|
20
|
-
def send(self, value: str):
|
|
21
|
-
"""Output some text"""
|
|
22
|
-
if value:
|
|
23
|
-
print(value, file=sys.stderr)
|
|
24
|
-
|
|
25
|
-
def get_option_mini(self, chooser):
|
|
26
|
-
"""Get a choice from the user with a single keystroke"""
|
|
27
|
-
# key = rlinput(chooser.prompt_string)
|
|
28
|
-
print(chooser.prompt_string, end='', file=sys.stderr, flush=True)
|
|
29
|
-
if INTERACTIVE:
|
|
30
|
-
key = readkey()
|
|
31
|
-
else:
|
|
32
|
-
key = sys.stdin.read(1)
|
|
33
|
-
print(file=sys.stderr, flush=True)
|
|
34
|
-
return chooser.choice_by_key(key)
|
|
35
|
-
|
|
36
|
-
def get_text(self, prompt='', choices=[], default=''):
|
|
37
|
-
"""Allow the user to input an arbitrary line of text, with possible tab
|
|
38
|
-
completion"""
|
|
39
|
-
sys.stderr.write(prompt)
|
|
40
|
-
sys.stderr.flush()
|
|
41
|
-
value = ShellLineEditor(choices, default).edit()
|
|
42
|
-
return value
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|