pyclack-lib 1.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.
- pyclack/__init__.py +7 -0
- pyclack/config/__init__.py +3 -0
- pyclack/config/theme_conf.py +32 -0
- pyclack/prompts/__init__.py +19 -0
- pyclack/prompts/ask.py +258 -0
- pyclack/prompts/autocomplete.py +466 -0
- pyclack/prompts/autocomplete_multiselect.py +564 -0
- pyclack/prompts/confirm.py +203 -0
- pyclack/prompts/multiline.py +279 -0
- pyclack/prompts/multiselect.py +472 -0
- pyclack/prompts/password.py +251 -0
- pyclack/prompts/pick_date.py +444 -0
- pyclack/prompts/prompt_base.py +210 -0
- pyclack/prompts/select.py +346 -0
- pyclack/prompts/select_key.py +318 -0
- pyclack/prompts/select_path.py +521 -0
- pyclack/prompts/util.py +785 -0
- pyclack/prompts_async/__init__.py +11 -0
- pyclack/prompts_async/async_prompts.py +458 -0
- pyclack/py.typed +0 -0
- pyclack/renderer/__init__.py +9 -0
- pyclack/renderer/render_frame.py +122 -0
- pyclack/renderer/symbols.py +233 -0
- pyclack/renderer/text.py +146 -0
- pyclack/renderer/themes.py +1479 -0
- pyclack/terminal/__init__.py +4 -0
- pyclack/terminal/base_key.py +37 -0
- pyclack/terminal/cursor_controller.py +139 -0
- pyclack/terminal/echo_controller.py +60 -0
- pyclack/terminal/key_reader.py +60 -0
- pyclack/terminal/os_utils.py +19 -0
- pyclack/terminal/posix_keys.py +40 -0
- pyclack/terminal/stdout.py +33 -0
- pyclack/terminal/win_keys.py +34 -0
- pyclack/widgets/__init__.py +13 -0
- pyclack/widgets/activity.py +317 -0
- pyclack/widgets/box.py +81 -0
- pyclack/widgets/cancel.py +37 -0
- pyclack/widgets/intro.py +33 -0
- pyclack/widgets/log/__init__.py +1 -0
- pyclack/widgets/log/log.py +214 -0
- pyclack/widgets/note.py +51 -0
- pyclack/widgets/outro.py +34 -0
- pyclack/widgets/progress.py +329 -0
- pyclack/widgets/spinner.py +283 -0
- pyclack/widgets/stream/__init__.py +3 -0
- pyclack/widgets/stream/stream.py +40 -0
- pyclack/widgets/stream/util.py +81 -0
- pyclack/widgets/task_log.py +218 -0
- pyclack/widgets_async/__init__.py +11 -0
- pyclack/widgets_async/async_log/__init__.py +1 -0
- pyclack/widgets_async/async_log/async_log.py +85 -0
- pyclack/widgets_async/async_stream/__init__.py +3 -0
- pyclack/widgets_async/async_stream/async_stream.py +33 -0
- pyclack/widgets_async/async_widgets.py +502 -0
- pyclack_lib-1.0.0.dist-info/METADATA +2212 -0
- pyclack_lib-1.0.0.dist-info/RECORD +58 -0
- pyclack_lib-1.0.0.dist-info/WHEEL +4 -0
pyclack/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .config import set_print_mode_ascii as set_print_mode_ascii
|
|
2
|
+
from .config import set_active_theme as set_active_theme
|
|
3
|
+
from .config import get_active_theme as get_active_theme
|
|
4
|
+
from .prompts import CancelException as CancelException
|
|
5
|
+
from .prompts import ClackOption as ClackOption
|
|
6
|
+
from .prompts import Alignment as Alignment
|
|
7
|
+
from .renderer import Themes as Themes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from ..renderer import Theme, Themes
|
|
2
|
+
from ..renderer import symbols
|
|
3
|
+
|
|
4
|
+
active_theme: Theme = Themes.DEFAULT
|
|
5
|
+
|
|
6
|
+
def set_active_theme(theme: Theme) -> None:
|
|
7
|
+
'''
|
|
8
|
+
Set the active theme for the application. This function updates the global variable `active_theme` to the provided theme.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
theme (Theme): The theme to set as active.
|
|
12
|
+
'''
|
|
13
|
+
|
|
14
|
+
global active_theme
|
|
15
|
+
active_theme = theme
|
|
16
|
+
|
|
17
|
+
def get_active_theme() -> Theme:
|
|
18
|
+
'''
|
|
19
|
+
Get the currently active theme for the application.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Theme: The currently active theme.
|
|
23
|
+
'''
|
|
24
|
+
|
|
25
|
+
return active_theme
|
|
26
|
+
|
|
27
|
+
def set_print_mode_ascii():
|
|
28
|
+
'''
|
|
29
|
+
Set the print mode to always use ASCII symbols. This function updates the global variable `always_use_ascii` to True.
|
|
30
|
+
'''
|
|
31
|
+
|
|
32
|
+
symbols.always_use_ascii = True
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .prompt_base import CancelException as CancelException
|
|
2
|
+
from .prompt_base import PromptState as PromptState
|
|
3
|
+
from .prompt_base import PromptBase as PromptBase
|
|
4
|
+
from .prompt_base import ClackOption as ClackOption
|
|
5
|
+
from .prompt_base import Alignment
|
|
6
|
+
from . import util as util
|
|
7
|
+
|
|
8
|
+
# Prompts
|
|
9
|
+
from .ask import ask as ask
|
|
10
|
+
from .password import password as password
|
|
11
|
+
from .confirm import confirm as confirm
|
|
12
|
+
from .pick_date import pick_date as pick_date
|
|
13
|
+
from .multiline import multiline as multiline
|
|
14
|
+
from .select import select as select
|
|
15
|
+
from .multiselect import multiselect as multiselect
|
|
16
|
+
from .autocomplete import autocomplete as autocomplete
|
|
17
|
+
from .autocomplete_multiselect import autocomplete_multiselect as autocomplete_multiselect
|
|
18
|
+
from .select_key import select_key as select_key
|
|
19
|
+
from .select_path import select_path as select_path
|
pyclack/prompts/ask.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
from .util import build_wrapped_lines, build_message_header, build_message_close, apply_cursor_style, TextBoxController
|
|
2
|
+
from ..renderer import RenderFrame, Text, FrameBuilder, Style, Theme
|
|
3
|
+
from .prompt_base import PromptBase, CancelException
|
|
4
|
+
from ..terminal import CursorController as cc
|
|
5
|
+
from typing import Callable, override
|
|
6
|
+
from ..config import get_active_theme
|
|
7
|
+
from ..terminal import Stdout
|
|
8
|
+
from copy import copy
|
|
9
|
+
|
|
10
|
+
def ask(message: str,
|
|
11
|
+
placeholder: str | None = None,
|
|
12
|
+
initial_value: str | None = None,
|
|
13
|
+
validate: Callable[[str], str | None] | None = None,
|
|
14
|
+
abort_time: float | None = None) -> str:
|
|
15
|
+
'''
|
|
16
|
+
Ask the user for input with a message, placeholder, initial value, and validation function.
|
|
17
|
+
Controls are as follows:
|
|
18
|
+
- Use the arrow keys to move the cursor within the input.
|
|
19
|
+
- Press 'Enter' to submit the input.
|
|
20
|
+
- Press 'Backspace' to delete the character before the cursor.
|
|
21
|
+
- Press 'Ctrl+C' or 'esc' to cancel the operation.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
message (str): The message to display to the user.
|
|
25
|
+
placeholder (str, optional): The placeholder text to display when the input is empty.
|
|
26
|
+
initial_value (str, optional): The initial value of the input.
|
|
27
|
+
validate (Callable[[str], str | None], optional): A function to validate the input.
|
|
28
|
+
abort_time (float, optional): Floating point number representing seconds in time before the prompt is auto-cancelled, if set to None the prompt will never auto-cancel. Defaults to None.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: The user's input.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
CancelException: If the user cancels the operation.
|
|
35
|
+
'''
|
|
36
|
+
|
|
37
|
+
prompt: Ask = Ask(message, placeholder, initial_value, validate, abort_time)
|
|
38
|
+
return prompt.text_box_controller.get_input()
|
|
39
|
+
|
|
40
|
+
class Ask(PromptBase):
|
|
41
|
+
'''
|
|
42
|
+
A prompt for asking the user for text input.
|
|
43
|
+
'''
|
|
44
|
+
|
|
45
|
+
def __init__(self,
|
|
46
|
+
message: str,
|
|
47
|
+
placeholder: str | None,
|
|
48
|
+
initial_value: str | None,
|
|
49
|
+
validate: Callable[[str], str | None] | None,
|
|
50
|
+
abort_time: float | None):
|
|
51
|
+
'''
|
|
52
|
+
Initialize an Ask prompt.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
message (str): The message to display to the user.
|
|
56
|
+
placeholder (str, optional): The placeholder text to display when the input is empty.
|
|
57
|
+
initial_value (str, optional): The initial value of the input.
|
|
58
|
+
validate (Callable[[str], str | None], optional): A function to validate the input.
|
|
59
|
+
abort_time (float, optional): Floating point number representing seconds in time before the prompt is auto-cancelled, if set to None the prompt will never auto-cancel. Defaults to None.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
CancelException: If the user cancels the operation.
|
|
63
|
+
'''
|
|
64
|
+
|
|
65
|
+
super().__init__()
|
|
66
|
+
|
|
67
|
+
self.message: str = message
|
|
68
|
+
self.placeholder: str | None = placeholder
|
|
69
|
+
self.initial_value: str | None = initial_value
|
|
70
|
+
self.validate: Callable[[str], str | None] | None = validate
|
|
71
|
+
|
|
72
|
+
self.render_frame: RenderFrame = RenderFrame()
|
|
73
|
+
self.text_box_controller: TextBoxController = TextBoxController()
|
|
74
|
+
if initial_value:
|
|
75
|
+
self.text_box_controller.set_input(initial_value, 0)
|
|
76
|
+
self.text_box_controller.cursor_end()
|
|
77
|
+
|
|
78
|
+
self.propagate_key_after_error = True
|
|
79
|
+
self.abort_time = abort_time
|
|
80
|
+
|
|
81
|
+
self.allowed_inputs: tuple[str, ...] = self._construct_allowed_inputs()
|
|
82
|
+
|
|
83
|
+
super().activate()
|
|
84
|
+
|
|
85
|
+
def _construct_allowed_inputs(self) -> tuple[str, ...]:
|
|
86
|
+
'''
|
|
87
|
+
Construct a tuple of allowed inputs for the prompt.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
tuple[str]: A tuple of allowed input keys.
|
|
91
|
+
'''
|
|
92
|
+
|
|
93
|
+
allowed_chars: tuple[str, ...] = tuple(chr(i) for i in range(32, 127))
|
|
94
|
+
return ('BACKSPACE', 'ENTER', 'LEFT', 'RIGHT', 'TAB', 'SPACE') + allowed_chars
|
|
95
|
+
|
|
96
|
+
@override
|
|
97
|
+
def handle_active(self, key: str | None) -> bool:
|
|
98
|
+
Stdout.put(cc.hide_cursor())
|
|
99
|
+
|
|
100
|
+
if key not in self.allowed_inputs: key = ''
|
|
101
|
+
|
|
102
|
+
theme: Theme = get_active_theme()
|
|
103
|
+
step_marker_active: str = theme.symbols.step_marker_active.resolve()
|
|
104
|
+
connector_bar_vertical: str = theme.symbols.connector_bar_vertical.resolve()
|
|
105
|
+
connector_bar_end: str = theme.symbols.connector_bar_end.resolve()
|
|
106
|
+
prefix_active: Text = Text(f'{connector_bar_vertical} ', theme.active)
|
|
107
|
+
prefix_muted: Text = Text(f'{connector_bar_vertical} ', theme.muted)
|
|
108
|
+
|
|
109
|
+
match key:
|
|
110
|
+
case 'BACKSPACE': self.text_box_controller.delete()
|
|
111
|
+
case 'ENTER': return True # Advance to the next state (submit)
|
|
112
|
+
case 'LEFT': self.text_box_controller.cursor_left()
|
|
113
|
+
case 'RIGHT': self.text_box_controller.cursor_right()
|
|
114
|
+
case _:
|
|
115
|
+
map: dict[str, str] = {
|
|
116
|
+
'SPACE': ' ',
|
|
117
|
+
'TAB': '\t'}
|
|
118
|
+
char: str = map.get(key, key) # Translate key to character (if applicable)
|
|
119
|
+
if key != '': self.text_box_controller.insert(char)
|
|
120
|
+
|
|
121
|
+
# Create and render next frame based on the current input buffer and state
|
|
122
|
+
input_buffer: str = self.text_box_controller.get_input()
|
|
123
|
+
input_index: int = self.text_box_controller.get_cursor_position()
|
|
124
|
+
|
|
125
|
+
frame_builder: FrameBuilder = FrameBuilder()
|
|
126
|
+
frame_builder.add_line(prefix_muted)
|
|
127
|
+
message_lines: list[Text] = build_message_header(
|
|
128
|
+
self.message,
|
|
129
|
+
theme.text,
|
|
130
|
+
f'{step_marker_active} ',
|
|
131
|
+
theme.active,
|
|
132
|
+
prefix_active)
|
|
133
|
+
frame_builder.add_lines(*message_lines)
|
|
134
|
+
|
|
135
|
+
if self.placeholder and len(input_buffer) <= 0:
|
|
136
|
+
placeholder_text: Text = Text(self.placeholder, theme.muted)
|
|
137
|
+
placeholder_text = apply_cursor_style(placeholder_text, 0, theme.cursor)
|
|
138
|
+
frame_builder.add_lines(*build_wrapped_lines(placeholder_text, prefix_active))
|
|
139
|
+
else:
|
|
140
|
+
input_buffer_text: Text = Text(input_buffer, theme.text)
|
|
141
|
+
input_buffer_text = apply_cursor_style(input_buffer_text, input_index, theme.cursor)
|
|
142
|
+
frame_builder.add_lines(*build_wrapped_lines(input_buffer_text, prefix_active))
|
|
143
|
+
|
|
144
|
+
frame_builder.add_line(Text(connector_bar_end, theme.active))
|
|
145
|
+
|
|
146
|
+
frame: tuple[Text, ...] = frame_builder.build()
|
|
147
|
+
self.render_frame.draw_frame(*frame)
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
@override
|
|
151
|
+
def handle_submit(self) -> bool:
|
|
152
|
+
theme: Theme = get_active_theme()
|
|
153
|
+
step_marker_submit: str = theme.symbols.step_marker_submit.resolve()
|
|
154
|
+
connector_bar_vertical: str = theme.symbols.connector_bar_vertical.resolve()
|
|
155
|
+
prefix_muted: Text = Text(f'{connector_bar_vertical} ', theme.muted)
|
|
156
|
+
|
|
157
|
+
# Create and render next frame based on the current input buffer and state
|
|
158
|
+
input_buffer: str = self.text_box_controller.get_input()
|
|
159
|
+
|
|
160
|
+
frame_builder: FrameBuilder = FrameBuilder()
|
|
161
|
+
frame_builder.add_line(prefix_muted)
|
|
162
|
+
message_lines: list[Text] = build_message_header(
|
|
163
|
+
self.message,
|
|
164
|
+
theme.text,
|
|
165
|
+
f'{step_marker_submit} ',
|
|
166
|
+
theme.submit,
|
|
167
|
+
prefix_muted)
|
|
168
|
+
frame_builder.add_lines(*message_lines)
|
|
169
|
+
|
|
170
|
+
input_buffer_text: Text = Text(input_buffer, theme.muted)
|
|
171
|
+
frame_builder.add_lines(*build_wrapped_lines(input_buffer_text, prefix_muted))
|
|
172
|
+
|
|
173
|
+
frame: tuple[Text, ...] = frame_builder.build()
|
|
174
|
+
self.render_frame.draw_frame(*frame)
|
|
175
|
+
|
|
176
|
+
Stdout.put(cc.show_cursor())
|
|
177
|
+
return False if self.validate and self.validate(input_buffer) else True
|
|
178
|
+
|
|
179
|
+
@override
|
|
180
|
+
def handle_error(self, key: str | None) -> bool:
|
|
181
|
+
Stdout.put(cc.hide_cursor())
|
|
182
|
+
|
|
183
|
+
theme: Theme = get_active_theme()
|
|
184
|
+
step_marker_error: str = theme.symbols.step_marker_error.resolve()
|
|
185
|
+
connector_bar_vertical: str = theme.symbols.connector_bar_vertical.resolve()
|
|
186
|
+
connector_bar_end: str = theme.symbols.connector_bar_end.resolve()
|
|
187
|
+
prefix_muted: Text = Text(f'{connector_bar_vertical} ', theme.muted)
|
|
188
|
+
prefix_error: Text = Text(f'{connector_bar_vertical} ', theme.error)
|
|
189
|
+
closing_prefix_error: Text = Text(f'{connector_bar_end} ', theme.error)
|
|
190
|
+
|
|
191
|
+
# Create and render next frame based on the current input buffer and state
|
|
192
|
+
input_buffer: str = self.text_box_controller.get_input()
|
|
193
|
+
input_index: int = self.text_box_controller.get_cursor_position()
|
|
194
|
+
|
|
195
|
+
frame_builder: FrameBuilder = FrameBuilder()
|
|
196
|
+
frame_builder.add_line(prefix_muted)
|
|
197
|
+
message_lines: list[Text] = build_message_header(
|
|
198
|
+
self.message,
|
|
199
|
+
theme.text,
|
|
200
|
+
f'{step_marker_error} ',
|
|
201
|
+
theme.error,
|
|
202
|
+
prefix_error)
|
|
203
|
+
frame_builder.add_lines(*message_lines)
|
|
204
|
+
|
|
205
|
+
if self.placeholder and len(input_buffer) <= 0:
|
|
206
|
+
placeholder_text: Text = Text(self.placeholder, theme.muted)
|
|
207
|
+
placeholder_text = apply_cursor_style(placeholder_text, 0, theme.cursor)
|
|
208
|
+
frame_builder.add_lines(*build_wrapped_lines(placeholder_text, prefix_error))
|
|
209
|
+
else:
|
|
210
|
+
input_buffer_text: Text = Text(input_buffer, theme.text)
|
|
211
|
+
input_buffer_text = apply_cursor_style(input_buffer_text, input_index, theme.cursor)
|
|
212
|
+
frame_builder.add_lines(*build_wrapped_lines(input_buffer_text, prefix_error))
|
|
213
|
+
|
|
214
|
+
validation_error_message: str = self.validate(input_buffer) # self.validate must be defined if we are in the error state, and it must return something
|
|
215
|
+
error_lines: list[Text] = build_message_close(
|
|
216
|
+
validation_error_message,
|
|
217
|
+
theme.error,
|
|
218
|
+
prefix_error,
|
|
219
|
+
closing_prefix_error)
|
|
220
|
+
frame_builder.add_lines(*error_lines)
|
|
221
|
+
|
|
222
|
+
frame: tuple[Text, ...] = frame_builder.build()
|
|
223
|
+
self.render_frame.draw_frame(*frame)
|
|
224
|
+
|
|
225
|
+
if key == 'ENTER' or key is None: return False
|
|
226
|
+
else: return True
|
|
227
|
+
|
|
228
|
+
@override
|
|
229
|
+
def handle_cancel(self) -> None:
|
|
230
|
+
theme: Theme = get_active_theme()
|
|
231
|
+
step_marker_cancel: str = theme.symbols.step_marker_cancel.resolve()
|
|
232
|
+
connector_bar_vertical: str = theme.symbols.connector_bar_vertical.resolve()
|
|
233
|
+
prefix_muted: Text = Text(f'{connector_bar_vertical} ', theme.muted)
|
|
234
|
+
|
|
235
|
+
# Create and render next frame based on the current input buffer and state
|
|
236
|
+
input_buffer: str = self.text_box_controller.get_input()
|
|
237
|
+
|
|
238
|
+
frame_builder: FrameBuilder = FrameBuilder()
|
|
239
|
+
frame_builder.add_line(prefix_muted)
|
|
240
|
+
message_lines: list[Text] = build_message_header(
|
|
241
|
+
self.message,
|
|
242
|
+
theme.text,
|
|
243
|
+
f'{step_marker_cancel} ',
|
|
244
|
+
theme.cancel,
|
|
245
|
+
prefix_muted)
|
|
246
|
+
frame_builder.add_lines(*message_lines)
|
|
247
|
+
|
|
248
|
+
text_style: Style = copy(theme.muted)
|
|
249
|
+
text_style.strikethrough = True
|
|
250
|
+
if len(input_buffer) > 0:
|
|
251
|
+
input_buffer_text: Text = Text(input_buffer, text_style)
|
|
252
|
+
frame_builder.add_lines(*build_wrapped_lines(input_buffer_text, prefix_muted))
|
|
253
|
+
|
|
254
|
+
frame: tuple[Text, ...] = frame_builder.build()
|
|
255
|
+
self.render_frame.draw_frame(*frame)
|
|
256
|
+
|
|
257
|
+
Stdout.put(cc.show_cursor())
|
|
258
|
+
raise CancelException(input_buffer)
|