pysual 0.1.2__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 (127) hide show
  1. pysual/__init__.py +154 -0
  2. pysual/__main__.py +6 -0
  3. pysual/_blueprint.py +102 -0
  4. pysual/_build_assets.py +53 -0
  5. pysual/_catalog.py +91 -0
  6. pysual/_config.py +236 -0
  7. pysual/_controls/__init__.py +5 -0
  8. pysual/_controls/actions.py +164 -0
  9. pysual/_controls/basic.py +150 -0
  10. pysual/_controls/choices.py +253 -0
  11. pysual/_controls/indicators.py +328 -0
  12. pysual/_controls/lists.py +261 -0
  13. pysual/_controls/media.py +39 -0
  14. pysual/_controls/ranges.py +357 -0
  15. pysual/_controls/viewports.py +148 -0
  16. pysual/_controls/windows.py +324 -0
  17. pysual/_drawing.py +70 -0
  18. pysual/_engine.py +718 -0
  19. pysual/_factory_types.py +8 -0
  20. pysual/_factory_types.pyi +1137 -0
  21. pysual/_grapheme_data.py +2025 -0
  22. pysual/_image_fit.py +22 -0
  23. pysual/_modality.py +327 -0
  24. pysual/_namespace_handlers.py +105 -0
  25. pysual/_nine_slice.py +41 -0
  26. pysual/_png.py +339 -0
  27. pysual/_ranges.py +35 -0
  28. pysual/_registry.py +173 -0
  29. pysual/_scrolling.py +140 -0
  30. pysual/_terminal_data.py +124 -0
  31. pysual/_text_display.py +6 -0
  32. pysual/_value_bag.py +20 -0
  33. pysual/_value_bag.pyi +69 -0
  34. pysual/app.py +441 -0
  35. pysual/assets/DejaVu-LICENSE.txt +187 -0
  36. pysual/assets/DejaVuSans.ttf +0 -0
  37. pysual/assets/DejaVuSansMono.ttf +0 -0
  38. pysual/assets/UNICODE-LICENSE.txt +39 -0
  39. pysual/assets/manifest.json +12 -0
  40. pysual/autoconfig.py +11 -0
  41. pysual/backends/__init__.py +140 -0
  42. pysual/backends/_files.py +190 -0
  43. pysual/backends/_font.py +182 -0
  44. pysual/backends/_gradient.py +29 -0
  45. pysual/backends/_native.py +105 -0
  46. pysual/backends/_native_client.py +592 -0
  47. pysual/backends/_term_cells.py +1007 -0
  48. pysual/backends/_term_images.py +278 -0
  49. pysual/backends/_term_input.py +299 -0
  50. pysual/backends/_term_io.py +284 -0
  51. pysual/backends/_term_session.py +516 -0
  52. pysual/backends/_web_native.py +949 -0
  53. pysual/backends/_web_pyodide.py +410 -0
  54. pysual/backends/_web_svg.py +957 -0
  55. pysual/backends/native.py +714 -0
  56. pysual/backends/term_text.py +744 -0
  57. pysual/backends/terminal.py +358 -0
  58. pysual/backends/web.py +32 -0
  59. pysual/behaviors.py +94 -0
  60. pysual/bundle.py +210 -0
  61. pysual/cards.py +458 -0
  62. pysual/charts.py +441 -0
  63. pysual/cli.py +296 -0
  64. pysual/commands.py +123 -0
  65. pysual/composition.py +426 -0
  66. pysual/controls.py +1348 -0
  67. pysual/data_grid.py +1173 -0
  68. pysual/dates.py +359 -0
  69. pysual/editing.py +178 -0
  70. pysual/editors.py +372 -0
  71. pysual/errors.py +25 -0
  72. pysual/events.py +489 -0
  73. pysual/extras.py +113 -0
  74. pysual/feedback.py +399 -0
  75. pysual/fields.py +221 -0
  76. pysual/geometry.py +40 -0
  77. pysual/graphemes.py +141 -0
  78. pysual/hints.py +129 -0
  79. pysual/host.py +535 -0
  80. pysual/icons.py +308 -0
  81. pysual/image_resources.py +57 -0
  82. pysual/input.py +941 -0
  83. pysual/inspection.py +120 -0
  84. pysual/layout.py +875 -0
  85. pysual/menus.py +682 -0
  86. pysual/motion.py +273 -0
  87. pysual/navigation.py +458 -0
  88. pysual/painting.py +1562 -0
  89. pysual/popup.py +140 -0
  90. pysual/presentation.py +81 -0
  91. pysual/py.typed +0 -0
  92. pysual/render_cache.py +337 -0
  93. pysual/runtime.py +1112 -0
  94. pysual/schema.py +730 -0
  95. pysual/selection.py +15 -0
  96. pysual/text.py +1293 -0
  97. pysual/theme.py +1720 -0
  98. pysual/tree.py +534 -0
  99. pysual/web/__init__.py +1 -0
  100. pysual/web/host.js +329 -0
  101. pysual/web/input.js +52 -0
  102. pysual/web/live.html +31 -0
  103. pysual/web/live.js +195 -0
  104. pysual/web/notices/CPython-LICENSE.txt +1134 -0
  105. pysual/web/notices/Emscripten-LICENSE.txt +102 -0
  106. pysual/web/notices/HACL-LICENSE.txt +23 -0
  107. pysual/web/notices/LLVM-RUNTIME-LICENSES.txt +1266 -0
  108. pysual/web/notices/MiniLZ4-LICENSE.txt +29 -0
  109. pysual/web/notices/Pyodide-LICENSE.txt +373 -0
  110. pysual/web/notices/WEB-NOTICES.txt +42 -0
  111. pysual/web/notices/bzip2-LICENSE.txt +42 -0
  112. pysual/web/notices/hiwire-LICENSE.txt +373 -0
  113. pysual/web/notices/libffi-LICENSE.txt +21 -0
  114. pysual/web/notices/manifest.json +429 -0
  115. pysual/web/notices/musl-COPYRIGHT.txt +193 -0
  116. pysual/web/notices/musl-INCORPORATED-NOTICES.txt +1000 -0
  117. pysual/web/notices/zlib-LICENSE.txt +22 -0
  118. pysual/web/services.js +217 -0
  119. pysual/web/standalone.js +91 -0
  120. pysual/web/svg.js +254 -0
  121. pysual/widgets.py +22 -0
  122. pysual-0.1.2.dist-info/METADATA +236 -0
  123. pysual-0.1.2.dist-info/RECORD +127 -0
  124. pysual-0.1.2.dist-info/WHEEL +5 -0
  125. pysual-0.1.2.dist-info/entry_points.txt +2 -0
  126. pysual-0.1.2.dist-info/licenses/LICENSE +21 -0
  127. pysual-0.1.2.dist-info/top_level.txt +1 -0
