bootstack 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.
- bootstack/__init__.py +157 -0
- bootstack/__main__.py +5 -0
- bootstack/_core/__init__.py +21 -0
- bootstack/_core/capabilities/__init__.py +45 -0
- bootstack/_core/capabilities/after.py +103 -0
- bootstack/_core/capabilities/bind.py +192 -0
- bootstack/_core/capabilities/bindtags.py +112 -0
- bootstack/_core/capabilities/busy.py +72 -0
- bootstack/_core/capabilities/clipboard.py +89 -0
- bootstack/_core/capabilities/focus.py +118 -0
- bootstack/_core/capabilities/grab.py +65 -0
- bootstack/_core/capabilities/grid.py +211 -0
- bootstack/_core/capabilities/localization.py +231 -0
- bootstack/_core/capabilities/pack.py +140 -0
- bootstack/_core/capabilities/place.py +113 -0
- bootstack/_core/capabilities/selection.py +136 -0
- bootstack/_core/capabilities/signals.py +244 -0
- bootstack/_core/capabilities/winfo.py +315 -0
- bootstack/_core/colorutils.py +234 -0
- bootstack/_core/exceptions.py +25 -0
- bootstack/_core/images.py +463 -0
- bootstack/_core/mixins/__init__.py +1 -0
- bootstack/_core/mixins/ttk_state.py +35 -0
- bootstack/_core/mixins/widget.py +132 -0
- bootstack/_core/paths.py +49 -0
- bootstack/_core/publisher.py +149 -0
- bootstack/_core/variables.py +62 -0
- bootstack/_runtime/__init__.py +3 -0
- bootstack/_runtime/app.py +930 -0
- bootstack/_runtime/base_window.py +945 -0
- bootstack/_runtime/events.py +399 -0
- bootstack/_runtime/shortcuts.py +496 -0
- bootstack/_runtime/tk_patch.py +43 -0
- bootstack/_runtime/toplevel.py +276 -0
- bootstack/_runtime/utility.py +457 -0
- bootstack/_runtime/visual_focus.py +240 -0
- bootstack/_runtime/window_utilities.py +1123 -0
- bootstack/assets/__init__.py +21 -0
- bootstack/assets/bootstack.ico +0 -0
- bootstack/assets/bootstack.png +0 -0
- bootstack/assets/elements/__init__.py +0 -0
- bootstack/assets/elements/badge-pill.png +0 -0
- bootstack/assets/elements/badge-square.png +0 -0
- bootstack/assets/elements/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
- bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
- bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
- bootstack/assets/elements/card.png +0 -0
- bootstack/assets/elements/checkbox-checked.png +0 -0
- bootstack/assets/elements/checkbox-indeterminate.png +0 -0
- bootstack/assets/elements/checkbox-unchecked.png +0 -0
- bootstack/assets/elements/field.png +0 -0
- bootstack/assets/elements/input-addon-compact.png +0 -0
- bootstack/assets/elements/input-addon-default.png +0 -0
- bootstack/assets/elements/input-compact.png +0 -0
- bootstack/assets/elements/input-default.png +0 -0
- bootstack/assets/elements/list-item-separated.png +0 -0
- bootstack/assets/elements/list-item.png +0 -0
- bootstack/assets/elements/listrow-compact.png +0 -0
- bootstack/assets/elements/listrow-default.png +0 -0
- bootstack/assets/elements/manifest.toml +361 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/navitem-compact.png +0 -0
- bootstack/assets/elements/navitem-default.png +0 -0
- bootstack/assets/elements/progressbar-h-compact.png +0 -0
- bootstack/assets/elements/progressbar-h-default.png +0 -0
- bootstack/assets/elements/progressbar-v-compact.png +0 -0
- bootstack/assets/elements/progressbar-v-default.png +0 -0
- bootstack/assets/elements/radiobutton.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-h.png +0 -0
- bootstack/assets/elements/slider-track-v.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tab-h.png +0 -0
- bootstack/assets/elements/tab-v.png +0 -0
- bootstack/assets/icons/bootstrap.ttf +0 -0
- bootstack/assets/icons/glyphmap.json +2080 -0
- bootstack/assets/icons/icon_metrics.json +12470 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
- bootstack/cli/__init__.py +133 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +395 -0
- bootstack/cli/appicon.py +285 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +313 -0
- bootstack/cli/demo.py +564 -0
- bootstack/cli/dev.py +153 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/icons.py +98 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +268 -0
- bootstack/cli/run.py +95 -0
- bootstack/cli/start.py +117 -0
- bootstack/cli/templates/__init__.py +931 -0
- bootstack/clipboard.py +48 -0
- bootstack/constants.py +318 -0
- bootstack/data/README.md +615 -0
- bootstack/data/__init__.py +78 -0
- bootstack/data/_observable.py +276 -0
- bootstack/data/base.py +780 -0
- bootstack/data/file_source.py +367 -0
- bootstack/data/memory_source.py +450 -0
- bootstack/data/query.py +367 -0
- bootstack/data/readers.py +289 -0
- bootstack/data/sqlite_source.py +869 -0
- bootstack/data/types.py +354 -0
- bootstack/data/writers.py +232 -0
- bootstack/dev/__init__.py +36 -0
- bootstack/dev/_capture.py +141 -0
- bootstack/dev/_env.py +43 -0
- bootstack/dev/_registry.py +140 -0
- bootstack/dev/_reloader.py +351 -0
- bootstack/dev/_reset.py +50 -0
- bootstack/dev/_watcher.py +91 -0
- bootstack/dialogs/__init__.py +948 -0
- bootstack/dialogs/_impl/__init__.py +47 -0
- bootstack/dialogs/_impl/colorchooser.py +588 -0
- bootstack/dialogs/_impl/datedialog.py +450 -0
- bootstack/dialogs/_impl/dialog.py +594 -0
- bootstack/dialogs/_impl/filterdialog.py +358 -0
- bootstack/dialogs/_impl/fontdialog.py +364 -0
- bootstack/dialogs/_impl/formdialog.py +564 -0
- bootstack/dialogs/_impl/message.py +486 -0
- bootstack/dialogs/_impl/query.py +570 -0
- bootstack/errors.py +67 -0
- bootstack/events/__init__.py +111 -0
- bootstack/events/_event.py +135 -0
- bootstack/events/_payloads.py +539 -0
- bootstack/events/_subscription.py +38 -0
- bootstack/i18n/README.md +77 -0
- bootstack/i18n/__init__.py +23 -0
- bootstack/i18n/catalog.py +121 -0
- bootstack/i18n/intl_format.py +584 -0
- bootstack/i18n/msgcat.py +425 -0
- bootstack/i18n/specs.py +156 -0
- bootstack/images.py +563 -0
- bootstack/py.typed +1 -0
- bootstack/scheduling/__init__.py +11 -0
- bootstack/scheduling/_schedule.py +218 -0
- bootstack/shortcuts.py +21 -0
- bootstack/signals/README.md +98 -0
- bootstack/signals/__init__.py +10 -0
- bootstack/signals/integration.py +100 -0
- bootstack/signals/signal.py +353 -0
- bootstack/signals/types.py +6 -0
- bootstack/store.py +286 -0
- bootstack/streams/__init__.py +12 -0
- bootstack/streams/_stream.py +321 -0
- bootstack/style/__init__.py +38 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +46 -0
- bootstack/style/builders/button.py +339 -0
- bootstack/style/builders/buttongroup.py +311 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +110 -0
- bootstack/style/builders/combobox.py +113 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +82 -0
- bootstack/style/builders/expander.py +148 -0
- bootstack/style/builders/field.py +335 -0
- bootstack/style/builders/frame.py +50 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +34 -0
- bootstack/style/builders/listview.py +369 -0
- bootstack/style/builders/menubar.py +91 -0
- bootstack/style/builders/menubutton.py +359 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +67 -0
- bootstack/style/builders/radiobutton.py +99 -0
- bootstack/style/builders/scale.py +64 -0
- bootstack/style/builders/scrollbar.py +225 -0
- bootstack/style/builders/separator.py +49 -0
- bootstack/style/builders/sidenav.py +643 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +70 -0
- bootstack/style/builders/tabitem.py +204 -0
- bootstack/style/builders/togglegroup.py +294 -0
- bootstack/style/builders/toolbutton.py +275 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +193 -0
- bootstack/style/builders/utils.py +455 -0
- bootstack/style/builders_tk/__init__.py +16 -0
- bootstack/style/builders_tk/defaults.py +229 -0
- bootstack/style/element.py +173 -0
- bootstack/style/fonts.py +123 -0
- bootstack/style/style.py +609 -0
- bootstack/style/style_builder_base.py +716 -0
- bootstack/style/style_builder_mixed.py +93 -0
- bootstack/style/style_builder_tk.py +109 -0
- bootstack/style/style_builder_ttk.py +353 -0
- bootstack/style/style_resolver.py +447 -0
- bootstack/style/theme.py +245 -0
- bootstack/style/theme_provider.py +471 -0
- bootstack/style/themes/__init__.py +128 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +41 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +523 -0
- bootstack/style/utility.py +746 -0
- bootstack/types.py +39 -0
- bootstack/validation/__init__.py +6 -0
- bootstack/validation/types.py +15 -0
- bootstack/validation/validation_result.py +17 -0
- bootstack/validation/validation_rules.py +205 -0
- bootstack/widgets/__init__.py +74 -0
- bootstack/widgets/_core/__init__.py +31 -0
- bootstack/widgets/_core/app_config.py +266 -0
- bootstack/widgets/_core/base.py +461 -0
- bootstack/widgets/_core/container.py +334 -0
- bootstack/widgets/_core/context.py +35 -0
- bootstack/widgets/_core/events.py +130 -0
- bootstack/widgets/_core/field_mixin.py +353 -0
- bootstack/widgets/_core/icon_image_props.py +72 -0
- bootstack/widgets/_core/image_binding.py +93 -0
- bootstack/widgets/_core/navmodel.py +345 -0
- bootstack/widgets/_core/options.py +210 -0
- bootstack/widgets/_core/selection_group.py +130 -0
- bootstack/widgets/_core/window_controls.py +96 -0
- bootstack/widgets/_core/window_menu.py +246 -0
- bootstack/widgets/_impl/__init__.py +1 -0
- bootstack/widgets/_impl/_internal/__init__.py +0 -0
- bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
- bootstack/widgets/_impl/_parts/__init__.py +11 -0
- bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
- bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
- bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
- bootstack/widgets/_impl/composites/__init__.py +33 -0
- bootstack/widgets/_impl/composites/_dateutils.py +33 -0
- bootstack/widgets/_impl/composites/_image_fit.py +105 -0
- bootstack/widgets/_impl/composites/accordion.py +381 -0
- bootstack/widgets/_impl/composites/avatar.py +191 -0
- bootstack/widgets/_impl/composites/buttongroup.py +371 -0
- bootstack/widgets/_impl/composites/calendar.py +972 -0
- bootstack/widgets/_impl/composites/carousel.py +567 -0
- bootstack/widgets/_impl/composites/chart.py +882 -0
- bootstack/widgets/_impl/composites/compositeframe.py +298 -0
- bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
- bootstack/widgets/_impl/composites/dateentry.py +404 -0
- bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
- bootstack/widgets/_impl/composites/expander.py +515 -0
- bootstack/widgets/_impl/composites/field.py +670 -0
- bootstack/widgets/_impl/composites/form.py +1066 -0
- bootstack/widgets/_impl/composites/gallery.py +551 -0
- bootstack/widgets/_impl/composites/list/__init__.py +15 -0
- bootstack/widgets/_impl/composites/list/listitem.py +802 -0
- bootstack/widgets/_impl/composites/list/listview.py +1433 -0
- bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
- bootstack/widgets/_impl/composites/menu/model.py +358 -0
- bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
- bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
- bootstack/widgets/_impl/composites/meter.py +860 -0
- bootstack/widgets/_impl/composites/numericentry.py +201 -0
- bootstack/widgets/_impl/composites/pagestack.py +395 -0
- bootstack/widgets/_impl/composites/passwordentry.py +142 -0
- bootstack/widgets/_impl/composites/pathentry.py +168 -0
- bootstack/widgets/_impl/composites/picture.py +289 -0
- bootstack/widgets/_impl/composites/radiogroup.py +511 -0
- bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
- bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/_impl/composites/scrollview.py +764 -0
- bootstack/widgets/_impl/composites/selectbox.py +1026 -0
- bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
- bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
- bootstack/widgets/_impl/composites/shell/layout.py +335 -0
- bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
- bootstack/widgets/_impl/composites/shell/providers.py +558 -0
- bootstack/widgets/_impl/composites/shell/rail.py +117 -0
- bootstack/widgets/_impl/composites/shell/shell.py +581 -0
- bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
- bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
- bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
- bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
- bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
- bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
- bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
- bootstack/widgets/_impl/composites/slider/slider.py +851 -0
- bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
- bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
- bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
- bootstack/widgets/_impl/composites/tableview/types.py +169 -0
- bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
- bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
- bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
- bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
- bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
- bootstack/widgets/_impl/composites/textarea/change.py +46 -0
- bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
- bootstack/widgets/_impl/composites/textarea/core.py +495 -0
- bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
- bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
- bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
- bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
- bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
- bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
- bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
- bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
- bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
- bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
- bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
- bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
- bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
- bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
- bootstack/widgets/_impl/composites/textentry.py +57 -0
- bootstack/widgets/_impl/composites/timeentry.py +176 -0
- bootstack/widgets/_impl/composites/toast.py +390 -0
- bootstack/widgets/_impl/composites/toast_stack.py +156 -0
- bootstack/widgets/_impl/composites/togglegroup.py +418 -0
- bootstack/widgets/_impl/composites/toolbar.py +608 -0
- bootstack/widgets/_impl/composites/tooltip.py +491 -0
- bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
- bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
- bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
- bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
- bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
- bootstack/widgets/_impl/mixins/__init__.py +24 -0
- bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
- bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
- bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
- bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
- bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
- bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
- bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
- bootstack/widgets/_impl/primitives/__init__.py +49 -0
- bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
- bootstack/widgets/_impl/primitives/badge.py +45 -0
- bootstack/widgets/_impl/primitives/button.py +76 -0
- bootstack/widgets/_impl/primitives/card.py +45 -0
- bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
- bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
- bootstack/widgets/_impl/primitives/combobox.py +156 -0
- bootstack/widgets/_impl/primitives/entry.py +87 -0
- bootstack/widgets/_impl/primitives/flexframe.py +448 -0
- bootstack/widgets/_impl/primitives/frame.py +185 -0
- bootstack/widgets/_impl/primitives/gridframe.py +546 -0
- bootstack/widgets/_impl/primitives/label.py +84 -0
- bootstack/widgets/_impl/primitives/labelframe.py +54 -0
- bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
- bootstack/widgets/_impl/primitives/packframe.py +227 -0
- bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
- bootstack/widgets/_impl/primitives/progressbar.py +83 -0
- bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
- bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
- bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
- bootstack/widgets/_impl/primitives/separator.py +43 -0
- bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
- bootstack/widgets/_impl/primitives/spinbox.py +95 -0
- bootstack/widgets/_impl/primitives/switch.py +44 -0
- bootstack/widgets/_impl/primitives/treeview.py +69 -0
- bootstack/widgets/app.py +371 -0
- bootstack/widgets/appshell.py +1179 -0
- bootstack/widgets/avatar.py +140 -0
- bootstack/widgets/boolean_controls.py +455 -0
- bootstack/widgets/button.py +224 -0
- bootstack/widgets/buttongroup.py +239 -0
- bootstack/widgets/calendar.py +195 -0
- bootstack/widgets/card.py +159 -0
- bootstack/widgets/carousel.py +241 -0
- bootstack/widgets/chart.py +302 -0
- bootstack/widgets/codeeditor.py +675 -0
- bootstack/widgets/contextmenu.py +371 -0
- bootstack/widgets/datatable.py +688 -0
- bootstack/widgets/datefield.py +395 -0
- bootstack/widgets/divider.py +60 -0
- bootstack/widgets/expander.py +579 -0
- bootstack/widgets/form.py +200 -0
- bootstack/widgets/gallery.py +250 -0
- bootstack/widgets/gauge.py +168 -0
- bootstack/widgets/grid.py +121 -0
- bootstack/widgets/groupbox.py +162 -0
- bootstack/widgets/label.py +229 -0
- bootstack/widgets/listview.py +354 -0
- bootstack/widgets/menubutton.py +419 -0
- bootstack/widgets/numberfield.py +431 -0
- bootstack/widgets/pagestack.py +345 -0
- bootstack/widgets/passwordfield.py +383 -0
- bootstack/widgets/pathfield.py +454 -0
- bootstack/widgets/picture.py +251 -0
- bootstack/widgets/progressbar.py +106 -0
- bootstack/widgets/radio_variants.py +271 -0
- bootstack/widgets/radiogroup.py +228 -0
- bootstack/widgets/scrollbar.py +64 -0
- bootstack/widgets/scrollview.py +186 -0
- bootstack/widgets/select.py +302 -0
- bootstack/widgets/selectbutton.py +182 -0
- bootstack/widgets/sidebar_toggle.py +130 -0
- bootstack/widgets/sizegrip.py +42 -0
- bootstack/widgets/slider.py +408 -0
- bootstack/widgets/spinbox.py +147 -0
- bootstack/widgets/spinnerfield.py +413 -0
- bootstack/widgets/splash.py +367 -0
- bootstack/widgets/splitview.py +482 -0
- bootstack/widgets/stacks.py +249 -0
- bootstack/widgets/statusbar.py +189 -0
- bootstack/widgets/tabs.py +425 -0
- bootstack/widgets/textarea.py +459 -0
- bootstack/widgets/textfield.py +410 -0
- bootstack/widgets/theme_toggle.py +92 -0
- bootstack/widgets/timefield.py +340 -0
- bootstack/widgets/toast.py +314 -0
- bootstack/widgets/togglegroup.py +231 -0
- bootstack/widgets/toolbar.py +347 -0
- bootstack/widgets/tooltip.py +94 -0
- bootstack/widgets/tree.py +648 -0
- bootstack/widgets/types.py +305 -0
- bootstack/widgets/window.py +307 -0
- bootstack-0.1.0.dist-info/METADATA +301 -0
- bootstack-0.1.0.dist-info/RECORD +471 -0
- bootstack-0.1.0.dist-info/WHEEL +5 -0
- bootstack-0.1.0.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
- bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
import weakref
|
|
3
|
+
from itertools import count
|
|
4
|
+
from typing import Any, Callable, Generic, Type, TypeVar
|
|
5
|
+
|
|
6
|
+
from bootstack.signals.types import TraceOperation
|
|
7
|
+
from bootstack._core.variables import SetVar
|
|
8
|
+
from bootstack.streams import Handle
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
U = TypeVar("U")
|
|
12
|
+
|
|
13
|
+
# Weak registry of all realized signals. Cleared on App.destroy() (when no
|
|
14
|
+
# other App is live) so module-level signals can be re-realized against the
|
|
15
|
+
# next App's interpreter without dangling against a dead one.
|
|
16
|
+
_realized: "weakref.WeakSet[Signal]" = weakref.WeakSet()
|
|
17
|
+
|
|
18
|
+
# Global subscriber ID counter — unique across all Signal instances so that
|
|
19
|
+
# Handle cancellation never accidentally removes the wrong entry.
|
|
20
|
+
_sub_counter = count(1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def reset_realized_signals() -> None:
|
|
24
|
+
"""Drop the Tk var from every realized signal.
|
|
25
|
+
|
|
26
|
+
Called on App.destroy() when no other App is live. Module-level signals
|
|
27
|
+
return to pending (unrealized) state and re-realize transparently the next
|
|
28
|
+
time a widget inside a new App binds them via .var / .name / .tk.
|
|
29
|
+
"""
|
|
30
|
+
for sig in list(_realized):
|
|
31
|
+
try:
|
|
32
|
+
sig._drop_var()
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _SignalTrace:
|
|
38
|
+
"""Manages the single bridge Tk trace on a realized Signal."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, tk_var: tk.Variable):
|
|
41
|
+
self._var = tk_var
|
|
42
|
+
self._traces: dict[str, tuple[TraceOperation, Callable[..., Any]]] = {}
|
|
43
|
+
|
|
44
|
+
def add(
|
|
45
|
+
self,
|
|
46
|
+
operation: TraceOperation,
|
|
47
|
+
callback: Callable[[T], Any],
|
|
48
|
+
get_value: Callable[[], T],
|
|
49
|
+
) -> str:
|
|
50
|
+
def traced_callback(name: str, index: str, mode: str) -> None:
|
|
51
|
+
callback(get_value())
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
fid = self._var.trace_add(operation, traced_callback)
|
|
55
|
+
except tk.TclError as e:
|
|
56
|
+
raise RuntimeError(f"failed to add trace: {e}") from e
|
|
57
|
+
self._traces[fid] = (operation, traced_callback)
|
|
58
|
+
return fid
|
|
59
|
+
|
|
60
|
+
def remove(self, fid: str) -> None:
|
|
61
|
+
op_cb = self._traces.pop(fid, None)
|
|
62
|
+
if op_cb is None:
|
|
63
|
+
return
|
|
64
|
+
operation, _ = op_cb
|
|
65
|
+
try:
|
|
66
|
+
self._var.trace_remove(operation, fid)
|
|
67
|
+
except tk.TclError:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Signal(Generic[T]):
|
|
72
|
+
"""
|
|
73
|
+
A reactive value that widgets can bind to.
|
|
74
|
+
|
|
75
|
+
Holds a typed value and notifies subscribers when it changes. Read it by
|
|
76
|
+
calling the signal, update it with `set()`, derive new signals with
|
|
77
|
+
`map()`, and react to changes with `subscribe()`.
|
|
78
|
+
|
|
79
|
+
Bind a signal to a widget by passing it as `textsignal=` for text-bearing
|
|
80
|
+
widgets, or `signal=` for boolean and numeric widgets.
|
|
81
|
+
|
|
82
|
+
Signals may be constructed at module level (before `bs.App()` exists). The
|
|
83
|
+
backing Tk variable is created lazily on the first widget binding and torn
|
|
84
|
+
down when the App is destroyed, so the same signal can be reused across
|
|
85
|
+
successive App lifecycles.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
_cnt = count(1)
|
|
89
|
+
|
|
90
|
+
def __init__(self, value: T, name: str | None = None, master: tk.Misc | None = None):
|
|
91
|
+
self._name = name or f"SIG{next(self._cnt)}"
|
|
92
|
+
self._type: Type[T] = type(value)
|
|
93
|
+
self._master: tk.Misc | None = master
|
|
94
|
+
self._object_mode = not self._is_tk_native(value)
|
|
95
|
+
# _last is always the authoritative Python value — before realization it
|
|
96
|
+
# is the only store; after realization it stays in sync via the bridge.
|
|
97
|
+
self._last: T = value
|
|
98
|
+
# Tk var and bridge trace — None until the first widget binding.
|
|
99
|
+
self._var: tk.Variable | None = None
|
|
100
|
+
self._trace: _SignalTrace | None = None
|
|
101
|
+
self._bridge_fid: str | None = None
|
|
102
|
+
# Python-owned subscriber list (int id → callback). The bridge trace fans
|
|
103
|
+
# these out when a widget writes the Tk var; set() notifies them directly
|
|
104
|
+
# while unrealized. Replaces the old one-Tk-trace-per-subscriber model.
|
|
105
|
+
self._subscribers: dict[int, Callable[[T], Any]] = {}
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _is_tk_native(value: Any) -> bool:
|
|
109
|
+
"""Whether a value maps to a Tk variable that round-trips it losslessly."""
|
|
110
|
+
return isinstance(value, (bool, int, float, str, set))
|
|
111
|
+
|
|
112
|
+
def _create_variable(self, value: T) -> tk.Variable:
|
|
113
|
+
if isinstance(value, bool):
|
|
114
|
+
return tk.BooleanVar(master=self._master, name=self._name, value=value)
|
|
115
|
+
elif isinstance(value, int):
|
|
116
|
+
return tk.IntVar(master=self._master, name=self._name, value=value)
|
|
117
|
+
elif isinstance(value, float):
|
|
118
|
+
return tk.DoubleVar(master=self._master, name=self._name, value=value)
|
|
119
|
+
elif isinstance(value, set):
|
|
120
|
+
return SetVar(master=self._master, name=self._name, value=value)
|
|
121
|
+
else:
|
|
122
|
+
return tk.StringVar(master=self._master, name=self._name, value=value)
|
|
123
|
+
|
|
124
|
+
def _realize(self) -> None:
|
|
125
|
+
"""Create the backing Tk variable and wire the single bridge trace.
|
|
126
|
+
|
|
127
|
+
Idempotent — safe to call multiple times. Only runs inside an App
|
|
128
|
+
context (widget bindings are always inside one), so a live interpreter
|
|
129
|
+
is guaranteed.
|
|
130
|
+
"""
|
|
131
|
+
if self._var is not None:
|
|
132
|
+
return
|
|
133
|
+
self._var = self._create_variable(self._last)
|
|
134
|
+
self._trace = _SignalTrace(self._var)
|
|
135
|
+
|
|
136
|
+
def _bridge(value: T) -> None:
|
|
137
|
+
if not self._object_mode:
|
|
138
|
+
self._last = value
|
|
139
|
+
for cb in list(self._subscribers.values()):
|
|
140
|
+
cb(self._last)
|
|
141
|
+
|
|
142
|
+
self._bridge_fid = self._trace.add("write", _bridge, self)
|
|
143
|
+
_realized.add(self)
|
|
144
|
+
|
|
145
|
+
def _drop_var(self) -> None:
|
|
146
|
+
"""Release the Tk var and bridge trace (called by reset_realized_signals)."""
|
|
147
|
+
if self._trace is not None and self._bridge_fid is not None:
|
|
148
|
+
self._trace.remove(self._bridge_fid)
|
|
149
|
+
self._var = None
|
|
150
|
+
self._trace = None
|
|
151
|
+
self._bridge_fid = None
|
|
152
|
+
|
|
153
|
+
def __call__(self) -> T:
|
|
154
|
+
"""Get the current value of the signal."""
|
|
155
|
+
if self._var is None or self._object_mode:
|
|
156
|
+
return self._last
|
|
157
|
+
try:
|
|
158
|
+
value = self._var.get()
|
|
159
|
+
self._last = value
|
|
160
|
+
return value
|
|
161
|
+
except tk.TclError:
|
|
162
|
+
return self._last
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def from_variable(
|
|
166
|
+
cls,
|
|
167
|
+
tk_var: tk.Variable,
|
|
168
|
+
*,
|
|
169
|
+
name: str | None = None,
|
|
170
|
+
coerce: Type[T] | None = None,
|
|
171
|
+
) -> "Signal[T]":
|
|
172
|
+
"""
|
|
173
|
+
Wrap an existing tkinter Variable as a Signal.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
tk_var: An existing tkinter.Variable instance (StringVar, IntVar, etc.).
|
|
177
|
+
name: Optional override of the Tcl variable name. Defaults to tk_var's name.
|
|
178
|
+
coerce: Optional Python type to treat the signal as (e.g., int/float/bool/str).
|
|
179
|
+
If omitted, the type is inferred from the tk_var subclass.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A Signal bound to the provided tk_var.
|
|
183
|
+
"""
|
|
184
|
+
if coerce is None:
|
|
185
|
+
if isinstance(tk_var, tk.BooleanVar):
|
|
186
|
+
py_type: Type[Any] = bool
|
|
187
|
+
elif isinstance(tk_var, tk.IntVar):
|
|
188
|
+
py_type = int
|
|
189
|
+
elif isinstance(tk_var, tk.DoubleVar):
|
|
190
|
+
py_type = float
|
|
191
|
+
elif isinstance(tk_var, SetVar):
|
|
192
|
+
py_type = set
|
|
193
|
+
else:
|
|
194
|
+
py_type = str
|
|
195
|
+
else:
|
|
196
|
+
py_type = coerce
|
|
197
|
+
|
|
198
|
+
self = cls.__new__(cls)
|
|
199
|
+
self._name = name or getattr(tk_var, "_name", str(tk_var))
|
|
200
|
+
self._type = py_type # type: ignore[assignment]
|
|
201
|
+
self._object_mode = False
|
|
202
|
+
self._master = getattr(tk_var, "_master", None)
|
|
203
|
+
self._subscribers = {}
|
|
204
|
+
try:
|
|
205
|
+
current = tk_var.get() # type: ignore[assignment]
|
|
206
|
+
except tk.TclError:
|
|
207
|
+
if py_type is float: # type: ignore[comparison-overlap]
|
|
208
|
+
current = 0.0
|
|
209
|
+
elif py_type is int: # type: ignore[comparison-overlap]
|
|
210
|
+
current = 0
|
|
211
|
+
elif py_type is bool: # type: ignore[comparison-overlap]
|
|
212
|
+
current = False
|
|
213
|
+
elif py_type is set:
|
|
214
|
+
current = set()
|
|
215
|
+
else:
|
|
216
|
+
current = ""
|
|
217
|
+
self._last = current # type: ignore[assignment]
|
|
218
|
+
|
|
219
|
+
# from_variable is immediately realized — the var already exists.
|
|
220
|
+
self._var = tk_var
|
|
221
|
+
self._trace = _SignalTrace(self._var)
|
|
222
|
+
|
|
223
|
+
def _bridge(value: T) -> None:
|
|
224
|
+
self._last = value
|
|
225
|
+
for cb in list(self._subscribers.values()):
|
|
226
|
+
cb(self._last)
|
|
227
|
+
|
|
228
|
+
self._bridge_fid = self._trace.add("write", _bridge, self)
|
|
229
|
+
_realized.add(self)
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
def set(self, value: T) -> None:
|
|
233
|
+
"""
|
|
234
|
+
Set the signal to a new value and notify subscribers.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
value: The new value. Must match the signal's type, except that an
|
|
238
|
+
`int` may be set on a `float`-typed signal (it is widened).
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
TypeError: If the value type does not match the signal's type (and is
|
|
242
|
+
not an `int` widened to a `float`).
|
|
243
|
+
"""
|
|
244
|
+
if type(value) is not self._type:
|
|
245
|
+
if self._type is float and type(value) is int:
|
|
246
|
+
value = float(value) # type: ignore[assignment]
|
|
247
|
+
else:
|
|
248
|
+
raise TypeError(
|
|
249
|
+
f"Expected {self._type.__name__}, got {type(value).__name__}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if self._var is None:
|
|
253
|
+
# Unrealized — notify Python subscribers directly.
|
|
254
|
+
if self._last == value:
|
|
255
|
+
return
|
|
256
|
+
self._last = value
|
|
257
|
+
for cb in list(self._subscribers.values()):
|
|
258
|
+
cb(self._last)
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
if self._object_mode:
|
|
262
|
+
if self._last == value:
|
|
263
|
+
return
|
|
264
|
+
self._last = value
|
|
265
|
+
try:
|
|
266
|
+
self._var.set(str(value))
|
|
267
|
+
except tk.TclError:
|
|
268
|
+
pass
|
|
269
|
+
# Bridge trace fires from var.set() → notifies subscribers.
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
# Native-type, realized — let the Tk var fire the bridge trace.
|
|
273
|
+
try:
|
|
274
|
+
if self._var.get() == value:
|
|
275
|
+
return
|
|
276
|
+
except tk.TclError:
|
|
277
|
+
pass
|
|
278
|
+
self._var.set(value)
|
|
279
|
+
self._last = value
|
|
280
|
+
|
|
281
|
+
def map(self, transform: Callable[[T], U]) -> 'Signal[U]':
|
|
282
|
+
"""
|
|
283
|
+
Create a derived signal that transforms this signal's value.
|
|
284
|
+
|
|
285
|
+
The derived signal recomputes whenever this signal changes. It is held
|
|
286
|
+
weakly, so keep a reference to it — for example, by binding it to a
|
|
287
|
+
widget — or it will stop updating once garbage-collected.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
transform: A function applied to the current and future values.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
A new read-only signal that stays updated with the transformed value.
|
|
294
|
+
"""
|
|
295
|
+
derived = Signal(transform(self()))
|
|
296
|
+
weak_derived = weakref.ref(derived)
|
|
297
|
+
|
|
298
|
+
def update(value: T) -> None:
|
|
299
|
+
d = weak_derived()
|
|
300
|
+
if d is None:
|
|
301
|
+
return
|
|
302
|
+
d.set(transform(value))
|
|
303
|
+
|
|
304
|
+
self.subscribe(update)
|
|
305
|
+
return derived
|
|
306
|
+
|
|
307
|
+
def subscribe(self, callback: Callable[[T], Any], *, immediate: bool = False) -> Handle:
|
|
308
|
+
"""
|
|
309
|
+
Subscribe to value changes of this signal.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
callback: A function called with the new value whenever it changes.
|
|
313
|
+
immediate: When True, also call `callback` once with the current
|
|
314
|
+
value at subscription time. Defaults to False.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
A cancellable `Handle` — call `.cancel()` to stop listening, or use
|
|
318
|
+
it as a context manager to unsubscribe on exit.
|
|
319
|
+
"""
|
|
320
|
+
fid = next(_sub_counter)
|
|
321
|
+
self._subscribers[fid] = callback
|
|
322
|
+
if immediate:
|
|
323
|
+
callback(self())
|
|
324
|
+
return Handle(lambda: self._subscribers.pop(fid, None))
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def name(self) -> str:
|
|
328
|
+
"""The internal Tcl variable name. Accessing this realizes the signal."""
|
|
329
|
+
self._realize()
|
|
330
|
+
return self._name
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def type(self) -> Type[T]:
|
|
334
|
+
"""The original type of the signal value."""
|
|
335
|
+
return self._type
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def var(self) -> tk.Variable:
|
|
339
|
+
"""The backing `tk.Variable`. Accessing this realizes the signal."""
|
|
340
|
+
self._realize()
|
|
341
|
+
return self._var # type: ignore[return-value]
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def tk(self) -> tk.Variable:
|
|
345
|
+
"""Underlying `tk.Variable`. UNSUPPORTED — escape-hatch use only."""
|
|
346
|
+
self._realize()
|
|
347
|
+
return self._var # type: ignore[return-value]
|
|
348
|
+
|
|
349
|
+
def __str__(self) -> str:
|
|
350
|
+
return self._name
|
|
351
|
+
|
|
352
|
+
def __repr__(self) -> str:
|
|
353
|
+
return self._name
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Type aliases for the signal and variable tracing system."""
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
TraceOperation = Literal["array", "read", "write", "unset"]
|
|
5
|
+
"""The kind of variable access a trace callback reports — an `array` element
|
|
6
|
+
access, a `read`, a `write`, or an `unset`."""
|
bootstack/store.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""`Store` — a small, file-backed key-value store for application preferences.
|
|
2
|
+
|
|
3
|
+
`Store` is the place to keep the little pieces of state an app wants to remember
|
|
4
|
+
between runs: the chosen theme, a "show tips on startup" flag, recent files, the
|
|
5
|
+
last-opened tab. It behaves like a dict, persists to a JSON file under the OS
|
|
6
|
+
config directory, and writes through on every change by default.
|
|
7
|
+
|
|
8
|
+
It is deliberately *not* a data source — there are no rows, ids, pagination, or
|
|
9
|
+
queries. Reach for `MemoryDataSource` / `SqliteDataSource` for record-oriented
|
|
10
|
+
data, and `Store` for "remember this setting".
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import copy
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import threading
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Iterator
|
|
21
|
+
|
|
22
|
+
from bootstack._core.paths import app_config_file
|
|
23
|
+
from bootstack.errors import SerializationError
|
|
24
|
+
|
|
25
|
+
__all__ = ["Store"]
|
|
26
|
+
|
|
27
|
+
_MISSING = object()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _active_app_name() -> str | None:
|
|
31
|
+
"""Return the current App's name, or None when there is no active App."""
|
|
32
|
+
try:
|
|
33
|
+
from bootstack._runtime.app import get_app_settings
|
|
34
|
+
|
|
35
|
+
return get_app_settings().app_name
|
|
36
|
+
except Exception:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Store:
|
|
41
|
+
"""A persistent, dict-like key-value store for app preferences.
|
|
42
|
+
|
|
43
|
+
Construct one with a logical name and it lives at `<config>/<app>/<name>.json`
|
|
44
|
+
(the per-platform config directory — `Library/Application Support` on macOS,
|
|
45
|
+
`%APPDATA%` on Windows, `$XDG_CONFIG_HOME` on Linux), or pass an explicit
|
|
46
|
+
`path`. Read with `get()` or `store[key]`, write with `set()` or
|
|
47
|
+
`store[key] = value`; by default every change is saved immediately with an
|
|
48
|
+
atomic write, so a crash cannot corrupt the file.
|
|
49
|
+
|
|
50
|
+
Values must be JSON-serializable (scalars, lists, dicts); anything else
|
|
51
|
+
raises `SerializationError`. Keys must be strings. A missing or unreadable
|
|
52
|
+
file simply starts the store empty rather than raising.
|
|
53
|
+
|
|
54
|
+
`Store` does not require a running `App` — it is plain file I/O. When no
|
|
55
|
+
`app_name` is given it uses the active App's name if there is one, otherwise
|
|
56
|
+
`'bootstack'`.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
name: Logical store name; the file is `<name>.json` under the per-app
|
|
60
|
+
config directory. Ignored when `path` is given.
|
|
61
|
+
path: An explicit file path, overriding `name`/`app_name`.
|
|
62
|
+
app_name: Override the per-app config sub-directory. Defaults to the
|
|
63
|
+
active App's name, or `'bootstack'`.
|
|
64
|
+
autosave: When True (default), persist on every change. When False,
|
|
65
|
+
changes stay in memory until `save()` is called.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
```python
|
|
69
|
+
store = Store("settings")
|
|
70
|
+
app = bs.App(theme=store.get("theme", "light"))
|
|
71
|
+
# later, when the user switches theme:
|
|
72
|
+
store.set("theme", "bootstrap-dark")
|
|
73
|
+
```
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
name: str = "settings",
|
|
79
|
+
*,
|
|
80
|
+
path: str | os.PathLike[str] | None = None,
|
|
81
|
+
app_name: str | None = None,
|
|
82
|
+
autosave: bool = True,
|
|
83
|
+
) -> None:
|
|
84
|
+
if path is not None:
|
|
85
|
+
self._path = Path(path)
|
|
86
|
+
else:
|
|
87
|
+
if not name:
|
|
88
|
+
raise ValueError("Store: provide a name or an explicit path=.")
|
|
89
|
+
leaf = str(name)
|
|
90
|
+
if not leaf.endswith(".json"):
|
|
91
|
+
leaf = f"{leaf}.json"
|
|
92
|
+
self._path = app_config_file(leaf, app_name or _active_app_name())
|
|
93
|
+
self._autosave = autosave
|
|
94
|
+
self._lock = threading.RLock()
|
|
95
|
+
self._data: dict[str, Any] = {}
|
|
96
|
+
self._load()
|
|
97
|
+
|
|
98
|
+
# ----- persistence -----
|
|
99
|
+
|
|
100
|
+
def _load(self) -> None:
|
|
101
|
+
try:
|
|
102
|
+
raw = self._path.read_text(encoding="utf-8")
|
|
103
|
+
except (FileNotFoundError, OSError):
|
|
104
|
+
self._data = {}
|
|
105
|
+
return
|
|
106
|
+
try:
|
|
107
|
+
data = json.loads(raw)
|
|
108
|
+
except (json.JSONDecodeError, ValueError):
|
|
109
|
+
# A corrupt file starts the store empty — prefs are not worth
|
|
110
|
+
# crashing an app over.
|
|
111
|
+
self._data = {}
|
|
112
|
+
return
|
|
113
|
+
self._data = data if isinstance(data, dict) else {}
|
|
114
|
+
|
|
115
|
+
def save(self) -> None:
|
|
116
|
+
"""Write the store to disk now, atomically.
|
|
117
|
+
|
|
118
|
+
Useful when `autosave=False`, or to force a flush. Writes to a temporary
|
|
119
|
+
file and renames it over the target so a partial write never replaces
|
|
120
|
+
good data.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
OSError: If the directory cannot be created or the file written.
|
|
124
|
+
"""
|
|
125
|
+
with self._lock:
|
|
126
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
tmp = self._path.with_name(f"{self._path.name}.tmp")
|
|
128
|
+
try:
|
|
129
|
+
tmp.write_text(
|
|
130
|
+
json.dumps(self._data, indent=2, sort_keys=True),
|
|
131
|
+
encoding="utf-8",
|
|
132
|
+
)
|
|
133
|
+
os.replace(tmp, self._path)
|
|
134
|
+
except BaseException:
|
|
135
|
+
try:
|
|
136
|
+
tmp.unlink()
|
|
137
|
+
except OSError:
|
|
138
|
+
pass
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
def reload(self) -> None:
|
|
142
|
+
"""Discard in-memory state and re-read the store from disk."""
|
|
143
|
+
with self._lock:
|
|
144
|
+
self._load()
|
|
145
|
+
|
|
146
|
+
def _autosaved(self) -> None:
|
|
147
|
+
if self._autosave:
|
|
148
|
+
self.save()
|
|
149
|
+
|
|
150
|
+
@staticmethod
|
|
151
|
+
def _check_key(key: Any) -> None:
|
|
152
|
+
if not isinstance(key, str):
|
|
153
|
+
raise TypeError(
|
|
154
|
+
f"Store keys must be strings, got {type(key).__name__}."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _check_value(key: str, value: Any) -> None:
|
|
159
|
+
try:
|
|
160
|
+
json.dumps(value)
|
|
161
|
+
except (TypeError, ValueError) as exc:
|
|
162
|
+
raise SerializationError(
|
|
163
|
+
f"Store value for key {key!r} is not JSON-serializable: {exc}. "
|
|
164
|
+
f"A Store holds JSON values only (scalars, lists, dicts)."
|
|
165
|
+
) from exc
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def path(self) -> Path:
|
|
169
|
+
"""The file this store reads from and writes to."""
|
|
170
|
+
return self._path
|
|
171
|
+
|
|
172
|
+
# ----- core access -----
|
|
173
|
+
|
|
174
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
175
|
+
"""Return the value for `key`, or `default` if it is not present."""
|
|
176
|
+
with self._lock:
|
|
177
|
+
return self._data.get(key, default)
|
|
178
|
+
|
|
179
|
+
def set(self, key: str, value: Any) -> None:
|
|
180
|
+
"""Set `key` to `value` and (by default) persist immediately.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
key: A string key.
|
|
184
|
+
value: A JSON-serializable value.
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
TypeError: If `key` is not a string.
|
|
188
|
+
SerializationError: If `value` is not JSON-serializable.
|
|
189
|
+
"""
|
|
190
|
+
self._check_key(key)
|
|
191
|
+
self._check_value(key, value)
|
|
192
|
+
with self._lock:
|
|
193
|
+
self._data[key] = value
|
|
194
|
+
self._autosaved()
|
|
195
|
+
|
|
196
|
+
def delete(self, key: str) -> None:
|
|
197
|
+
"""Remove `key` if present; a no-op when it is missing."""
|
|
198
|
+
with self._lock:
|
|
199
|
+
if key in self._data:
|
|
200
|
+
del self._data[key]
|
|
201
|
+
self._autosaved()
|
|
202
|
+
|
|
203
|
+
def setdefault(self, key: str, default: Any = None) -> Any:
|
|
204
|
+
"""Return `key`'s value, inserting `default` first if it is missing."""
|
|
205
|
+
self._check_key(key)
|
|
206
|
+
with self._lock:
|
|
207
|
+
if key not in self._data:
|
|
208
|
+
self._check_value(key, default)
|
|
209
|
+
self._data[key] = default
|
|
210
|
+
self._autosaved()
|
|
211
|
+
return self._data[key]
|
|
212
|
+
|
|
213
|
+
def update(self, values: "dict[str, Any] | None" = None, **kwargs: Any) -> None:
|
|
214
|
+
"""Merge values in, persisting once at the end.
|
|
215
|
+
|
|
216
|
+
Accepts a mapping, keyword arguments, or both (mirroring `dict.update`),
|
|
217
|
+
so `store.update(theme="dark")` and `store.update({"theme": "dark"})`
|
|
218
|
+
are equivalent.
|
|
219
|
+
"""
|
|
220
|
+
merged: dict[str, Any] = {}
|
|
221
|
+
if values:
|
|
222
|
+
merged.update(values)
|
|
223
|
+
if kwargs:
|
|
224
|
+
merged.update(kwargs)
|
|
225
|
+
for key, value in merged.items():
|
|
226
|
+
self._check_key(key)
|
|
227
|
+
self._check_value(key, value)
|
|
228
|
+
with self._lock:
|
|
229
|
+
if merged:
|
|
230
|
+
self._data.update(merged)
|
|
231
|
+
self._autosaved()
|
|
232
|
+
|
|
233
|
+
def clear(self) -> None:
|
|
234
|
+
"""Remove every key, persisting the now-empty store."""
|
|
235
|
+
with self._lock:
|
|
236
|
+
if self._data:
|
|
237
|
+
self._data.clear()
|
|
238
|
+
self._autosaved()
|
|
239
|
+
|
|
240
|
+
def keys(self) -> list[str]:
|
|
241
|
+
"""Return the keys, as a list snapshot."""
|
|
242
|
+
with self._lock:
|
|
243
|
+
return list(self._data.keys())
|
|
244
|
+
|
|
245
|
+
def values(self) -> list[Any]:
|
|
246
|
+
"""Return the values, as a list snapshot."""
|
|
247
|
+
with self._lock:
|
|
248
|
+
return list(self._data.values())
|
|
249
|
+
|
|
250
|
+
def items(self) -> list[tuple[str, Any]]:
|
|
251
|
+
"""Return the `(key, value)` pairs, as a list snapshot."""
|
|
252
|
+
with self._lock:
|
|
253
|
+
return list(self._data.items())
|
|
254
|
+
|
|
255
|
+
def as_dict(self) -> dict[str, Any]:
|
|
256
|
+
"""Return a deep copy of the store's contents as a plain dict."""
|
|
257
|
+
with self._lock:
|
|
258
|
+
return copy.deepcopy(self._data)
|
|
259
|
+
|
|
260
|
+
# ----- mapping protocol -----
|
|
261
|
+
|
|
262
|
+
def __getitem__(self, key: str) -> Any:
|
|
263
|
+
with self._lock:
|
|
264
|
+
return self._data[key]
|
|
265
|
+
|
|
266
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
267
|
+
self.set(key, value)
|
|
268
|
+
|
|
269
|
+
def __delitem__(self, key: str) -> None:
|
|
270
|
+
with self._lock:
|
|
271
|
+
del self._data[key]
|
|
272
|
+
self._autosaved()
|
|
273
|
+
|
|
274
|
+
def __contains__(self, key: object) -> bool:
|
|
275
|
+
with self._lock:
|
|
276
|
+
return key in self._data
|
|
277
|
+
|
|
278
|
+
def __iter__(self) -> Iterator[str]:
|
|
279
|
+
return iter(self.keys())
|
|
280
|
+
|
|
281
|
+
def __len__(self) -> int:
|
|
282
|
+
with self._lock:
|
|
283
|
+
return len(self._data)
|
|
284
|
+
|
|
285
|
+
def __repr__(self) -> str:
|
|
286
|
+
return f"Store(path={str(self._path)!r}, keys={len(self)})"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Composable event-stream pipelines for bootstack widgets.
|
|
2
|
+
|
|
3
|
+
Call an `on_*()` shorthand with no handler to get a `Stream`; chain
|
|
4
|
+
operators (`map`, `filter`, `debounce`, …) and finish with `listen()`,
|
|
5
|
+
which returns a cancellable `Handle`.
|
|
6
|
+
"""
|
|
7
|
+
from bootstack.streams._stream import Handle, Stream
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Handle",
|
|
11
|
+
"Stream",
|
|
12
|
+
]
|