wizlib 2.0.19__py3-none-any.whl → 3.0.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.

Potentially problematic release.


This version of wizlib might be problematic. Click here for more details.

wizlib/app.py CHANGED
@@ -8,13 +8,16 @@ from wizlib.command import WizHelpCommand
8
8
  from wizlib.super_wrapper import SuperWrapper
9
9
  from wizlib.parser import WizParser
10
10
  from wizlib.ui import UI
11
- from wizlib.ui.shell_ui import ShellUI
12
11
 
13
12
 
14
13
  RED = '\033[91m'
15
14
  RESET = '\033[0m'
16
15
 
17
16
 
17
+ class AppCancellation(BaseException):
18
+ pass
19
+
20
+
18
21
  class WizApp:
19
22
  """Root of all WizLib-based CLI applications. Subclass it. Can be
20
23
  instantiated and then run multiple commands."""
@@ -22,67 +25,74 @@ class WizApp:
22
25
  base_command = None
23
26
  name = ''
24
27
 
28
+ # Set some default types so linting works
29
+ ui: UI
30
+
25
31
  @classmethod
26
32
  def main(cls): # pragma: nocover
27
33
  """Call this from a __main__ entrypoint"""
28
- cls.run(*sys.argv[1:], debug=os.getenv('DEBUG'))
34
+ cls.start(*sys.argv[1:], debug=os.getenv('DEBUG'))
29
35
 
30
36
  @classmethod
31
- def run(cls, *args, debug=False):
37
+ def start(cls, *args, debug=False):
32
38
  """Call this from a Python entrypoint"""
33
39
  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:
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:
45
51
  if debug:
46
52
  raise error
47
53
  else:
48
- print(f"\n{RED}{type(error).__name__}: " +
49
- f"{error}{RESET}\n", file=sys.stderr)
54
+ name = type(error).__name__
55
+ print(f"\n{RED}{name}{': ' if str(error) else ''}" +
56
+ f"{error}{RESET}", file=sys.stderr)
50
57
  sys.exit(1)
51
58
 
52
59
  @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)
60
+ def initialize(cls, **vals):
61
+ """Converts argparse values (strings) into actual handlers and
62
+ instantiates the app"""
63
+ handlers = {}
58
64
  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))
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)
63
69
 
64
- subparsers = cls.parser.add_subparsers(dest='command')
65
- for command in cls.base_command.family_members('name'):
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'):
66
77
  key = command.get_member_attr('key')
67
78
  aliases = [key] if key else []
68
79
  subparser = subparsers.add_parser(command.name, aliases=aliases)
69
80
  command.add_args(subparser)
70
81
 
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"""
82
+ def run(self, *args):
83
+ vals = vars(self.parser.parse_args(args))
80
84
  if 'help' in vals:
81
85
  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)
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)
wizlib/class_family.py CHANGED
@@ -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):
wizlib/command.py CHANGED
@@ -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.input_handler import InputHandler
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
- self.handle_vals()
47
- result = method(self, *args, **kwargs)
48
- return result
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):
wizlib/config_handler.py CHANGED
@@ -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, value=None):
27
- self.file = value
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.appname:
38
- localpath = Path.cwd() / f".{self.appname}.yml"
39
- homepath = Path.home() / f".{self.appname}.yml"
40
- if (envvar := self.env(self.appname + '-config')):
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
- handler = cls()
81
-
82
- def fake_env(name):
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
wizlib/handler.py CHANGED
@@ -1,25 +1,24 @@
1
1
  from argparse import Action
2
2
 
3
+ from wizlib.parser import WizParser
4
+
3
5
 
4
6
  class Handler:
5
7
  """Base class for handlers"""
6
8
 
7
- appname = None
8
9
  default = ''
10
+ app = None
9
11
 
10
12
  @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
- }
13
+ def add_args(cls, parser: WizParser):
14
+ parser.add_argument(
15
+ f"--{cls.name}",
16
+ f"-{cls.name[0]}",
17
+ default=cls.default)
17
18
 
18
19
  @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
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 InputHandler(Handler):
10
+ class StreamHandler(Handler):
10
11
 
11
- name = 'input'
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 InputHandler with forced values, for testing"""
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
wizlib/ui/__init__.py CHANGED
@@ -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
- # class UserCancelError(Exception):
199
- # pass
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.stdout.write("\x1b[6n")
36
- sys.stdout.flush()
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 text.
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 WORD_SEPARATORS)
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
wizlib/ui/shell_ui.py CHANGED
@@ -1,12 +1,22 @@
1
+ from enum import StrEnum
1
2
  import sys
