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,141 @@
1
+ """Capture a ``with bs.App()`` block's body from source for hot reload.
2
+
3
+ PROVISIONAL — part of the carved-out ``bootstack.dev`` surface.
4
+
5
+ The body of a ``with`` block runs in the caller's frame and is never handed to
6
+ the context manager, so a flag alone cannot re-run it. In dev mode the App
7
+ captures *where* its body lives (file + the ``as`` target name) and, on each
8
+ reload, re-reads the file and re-extracts the matching block's statements — so a
9
+ save is picked up. Matching is by the ``as`` target name, which is stable across
10
+ edits that shift line numbers.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import ast
15
+ from dataclasses import dataclass
16
+ from types import FrameType
17
+ from typing import Any
18
+
19
+
20
+ @dataclass
21
+ class BodyCapture:
22
+ """Where a reloadable ``with`` block lives, plus its compiled body cache."""
23
+
24
+ filename: str
25
+ """Absolute path of the source file holding the ``with`` block."""
26
+ target_names: tuple[str, ...]
27
+ """The ``as <name>`` targets of the block (e.g. ``("app",)``)."""
28
+ module_globals: dict
29
+ """The module namespace the body executes in (frame globals)."""
30
+ with_lineno: int
31
+ """Line of the ``with`` statement at capture time (a matching hint only)."""
32
+
33
+ @property
34
+ def is_capturable(self) -> bool:
35
+ """Whether this capture can drive an in-process reload."""
36
+ return bool(self.filename) and bool(self.target_names)
37
+
38
+
39
+ def capture_from_frame(frame: FrameType) -> BodyCapture | None:
40
+ """Capture the enclosing ``with`` block from a caller frame.
41
+
42
+ Returns None when the block is not reloadable in-process — notably when it
43
+ is authored inside a function (frame locals differ from globals), so the
44
+ body's names cannot be safely re-bound by re-exec.
45
+ """
46
+ # A module-level `with` runs with locals IS globals. Inside a function the
47
+ # two differ; re-execing the body into globals would mis-scope its names.
48
+ if frame.f_locals is not frame.f_globals:
49
+ return None
50
+
51
+ filename = frame.f_code.co_filename
52
+ lineno = frame.f_lineno
53
+ try:
54
+ source = _read(filename)
55
+ except OSError:
56
+ return None
57
+
58
+ node = _find_with_node(source, lineno)
59
+ if node is None:
60
+ return None
61
+ targets = _with_target_names(node)
62
+ if not targets:
63
+ return None
64
+
65
+ return BodyCapture(
66
+ filename=filename,
67
+ target_names=targets,
68
+ module_globals=frame.f_globals,
69
+ with_lineno=node.lineno,
70
+ )
71
+
72
+
73
+ def find_current_body(capture: BodyCapture) -> list[ast.stmt]:
74
+ """Re-read the source and return the matching block's current statements.
75
+
76
+ Raises ``SyntaxError`` if the file no longer parses (the caller renders a
77
+ banner and keeps the process alive), or ``RuntimeError`` if the block can no
78
+ longer be located.
79
+ """
80
+ source = _read(capture.filename)
81
+ tree = ast.parse(source, capture.filename) # SyntaxError propagates
82
+ node = _match_with_node(tree, capture)
83
+ if node is None:
84
+ raise RuntimeError(
85
+ "hot reload: could not locate the `with` block to reload "
86
+ f"(looking for `as {', '.join(capture.target_names)}` in "
87
+ f"{capture.filename})"
88
+ )
89
+ return list(node.body)
90
+
91
+
92
+ def compile_body(filename: str, body: list[ast.stmt]) -> Any:
93
+ """Compile a block body (list of statements) into an executable code object."""
94
+ module = ast.Module(body=list(body), type_ignores=[])
95
+ ast.fix_missing_locations(module)
96
+ return compile(module, filename, "exec")
97
+
98
+
99
+ # --- internals ---------------------------------------------------------------
100
+
101
+
102
+ def _read(filename: str) -> str:
103
+ with open(filename, "r", encoding="utf-8") as fh:
104
+ return fh.read()
105
+
106
+
107
+ def _find_with_node(source: str, lineno: int) -> ast.With | None:
108
+ """Find the top-level ``with`` node at (or nearest to) a line."""
109
+ try:
110
+ tree = ast.parse(source)
111
+ except SyntaxError:
112
+ return None
113
+ candidates = [n for n in tree.body if isinstance(n, ast.With)]
114
+ if not candidates:
115
+ return None
116
+ for node in candidates:
117
+ if node.lineno == lineno:
118
+ return node
119
+ return min(candidates, key=lambda n: abs(n.lineno - lineno))
120
+
121
+
122
+ def _match_with_node(tree: ast.Module, capture: BodyCapture) -> ast.With | None:
123
+ """Locate the block to reload, preferring a target-name match."""
124
+ candidates = [n for n in tree.body if isinstance(n, ast.With)]
125
+ if not candidates:
126
+ return None
127
+ want = set(capture.target_names)
128
+ by_name = [n for n in candidates if want & set(_with_target_names(n))]
129
+ if by_name:
130
+ # Prefer the one nearest the original line if several share the name.
131
+ return min(by_name, key=lambda n: abs(n.lineno - capture.with_lineno))
132
+ return min(candidates, key=lambda n: abs(n.lineno - capture.with_lineno))
133
+
134
+
135
+ def _with_target_names(node: ast.With) -> tuple[str, ...]:
136
+ names = []
137
+ for item in node.items:
138
+ var = item.optional_vars
139
+ if isinstance(var, ast.Name):
140
+ names.append(var.id)
141
+ return tuple(names)
bootstack/dev/_env.py ADDED
@@ -0,0 +1,43 @@
1
+ """Dev-mode environment detection for bootstack hot reload.
2
+
3
+ PROVISIONAL — the `bootstack.dev` surface ships in 0.1.0 but is carved out of
4
+ the API freeze: it may change in a minor release while it settles.
5
+
6
+ `bootstack dev <file>` launches an app with these variables set so that
7
+ `App.run()` installs the hot-reload loop instead of a plain mainloop.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+
13
+ #: Set to ``"1"`` by ``bootstack dev`` to enable hot reload in the child process.
14
+ DEV_ENV_VAR = "BOOTSTACK_DEV"
15
+
16
+ #: Selects the reload strategy: ``"inprocess"`` (default — re-exec the with-body
17
+ #: in place, preserving state) or ``"restart"`` (re-exec the whole process on
18
+ #: save). ``bootstack dev --restart`` sets this to ``"restart"``.
19
+ DEV_MODE_VAR = "BOOTSTACK_DEV_MODE"
20
+
21
+ #: Reasonable minimum window size applied under ``bootstack dev`` when the app
22
+ #: did not set its own ``min_size`` — so the reload window starts usable and a
23
+ #: reload that shrinks content never collapses it below this floor. Dev-only;
24
+ #: production launches are unaffected.
25
+ DEV_MIN_SIZE = (640, 480)
26
+
27
+ #: Exit code the dev child uses to ask the ``bootstack dev`` supervisor to
28
+ #: relaunch it — the cross-platform restart signal. The app exits with this code
29
+ #: instead of calling ``os.execv``, which on Windows spawns a new process (new
30
+ #: PID) and terminates the caller rather than replacing it in place. The CLI
31
+ #: supervisor re-launches on this code; any other exit code ends the session.
32
+ DEV_RESTART_EXIT_CODE = 42
33
+
34
+
35
+ def is_dev_mode() -> bool:
36
+ """Return True when running under ``bootstack dev`` (hot reload enabled)."""
37
+ return os.environ.get(DEV_ENV_VAR) == "1"
38
+
39
+
40
+ def dev_mode() -> str:
41
+ """Return the configured reload strategy: ``"inprocess"`` or ``"restart"``."""
42
+ mode = os.environ.get(DEV_MODE_VAR, "inprocess")
43
+ return mode if mode in ("inprocess", "restart") else "inprocess"
@@ -0,0 +1,140 @@
1
+ """Registry behind ``@reloadable`` for deterministic multi-file hot reload.
2
+
3
+ PROVISIONAL — part of the carved-out ``bootstack.dev`` surface.
4
+
5
+ A page builder in another file calls bootstack widgets into whatever container
6
+ is active when it runs (the thread-local container stack). ``@reloadable`` marks
7
+ such a builder so the reloader can, when only that file changes, re-invoke the
8
+ *current* version of the builder into the exact region it last painted —
9
+ bypassing the entry module's possibly-stale ``from x import f`` binding and
10
+ giving per-page reload granularity.
11
+
12
+ In production (no dev mode) the decorator is a transparent no-op.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, Callable
17
+
18
+ from bootstack.dev._env import is_dev_mode
19
+
20
+ # qualified-name -> the most recently defined builder function. Reloading a page
21
+ # module re-runs its top level, which re-registers the builder under the same
22
+ # qualname, so this always points at the live version.
23
+ _builders: dict[str, Callable] = {}
24
+
25
+ # qualified-name -> list of mount records describing where the builder painted,
26
+ # so a per-page reload can clear and rebuild just those regions.
27
+ _mounts: dict[str, list["MountRecord"]] = {}
28
+
29
+
30
+ class MountRecord:
31
+ """Where a ``@reloadable`` builder painted, for targeted re-invocation."""
32
+
33
+ __slots__ = ("container", "args", "kwargs")
34
+
35
+ def __init__(self, container: Any, args: tuple, kwargs: dict) -> None:
36
+ self.container = container
37
+ self.args = args
38
+ self.kwargs = kwargs
39
+
40
+
41
+ def reloadable(func: Callable) -> Callable:
42
+ """Mark a builder function as a hot-reload unit (no-op in production).
43
+
44
+ Apply to a function that *builds* part of the UI by calling widgets into the
45
+ active container::
46
+
47
+ @reloadable
48
+ def build_dashboard(db):
49
+ bs.Label("Dashboard", font="heading-lg")
50
+ bs.DataTable(data_source=db)
51
+
52
+ In dev mode the framework records where the builder mounted; when its file is
53
+ saved, only that region is torn down and rebuilt with the new code. Outside
54
+ dev mode the function is returned unchanged.
55
+ """
56
+ if not is_dev_mode():
57
+ return func
58
+
59
+ qualname = f"{func.__module__}.{func.__qualname__}"
60
+ # Register at DECORATION time so importlib.reload (which re-runs the module
61
+ # top level, re-decorating) refreshes the builder to the new version. The
62
+ # wrapper then always invokes the current registration.
63
+ _builders[qualname] = func
64
+
65
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
66
+ from bootstack.widgets._core.context import current_container
67
+
68
+ container = current_container()
69
+ if container is not None:
70
+ _mounts.setdefault(qualname, []).append(
71
+ MountRecord(container, args, kwargs)
72
+ )
73
+ return _builders.get(qualname, func)(*args, **kwargs)
74
+
75
+ wrapper.__wrapped__ = func # type: ignore[attr-defined]
76
+ wrapper.__name__ = getattr(func, "__name__", "reloadable")
77
+ wrapper.__qualname__ = getattr(func, "__qualname__", "reloadable")
78
+ wrapper.__doc__ = func.__doc__
79
+ wrapper._bs_reloadable_qualname = qualname # type: ignore[attr-defined]
80
+ return wrapper
81
+
82
+
83
+ def builders_in_module(module_name: str) -> list[str]:
84
+ """Return the qualnames of registered builders defined in a module."""
85
+ prefix = module_name + "."
86
+ return [q for q in _builders if q.startswith(prefix)]
87
+
88
+
89
+ def reinvoke(qualname: str) -> bool:
90
+ """Clear and rebuild every region a builder painted. Returns True if any ran.
91
+
92
+ Used for per-page reload: when only a page file changed, its builder(s) are
93
+ re-invoked into their recorded regions instead of re-running the entry body.
94
+ """
95
+ from bootstack.dev._reset import clear_container
96
+ from bootstack.widgets._core.context import push_container, pop_container
97
+
98
+ func = _builders.get(qualname)
99
+ records = _mounts.get(qualname)
100
+ if func is None or not records:
101
+ return False
102
+
103
+ ran = False
104
+ for record in list(records):
105
+ container = record.container
106
+ if not _container_alive(container):
107
+ records.remove(record)
108
+ continue
109
+ clear_container(container)
110
+ push_container(container)
111
+ try:
112
+ func(*record.args, **record.kwargs)
113
+ ran = True
114
+ finally:
115
+ pop_container(container)
116
+ return ran
117
+
118
+
119
+ def reset_mounts() -> None:
120
+ """Forget mount records (used before a full rebuild re-records them).
121
+
122
+ Builders are kept — they are keyed by qualname and overwritten when their
123
+ module reloads, so there is nothing stale to clear, and dropping them would
124
+ discard registrations from modules that did not reload this pass.
125
+ """
126
+ _mounts.clear()
127
+
128
+
129
+ def reset() -> None:
130
+ """Forget all registered builders and mount records (used on teardown)."""
131
+ _builders.clear()
132
+ _mounts.clear()
133
+
134
+
135
+ def _container_alive(container: Any) -> bool:
136
+ internal = getattr(container, "_internal", container)
137
+ try:
138
+ return bool(internal.winfo_exists())
139
+ except Exception:
140
+ return False
@@ -0,0 +1,351 @@
1
+ """The hot-reload engine: watch, re-exec, recover.
2
+
3
+ PROVISIONAL — part of the carved-out ``bootstack.dev`` surface.
4
+
5
+ Two strategies, chosen in :func:`install_reloader`:
6
+
7
+ * **in-process** (default) — on save, re-read the entry file, reset the region
8
+ the ``with`` body built, and re-exec the body in place. Module-level state
9
+ (signals, datasources, stores) lives outside the body and survives. Multi-file
10
+ edits reload the changed page modules first; ``@reloadable`` builders rebuild
11
+ just their own region.
12
+ * **restart** — on save, exit with a sentinel code so the ``bootstack dev``
13
+ supervisor relaunches the process. The floor for apps that aren't a
14
+ module-level ``with bs.App()``; window geometry is persisted so it re-opens
15
+ where it was. (We avoid ``os.execv``: on Windows it does not replace the
16
+ process in place.)
17
+
18
+ The watcher only enqueues; all Tk work happens on a main-thread ``after`` pump,
19
+ so the one tkinter threading rule is never broken.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import queue
25
+ import sys
26
+ import traceback
27
+ from typing import Any
28
+
29
+ from bootstack.dev._capture import BodyCapture, compile_body, find_current_body
30
+ from bootstack.dev._env import dev_mode, is_dev_mode
31
+ from bootstack.dev._watcher import PollWatcher
32
+
33
+ _PREFIX = "[bootstack dev]"
34
+
35
+
36
+ def _project_watch_root(start: str) -> str:
37
+ """Widen the watch root from a starting file to the project tree.
38
+
39
+ Walks up from `start` looking for a `bootstack.toml`; when found, watches its
40
+ `src/` subtree if present (the conventional layout) else the project dir — so
41
+ edits to any module in the project, not just the entry file's directory, are
42
+ picked up. Falls back to the starting file's directory when no project marker
43
+ is found.
44
+ """
45
+ start = os.path.abspath(start)
46
+ cur = start if os.path.isdir(start) else os.path.dirname(start)
47
+ base = cur
48
+ for _ in range(40): # bounded walk-up
49
+ if os.path.isfile(os.path.join(cur, "bootstack.toml")):
50
+ src = os.path.join(cur, "src")
51
+ return src if os.path.isdir(src) else cur
52
+ parent = os.path.dirname(cur)
53
+ if parent == cur:
54
+ break
55
+ cur = parent
56
+ return base
57
+
58
+
59
+ def install_reloader(app: Any) -> None:
60
+ """Install the appropriate reloader for the running app, if in dev mode."""
61
+ if not is_dev_mode():
62
+ return
63
+ mode = dev_mode()
64
+ supports = getattr(app, "_dev_supports_inprocess", False)
65
+ capture: BodyCapture | None = getattr(app, "_dev_body", None)
66
+ in_process_ok = supports and capture is not None and capture.is_capturable
67
+
68
+ if mode == "restart" or not in_process_ok:
69
+ if mode != "restart":
70
+ if not supports:
71
+ _log(
72
+ "in-process reload isn't supported for this app type yet - "
73
+ "using process restart on save"
74
+ )
75
+ elif capture is None or not capture.is_capturable:
76
+ _log(
77
+ "in-process reload needs a module-level `with bs.App()` - "
78
+ "using process restart on save"
79
+ )
80
+ _RestartReloader(app).install()
81
+ return
82
+
83
+ _InProcessReloader(app, capture).install()
84
+
85
+
86
+ def _log(message: str) -> None:
87
+ print(f"{_PREFIX} {message}", flush=True)
88
+
89
+
90
+ class _BaseReloader:
91
+ """Shared watcher + main-thread pump plumbing."""
92
+
93
+ def __init__(self, app: Any) -> None:
94
+ self.app = app
95
+ self.root = app._internal
96
+ self._queue: "queue.Queue[set[str]]" = queue.Queue()
97
+ self._watcher: PollWatcher | None = None
98
+ self._pump_id: Any = None
99
+
100
+ def _watch_root(self) -> str:
101
+ raise NotImplementedError
102
+
103
+ def install(self) -> None:
104
+ self._watcher = PollWatcher(self._watch_root(), self._queue.put)
105
+ self._watcher.start()
106
+ self._schedule_pump()
107
+
108
+ def _schedule_pump(self) -> None:
109
+ try:
110
+ self._pump_id = self.root.after(120, self._pump)
111
+ except Exception:
112
+ self._pump_id = None
113
+
114
+ def _pump(self) -> None:
115
+ changed: set[str] = set()
116
+ try:
117
+ while True:
118
+ changed |= self._queue.get_nowait()
119
+ except queue.Empty:
120
+ pass
121
+ if changed:
122
+ try:
123
+ self._handle(changed)
124
+ except Exception:
125
+ traceback.print_exc()
126
+ try:
127
+ if self.root.winfo_exists():
128
+ self._schedule_pump()
129
+ except Exception:
130
+ pass
131
+
132
+ def _handle(self, changed: set[str]) -> None:
133
+ raise NotImplementedError
134
+
135
+
136
+ class _RestartReloader(_BaseReloader):
137
+ """Re-exec the whole process on save (the robust floor)."""
138
+
139
+ def install(self) -> None:
140
+ super().install()
141
+ _log(f"watching for changes - process restart on save (mode={dev_mode()})")
142
+
143
+ def _watch_root(self) -> str:
144
+ entry = sys.argv[0] if sys.argv and sys.argv[0] else os.getcwd()
145
+ return _project_watch_root(entry) or os.getcwd()
146
+
147
+ def _handle(self, changed: set[str]) -> None:
148
+ _log("change detected - restarting")
149
+ if self._watcher is not None:
150
+ self._watcher.stop()
151
+ # Persist geometry (and, for a shell, the selected page) so the
152
+ # relaunched process re-opens in place. We exit immediately, skipping
153
+ # destroy, so save now.
154
+ for attr in ("_save_window_state", "_save_nav_state"):
155
+ save = getattr(self.app._internal, attr, None)
156
+ if callable(save):
157
+ try:
158
+ save()
159
+ except Exception:
160
+ pass
161
+ sys.stdout.flush()
162
+ sys.stderr.flush()
163
+ # Ask the `bootstack dev` supervisor to relaunch us by exiting with the
164
+ # sentinel code. We do NOT os.execv: on Windows it spawns a new process
165
+ # and terminates this one, orphaning the app and dropping the supervisor
166
+ # after the first reload. os._exit skips interpreter cleanup/atexit — the
167
+ # supervisor owns the new process's lifecycle.
168
+ from bootstack.dev._env import DEV_RESTART_EXIT_CODE
169
+
170
+ os._exit(DEV_RESTART_EXIT_CODE)
171
+
172
+
173
+ class _InProcessReloader(_BaseReloader):
174
+ """Reset and re-exec the ``with`` body in place, preserving module state."""
175
+
176
+ def __init__(self, app: Any, capture: BodyCapture) -> None:
177
+ super().__init__(app)
178
+ self.capture = capture
179
+ self._error_banner: Any = None
180
+
181
+ def install(self) -> None:
182
+ super().install()
183
+ rel = os.path.relpath(self.capture.filename)
184
+ _log(f"hot reload active - editing {rel} updates the running app on save")
185
+
186
+ def _watch_root(self) -> str:
187
+ return _project_watch_root(self.capture.filename)
188
+
189
+ def _handle(self, changed: set[str]) -> None:
190
+ entry = os.path.abspath(self.capture.filename)
191
+ changed_abs = {os.path.abspath(p) for p in changed}
192
+ entry_changed = entry in changed_abs
193
+ page_files = changed_abs - {entry}
194
+
195
+ try:
196
+ # Per-page fast path: the entry is unchanged AND every changed file is a
197
+ # module that holds registered @reloadable builders — rebuild just those
198
+ # regions, skip the entry body. A changed module WITHOUT a builder
199
+ # (constants/models with module-level state) must take the full path:
200
+ # importlib.reload would otherwise split object identity (fresh objects
201
+ # while live widgets still hold the old ones) without rebinding. (#326)
202
+ builder_files = set(self._builder_module_files())
203
+ if not entry_changed and page_files and page_files <= builder_files:
204
+ reloaded = self._reload_modules(page_files)
205
+ if reloaded and self._reinvoke_pages(reloaded):
206
+ self._clear_error()
207
+ self.app._dev_after_rebuild()
208
+ _log("reloaded " + ", ".join(sorted(reloaded)))
209
+ return
210
+
211
+ # Full reload: reload any changed project modules first (so the re-exec'd
212
+ # entry body rebinds to the fresh objects), then re-run the body.
213
+ self._reload_modules(page_files)
214
+ self._full_reload()
215
+ self._clear_error()
216
+ _log("reloaded")
217
+ except SyntaxError as exc:
218
+ self._show_error(_format_syntax_error(exc))
219
+ _log("syntax error - fix and save again")
220
+ except Exception:
221
+ self._show_error(traceback.format_exc())
222
+ _log("reload failed - see the banner; fix and save again")
223
+
224
+ def _full_reload(self) -> None:
225
+ # Compile BEFORE tearing down, so a syntax error never blanks the UI.
226
+ body = find_current_body(self.capture)
227
+ code = compile_body(self.capture.filename, body)
228
+
229
+ route = _safe(self.app._dev_capture_route)
230
+ self.app._dev_reset_region()
231
+
232
+ from bootstack.widgets._core.context import push_container, pop_container
233
+
234
+ push_container(self.app)
235
+ try:
236
+ exec(code, self.capture.module_globals)
237
+ finally:
238
+ pop_container(self.app)
239
+
240
+ self.app._dev_after_rebuild()
241
+ if route is not None:
242
+ try:
243
+ self.app._dev_restore_route(route)
244
+ except Exception:
245
+ pass
246
+
247
+ def _reinvoke_pages(self, modules: set[str]) -> bool:
248
+ from bootstack.dev import _registry
249
+
250
+ ran = False
251
+ for module in modules:
252
+ for qualname in _registry.builders_in_module(module):
253
+ if _registry.reinvoke(qualname):
254
+ ran = True
255
+ return ran
256
+
257
+ def _builder_module_files(self) -> dict[str, str]:
258
+ """Map abspath -> module name for loaded modules holding registered builders."""
259
+ from bootstack.dev import _registry
260
+
261
+ out: dict[str, str] = {}
262
+ for name, module in list(sys.modules.items()):
263
+ path = getattr(module, "__file__", None)
264
+ if path and _registry.builders_in_module(name):
265
+ out[os.path.abspath(path)] = name
266
+ return out
267
+
268
+ def _reload_modules(self, files: set[str]) -> set[str]:
269
+ """importlib.reload any loaded project module whose file changed."""
270
+ if not files:
271
+ return set()
272
+ import importlib
273
+
274
+ reloaded: set[str] = set()
275
+ for name, module in list(sys.modules.items()):
276
+ path = getattr(module, "__file__", None)
277
+ if not path:
278
+ continue
279
+ if os.path.abspath(path) in files:
280
+ importlib.reload(module) # exceptions surface as a banner
281
+ reloaded.add(name)
282
+ return reloaded
283
+
284
+ # --- error banner --------------------------------------------------------
285
+
286
+ def _show_error(self, text: str) -> None:
287
+ import tkinter as tk
288
+
289
+ self._clear_error()
290
+ # `_content_frame` is an attribute on App but a method on AppShell/
291
+ # Workbench — resolve both to the content widget that hosts the banner.
292
+ frame = getattr(self.app, "_content_frame", None)
293
+ if callable(frame):
294
+ frame = frame()
295
+ if frame is None:
296
+ sys.stderr.write(text + "\n")
297
+ return
298
+ try:
299
+ banner = tk.Frame(frame, background="#2b0f10")
300
+ banner.place(relx=0, rely=0, relwidth=1, relheight=1)
301
+ tk.Label(
302
+ banner,
303
+ text="Hot reload failed - fix the error and save again",
304
+ background="#2b0f10",
305
+ foreground="#ff7b72",
306
+ font=("TkDefaultFont", 11, "bold"),
307
+ anchor="w",
308
+ ).pack(fill="x", padx=14, pady=(12, 4))
309
+ body = tk.Text(
310
+ banner,
311
+ background="#2b0f10",
312
+ foreground="#ffd7d5",
313
+ relief="flat",
314
+ borderwidth=0,
315
+ wrap="word",
316
+ padx=14,
317
+ pady=4,
318
+ highlightthickness=0,
319
+ )
320
+ body.insert("1.0", text)
321
+ body.configure(state="disabled")
322
+ body.pack(fill="both", expand=True)
323
+ self._error_banner = banner
324
+ except Exception:
325
+ sys.stderr.write(text + "\n")
326
+
327
+ def _clear_error(self) -> None:
328
+ banner = self._error_banner
329
+ self._error_banner = None
330
+ if banner is not None:
331
+ try:
332
+ banner.destroy()
333
+ except Exception:
334
+ pass
335
+
336
+
337
+ def _safe(fn: Any) -> Any:
338
+ try:
339
+ return fn()
340
+ except Exception:
341
+ return None
342
+
343
+
344
+ def _format_syntax_error(exc: SyntaxError) -> str:
345
+ where = f"{exc.filename}:{exc.lineno}"
346
+ parts = [f"SyntaxError: {exc.msg}", f" at {where}"]
347
+ if exc.text:
348
+ parts.append(" " + exc.text.rstrip())
349
+ if exc.offset:
350
+ parts.append(" " + " " * (exc.offset - 1) + "^")
351
+ return "\n".join(parts)