pysual/__init__.py ADDED
@@ -0,0 +1,154 @@
1
+ """Pysual: typed Python controls with shared desktop and web behavior."""
2
+
3
+ from .app import App as App, Window as Window
4
+ from ._config import configure as configure
5
+ from ._engine import shutdown as shutdown
6
+ from .runtime import wait as wait, wait_async as wait_async
7
+ from .backends.terminal import terminal as terminal
8
+
9
+ __version__ = "0.1.2"
10
+ from .controls import (
11
+ Button as Button,
12
+ Container as Container,
13
+ Control as Control,
14
+ Label as Label,
15
+ )
16
+ from .controls import register_control as register_control
17
+ from .controls import registered_controls as registered_controls
18
+ from .events import (
19
+ ClickEvent as ClickEvent,
20
+ ClosingEvent as ClosingEvent,
21
+ Event as Event,
22
+ UiEvent as UiEvent,
23
+ )
24
+ from .schema import Dirty as Dirty, prop as prop
25
+ from .behaviors import PressBehavior as PressBehavior
26
+
27
+ from .widgets import (
28
+ CheckBox as CheckBox,
29
+ Slider as Slider,
30
+ TextBox as TextBox,
31
+ ScrollArea as ScrollArea,
32
+ ListView as ListView,
33
+ Image as Image,
34
+ SubWindow as SubWindow,
35
+ )
36
+ from .events import (
37
+ ChangeEvent as ChangeEvent,
38
+ PointerEvent as PointerEvent,
39
+ KeyEvent as KeyEvent,
40
+ )
41
+ from .theme import (
42
+ Theme as Theme,
43
+ Tokens as Tokens,
44
+ Style as Style,
45
+ Rule as Rule,
46
+ dark as dark,
47
+ get_theme as get_theme,
48
+ theme_names as theme_names,
49
+ light as light,
50
+ load_theme as load_theme,
51
+ save_theme as save_theme,
52
+ )
53
+ from .geometry import Rect as Rect
54
+ from .painting import Painter as Painter
55
+
56
+ from .host import CapabilityError as CapabilityError
57
+ from .host import TextFile as TextFile
58
+ from .host import RenderDiagnostics as RenderDiagnostics
59
+ from .runtime import FrameTiming as FrameTiming
60
+ from .tree import TreeNode as TreeNode, TreeView as TreeView, TreeEvent as TreeEvent
61
+ from .popup import Popup as Popup
62
+ from .charts import LineChart as LineChart, ChartSeries as ChartSeries
63
+ from .data_grid import (
64
+ DataGrid as DataGrid,
65
+ GridColumn as GridColumn,
66
+ GridRow as GridRow,
67
+ CellValue as CellValue,
68
+ GridCellEvent as GridCellEvent,
69
+ GridEditEvent as GridEditEvent,
70
+ )
71
+ from .commands import (
72
+ MenuItem as MenuItem,
73
+ MenuGroup as MenuGroup,
74
+ CommandEvent as CommandEvent,
75
+ )
76
+ from .menus import Menu as Menu, MenuBar as MenuBar
77
+ from .composition import (
78
+ TabControl as TabControl,
79
+ TabPage as TabPage,
80
+ SplitPane as SplitPane,
81
+ )
82
+ from .selection import (
83
+ Dropdown as Dropdown,
84
+ ComboBox as ComboBox,
85
+ Toggle as Toggle,
86
+ RadioButton as RadioButton,
87
+ ProgressBar as ProgressBar,
88
+ Rating as Rating,
89
+ )
90
+
91
+ from .icons import icon_names as icon_names
92
+
93
+ from .render_cache import CacheStats as CacheStats
94
+
95
+ from .editors import NumericInput as NumericInput, ColorPicker as ColorPicker
96
+
97
+ from .extras import GroupBox as GroupBox, Separator as Separator, Hyperlink as Hyperlink
98
+ from .charts import DonutChart as DonutChart, ChartSlice as ChartSlice
99
+ from .events import DragPayload as DragPayload, DragEvent as DragEvent
100
+ from .host import Viewport as Viewport
101
+ from .inspection import (
102
+ ControlSnapshot as ControlSnapshot,
103
+ HandlerSnapshot as HandlerSnapshot,
104
+ TaskSnapshot as TaskSnapshot,
105
+ )
106
+
107
+ from .controls import blueprint as blueprint
108
+ from ._blueprint import Blueprint as Blueprint
109
+
110
+ from .image_resources import image_source as image_source, package_image as package_image
111
+ from .editing import EditorContent as EditorContent, EditorPopup as EditorPopup
112
+ from .editors import NumericEditor as NumericEditor, ColorEditor as ColorEditor
113
+ from .fields import SearchField as SearchField
114
+ from .dates import DatePicker as DatePicker, CalendarEditor as CalendarEditor
115
+ from ._controls.indicators import (
116
+ RangeSlider as RangeSlider, CircularProgress as CircularProgress,
117
+ )
118
+ from .navigation import (
119
+ SegmentedControl as SegmentedControl, Breadcrumb as Breadcrumb,
120
+ BreadcrumbEvent as BreadcrumbEvent, Pagination as Pagination,
121
+ )
122
+ from .cards import ChoiceCard as ChoiceCard, Disclosure as Disclosure
123
+ from .feedback import Badge as Badge, AlertBanner as AlertBanner, Meter as Meter
124
+
125
+ from ._catalog import install_builtin_controls as _install_builtin_controls
126
+
127
+ _install_builtin_controls()
128
+
129
+ # Keep loaded implementation modules out of wildcard imports. Argument
130
+ # processing remains an explicit opt-in through pysual.autoconfig.
131
+ __all__ = (
132
+ "App", "Window", "configure", "shutdown", "wait", "wait_async", "terminal",
133
+ "Control", "Container", "Button", "Label", "register_control",
134
+ "registered_controls", "blueprint", "Blueprint", "Dirty", "prop", "PressBehavior",
135
+ "CheckBox", "Slider", "TextBox", "ScrollArea", "ListView", "Image",
136
+ "SubWindow", "Popup", "TreeNode", "TreeView", "TreeEvent",
137
+ "MenuItem", "MenuGroup", "Menu", "MenuBar", "TabControl", "TabPage",
138
+ "SplitPane", "Dropdown", "ComboBox", "Toggle", "RadioButton", "ProgressBar", "Rating",
139
+ "NumericInput", "ColorPicker", "GroupBox", "Separator", "Hyperlink",
140
+ "EditorContent", "EditorPopup", "NumericEditor", "ColorEditor",
141
+ "SearchField", "DatePicker", "CalendarEditor",
142
+ "RangeSlider", "CircularProgress", "SegmentedControl", "Breadcrumb",
143
+ "BreadcrumbEvent", "Pagination",
144
+ "ChoiceCard", "Disclosure", "Badge", "AlertBanner", "Meter",
145
+ "LineChart", "ChartSeries", "DonutChart", "ChartSlice", "DataGrid",
146
+ "GridColumn", "GridRow", "CellValue", "GridCellEvent", "GridEditEvent",
147
+ "Event", "UiEvent", "ClickEvent", "ClosingEvent", "ChangeEvent",
148
+ "PointerEvent", "KeyEvent", "CommandEvent", "DragPayload", "DragEvent",
149
+ "Theme", "Tokens", "Style", "Rule", "dark", "light", "get_theme",
150
+ "theme_names", "load_theme", "save_theme", "icon_names",
151
+ "Rect", "Painter", "CapabilityError", "TextFile", "Viewport",
152
+ "FrameTiming", "RenderDiagnostics", "CacheStats", "ControlSnapshot", "HandlerSnapshot",
153
+ "TaskSnapshot", "image_source", "package_image",
154
+ )
pysual/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """`python -m pysual` builds or runs an application; see `pysual --help`."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
pysual/_blueprint.py ADDED
@@ -0,0 +1,102 @@
1
+ """Immutable construction descriptors; live controls remain in their ordinary tree."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field, replace
6
+ import keyword
7
+ from types import MappingProxyType
8
+ from typing import TYPE_CHECKING, Any, Generic, Mapping, TypeVar, overload
9
+
10
+ from .errors import BindingError, LifecycleError
11
+
12
+ if TYPE_CHECKING:
13
+ from .controls import Container, Control
14
+
15
+ C = TypeVar("C", bound="Control")
16
+
17
+
18
+ @dataclass(frozen=True, eq=False)
19
+ class Blueprint(Generic[C]):
20
+ """A typed per-instance declaration, created with pysual.blueprint()."""
21
+
22
+ control_type: type[C]
23
+ _properties: tuple[tuple[str, object], ...]
24
+ parent_name: str | None = None
25
+ _name: str | None = field(default=None, init=False, repr=False)
26
+
27
+ @property
28
+ def properties(self) -> Mapping[str, object]:
29
+ return MappingProxyType(dict(self._properties))
30
+
31
+ def under(self, parent_name: str) -> Blueprint[C]:
32
+ """Use a Container declaration in the same owner as the visual parent."""
33
+ if (
34
+ not isinstance(parent_name, str)
35
+ or not parent_name.isidentifier()
36
+ or parent_name.startswith("_")
37
+ or keyword.iskeyword(parent_name)
38
+ ):
39
+ raise BindingError("Blueprint parent must be a public declaration name")
40
+ return replace(self, parent_name=parent_name)
41
+
42
+ def __set_name__(self, owner: type, name: str) -> None:
43
+ if self._name is not None:
44
+ raise BindingError("Create a fresh blueprint for each class declaration")
45
+ if (
46
+ not name.isidentifier()
47
+ or name.startswith("_")
48
+ or keyword.iskeyword(name)
49
+ or name == owner.__name__
50
+ or any(
51
+ name in vars(base) and not isinstance(vars(base)[name], Blueprint)
52
+ for base in owner.__mro__[1:]
53
+ )
54
+ ):
55
+ raise BindingError(f"Blueprint {name!r} collides with a framework member")
56
+ object.__setattr__(self, "_name", name)
57
+
58
+ @overload
59
+ def __get__(self, instance: None, owner: type | None = None) -> Blueprint[C]: ...
60
+
61
+ @overload
62
+ def __get__(self, instance: Container, owner: type | None = None) -> C: ...
63
+
64
+ def __get__(
65
+ self, instance: Container | None, owner: type | None = None
66
+ ) -> Blueprint[C] | C:
67
+ if instance is None:
68
+ return self
69
+ from ._engine import call, is_ui_thread
70
+
71
+ if not is_ui_thread():
72
+ return call(self.__get__, instance, owner)
73
+ instance._check_live()
74
+ assert self._name is not None
75
+ value = vars(instance).get(self._name)
76
+ if not isinstance(value, self.control_type):
77
+ raise LifecycleError(
78
+ f"Blueprint {self._name!r} is available after construction completes"
79
+ )
80
+ return value
81
+
82
+ def __set__(self, instance: Container, value: C) -> None:
83
+ if not isinstance(value, self.control_type):
84
+ raise TypeError(
85
+ f"Blueprint {self._name!r} requires {self.control_type.__name__}"
86
+ )
87
+ assert self._name is not None
88
+ vars(instance)[self._name] = value
89
+
90
+
91
+ def declarations(owner_type: type) -> dict[str, Blueprint[Any]]:
92
+ """Resolve inherited declarations without evaluating descriptors or user code."""
93
+ result: dict[str, Blueprint[Any]] = {}
94
+ for base in reversed(owner_type.__mro__):
95
+ for name, value in vars(base).items():
96
+ if isinstance(value, Blueprint):
97
+ result[name] = value
98
+ elif name in result:
99
+ raise BindingError(
100
+ f"Override inherited blueprint {name!r} with a blueprint"
101
+ )
102
+ return result
@@ -0,0 +1,53 @@
1
+ """Validate application assets without traversing filesystem redirects."""
2
+
3
+ from pathlib import Path
4
+ import os
5
+ import stat
6
+
7
+
8
+ def _checked_path(root: Path, path: Path) -> tuple[os.stat_result, Path]:
9
+ # Check every component: an explicitly included file can itself have a
10
+ # junction/symlink parent. st_file_attributes works on Python 3.11 too.
11
+ current = root
12
+ for part in path.relative_to(root).parts:
13
+ current /= part
14
+ info = current.lstat()
15
+ if stat.S_ISLNK(info.st_mode) or getattr(info, "st_file_attributes", 0) & 0x400:
16
+ raise ValueError(f"Included symlinks and reparse points are unsupported: {current}")
17
+ resolved = path.resolve(strict=True)
18
+ if not resolved.is_relative_to(root):
19
+ raise ValueError(f"Included files must be inside the application directory: {path}")
20
+ return path.lstat(), resolved
21
+
22
+
23
+ def include_files(root: Path, item: str | Path, output: Path) -> tuple[Path, list[Path]]:
24
+ """Return a validated include root and its regular files, in stable order.
25
+
26
+ Inputs must remain unchanged while a build runs. This is a packaging
27
+ boundary, not a sandbox against concurrent hostile filesystem mutation.
28
+ """
29
+ relative = Path(item)
30
+ if relative.is_absolute() or ".." in relative.parts:
31
+ raise ValueError("Included files must be inside the application directory, using relative paths")
32
+ candidate = root / relative
33
+ try:
34
+ info, resolved = _checked_path(root, candidate)
35
+ # Windows accepts aliases such as a trailing dot or an 8.3 short name.
36
+ # Compare canonical paths only after checking lexical redirect parents.
37
+ if output == resolved or stat.S_ISDIR(info.st_mode) and output.is_relative_to(resolved):
38
+ raise ValueError("The generated output cannot be inside an included path")
39
+ pending: list[Path] = [candidate]
40
+ files: list[Path] = []
41
+ while pending:
42
+ path = pending.pop()
43
+ info, _ = _checked_path(root, path)
44
+ if stat.S_ISDIR(info.st_mode):
45
+ # Validate before descending, so junction cycles cannot recurse.
46
+ pending.extend(reversed(sorted(path.iterdir())))
47
+ elif stat.S_ISREG(info.st_mode):
48
+ files.append(path)
49
+ else:
50
+ raise ValueError(f"Included path must be a regular file or directory: {path}")
51
+ except OSError as error:
52
+ raise ValueError(f"Cannot read included path {item}: {error}") from error
53
+ return candidate, files
pysual/_catalog.py ADDED
@@ -0,0 +1,91 @@
1
+ """Built-in catalog metadata and installation, independent of implementation files.
2
+
3
+ New entries default to constructor/create-only registration. The historical
4
+ factory aliases are explicit compatibility choices, not an ever-growing set of
5
+ reserved Container members. Public Python exports remain explicit for typing.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from .controls import Button, Container, Control, Label
11
+ from .widgets import CheckBox, Image, ListView, ScrollArea, Slider, SubWindow, TextBox
12
+ from .selection import ComboBox, Dropdown, ProgressBar, RadioButton, Rating, Toggle
13
+ from .composition import SplitPane, TabControl, TabPage
14
+ from .charts import DonutChart, LineChart
15
+ from .data_grid import DataGrid
16
+ from .editors import ColorPicker, NumericInput
17
+ from .fields import SearchField
18
+ from .dates import DatePicker
19
+ from ._controls.indicators import RangeSlider, CircularProgress
20
+ from .navigation import SegmentedControl, Breadcrumb, Pagination
21
+ from .cards import ChoiceCard, Disclosure
22
+ from .feedback import Badge, AlertBanner, Meter
23
+ from .extras import GroupBox, Hyperlink, Separator
24
+ from .menus import Menu, MenuBar
25
+ from .popup import Popup
26
+ from .tree import TreeView
27
+ from ._registry import register_control
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class BuiltinControl:
32
+ name: str
33
+ control_type: type[Control]
34
+ family: str
35
+ legacy_factory: bool = False
36
+ authorable: bool = True
37
+
38
+
39
+ BUILTIN_CONTROLS = (
40
+ BuiltinControl("label", Label, "basic", legacy_factory=True),
41
+ BuiltinControl("button", Button, "basic", legacy_factory=True),
42
+ BuiltinControl("panel", Container, "layout", legacy_factory=True),
43
+ BuiltinControl("check_box", CheckBox, "actions", legacy_factory=True),
44
+ BuiltinControl("slider", Slider, "ranges", legacy_factory=True),
45
+ BuiltinControl("text_box", TextBox, "text", legacy_factory=True),
46
+ BuiltinControl("scroll_area", ScrollArea, "viewports", legacy_factory=True),
47
+ BuiltinControl("list_view", ListView, "collections", legacy_factory=True),
48
+ BuiltinControl("image", Image, "media", legacy_factory=True),
49
+ BuiltinControl("sub_window", SubWindow, "windows", legacy_factory=True),
50
+ BuiltinControl("tree_view", TreeView, "collections", legacy_factory=True),
51
+ BuiltinControl("popup", Popup, "overlays", legacy_factory=True),
52
+ BuiltinControl("line_chart", LineChart, "charts", legacy_factory=True),
53
+ BuiltinControl("donut_chart", DonutChart, "charts", legacy_factory=True),
54
+ BuiltinControl("data_grid", DataGrid, "collections", legacy_factory=True),
55
+ BuiltinControl("menu", Menu, "commands", legacy_factory=True),
56
+ BuiltinControl("menu_bar", MenuBar, "commands", legacy_factory=True),
57
+ BuiltinControl("tab_page", TabPage, "layout", legacy_factory=True),
58
+ BuiltinControl("tab_control", TabControl, "layout", legacy_factory=True),
59
+ BuiltinControl("split_pane", SplitPane, "layout", legacy_factory=True),
60
+ BuiltinControl("toggle", Toggle, "actions", legacy_factory=True),
61
+ BuiltinControl("radio_button", RadioButton, "actions", legacy_factory=True),
62
+ BuiltinControl("progress_bar", ProgressBar, "ranges", legacy_factory=True),
63
+ BuiltinControl("dropdown", Dropdown, "choices", legacy_factory=True),
64
+ BuiltinControl("combo_box", ComboBox, "choices", legacy_factory=True),
65
+ BuiltinControl("numeric_input", NumericInput, "editors", legacy_factory=True),
66
+ BuiltinControl("color_picker", ColorPicker, "editors", legacy_factory=True),
67
+ BuiltinControl("group_box", GroupBox, "layout", legacy_factory=True),
68
+ BuiltinControl("separator", Separator, "layout", legacy_factory=True),
69
+ BuiltinControl("hyperlink", Hyperlink, "actions", legacy_factory=True),
70
+ BuiltinControl("rating", Rating, "ranges"),
71
+ BuiltinControl("search_field", SearchField, "text"),
72
+ BuiltinControl("date_picker", DatePicker, "dates"),
73
+ BuiltinControl("range_slider", RangeSlider, "ranges"),
74
+ BuiltinControl("circular_progress", CircularProgress, "ranges"),
75
+ BuiltinControl("segmented_control", SegmentedControl, "actions"),
76
+ BuiltinControl("breadcrumb", Breadcrumb, "navigation"),
77
+ BuiltinControl("pagination", Pagination, "navigation"),
78
+ BuiltinControl("choice_card", ChoiceCard, "actions"),
79
+ BuiltinControl("disclosure", Disclosure, "layout"),
80
+ BuiltinControl("badge", Badge, "feedback"),
81
+ BuiltinControl("alert_banner", AlertBanner, "feedback"),
82
+ BuiltinControl("meter", Meter, "feedback"),
83
+ )
84
+
85
+
86
+ def install_builtin_controls() -> None:
87
+ for entry in BUILTIN_CONTROLS:
88
+ register_control(
89
+ entry.control_type, name=entry.name, authorable=entry.authorable,
90
+ factory=entry.legacy_factory,
91
+ )
pysual/_config.py ADDED
@@ -0,0 +1,236 @@
1
+ """Validated startup defaults; external configuration is explicitly opt-in."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ import math
7
+ import os
8
+ import sys
9
+ from threading import RLock
10
+ from typing import TYPE_CHECKING, cast
11
+
12
+ from .backends import BACKEND_NAMES, BackendName
13
+ from .errors import LifecycleError
14
+
15
+ if TYPE_CHECKING:
16
+ from .host import Host
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Settings:
21
+ backend: str | None = None
22
+ terminal_renderer: str | None = None
23
+ theme: str | None = None
24
+ ui_scale: float | None = None
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class Option:
29
+ field: str
30
+ flag: str
31
+ environment: str
32
+ description: str
33
+
34
+
35
+ OPTIONS = (
36
+ Option(
37
+ "backend",
38
+ "--backend",
39
+ "PYSUAL_BACKEND",
40
+ "UI backend: " + ", ".join(BACKEND_NAMES),
41
+ ),
42
+ Option(
43
+ "terminal_renderer",
44
+ "--terminal",
45
+ "PYSUAL_TERMINAL",
46
+ "Terminal renderer: auto, c, or python",
47
+ ),
48
+ Option(
49
+ "theme",
50
+ "--pysual-theme",
51
+ "PYSUAL_THEME",
52
+ "Default window theme (catalog name)",
53
+ ),
54
+ Option(
55
+ "ui_scale",
56
+ "--pysual-scale",
57
+ "PYSUAL_SCALE",
58
+ "Default window scale (finite number, at least 0.25)",
59
+ ),
60
+ )
61
+ _BY_FLAG = {option.flag: option for option in OPTIONS}
62
+ _defaults: dict[str, object] = {}
63
+ _environment: dict[str, object] = {}
64
+ _command_line: dict[str, object] = {}
65
+ _activated = False
66
+ _frozen: Settings | None = None
67
+ _lock = RLock()
68
+
69
+
70
+ def _validate(name: str, value: object) -> object:
71
+ if name == "backend":
72
+ if value is not None and (
73
+ not isinstance(value, str) or value not in BACKEND_NAMES
74
+ ):
75
+ raise ValueError("backend must be one of: " + ", ".join(BACKEND_NAMES))
76
+ elif name == "terminal_renderer":
77
+ if value is not None and value not in ("auto", "c", "python"):
78
+ raise ValueError("terminal renderer must be auto, c, or python")
79
+ elif name == "theme":
80
+ from .theme import theme_names
81
+
82
+ if value is not None and value not in theme_names():
83
+ raise ValueError("theme must be a built-in theme name")
84
+ elif name == "ui_scale":
85
+ if value is not None:
86
+ if type(value) is int or type(value) is float:
87
+ if not math.isfinite(value) or value < 0.25:
88
+ raise ValueError("ui_scale must be a finite number of at least 0.25")
89
+ else:
90
+ raise ValueError("ui_scale must be a finite number of at least 0.25")
91
+ else:
92
+ raise TypeError(f"Unknown startup setting {name!r}")
93
+ return value
94
+
95
+
96
+ def configure(
97
+ *,
98
+ backend: str | None = None,
99
+ terminal_renderer: str | None = None,
100
+ theme: str | None = None,
101
+ ui_scale: float | None = None,
102
+ ) -> Settings:
103
+ """Set startup defaults before constructing UI objects; never read argv/env.
104
+
105
+ Opted-in command-line values override these defaults. Theme and scale defaults
106
+ do not replace an explicit window constructor argument.
107
+ """
108
+ values = {name: value for name, value in locals().items() if value is not None}
109
+ values = {name: _validate(name, value) for name, value in values.items()}
110
+ with _lock:
111
+ if _frozen is not None:
112
+ raise LifecycleError("Configure Pysual before constructing UI objects")
113
+ _defaults.update(values)
114
+ return current()
115
+
116
+
117
+ def current() -> Settings:
118
+ with _lock:
119
+ if _frozen is not None:
120
+ return _frozen
121
+ values = {**_environment, **_defaults, **_command_line}
122
+ return Settings(
123
+ backend=cast(str | None, values.get("backend")),
124
+ terminal_renderer=cast(str | None, values.get("terminal_renderer")),
125
+ theme=cast(str | None, values.get("theme")),
126
+ ui_scale=cast(float | None, values.get("ui_scale")),
127
+ )
128
+
129
+
130
+ def freeze() -> Settings:
131
+ global _frozen
132
+ with _lock:
133
+ if _frozen is None:
134
+ _frozen = current()
135
+ return _frozen
136
+
137
+
138
+ def window_defaults() -> dict[str, object]:
139
+ settings = current()
140
+ defaults: dict[str, object] = {}
141
+ if settings.ui_scale is not None:
142
+ defaults["ui_scale"] = settings.ui_scale
143
+ if settings.theme is not None:
144
+ from .theme import get_theme
145
+
146
+ defaults["theme"] = get_theme(settings.theme)
147
+ return defaults
148
+
149
+
150
+ def _convert(option: Option, value: str) -> object:
151
+ if option.field == "ui_scale":
152
+ try:
153
+ parsed = float(value)
154
+ except (ValueError, TypeError):
155
+ raise ValueError(f"{option.flag} requires a number") from None
156
+ return _validate(option.field, parsed)
157
+ return _validate(option.field, value)
158
+
159
+
160
+ def activate() -> None:
161
+ """Consume only recognized arguments, transactionally and once per process."""
162
+ global _activated
163
+ with _lock:
164
+ if _activated:
165
+ return
166
+ if _frozen is not None:
167
+ raise LifecycleError(
168
+ "Import pysual.autoconfig before constructing UI objects"
169
+ )
170
+ arguments = list(sys.argv)
171
+ remaining = arguments[:1]
172
+ selected: dict[str, object] = {}
173
+ help_requested = False
174
+ index = 1
175
+ while index < len(arguments):
176
+ token = arguments[index]
177
+ if token == "--":
178
+ remaining.extend(arguments[index:])
179
+ break
180
+ flag, separator, value = token.partition("=")
181
+ if flag == "--pysual-help":
182
+ if separator:
183
+ raise ValueError("--pysual-help does not take a value")
184
+ help_requested = True
185
+ index += 1
186
+ continue
187
+ option = _BY_FLAG.get(flag)
188
+ if option is None:
189
+ if flag.startswith("--pysual-"):
190
+ raise ValueError(
191
+ f"Unknown Pysual option {flag!r}; use --pysual-help"
192
+ )
193
+ remaining.append(token)
194
+ index += 1
195
+ continue
196
+ if option.field in selected:
197
+ raise ValueError(f"Repeated Pysual option {flag}")
198
+ if not separator:
199
+ index += 1
200
+ if index >= len(arguments) or arguments[index].startswith("--"):
201
+ raise ValueError(f"{flag} requires a value")
202
+ value = arguments[index]
203
+ if not value:
204
+ raise ValueError(f"{flag} requires a value")
205
+ selected[option.field] = _convert(option, value)
206
+ index += 1
207
+ environment = {
208
+ option.field: _convert(option, os.environ[option.environment])
209
+ for option in OPTIONS
210
+ if option.environment in os.environ
211
+ and option.field not in selected
212
+ and option.field not in _defaults
213
+ }
214
+ if help_requested:
215
+ print("Pysual options (opt in with 'from pysual import autoconfig'):")
216
+ for option in OPTIONS:
217
+ print(f" {option.flag} VALUE {option.description}")
218
+ print(" --pysual-help Show this help; application --help is untouched")
219
+ raise SystemExit(0)
220
+ _environment.update(environment)
221
+ _command_line.update(selected)
222
+ sys.argv[:] = remaining
223
+ _activated = True
224
+
225
+
226
+ def resolved_backend(
227
+ preference: Host | BackendName | None = None,
228
+ ) -> Host | BackendName | None:
229
+ """CLI overrides a named preference; concrete test Hosts remain explicit."""
230
+ if preference is not None and not isinstance(preference, str):
231
+ return preference
232
+ with _lock:
233
+ return cast(
234
+ BackendName | None,
235
+ _command_line.get("backend") or preference or current().backend,
236
+ )
@@ -0,0 +1,5 @@
1
+ """Focused control implementations; public modules provide stable imports.
2
+
3
+ New families belong here or in an existing focused module. Registration is
4
+ owned by the built-in catalog, not implementation-module side effects.
5
+ """