wizlib 2.0.18__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wizlib might be problematic. Click here for more details.
- {wizlib-2.0.18 → wizlib-3.0.0}/PKG-INFO +1 -1
- {wizlib-2.0.18 → wizlib-3.0.0}/pyproject.toml +1 -1
- wizlib-3.0.0/wizlib/app.py +98 -0
- {wizlib-2.0.18 → wizlib-3.0.0}/wizlib/class_family.py +3 -2
- {wizlib-2.0.18 → wizlib-3.0.0}/wizlib/command.py +13 -8
- {wizlib-2.0.18 → wizlib-3.0.0}/wizlib/config_handler.py +10 -14
- wizlib-3.0.0/wizlib/handler.py +24 -0
- wizlib-2.0.18/wizlib/input_handler.py → wizlib-3.0.0/wizlib/stream_handler.py +4 -3
- {wizlib-2.0.18 → wizlib-3.0.0}/wizlib/ui/__init__.py +10 -64
- wizlib-3.0.0/wizlib/ui/shell/__init__.py +40 -0
- wizlib-2.0.18/wizlib/ui/shell_line_editor.py → wizlib-3.0.0/wizlib/ui/shell/line_editor.py +30 -52
- wizlib-3.0.0/wizlib/ui/shell_ui.py +67 -0
- {wizlib-2.0.18 → wizlib-3.0.0}/wizlib/ui_handler.py +7 -6
- {wizlib-2.0.18 → wizlib-3.0.0}/wizlib/util.py +1 -1
- wizlib-2.0.18/wizlib/app.py +0 -88
- wizlib-2.0.18/wizlib/handler.py +0 -25
- wizlib-2.0.18/wizlib/ui/shell_ui.py +0 -42
- {wizlib-2.0.18 → wizlib-3.0.0}/README.md +0 -0
- {wizlib-2.0.18 → wizlib-3.0.0}/wizlib/__init__.py +0 -0
- {wizlib-2.0.18 → wizlib-3.0.0}/wizlib/error.py +0 -0
- {wizlib-2.0.18 → wizlib-3.0.0}/wizlib/parser.py +0 -0
- {wizlib-2.0.18 → wizlib-3.0.0}/wizlib/rlinput.py +0 -0
- {wizlib-2.0.18 → wizlib-3.0.0}/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):
|
|
@@ -3,11 +3,15 @@ from pathlib import Path
|
|
|
3
3
|
|
|
4
4
|
from wizlib.class_family import ClassFamily
|
|
5
5
|
from wizlib.config_handler import ConfigHandler
|
|
6
|
-
from wizlib.
|
|
6
|
+
from wizlib.stream_handler import StreamHandler
|
|
7
7
|
from wizlib.super_wrapper import SuperWrapper
|
|
8
8
|
from wizlib.ui import UI
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
class CommandCancellation(BaseException):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
11
15
|
class WizCommand(ClassFamily, SuperWrapper):
|
|
12
16
|
"""Define all the args you want, but stdin always works."""
|
|
13
17
|
|
|
@@ -21,12 +25,10 @@ class WizCommand(ClassFamily, SuperWrapper):
|
|
|
21
25
|
Add global arguments in the base class. Not wrapped."""
|
|
22
26
|
pass
|
|
23
27
|
|
|
24
|
-
def __init__(self, **vals):
|
|
28
|
+
def __init__(self, app=None, **vals):
|
|
29
|
+
self.app = app
|
|
25
30
|
for key in vals:
|
|
26
31
|
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
32
|
|
|
31
33
|
def handle_vals(self):
|
|
32
34
|
"""Clean up vals, calculate any, ask through UI, etc. - override
|
|
@@ -43,9 +45,12 @@ class WizCommand(ClassFamily, SuperWrapper):
|
|
|
43
45
|
def execute(self, method, *args, **kwargs):
|
|
44
46
|
"""Actually perform the command - override and wrap this via
|
|
45
47
|
SuperWrapper"""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
try:
|
|
49
|
+
self.handle_vals()
|
|
50
|
+
result = method(self, *args, **kwargs)
|
|
51
|
+
return result
|
|
52
|
+
except CommandCancellation as cancellation:
|
|
53
|
+
self.status = str(cancellation) if str(cancellation) else None
|
|
49
54
|
|
|
50
55
|
|
|
51
56
|
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
|
|
@@ -129,71 +137,9 @@ class Chooser(Prompt):
|
|
|
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,37 +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
|
-
|
|
74
|
-
|
|
75
53
|
def write(key):
|
|
76
54
|
sys.stderr.write(key)
|
|
77
55
|
sys.stderr.flush()
|
|
78
56
|
|
|
79
57
|
|
|
80
58
|
# "Fill" refers to the lighter text in the editor, to the right of (or instead
|
|
81
|
-
# of) user-typed
|
|
59
|
+
# of) user-typed texS.
|
|
82
60
|
#
|
|
83
61
|
# States:
|
|
84
62
|
# - USER: User has typed something, so no fill
|
|
@@ -108,63 +86,63 @@ class ShellLineEditor: # pragma: nocover
|
|
|
108
86
|
self.fillstate = FillState.DEFAULT
|
|
109
87
|
|
|
110
88
|
def edit(self):
|
|
111
|
-
write(RESET)
|
|
89
|
+
write(S.RESET)
|
|
112
90
|
while True:
|
|
113
91
|
self.write_fill()
|
|
114
92
|
key = readkey()
|
|
115
93
|
self.clear_fill()
|
|
116
|
-
if key == RETURN:
|
|
94
|
+
if key == S.RETURN:
|
|
117
95
|
break
|
|
118
96
|
if key.isprintable():
|
|
119
|
-
write(BOLD + key + self.buf[self.pos:] +
|
|
120
|
-
('\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)
|
|
121
99
|
self.buf = self.buf[:self.pos] + key + self.buf[self.pos:]
|
|
122
100
|
self.pos += 1
|
|
123
101
|
self.fillstate = FillState.USER
|
|
124
|
-
elif (key in [BACKSPACE, KILL]) and self.has_fill:
|
|
102
|
+
elif (key in [S.BACKSPACE, S.KILL]) and self.has_fill:
|
|
125
103
|
# Backspace clears the fill
|
|
126
104
|
self.fillstate = FillState.BLANK
|
|
127
|
-
elif (key == BACKSPACE) and (self.pos > 0):
|
|
128
|
-
write(BOLD + '\b' + self.buf[self.pos:] + ' ' +
|
|
129
|
-
('\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)
|
|
130
108
|
self.buf = self.buf[:self.pos-1] + self.buf[self.pos:]
|
|
131
109
|
self.pos -= 1
|
|
132
110
|
self.fillstate = FillState.USER if (
|
|
133
111
|
self.pos > 0) else FillState.DEFAULT
|
|
134
|
-
elif key == LEFT and self.pos > 0:
|
|
112
|
+
elif key == S.LEFT and self.pos > 0:
|
|
135
113
|
self.move_left()
|
|
136
114
|
self.fillstate = FillState.USER
|
|
137
|
-
elif key == RIGHT and self.pos < len(self.buf):
|
|
115
|
+
elif key == S.RIGHT and self.pos < len(self.buf):
|
|
138
116
|
self.move_right()
|
|
139
117
|
self.fillstate = FillState.USER
|
|
140
|
-
elif key == BEGINNING and self.pos > 0:
|
|
118
|
+
elif key == S.BEGINNING and self.pos > 0:
|
|
141
119
|
self.move_beginning()
|
|
142
120
|
self.fillstate = FillState.USER
|
|
143
|
-
elif key == END and self.has_fill:
|
|
121
|
+
elif key == S.END and self.has_fill:
|
|
144
122
|
self.accept_fill()
|
|
145
123
|
self.fillstate = FillState.USER
|
|
146
|
-
elif key == END and self.pos < len(self.buf):
|
|
124
|
+
elif key == S.END and self.pos < len(self.buf):
|
|
147
125
|
self.move_end_buf()
|
|
148
126
|
self.fillstate = FillState.USER
|
|
149
|
-
elif key == TAB and (choices := self.valid_choices):
|
|
127
|
+
elif key == S.TAB and (choices := self.valid_choices):
|
|
150
128
|
self.index = (self.index + 1) % len(choices)
|
|
151
129
|
self.fillstate = FillState.TAB
|
|
152
|
-
elif key == SHIFT_TAB and self.index > -1:
|
|
130
|
+
elif key == S.SHIFT_TAB and self.index > -1:
|
|
153
131
|
self.index = (self.index - 1) % len(self.valid_choices)
|
|
154
132
|
self.fillstate = FillState.TAB
|
|
155
|
-
elif key == LEFT_WORD and self.pos > 0:
|
|
133
|
+
elif key == S.LEFT_WORD and self.pos > 0:
|
|
156
134
|
while (self.pos > 0) and self.is_sep(self.pos - 1):
|
|
157
135
|
self.move_left()
|
|
158
136
|
while (self.pos > 0) and not self.is_sep(self.pos - 1):
|
|
159
137
|
self.move_left()
|
|
160
138
|
self.fillstate = FillState.USER
|
|
161
|
-
elif key == RIGHT_WORD and self.pos < len(self.buf):
|
|
139
|
+
elif key == S.RIGHT_WORD and self.pos < len(self.buf):
|
|
162
140
|
while (self.pos < len(self.buf)) and self.is_sep(self.pos):
|
|
163
141
|
self.move_right()
|
|
164
142
|
while (self.pos < len(self.buf)) and not self.is_sep(self.pos):
|
|
165
143
|
self.move_right()
|
|
166
144
|
self.fillstate = FillState.USER
|
|
167
|
-
elif key == KILL:
|
|
145
|
+
elif key == S.KILL:
|
|
168
146
|
chars = len(self.buf) - self.pos
|
|
169
147
|
write(' ' * chars + '\b' * chars)
|
|
170
148
|
self.buf = self.buf[:self.pos]
|
|
@@ -172,11 +150,11 @@ class ShellLineEditor: # pragma: nocover
|
|
|
172
150
|
else:
|
|
173
151
|
pass
|
|
174
152
|
self.accept_fill()
|
|
175
|
-
write(RETURN)
|
|
153
|
+
write(S.RETURN)
|
|
176
154
|
return self.buf
|
|
177
155
|
|
|
178
156
|
def is_sep(self, pos):
|
|
179
|
-
return (self.buf[pos] in
|
|
157
|
+
return (self.buf[pos] in S.SEPARATORS)
|
|
180
158
|
|
|
181
159
|
@property
|
|
182
160
|
def has_fill(self):
|
|
@@ -184,16 +162,16 @@ class ShellLineEditor: # pragma: nocover
|
|
|
184
162
|
|
|
185
163
|
def accept_fill(self):
|
|
186
164
|
if self.has_fill:
|
|
187
|
-
write(BOLD + self.fill + RESET)
|
|
165
|
+
write(S.BOLD + self.fill + S.RESET)
|
|
188
166
|
self.buf += self.fill
|
|
189
167
|
self.pos = len(self.buf)
|
|
190
168
|
|
|
191
169
|
def move_left(self):
|
|
192
|
-
write(LEFT)
|
|
170
|
+
write(S.LEFT)
|
|
193
171
|
self.pos -= 1
|
|
194
172
|
|
|
195
173
|
def move_right(self):
|
|
196
|
-
write(RIGHT)
|
|
174
|
+
write(S.RIGHT)
|
|
197
175
|
self.pos += 1
|
|
198
176
|
|
|
199
177
|
def move_beginning(self):
|
|
@@ -207,7 +185,7 @@ class ShellLineEditor: # pragma: nocover
|
|
|
207
185
|
def write_fill(self):
|
|
208
186
|
if self.fillstate in [FillState.DEFAULT, FillState.TAB]:
|
|
209
187
|
self.move_end_buf()
|
|
210
|
-
write(FAINT + self.fill + '\b' * len(self.fill) + RESET)
|
|
188
|
+
write(S.FAINT + self.fill + '\b' * len(self.fill) + S.RESET)
|
|
211
189
|
|
|
212
190
|
def clear_fill(self):
|
|
213
191
|
if self.has_fill:
|
|
@@ -227,7 +205,7 @@ class ShellLineEditor: # pragma: nocover
|
|
|
227
205
|
@property
|
|
228
206
|
def last_word(self):
|
|
229
207
|
index = next((c for c in reversed(range(len(self.buf)))
|
|
230
|
-
if self.buf[c]
|
|
208
|
+
if self.buf[c] == S.SPACE), -1)
|
|
231
209
|
return self.buf[index+1:]
|
|
232
210
|
|
|
233
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
|
|
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.18/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.18/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
|