2
3
 
3
4
  from readchar import readkey
5
+
4
6
  from wizlib.rlinput import rlinput
5
- from wizlib.ui import UI
6
- from wizlib.ui.shell_line_editor import ShellLineEditor
7
+ from wizlib.ui import UI, Chooser, Emphasis
8
+ from wizlib.ui.shell.line_editor import ShellLineEditor
9
+ from wizlib.ui.shell import S
7
10
 
8
11
  INTERACTIVE = sys.stdin.isatty()
9
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
+
10
20
 
11
21
  class ShellUI(UI):
12
22
 
@@ -17,26 +27,41 @@ class ShellUI(UI):
17
27
 
18
28
  name = "shell"
19
29
 
20
- def send(self, value: str):
30
+ def send(self, value: str = '', emphasis: Emphasis = Emphasis.GENERAL):
21
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"""
22
37
  if value:
23
- print(value, file=sys.stderr)
38
+ sys.stderr.write(S.GREEN + value + S.RESET)
39
+ sys.stderr.flush()
24
40
 
25
- def get_option_mini(self, chooser):
41
+ def get_option(self, chooser: Chooser):
26
42
  """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)
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
35
58
 
36
59
  def get_text(self, prompt='', choices=[], default=''):
37
60
  """Allow the user to input an arbitrary line of text, with possible tab
38
61
  completion"""
39
- sys.stderr.write(prompt)
40
- sys.stderr.flush()
41
- value = ShellLineEditor(choices, default).edit()
62
+ self.ask(prompt)
63
+ if INTERACTIVE:
64
+ value = ShellLineEditor(choices, default).edit()
65
+ else:
66
+ value = input()
42
67
  return value
wizlib/ui_handler.py CHANGED
@@ -15,9 +15,10 @@ class UIHandler(Handler):
15
15
  default = 'shell'
16
16
 
17
17
  @classmethod
18
- def setup(cls, name=None):
19
- def ui(uitype='shell'):
20
- """Instead of instantiating the handler, return a new instance of
21
- the chosen UI object."""
22
- return UI.family_member('name', uitype)()
23
- return ui
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/util.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from unittest.mock import Mock, patch
2
2
 
3
3
 
4
- RK = 'wizlib.ui.shell_line_editor.readkey'
4
+ RK = 'wizlib.ui.shell.line_editor.readkey'
5
5
 
6
6
 
7
7
  def mock_keys(keys: str):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wizlib
3
- Version: 2.0.19
3
+ Version: 3.0.0
4
4
  Summary: Framework for flexible and powerful command-line applications
5
5
  License: MIT
6
6
  Author: Steampunk Wizard
