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.
Files changed (471) hide show
  1. bootstack/__init__.py +157 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/_core/__init__.py +21 -0
  4. bootstack/_core/capabilities/__init__.py +45 -0
  5. bootstack/_core/capabilities/after.py +103 -0
  6. bootstack/_core/capabilities/bind.py +192 -0
  7. bootstack/_core/capabilities/bindtags.py +112 -0
  8. bootstack/_core/capabilities/busy.py +72 -0
  9. bootstack/_core/capabilities/clipboard.py +89 -0
  10. bootstack/_core/capabilities/focus.py +118 -0
  11. bootstack/_core/capabilities/grab.py +65 -0
  12. bootstack/_core/capabilities/grid.py +211 -0
  13. bootstack/_core/capabilities/localization.py +231 -0
  14. bootstack/_core/capabilities/pack.py +140 -0
  15. bootstack/_core/capabilities/place.py +113 -0
  16. bootstack/_core/capabilities/selection.py +136 -0
  17. bootstack/_core/capabilities/signals.py +244 -0
  18. bootstack/_core/capabilities/winfo.py +315 -0
  19. bootstack/_core/colorutils.py +234 -0
  20. bootstack/_core/exceptions.py +25 -0
  21. bootstack/_core/images.py +463 -0
  22. bootstack/_core/mixins/__init__.py +1 -0
  23. bootstack/_core/mixins/ttk_state.py +35 -0
  24. bootstack/_core/mixins/widget.py +132 -0
  25. bootstack/_core/paths.py +49 -0
  26. bootstack/_core/publisher.py +149 -0
  27. bootstack/_core/variables.py +62 -0
  28. bootstack/_runtime/__init__.py +3 -0
  29. bootstack/_runtime/app.py +930 -0
  30. bootstack/_runtime/base_window.py +945 -0
  31. bootstack/_runtime/events.py +399 -0
  32. bootstack/_runtime/shortcuts.py +496 -0
  33. bootstack/_runtime/tk_patch.py +43 -0
  34. bootstack/_runtime/toplevel.py +276 -0
  35. bootstack/_runtime/utility.py +457 -0
  36. bootstack/_runtime/visual_focus.py +240 -0
  37. bootstack/_runtime/window_utilities.py +1123 -0
  38. bootstack/assets/__init__.py +21 -0
  39. bootstack/assets/bootstack.ico +0 -0
  40. bootstack/assets/bootstack.png +0 -0
  41. bootstack/assets/elements/__init__.py +0 -0
  42. bootstack/assets/elements/badge-pill.png +0 -0
  43. bootstack/assets/elements/badge-square.png +0 -0
  44. bootstack/assets/elements/button-compact.png +0 -0
  45. bootstack/assets/elements/button-default.png +0 -0
  46. bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
  47. bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
  48. bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
  49. bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
  50. bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
  51. bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
  52. bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
  53. bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
  54. bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
  55. bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
  56. bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
  57. bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
  58. bootstack/assets/elements/card.png +0 -0
  59. bootstack/assets/elements/checkbox-checked.png +0 -0
  60. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  61. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  62. bootstack/assets/elements/field.png +0 -0
  63. bootstack/assets/elements/input-addon-compact.png +0 -0
  64. bootstack/assets/elements/input-addon-default.png +0 -0
  65. bootstack/assets/elements/input-compact.png +0 -0
  66. bootstack/assets/elements/input-default.png +0 -0
  67. bootstack/assets/elements/list-item-separated.png +0 -0
  68. bootstack/assets/elements/list-item.png +0 -0
  69. bootstack/assets/elements/listrow-compact.png +0 -0
  70. bootstack/assets/elements/listrow-default.png +0 -0
  71. bootstack/assets/elements/manifest.toml +361 -0
  72. bootstack/assets/elements/menu-item.png +0 -0
  73. bootstack/assets/elements/navitem-compact.png +0 -0
  74. bootstack/assets/elements/navitem-default.png +0 -0
  75. bootstack/assets/elements/progressbar-h-compact.png +0 -0
  76. bootstack/assets/elements/progressbar-h-default.png +0 -0
  77. bootstack/assets/elements/progressbar-v-compact.png +0 -0
  78. bootstack/assets/elements/progressbar-v-default.png +0 -0
  79. bootstack/assets/elements/radiobutton.png +0 -0
  80. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  81. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  82. bootstack/assets/elements/slider-handle.png +0 -0
  83. bootstack/assets/elements/slider-track-h.png +0 -0
  84. bootstack/assets/elements/slider-track-v.png +0 -0
  85. bootstack/assets/elements/switch-off.png +0 -0
  86. bootstack/assets/elements/switch-on.png +0 -0
  87. bootstack/assets/elements/tab-h.png +0 -0
  88. bootstack/assets/elements/tab-v.png +0 -0
  89. bootstack/assets/icons/bootstrap.ttf +0 -0
  90. bootstack/assets/icons/glyphmap.json +2080 -0
  91. bootstack/assets/icons/icon_metrics.json +12470 -0
  92. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  93. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
  94. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
  95. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
  96. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  97. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
  98. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  99. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
  100. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  101. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
  102. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  103. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
  104. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  105. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
  106. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  107. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
  108. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  109. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
  110. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  111. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
  112. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  113. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
  114. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  115. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
  116. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  117. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
  118. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  119. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
  120. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  121. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
  122. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  123. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
  124. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  125. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
  126. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  127. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
  128. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  129. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
  130. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  131. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
  132. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  133. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
  134. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  135. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
  136. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  137. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
  138. bootstack/cli/__init__.py +133 -0
  139. bootstack/cli/__main__.py +6 -0
  140. bootstack/cli/add.py +395 -0
  141. bootstack/cli/appicon.py +285 -0
  142. bootstack/cli/build.py +115 -0
  143. bootstack/cli/config.py +313 -0
  144. bootstack/cli/demo.py +564 -0
  145. bootstack/cli/dev.py +153 -0
  146. bootstack/cli/doctor.py +195 -0
  147. bootstack/cli/icons.py +98 -0
  148. bootstack/cli/promote.py +120 -0
  149. bootstack/cli/pyinstaller.py +268 -0
  150. bootstack/cli/run.py +95 -0
  151. bootstack/cli/start.py +117 -0
  152. bootstack/cli/templates/__init__.py +931 -0
  153. bootstack/clipboard.py +48 -0
  154. bootstack/constants.py +318 -0
  155. bootstack/data/README.md +615 -0
  156. bootstack/data/__init__.py +78 -0
  157. bootstack/data/_observable.py +276 -0
  158. bootstack/data/base.py +780 -0
  159. bootstack/data/file_source.py +367 -0
  160. bootstack/data/memory_source.py +450 -0
  161. bootstack/data/query.py +367 -0
  162. bootstack/data/readers.py +289 -0
  163. bootstack/data/sqlite_source.py +869 -0
  164. bootstack/data/types.py +354 -0
  165. bootstack/data/writers.py +232 -0
  166. bootstack/dev/__init__.py +36 -0
  167. bootstack/dev/_capture.py +141 -0
  168. bootstack/dev/_env.py +43 -0
  169. bootstack/dev/_registry.py +140 -0
  170. bootstack/dev/_reloader.py +351 -0
  171. bootstack/dev/_reset.py +50 -0
  172. bootstack/dev/_watcher.py +91 -0
  173. bootstack/dialogs/__init__.py +948 -0
  174. bootstack/dialogs/_impl/__init__.py +47 -0
  175. bootstack/dialogs/_impl/colorchooser.py +588 -0
  176. bootstack/dialogs/_impl/datedialog.py +450 -0
  177. bootstack/dialogs/_impl/dialog.py +594 -0
  178. bootstack/dialogs/_impl/filterdialog.py +358 -0
  179. bootstack/dialogs/_impl/fontdialog.py +364 -0
  180. bootstack/dialogs/_impl/formdialog.py +564 -0
  181. bootstack/dialogs/_impl/message.py +486 -0
  182. bootstack/dialogs/_impl/query.py +570 -0
  183. bootstack/errors.py +67 -0
  184. bootstack/events/__init__.py +111 -0
  185. bootstack/events/_event.py +135 -0
  186. bootstack/events/_payloads.py +539 -0
  187. bootstack/events/_subscription.py +38 -0
  188. bootstack/i18n/README.md +77 -0
  189. bootstack/i18n/__init__.py +23 -0
  190. bootstack/i18n/catalog.py +121 -0
  191. bootstack/i18n/intl_format.py +584 -0
  192. bootstack/i18n/msgcat.py +425 -0
  193. bootstack/i18n/specs.py +156 -0
  194. bootstack/images.py +563 -0
  195. bootstack/py.typed +1 -0
  196. bootstack/scheduling/__init__.py +11 -0
  197. bootstack/scheduling/_schedule.py +218 -0
  198. bootstack/shortcuts.py +21 -0
  199. bootstack/signals/README.md +98 -0
  200. bootstack/signals/__init__.py +10 -0
  201. bootstack/signals/integration.py +100 -0
  202. bootstack/signals/signal.py +353 -0
  203. bootstack/signals/types.py +6 -0
  204. bootstack/store.py +286 -0
  205. bootstack/streams/__init__.py +12 -0
  206. bootstack/streams/_stream.py +321 -0
  207. bootstack/style/__init__.py +38 -0
  208. bootstack/style/builders/__init__.py +51 -0
  209. bootstack/style/builders/badge.py +46 -0
  210. bootstack/style/builders/button.py +339 -0
  211. bootstack/style/builders/buttongroup.py +311 -0
  212. bootstack/style/builders/calendar.py +271 -0
  213. bootstack/style/builders/checkbutton.py +110 -0
  214. bootstack/style/builders/combobox.py +113 -0
  215. bootstack/style/builders/contextmenu.py +268 -0
  216. bootstack/style/builders/entry.py +82 -0
  217. bootstack/style/builders/expander.py +148 -0
  218. bootstack/style/builders/field.py +335 -0
  219. bootstack/style/builders/frame.py +50 -0
  220. bootstack/style/builders/label.py +28 -0
  221. bootstack/style/builders/labelframe.py +34 -0
  222. bootstack/style/builders/listview.py +369 -0
  223. bootstack/style/builders/menubar.py +91 -0
  224. bootstack/style/builders/menubutton.py +359 -0
  225. bootstack/style/builders/panedwindow.py +25 -0
  226. bootstack/style/builders/progressbar.py +67 -0
  227. bootstack/style/builders/radiobutton.py +99 -0
  228. bootstack/style/builders/scale.py +64 -0
  229. bootstack/style/builders/scrollbar.py +225 -0
  230. bootstack/style/builders/separator.py +49 -0
  231. bootstack/style/builders/sidenav.py +643 -0
  232. bootstack/style/builders/sizegrip.py +15 -0
  233. bootstack/style/builders/spinbox.py +119 -0
  234. bootstack/style/builders/switch.py +70 -0
  235. bootstack/style/builders/tabitem.py +204 -0
  236. bootstack/style/builders/togglegroup.py +294 -0
  237. bootstack/style/builders/toolbutton.py +275 -0
  238. bootstack/style/builders/tooltip.py +26 -0
  239. bootstack/style/builders/treeview.py +193 -0
  240. bootstack/style/builders/utils.py +455 -0
  241. bootstack/style/builders_tk/__init__.py +16 -0
  242. bootstack/style/builders_tk/defaults.py +229 -0
  243. bootstack/style/element.py +173 -0
  244. bootstack/style/fonts.py +123 -0
  245. bootstack/style/style.py +609 -0
  246. bootstack/style/style_builder_base.py +716 -0
  247. bootstack/style/style_builder_mixed.py +93 -0
  248. bootstack/style/style_builder_tk.py +109 -0
  249. bootstack/style/style_builder_ttk.py +353 -0
  250. bootstack/style/style_resolver.py +447 -0
  251. bootstack/style/theme.py +245 -0
  252. bootstack/style/theme_provider.py +471 -0
  253. bootstack/style/themes/__init__.py +128 -0
  254. bootstack/style/tk_patch.py +5 -0
  255. bootstack/style/token_maps.py +41 -0
  256. bootstack/style/types.py +32 -0
  257. bootstack/style/typography.py +523 -0
  258. bootstack/style/utility.py +746 -0
  259. bootstack/types.py +39 -0
  260. bootstack/validation/__init__.py +6 -0
  261. bootstack/validation/types.py +15 -0
  262. bootstack/validation/validation_result.py +17 -0
  263. bootstack/validation/validation_rules.py +205 -0
  264. bootstack/widgets/__init__.py +74 -0
  265. bootstack/widgets/_core/__init__.py +31 -0
  266. bootstack/widgets/_core/app_config.py +266 -0
  267. bootstack/widgets/_core/base.py +461 -0
  268. bootstack/widgets/_core/container.py +334 -0
  269. bootstack/widgets/_core/context.py +35 -0
  270. bootstack/widgets/_core/events.py +130 -0
  271. bootstack/widgets/_core/field_mixin.py +353 -0
  272. bootstack/widgets/_core/icon_image_props.py +72 -0
  273. bootstack/widgets/_core/image_binding.py +93 -0
  274. bootstack/widgets/_core/navmodel.py +345 -0
  275. bootstack/widgets/_core/options.py +210 -0
  276. bootstack/widgets/_core/selection_group.py +130 -0
  277. bootstack/widgets/_core/window_controls.py +96 -0
  278. bootstack/widgets/_core/window_menu.py +246 -0
  279. bootstack/widgets/_impl/__init__.py +1 -0
  280. bootstack/widgets/_impl/_internal/__init__.py +0 -0
  281. bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
  282. bootstack/widgets/_impl/_parts/__init__.py +11 -0
  283. bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
  284. bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
  285. bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
  286. bootstack/widgets/_impl/composites/__init__.py +33 -0
  287. bootstack/widgets/_impl/composites/_dateutils.py +33 -0
  288. bootstack/widgets/_impl/composites/_image_fit.py +105 -0
  289. bootstack/widgets/_impl/composites/accordion.py +381 -0
  290. bootstack/widgets/_impl/composites/avatar.py +191 -0
  291. bootstack/widgets/_impl/composites/buttongroup.py +371 -0
  292. bootstack/widgets/_impl/composites/calendar.py +972 -0
  293. bootstack/widgets/_impl/composites/carousel.py +567 -0
  294. bootstack/widgets/_impl/composites/chart.py +882 -0
  295. bootstack/widgets/_impl/composites/compositeframe.py +298 -0
  296. bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
  297. bootstack/widgets/_impl/composites/dateentry.py +404 -0
  298. bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
  299. bootstack/widgets/_impl/composites/expander.py +515 -0
  300. bootstack/widgets/_impl/composites/field.py +670 -0
  301. bootstack/widgets/_impl/composites/form.py +1066 -0
  302. bootstack/widgets/_impl/composites/gallery.py +551 -0
  303. bootstack/widgets/_impl/composites/list/__init__.py +15 -0
  304. bootstack/widgets/_impl/composites/list/listitem.py +802 -0
  305. bootstack/widgets/_impl/composites/list/listview.py +1433 -0
  306. bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
  307. bootstack/widgets/_impl/composites/menu/model.py +358 -0
  308. bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
  309. bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
  310. bootstack/widgets/_impl/composites/meter.py +860 -0
  311. bootstack/widgets/_impl/composites/numericentry.py +201 -0
  312. bootstack/widgets/_impl/composites/pagestack.py +395 -0
  313. bootstack/widgets/_impl/composites/passwordentry.py +142 -0
  314. bootstack/widgets/_impl/composites/pathentry.py +168 -0
  315. bootstack/widgets/_impl/composites/picture.py +289 -0
  316. bootstack/widgets/_impl/composites/radiogroup.py +511 -0
  317. bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
  318. bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
  319. bootstack/widgets/_impl/composites/scrollview.py +764 -0
  320. bootstack/widgets/_impl/composites/selectbox.py +1026 -0
  321. bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
  322. bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
  323. bootstack/widgets/_impl/composites/shell/layout.py +335 -0
  324. bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
  325. bootstack/widgets/_impl/composites/shell/providers.py +558 -0
  326. bootstack/widgets/_impl/composites/shell/rail.py +117 -0
  327. bootstack/widgets/_impl/composites/shell/shell.py +581 -0
  328. bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
  329. bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
  330. bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
  331. bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
  332. bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
  333. bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
  334. bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
  335. bootstack/widgets/_impl/composites/slider/slider.py +851 -0
  336. bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
  337. bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
  338. bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
  339. bootstack/widgets/_impl/composites/tableview/types.py +169 -0
  340. bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
  341. bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
  342. bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
  343. bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
  344. bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
  345. bootstack/widgets/_impl/composites/textarea/change.py +46 -0
  346. bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
  347. bootstack/widgets/_impl/composites/textarea/core.py +495 -0
  348. bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
  349. bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
  350. bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
  351. bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
  352. bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
  353. bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
  354. bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
  355. bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
  356. bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
  357. bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
  358. bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
  359. bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
  360. bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
  361. bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
  362. bootstack/widgets/_impl/composites/textentry.py +57 -0
  363. bootstack/widgets/_impl/composites/timeentry.py +176 -0
  364. bootstack/widgets/_impl/composites/toast.py +390 -0
  365. bootstack/widgets/_impl/composites/toast_stack.py +156 -0
  366. bootstack/widgets/_impl/composites/togglegroup.py +418 -0
  367. bootstack/widgets/_impl/composites/toolbar.py +608 -0
  368. bootstack/widgets/_impl/composites/tooltip.py +491 -0
  369. bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
  370. bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
  371. bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
  372. bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
  373. bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
  374. bootstack/widgets/_impl/mixins/__init__.py +24 -0
  375. bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
  376. bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
  377. bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
  378. bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
  379. bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
  380. bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
  381. bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
  382. bootstack/widgets/_impl/primitives/__init__.py +49 -0
  383. bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
  384. bootstack/widgets/_impl/primitives/badge.py +45 -0
  385. bootstack/widgets/_impl/primitives/button.py +76 -0
  386. bootstack/widgets/_impl/primitives/card.py +45 -0
  387. bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
  388. bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
  389. bootstack/widgets/_impl/primitives/combobox.py +156 -0
  390. bootstack/widgets/_impl/primitives/entry.py +87 -0
  391. bootstack/widgets/_impl/primitives/flexframe.py +448 -0
  392. bootstack/widgets/_impl/primitives/frame.py +185 -0
  393. bootstack/widgets/_impl/primitives/gridframe.py +546 -0
  394. bootstack/widgets/_impl/primitives/label.py +84 -0
  395. bootstack/widgets/_impl/primitives/labelframe.py +54 -0
  396. bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
  397. bootstack/widgets/_impl/primitives/packframe.py +227 -0
  398. bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
  399. bootstack/widgets/_impl/primitives/progressbar.py +83 -0
  400. bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
  401. bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
  402. bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
  403. bootstack/widgets/_impl/primitives/separator.py +43 -0
  404. bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
  405. bootstack/widgets/_impl/primitives/spinbox.py +95 -0
  406. bootstack/widgets/_impl/primitives/switch.py +44 -0
  407. bootstack/widgets/_impl/primitives/treeview.py +69 -0
  408. bootstack/widgets/app.py +371 -0
  409. bootstack/widgets/appshell.py +1179 -0
  410. bootstack/widgets/avatar.py +140 -0
  411. bootstack/widgets/boolean_controls.py +455 -0
  412. bootstack/widgets/button.py +224 -0
  413. bootstack/widgets/buttongroup.py +239 -0
  414. bootstack/widgets/calendar.py +195 -0
  415. bootstack/widgets/card.py +159 -0
  416. bootstack/widgets/carousel.py +241 -0
  417. bootstack/widgets/chart.py +302 -0
  418. bootstack/widgets/codeeditor.py +675 -0
  419. bootstack/widgets/contextmenu.py +371 -0
  420. bootstack/widgets/datatable.py +688 -0
  421. bootstack/widgets/datefield.py +395 -0
  422. bootstack/widgets/divider.py +60 -0
  423. bootstack/widgets/expander.py +579 -0
  424. bootstack/widgets/form.py +200 -0
  425. bootstack/widgets/gallery.py +250 -0
  426. bootstack/widgets/gauge.py +168 -0
  427. bootstack/widgets/grid.py +121 -0
  428. bootstack/widgets/groupbox.py +162 -0
  429. bootstack/widgets/label.py +229 -0
  430. bootstack/widgets/listview.py +354 -0
  431. bootstack/widgets/menubutton.py +419 -0
  432. bootstack/widgets/numberfield.py +431 -0
  433. bootstack/widgets/pagestack.py +345 -0
  434. bootstack/widgets/passwordfield.py +383 -0
  435. bootstack/widgets/pathfield.py +454 -0
  436. bootstack/widgets/picture.py +251 -0
  437. bootstack/widgets/progressbar.py +106 -0
  438. bootstack/widgets/radio_variants.py +271 -0
  439. bootstack/widgets/radiogroup.py +228 -0
  440. bootstack/widgets/scrollbar.py +64 -0
  441. bootstack/widgets/scrollview.py +186 -0
  442. bootstack/widgets/select.py +302 -0
  443. bootstack/widgets/selectbutton.py +182 -0
  444. bootstack/widgets/sidebar_toggle.py +130 -0
  445. bootstack/widgets/sizegrip.py +42 -0
  446. bootstack/widgets/slider.py +408 -0
  447. bootstack/widgets/spinbox.py +147 -0
  448. bootstack/widgets/spinnerfield.py +413 -0
  449. bootstack/widgets/splash.py +367 -0
  450. bootstack/widgets/splitview.py +482 -0
  451. bootstack/widgets/stacks.py +249 -0
  452. bootstack/widgets/statusbar.py +189 -0
  453. bootstack/widgets/tabs.py +425 -0
  454. bootstack/widgets/textarea.py +459 -0
  455. bootstack/widgets/textfield.py +410 -0
  456. bootstack/widgets/theme_toggle.py +92 -0
  457. bootstack/widgets/timefield.py +340 -0
  458. bootstack/widgets/toast.py +314 -0
  459. bootstack/widgets/togglegroup.py +231 -0
  460. bootstack/widgets/toolbar.py +347 -0
  461. bootstack/widgets/tooltip.py +94 -0
  462. bootstack/widgets/tree.py +648 -0
  463. bootstack/widgets/types.py +305 -0
  464. bootstack/widgets/window.py +307 -0
  465. bootstack-0.1.0.dist-info/METADATA +301 -0
  466. bootstack-0.1.0.dist-info/RECORD +471 -0
  467. bootstack-0.1.0.dist-info/WHEEL +5 -0
  468. bootstack-0.1.0.dist-info/entry_points.txt +2 -0
  469. bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  470. bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
  471. 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
+ ]