pyinkcli 0.1.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.
- pyinkcli/__init__.py +59 -0
- pyinkcli/_component_runtime.py +284 -0
- pyinkcli/_restore_cursor.py +39 -0
- pyinkcli/_suspense_runtime.py +127 -0
- pyinkcli/_yoga.py +501 -0
- pyinkcli/ansi_tokenizer.py +323 -0
- pyinkcli/colorize.py +157 -0
- pyinkcli/component.py +10 -0
- pyinkcli/components/AccessibilityContext.py +5 -0
- pyinkcli/components/App.py +170 -0
- pyinkcli/components/AppContext.py +5 -0
- pyinkcli/components/BackgroundContext.py +5 -0
- pyinkcli/components/Box.py +89 -0
- pyinkcli/components/CursorContext.py +30 -0
- pyinkcli/components/ErrorBoundary.py +35 -0
- pyinkcli/components/ErrorOverview.py +50 -0
- pyinkcli/components/FocusContext.py +40 -0
- pyinkcli/components/Newline.py +8 -0
- pyinkcli/components/Spacer.py +12 -0
- pyinkcli/components/Static.py +67 -0
- pyinkcli/components/StderrContext.py +37 -0
- pyinkcli/components/StdinContext.py +44 -0
- pyinkcli/components/StdoutContext.py +37 -0
- pyinkcli/components/Text.py +121 -0
- pyinkcli/components/Transform.py +15 -0
- pyinkcli/components/__init__.py +29 -0
- pyinkcli/components/_accessibility_runtime.py +26 -0
- pyinkcli/components/_app_context_runtime.py +45 -0
- pyinkcli/components/_background_runtime.py +28 -0
- pyinkcli/cursor_helpers.py +116 -0
- pyinkcli/dom.py +41 -0
- pyinkcli/get_max_width.py +18 -0
- pyinkcli/hooks/__init__.py +37 -0
- pyinkcli/hooks/_runtime.py +653 -0
- pyinkcli/hooks/state.py +30 -0
- pyinkcli/hooks/use_app.py +99 -0
- pyinkcli/hooks/use_box_metrics.py +80 -0
- pyinkcli/hooks/use_cursor.py +98 -0
- pyinkcli/hooks/use_focus.py +241 -0
- pyinkcli/hooks/use_focus_manager.py +60 -0
- pyinkcli/hooks/use_input.py +204 -0
- pyinkcli/hooks/use_is_screen_reader_enabled.py +27 -0
- pyinkcli/hooks/use_paste.py +52 -0
- pyinkcli/hooks/use_stderr.py +88 -0
- pyinkcli/hooks/use_stdin.py +198 -0
- pyinkcli/hooks/use_stdout.py +146 -0
- pyinkcli/hooks/use_window_size.py +32 -0
- pyinkcli/index.py +53 -0
- pyinkcli/ink.py +1090 -0
- pyinkcli/input_parser.py +272 -0
- pyinkcli/instances.py +7 -0
- pyinkcli/kitty_keyboard.py +30 -0
- pyinkcli/log_update.py +285 -0
- pyinkcli/measure_element.py +58 -0
- pyinkcli/measure_text.py +34 -0
- pyinkcli/output.py +5 -0
- pyinkcli/packages/__init__.py +2 -0
- pyinkcli/packages/ink/__init__.py +68 -0
- pyinkcli/packages/ink/dom.py +413 -0
- pyinkcli/packages/ink/host_config.py +22 -0
- pyinkcli/packages/ink/output.py +669 -0
- pyinkcli/packages/ink/render_background.py +41 -0
- pyinkcli/packages/ink/render_border.py +92 -0
- pyinkcli/packages/ink/render_node_to_output.py +285 -0
- pyinkcli/packages/ink/renderer.py +75 -0
- pyinkcli/packages/ink/styles.py +474 -0
- pyinkcli/packages/react_devtools_core/__init__.py +35 -0
- pyinkcli/packages/react_devtools_core/backend.py +3 -0
- pyinkcli/packages/react_devtools_core/backend_constants.py +10 -0
- pyinkcli/packages/react_devtools_core/backend_handlers.py +494 -0
- pyinkcli/packages/react_devtools_core/backend_inspection.py +319 -0
- pyinkcli/packages/react_devtools_core/hydration.py +1787 -0
- pyinkcli/packages/react_devtools_core/src/__init__.py +2 -0
- pyinkcli/packages/react_devtools_core/src/backend.py +365 -0
- pyinkcli/packages/react_devtools_core/src/editor.py +164 -0
- pyinkcli/packages/react_devtools_core/src/standalone.py +166 -0
- pyinkcli/packages/react_devtools_core/standalone.py +3 -0
- pyinkcli/packages/react_devtools_core/window_polyfill.py +28 -0
- pyinkcli/packages/react_reconciler/ReactChildFiber.py +451 -0
- pyinkcli/packages/react_reconciler/ReactEventPriorities.py +14 -0
- pyinkcli/packages/react_reconciler/ReactFiberClassComponent.py +421 -0
- pyinkcli/packages/react_reconciler/ReactFiberCommitWork.py +54 -0
- pyinkcli/packages/react_reconciler/ReactFiberComponentStack.py +102 -0
- pyinkcli/packages/react_reconciler/ReactFiberConfig.py +213 -0
- pyinkcli/packages/react_reconciler/ReactFiberContainerUpdate.py +286 -0
- pyinkcli/packages/react_reconciler/ReactFiberDevToolsCommands.py +572 -0
- pyinkcli/packages/react_reconciler/ReactFiberDevToolsHook.py +81 -0
- pyinkcli/packages/react_reconciler/ReactFiberDevToolsInspection.py +1285 -0
- pyinkcli/packages/react_reconciler/ReactFiberHostContext.py +29 -0
- pyinkcli/packages/react_reconciler/ReactFiberReconciler.py +108 -0
- pyinkcli/packages/react_reconciler/ReactFiberReconcilerClassComponent.py +182 -0
- pyinkcli/packages/react_reconciler/ReactFiberReconcilerCommands.py +293 -0
- pyinkcli/packages/react_reconciler/ReactFiberReconcilerDevTools.py +26 -0
- pyinkcli/packages/react_reconciler/ReactFiberReconcilerFacade.py +48 -0
- pyinkcli/packages/react_reconciler/ReactFiberReconcilerInspection.py +326 -0
- pyinkcli/packages/react_reconciler/ReactFiberReconcilerMutation.py +150 -0
- pyinkcli/packages/react_reconciler/ReactFiberReconcilerRoot.py +118 -0
- pyinkcli/packages/react_reconciler/ReactFiberReconcilerState.py +153 -0
- pyinkcli/packages/react_reconciler/ReactFiberReconcilerWorkLoop.py +70 -0
- pyinkcli/packages/react_reconciler/ReactFiberRoot.py +28 -0
- pyinkcli/packages/react_reconciler/ReactFiberTreeReflection.py +179 -0
- pyinkcli/packages/react_reconciler/ReactFiberWorkLoop.py +86 -0
- pyinkcli/packages/react_reconciler/ReactReconcilerConstants.py +9 -0
- pyinkcli/packages/react_reconciler/__init__.py +25 -0
- pyinkcli/packages/react_reconciler/constants.py +3 -0
- pyinkcli/packages/react_reconciler/index.py +3 -0
- pyinkcli/packages/react_reconciler/reconciler.py +33 -0
- pyinkcli/packages/react_reconciler/reflection.py +3 -0
- pyinkcli/packages/react_router/__init__.py +158 -0
- pyinkcli/packages/react_router/actions.py +113 -0
- pyinkcli/packages/react_router/components.py +317 -0
- pyinkcli/packages/react_router/context.py +114 -0
- pyinkcli/packages/react_router/errors.py +112 -0
- pyinkcli/packages/react_router/hooks.py +266 -0
- pyinkcli/packages/react_router/href.py +55 -0
- pyinkcli/packages/react_router/router/__init__.py +105 -0
- pyinkcli/packages/react_router/router/history.py +234 -0
- pyinkcli/packages/react_router/router/utils.py +933 -0
- pyinkcli/parse_keypress.py +499 -0
- pyinkcli/patch_console.py +89 -0
- pyinkcli/py.typed +0 -0
- pyinkcli/reconciler.py +4 -0
- pyinkcli/render.py +30 -0
- pyinkcli/render_background.py +5 -0
- pyinkcli/render_border.py +5 -0
- pyinkcli/render_node_to_output.py +20 -0
- pyinkcli/render_to_string.py +106 -0
- pyinkcli/renderer.py +5 -0
- pyinkcli/sanitize_ansi.py +44 -0
- pyinkcli/squash_text_nodes.py +12 -0
- pyinkcli/styles.py +5 -0
- pyinkcli/suspense_runtime.py +21 -0
- pyinkcli/utils/__init__.py +99 -0
- pyinkcli/utils/ansi_escapes.py +227 -0
- pyinkcli/utils/cli_boxes.py +135 -0
- pyinkcli/utils/string_width.py +93 -0
- pyinkcli/utils/wrap_ansi.py +338 -0
- pyinkcli/utils.py +23 -0
- pyinkcli/wrap_text.py +52 -0
- pyinkcli/write_synchronized.py +23 -0
- pyinkcli/yoga_compat.py +117 -0
- pyinkcli-0.1.0.dist-info/METADATA +95 -0
- pyinkcli-0.1.0.dist-info/RECORD +145 -0
- pyinkcli-0.1.0.dist-info/WHEEL +4 -0
- pyinkcli-0.1.0.dist-info/licenses/LICENSE +10 -0
pyinkcli/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""pyinkcli public package surface."""
|
|
2
|
+
|
|
3
|
+
from pyinkcli.index import (
|
|
4
|
+
DOMElement,
|
|
5
|
+
Box,
|
|
6
|
+
Key,
|
|
7
|
+
Newline,
|
|
8
|
+
Spacer,
|
|
9
|
+
Static,
|
|
10
|
+
Text,
|
|
11
|
+
Transform,
|
|
12
|
+
kittyFlags,
|
|
13
|
+
kittyModifiers,
|
|
14
|
+
measureElement,
|
|
15
|
+
render,
|
|
16
|
+
renderToString,
|
|
17
|
+
useApp,
|
|
18
|
+
useBoxMetrics,
|
|
19
|
+
useCursor,
|
|
20
|
+
useFocus,
|
|
21
|
+
useFocusManager,
|
|
22
|
+
useInput,
|
|
23
|
+
useIsScreenReaderEnabled,
|
|
24
|
+
usePaste,
|
|
25
|
+
useStderr,
|
|
26
|
+
useStdin,
|
|
27
|
+
useStdout,
|
|
28
|
+
useWindowSize,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"render",
|
|
35
|
+
"renderToString",
|
|
36
|
+
"Box",
|
|
37
|
+
"Text",
|
|
38
|
+
"Static",
|
|
39
|
+
"Transform",
|
|
40
|
+
"Newline",
|
|
41
|
+
"Spacer",
|
|
42
|
+
"Key",
|
|
43
|
+
"useInput",
|
|
44
|
+
"usePaste",
|
|
45
|
+
"useApp",
|
|
46
|
+
"useStdin",
|
|
47
|
+
"useStdout",
|
|
48
|
+
"useStderr",
|
|
49
|
+
"useFocus",
|
|
50
|
+
"useFocusManager",
|
|
51
|
+
"useIsScreenReaderEnabled",
|
|
52
|
+
"useCursor",
|
|
53
|
+
"useWindowSize",
|
|
54
|
+
"useBoxMetrics",
|
|
55
|
+
"measureElement",
|
|
56
|
+
"DOMElement",
|
|
57
|
+
"kittyFlags",
|
|
58
|
+
"kittyModifiers",
|
|
59
|
+
]
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal component runtime for pyinkcli.
|
|
3
|
+
|
|
4
|
+
This module holds the actual element/component implementation.
|
|
5
|
+
Public compatibility imports remain in `component.py`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from contextlib import AbstractContextManager
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _Element:
|
|
15
|
+
__slots__ = ("type", "props", "children", "key")
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _ScopedNode:
|
|
20
|
+
__slots__ = ("node", "context_manager_factories")
|
|
21
|
+
|
|
22
|
+
def __getattr__(self, name: str) -> Any:
|
|
23
|
+
return getattr(self.node, name)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
RenderableNode = Union["_Element", "_ScopedNode", str, None]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _normalize_props(props: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
30
|
+
return props or {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _create_element_record(
|
|
34
|
+
type: Union[str, Callable, type],
|
|
35
|
+
props: Optional[Dict[str, Any]],
|
|
36
|
+
children: List[RenderableNode],
|
|
37
|
+
key: Optional[str],
|
|
38
|
+
) -> "_Element":
|
|
39
|
+
element = object.__new__(_Element)
|
|
40
|
+
element.type = type
|
|
41
|
+
element.props = _normalize_props(props)
|
|
42
|
+
element.children = children
|
|
43
|
+
element.key = key
|
|
44
|
+
return element
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_element_record(value: Any) -> bool:
|
|
48
|
+
return isinstance(value, _Element)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _is_scoped_node(value: Any) -> bool:
|
|
52
|
+
return isinstance(value, _ScopedNode)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_renderable_node(value: Any) -> bool:
|
|
56
|
+
return _is_element_record(value) or _is_scoped_node(value) or isinstance(value, str)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _is_text_renderable(value: Any) -> bool:
|
|
60
|
+
return isinstance(value, str)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_render_component_passthrough(value: Any) -> bool:
|
|
64
|
+
return _is_text_renderable(value) or isElement(value)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _normalize_child(child: Any) -> RenderableNode:
|
|
68
|
+
if child is None:
|
|
69
|
+
return None
|
|
70
|
+
if _is_renderable_node(child):
|
|
71
|
+
return child
|
|
72
|
+
return str(child)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _normalize_children(children: Any) -> List[RenderableNode]:
|
|
76
|
+
processed_children: List[RenderableNode] = []
|
|
77
|
+
for child in children:
|
|
78
|
+
if isinstance(child, (list, tuple)):
|
|
79
|
+
for subchild in child:
|
|
80
|
+
normalized = _normalize_child(subchild)
|
|
81
|
+
if normalized is not None:
|
|
82
|
+
processed_children.append(normalized)
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
normalized = _normalize_child(child)
|
|
86
|
+
if normalized is not None:
|
|
87
|
+
processed_children.append(normalized)
|
|
88
|
+
|
|
89
|
+
return processed_children
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _coerce_render_result(result: Any) -> RenderableNode:
|
|
93
|
+
if _is_component_instance(result):
|
|
94
|
+
return _coerce_render_result(result.render())
|
|
95
|
+
if _is_renderable_node(result):
|
|
96
|
+
return result
|
|
97
|
+
if result is None:
|
|
98
|
+
return None
|
|
99
|
+
return str(result)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_component_instance(value: Any) -> bool:
|
|
103
|
+
return isinstance(value, _Component)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _is_component_class(value: Any) -> bool:
|
|
107
|
+
return isinstance(value, type) and issubclass(value, _Component)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _coalesce_component_children(
|
|
111
|
+
children: tuple[RenderableNode, ...],
|
|
112
|
+
) -> RenderableNode:
|
|
113
|
+
normalized_children = _normalize_children(children)
|
|
114
|
+
if not normalized_children:
|
|
115
|
+
return None
|
|
116
|
+
if len(normalized_children) == 1:
|
|
117
|
+
return normalized_children[0]
|
|
118
|
+
return _fragment(*normalized_children)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _merge_component_props(
|
|
122
|
+
children: tuple[RenderableNode, ...],
|
|
123
|
+
props: Dict[str, Any],
|
|
124
|
+
) -> Dict[str, Any]:
|
|
125
|
+
merged_props = dict(props)
|
|
126
|
+
if "children" not in merged_props:
|
|
127
|
+
merged_props["children"] = _coalesce_component_children(children)
|
|
128
|
+
return merged_props
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _create_component_instance(
|
|
132
|
+
component: type["_Component"],
|
|
133
|
+
children: tuple[RenderableNode, ...],
|
|
134
|
+
props: Dict[str, Any],
|
|
135
|
+
) -> "_Component":
|
|
136
|
+
return component(**_merge_component_props(children, props))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _invoke_component(
|
|
140
|
+
component: Callable,
|
|
141
|
+
children: tuple[RenderableNode, ...],
|
|
142
|
+
props: Dict[str, Any],
|
|
143
|
+
) -> Any:
|
|
144
|
+
if _is_component_class(component):
|
|
145
|
+
instance = _create_component_instance(component, children, props)
|
|
146
|
+
return instance.render()
|
|
147
|
+
return component(*children, **props)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def createElement(
|
|
151
|
+
type: Union[str, Callable, type],
|
|
152
|
+
*children: RenderableNode,
|
|
153
|
+
key: Optional[str] = None,
|
|
154
|
+
**props: Any,
|
|
155
|
+
) -> RenderableNode:
|
|
156
|
+
return _create_element_record(
|
|
157
|
+
type=type,
|
|
158
|
+
props=props,
|
|
159
|
+
children=_normalize_children(children),
|
|
160
|
+
key=key,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def scopeRender(
|
|
165
|
+
node: RenderableNode,
|
|
166
|
+
*context_manager_factories: Callable[[], AbstractContextManager[Any]],
|
|
167
|
+
) -> RenderableNode:
|
|
168
|
+
scoped = object.__new__(_ScopedNode)
|
|
169
|
+
scoped.node = node
|
|
170
|
+
scoped.context_manager_factories = tuple(context_manager_factories)
|
|
171
|
+
return scoped
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _Component:
|
|
175
|
+
def __init__(self, **props: Any):
|
|
176
|
+
self.props = props
|
|
177
|
+
self.state: Dict[str, Any] = {}
|
|
178
|
+
self._state_version = 0
|
|
179
|
+
self._is_unmounted = False
|
|
180
|
+
self._is_mounted = False
|
|
181
|
+
self._last_rendered_node: RenderableNode = None
|
|
182
|
+
self._pending_previous_state: Optional[Dict[str, Any]] = None
|
|
183
|
+
self._nearest_error_boundary: Optional["_Component"] = None
|
|
184
|
+
|
|
185
|
+
def render(self) -> RenderableNode:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
def set_state(
|
|
189
|
+
self,
|
|
190
|
+
update: Optional[
|
|
191
|
+
Union[
|
|
192
|
+
Dict[str, Any],
|
|
193
|
+
Callable[[Dict[str, Any], Dict[str, Any]], Optional[Dict[str, Any]]],
|
|
194
|
+
]
|
|
195
|
+
] = None,
|
|
196
|
+
**kwargs: Any,
|
|
197
|
+
) -> None:
|
|
198
|
+
if self._is_unmounted:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
partial_state: Dict[str, Any] = {}
|
|
202
|
+
if callable(update):
|
|
203
|
+
computed = update(dict(self.state), dict(self.props))
|
|
204
|
+
if isinstance(computed, dict):
|
|
205
|
+
partial_state.update(computed)
|
|
206
|
+
elif isinstance(update, dict):
|
|
207
|
+
partial_state.update(update)
|
|
208
|
+
|
|
209
|
+
if kwargs:
|
|
210
|
+
partial_state.update(kwargs)
|
|
211
|
+
|
|
212
|
+
if not partial_state:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
if self._pending_previous_state is None:
|
|
216
|
+
self._pending_previous_state = dict(self.state)
|
|
217
|
+
self.state.update(partial_state)
|
|
218
|
+
self._state_version += 1
|
|
219
|
+
from pyinkcli.hooks._runtime import _request_rerender
|
|
220
|
+
|
|
221
|
+
_request_rerender()
|
|
222
|
+
|
|
223
|
+
def force_update(self) -> None:
|
|
224
|
+
if self._is_unmounted:
|
|
225
|
+
return
|
|
226
|
+
from pyinkcli.hooks._runtime import _request_rerender
|
|
227
|
+
|
|
228
|
+
_request_rerender()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def component(
|
|
232
|
+
func: Optional[Callable] = None, *, name: Optional[str] = None
|
|
233
|
+
) -> Callable:
|
|
234
|
+
def wrapper(fn: Callable) -> Callable:
|
|
235
|
+
fn._is_component = True
|
|
236
|
+
fn._component_name = name or fn.__name__
|
|
237
|
+
return fn
|
|
238
|
+
|
|
239
|
+
if func is not None:
|
|
240
|
+
return wrapper(func)
|
|
241
|
+
return wrapper
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def is_component(obj: Any) -> bool:
|
|
245
|
+
return callable(obj) and obj is not _Fragment
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def isElement(obj: Any) -> bool:
|
|
249
|
+
return _is_element_record(obj) or _is_scoped_node(obj)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def renderComponent(
|
|
253
|
+
component: Union[Callable, _Component, RenderableNode],
|
|
254
|
+
*children: RenderableNode,
|
|
255
|
+
**props: Any,
|
|
256
|
+
) -> RenderableNode:
|
|
257
|
+
if component is None:
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
if _is_render_component_passthrough(component):
|
|
261
|
+
return component
|
|
262
|
+
|
|
263
|
+
if _is_component_instance(component):
|
|
264
|
+
return _coerce_render_result(component.render())
|
|
265
|
+
|
|
266
|
+
if _is_component_class(component):
|
|
267
|
+
instance = _create_component_instance(component, children, props)
|
|
268
|
+
return _coerce_render_result(instance.render())
|
|
269
|
+
|
|
270
|
+
if callable(component):
|
|
271
|
+
return _coerce_render_result(_invoke_component(component, children, props))
|
|
272
|
+
|
|
273
|
+
return str(component)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class _Fragment:
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _fragment(*children: RenderableNode) -> RenderableNode:
|
|
281
|
+
return createElement(_Fragment, *children)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
__all__ = ["createElement", "component", "isElement", "RenderableNode", "scopeRender"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Process-exit cursor restore parity with JS cli-cursor/restore-cursor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from pyinkcli.cursor_helpers import showCursorEscape
|
|
9
|
+
|
|
10
|
+
_registered = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _restore_cursor_on_exit() -> None:
|
|
14
|
+
stream = sys.stderr
|
|
15
|
+
is_tty = stream.isatty() if hasattr(stream, "isatty") else False
|
|
16
|
+
if not is_tty:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
writer = getattr(stream, "raw_write", None) or getattr(stream, "write", None)
|
|
21
|
+
if callable(writer):
|
|
22
|
+
writer(showCursorEscape)
|
|
23
|
+
flush = getattr(stream, "flush", None)
|
|
24
|
+
if callable(flush):
|
|
25
|
+
flush()
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ensureCursorRestoreRegistered() -> None:
|
|
31
|
+
global _registered
|
|
32
|
+
if _registered:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
atexit.register(_restore_cursor_on_exit)
|
|
36
|
+
_registered = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ["ensureCursorRestoreRegistered"]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Internal suspense resource runtime for pyinkcli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Callable, Hashable
|
|
8
|
+
|
|
9
|
+
from pyinkcli.hooks._runtime import _has_rerender_target, _request_rerender
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SuspendSignal(Exception):
|
|
13
|
+
def __init__(self, key: Hashable):
|
|
14
|
+
super().__init__(f"Suspended on resource {key!r}")
|
|
15
|
+
self.key = key
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class _ResourceRecord:
|
|
20
|
+
status: str = "pending"
|
|
21
|
+
value: Any = None
|
|
22
|
+
error: Exception | None = None
|
|
23
|
+
started: bool = False
|
|
24
|
+
lock: threading.Lock = field(default_factory=threading.Lock)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_records: dict[Hashable, _ResourceRecord] = {}
|
|
28
|
+
_records_lock = threading.Lock()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_resource(
|
|
32
|
+
key: Hashable,
|
|
33
|
+
record: _ResourceRecord,
|
|
34
|
+
loader: Callable[[], Any],
|
|
35
|
+
) -> None:
|
|
36
|
+
try:
|
|
37
|
+
value = loader()
|
|
38
|
+
except Exception as error: # pragma: no cover
|
|
39
|
+
with _records_lock:
|
|
40
|
+
current = _records.get(key)
|
|
41
|
+
if current is not record:
|
|
42
|
+
return
|
|
43
|
+
with record.lock:
|
|
44
|
+
record.status = "rejected"
|
|
45
|
+
record.error = error
|
|
46
|
+
else:
|
|
47
|
+
with _records_lock:
|
|
48
|
+
current = _records.get(key)
|
|
49
|
+
if current is not record:
|
|
50
|
+
return
|
|
51
|
+
with record.lock:
|
|
52
|
+
record.status = "resolved"
|
|
53
|
+
record.value = value
|
|
54
|
+
|
|
55
|
+
if _has_rerender_target():
|
|
56
|
+
_request_rerender()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def readResource(key: Hashable, loader: Callable[[], Any]) -> Any:
|
|
60
|
+
with _records_lock:
|
|
61
|
+
record = _records.get(key)
|
|
62
|
+
if record is None:
|
|
63
|
+
record = _ResourceRecord()
|
|
64
|
+
_records[key] = record
|
|
65
|
+
|
|
66
|
+
with record.lock:
|
|
67
|
+
if record.status == "resolved":
|
|
68
|
+
return record.value
|
|
69
|
+
if record.status == "rejected":
|
|
70
|
+
raise record.error # type: ignore[misc]
|
|
71
|
+
if not record.started:
|
|
72
|
+
record.started = True
|
|
73
|
+
threading.Thread(
|
|
74
|
+
target=_resolve_resource,
|
|
75
|
+
args=(key, record, loader),
|
|
76
|
+
daemon=True,
|
|
77
|
+
).start()
|
|
78
|
+
|
|
79
|
+
raise SuspendSignal(key)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def preloadResource(key: Hashable, loader: Callable[[], Any]) -> None:
|
|
83
|
+
with _records_lock:
|
|
84
|
+
record = _records.get(key)
|
|
85
|
+
if record is None:
|
|
86
|
+
record = _ResourceRecord()
|
|
87
|
+
_records[key] = record
|
|
88
|
+
|
|
89
|
+
with record.lock:
|
|
90
|
+
if record.status in {"resolved", "rejected"} or record.started:
|
|
91
|
+
return
|
|
92
|
+
record.started = True
|
|
93
|
+
threading.Thread(
|
|
94
|
+
target=_resolve_resource,
|
|
95
|
+
args=(key, record, loader),
|
|
96
|
+
daemon=True,
|
|
97
|
+
).start()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def peekResource(key: Hashable) -> Any:
|
|
101
|
+
with _records_lock:
|
|
102
|
+
record = _records.get(key)
|
|
103
|
+
if record is None:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
with record.lock:
|
|
107
|
+
if record.status == "resolved":
|
|
108
|
+
return record.value
|
|
109
|
+
if record.status == "rejected":
|
|
110
|
+
raise record.error # type: ignore[misc]
|
|
111
|
+
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def invalidateResource(key: Hashable) -> None:
|
|
116
|
+
with _records_lock:
|
|
117
|
+
_records.pop(key, None)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def resetResource(key: Hashable) -> None:
|
|
121
|
+
with _records_lock:
|
|
122
|
+
_records.pop(key, None)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def resetAllResources() -> None:
|
|
126
|
+
with _records_lock:
|
|
127
|
+
_records.clear()
|