@@ -0,0 +1,20 @@
1
+ wizlib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ wizlib/app.py,sha256=O5126FXF2_JSGlHycMv3-ec7EfYI875wzuYsDrxq5ZI,3263
3
+ wizlib/class_family.py,sha256=tORSVAaPeWTQMcz2DX-MQClj1GQR3vCmkALPXxHa_pk,4506
4
+ wizlib/command.py,sha256=aiDYMBXuhKTwPN5xdv_qOrvxvXGibLnqxwQgW0aUVQo,1686
5
+ wizlib/config_handler.py,sha256=hoDavSMiGM_7PAHI8XIwC8nxPWOZDk302ryyjluoLGg,2588
6
+ wizlib/error.py,sha256=ypwdMOYhtgKWd48ccfOX8idmCXmm-Skwx3gkPwqJB3c,46
7
+ wizlib/handler.py,sha256=Oz80aPhDyeY9tdppZ1dvtN-19JU5ydEDVW6jtppVoD4,446
8
+ wizlib/parser.py,sha256=O34azN4ttVfwwAsza0hujxGxDpzc4xUEVAf26DXJS5g,1505
9
+ wizlib/rlinput.py,sha256=l00Pa3rxNeY6LJgz8Aws_rTKoEchw33fuL8yqHF9_-o,1754
10
+ wizlib/stream_handler.py,sha256=i1EgcBrDiYhFK-CI8At7JtUtMCNunJmSkJLSlili-as,663
11
+ wizlib/super_wrapper.py,sha256=F834ytHqA7zegTD1ezk_uxlF9PLygh84wReuiqcI7BI,272
12
+ wizlib/ui/__init__.py,sha256=JIFBvlqPpHyU6hnyXoherf9va1_ynFuDKlMnJn1SCm8,4249
13
+ wizlib/ui/shell/__init__.py,sha256=sPrYe4bG_Xf7Nwssx_dqXVk9jeyYBFUjh4oLdlSOeRY,943
14
+ wizlib/ui/shell/line_editor.py,sha256=frpsqU5NggcvEz3XeB8YyqgmlExlyx6K1CKPzvARtPk,7419
15
+ wizlib/ui/shell_ui.py,sha256=qg7DRAYTkkIfe7BKCfclcaupA42YaAk2expIV1fSm40,2030
16
+ wizlib/ui_handler.py,sha256=JoZadtw9DKAtGvHKP3_BJF2NaYqmcQYNdsY4PeRnOjg,634
17
+ wizlib/util.py,sha256=x1SyL3iXot0ET2r8Jjb2ySTd_J-3Uu7J1b_CHvFgzew,156
18
+ wizlib-3.0.0.dist-info/METADATA,sha256=t1ESltdsHBUdEP5dp4WR4VnWcYVxfLfPwULW57KPQRA,1780
19
+ wizlib-3.0.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
20
+ wizlib-3.0.0.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- wizlib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- wizlib/app.py,sha256=LCTfM4eQJOhPcmDOyHbr59IETLkU19WMSbigmUnQlO8,2919
3
- wizlib/class_family.py,sha256=EX1ZZmrmC2M-PV_iorHXCWqETE2thrwQE45RtHxojbs,4424
4
- wizlib/command.py,sha256=X6hytea4BVWl5cUN7QXqGarqCRjmFoBVdCAtxMNfWBo,1588
5
- wizlib/config_handler.py,sha256=alHpJzFY39XXDv94W4fLwda6hjruySADAJD944rE3HU,2639
6
- wizlib/error.py,sha256=ypwdMOYhtgKWd48ccfOX8idmCXmm-Skwx3gkPwqJB3c,46
7
- wizlib/handler.py,sha256=ddCO1h0llSRWMn5Wn952j8_Iad5D8CXDe0iboMzy8zs,693
8
- wizlib/input_handler.py,sha256=QFiR9CC5bkW7HofNAKZEq_hL089hbWzyrFGAjAjvozg,624
9
- wizlib/parser.py,sha256=O34azN4ttVfwwAsza0hujxGxDpzc4xUEVAf26DXJS5g,1505
10
- wizlib/rlinput.py,sha256=l00Pa3rxNeY6LJgz8Aws_rTKoEchw33fuL8yqHF9_-o,1754
11
- wizlib/super_wrapper.py,sha256=F834ytHqA7zegTD1ezk_uxlF9PLygh84wReuiqcI7BI,272
12
- wizlib/ui/__init__.py,sha256=55tzM_XphYFqm_jVxQc5vkfPLgD1Z3aqSjDl50GM44A,6139
13
- wizlib/ui/shell_line_editor.py,sha256=3YZmCskpJrp8Io-pBwLNF56uTG6IsBSCoiBPVUF79mI,7820
14
- wizlib/ui/shell_ui.py,sha256=UdlMzCB2zk5NHT6KFE6n7aAMrXRb9lEAuG_gEg8mscA,1289
15
- wizlib/ui_handler.py,sha256=nvMRuGVCwJ-_H59iZQ72--_tiiqXAsSHP7kYnd43P68,716
16
- wizlib/util.py,sha256=6crNn_nI5Ytx5yu4IzcmI0iCfwCWbCN5NOnPcrYKB1c,156
17
- wizlib-2.0.19.dist-info/METADATA,sha256=ctEkxTliADbtJvJWFaqYanjbESiJkep-2piMqlmHLjk,1781
18
- wizlib-2.0.19.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
19
- wizlib-2.0.19.dist-info/RECORD,,