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.
Files changed (58) hide show
  1. pyclack/__init__.py +7 -0
  2. pyclack/config/__init__.py +3 -0
  3. pyclack/config/theme_conf.py +32 -0
  4. pyclack/prompts/__init__.py +19 -0
  5. pyclack/prompts/ask.py +258 -0
  6. pyclack/prompts/autocomplete.py +466 -0
  7. pyclack/prompts/autocomplete_multiselect.py +564 -0
  8. pyclack/prompts/confirm.py +203 -0
  9. pyclack/prompts/multiline.py +279 -0
  10. pyclack/prompts/multiselect.py +472 -0
  11. pyclack/prompts/password.py +251 -0
  12. pyclack/prompts/pick_date.py +444 -0
  13. pyclack/prompts/prompt_base.py +210 -0
  14. pyclack/prompts/select.py +346 -0
  15. pyclack/prompts/select_key.py +318 -0
  16. pyclack/prompts/select_path.py +521 -0
  17. pyclack/prompts/util.py +785 -0
  18. pyclack/prompts_async/__init__.py +11 -0
  19. pyclack/prompts_async/async_prompts.py +458 -0
  20. pyclack/py.typed +0 -0
  21. pyclack/renderer/__init__.py +9 -0
  22. pyclack/renderer/render_frame.py +122 -0
  23. pyclack/renderer/symbols.py +233 -0
  24. pyclack/renderer/text.py +146 -0
  25. pyclack/renderer/themes.py +1479 -0
  26. pyclack/terminal/__init__.py +4 -0
  27. pyclack/terminal/base_key.py +37 -0
  28. pyclack/terminal/cursor_controller.py +139 -0
  29. pyclack/terminal/echo_controller.py +60 -0
  30. pyclack/terminal/key_reader.py +60 -0
  31. pyclack/terminal/os_utils.py +19 -0
  32. pyclack/terminal/posix_keys.py +40 -0
  33. pyclack/terminal/stdout.py +33 -0
  34. pyclack/terminal/win_keys.py +34 -0
  35. pyclack/widgets/__init__.py +13 -0
  36. pyclack/widgets/activity.py +317 -0
  37. pyclack/widgets/box.py +81 -0
  38. pyclack/widgets/cancel.py +37 -0
  39. pyclack/widgets/intro.py +33 -0
  40. pyclack/widgets/log/__init__.py +1 -0
  41. pyclack/widgets/log/log.py +214 -0
  42. pyclack/widgets/note.py +51 -0
  43. pyclack/widgets/outro.py +34 -0
  44. pyclack/widgets/progress.py +329 -0
  45. pyclack/widgets/spinner.py +283 -0
  46. pyclack/widgets/stream/__init__.py +3 -0
  47. pyclack/widgets/stream/stream.py +40 -0
  48. pyclack/widgets/stream/util.py +81 -0
  49. pyclack/widgets/task_log.py +218 -0
  50. pyclack/widgets_async/__init__.py +11 -0
  51. pyclack/widgets_async/async_log/__init__.py +1 -0
  52. pyclack/widgets_async/async_log/async_log.py +85 -0
  53. pyclack/widgets_async/async_stream/__init__.py +3 -0
  54. pyclack/widgets_async/async_stream/async_stream.py +33 -0
  55. pyclack/widgets_async/async_widgets.py +502 -0
  56. pyclack_lib-1.0.0.dist-info/METADATA +2212 -0
  57. pyclack_lib-1.0.0.dist-info/RECORD +58 -0
  58. 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,3 @@
1
+ from .theme_conf import set_active_theme as set_active_theme
2
+ from .theme_conf import get_active_theme as get_active_theme
3
+ from .theme_conf import set_print_mode_ascii as set_print_mode_ascii
@@ -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)