meltygui 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 (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,188 @@
1
+ from meltygui.code.fileref import Address
2
+
3
+
4
+ class _Unset:
5
+ """Sentinel indicating a step hasn't produced a real value yet."""
6
+ def __repr__(self):
7
+ return "UNSET_VALUE"
8
+
9
+ UNSET_VALUE = _Unset()
10
+
11
+ # Types safe for early cutoff equality comparison
12
+ _CUTOFF_TYPES = {Address, str, int, float, bool, tuple, bytes, frozenset}
13
+
14
+ def register_cutoff_type(*types):
15
+ """Register types as safe for early cutoff equality comparison."""
16
+ _CUTOFF_TYPES.update(types)
17
+
18
+
19
+ class CacheNode:
20
+ def __init__(self, type_key):
21
+ self.type_key = type_key
22
+ self.value = UNSET_VALUE
23
+ self.children = []
24
+ self._child_cursor = 0
25
+
26
+ def __repr__(self):
27
+ kids = [c.type_key for c in self.children]
28
+ return f"CacheNode({self.type_key}, value={self.value!r}, children={kids})"
29
+
30
+
31
+ class CacheTree:
32
+ """
33
+ Tree-shaped cache that auto-shares nodes for symmetric type sequences.
34
+ Types are inferred from output values.
35
+
36
+ Builds progressively — UNSET positions are filled in on later walks.
37
+ Handles type changes by replacing nodes and invalidating subtrees.
38
+ """
39
+
40
+ def __init__(self):
41
+ self._root = CacheNode("__root__")
42
+ self._mapping = []
43
+ self._stack = None
44
+ self._cursor = 0
45
+
46
+ def begin(self):
47
+ """Reset cursors for a new walk."""
48
+ self._cursor = 0
49
+ self._stack = [self._root]
50
+ self._reset_child_cursors(self._root)
51
+
52
+ def step(self, changed, value):
53
+ """
54
+ Advance one position. Returns (changed, cached_value).
55
+
56
+ Early cutoff: if a step re-executes but produces the same result
57
+ as what's cached, changed is flipped to False — stopping propagation.
58
+ """
59
+ if value is UNSET_VALUE or value is None:
60
+ return self._step_unset()
61
+
62
+ type_key = type(value).__name__
63
+ node = self._resolve(type_key)
64
+
65
+ # Check if mapping changed at this position
66
+ if self._cursor < len(self._mapping):
67
+ old_node = self._mapping[self._cursor]
68
+ if old_node is not node:
69
+ if old_node is not None:
70
+ self._clear(old_node)
71
+ self._mapping[self._cursor] = node
72
+ else:
73
+ self._mapping.append(node)
74
+
75
+ if changed or node.value is UNSET_VALUE:
76
+ node.value = value
77
+
78
+ self._cursor += 1
79
+ return node.value
80
+
81
+ def _step_unset(self):
82
+ """Handle an UNSET_VALUE step."""
83
+ if self._cursor < len(self._mapping) and self._mapping[self._cursor] is not None:
84
+ # Replay old type through stack to keep it consistent
85
+ old_node = self._mapping[self._cursor]
86
+ self._resolve(old_node.type_key)
87
+ self._cursor += 1
88
+ return old_node.value
89
+ else:
90
+ # No prior mapping - can't resolve, leave placeholder
91
+ if self._cursor >= len(self._mapping):
92
+ self._mapping.append(None)
93
+ self._cursor += 1
94
+ return UNSET_VALUE
95
+
96
+ def peek(self):
97
+ """Return the cached value at the current cursor without advancing."""
98
+ if self._cursor < len(self._mapping):
99
+ node = self._mapping[self._cursor]
100
+ if node is not None:
101
+ return node.value
102
+ return UNSET_VALUE
103
+
104
+ def end(self):
105
+ """End of walk. Stack is rebuilt each begin(), nothing to finalize."""
106
+ self._stack = None
107
+
108
+ def _resolve(self, type_key):
109
+ current = self._stack[-1]
110
+
111
+ # Same as current → stay at this level
112
+ if current.type_key == type_key:
113
+ return current
114
+
115
+ # Parent matches → pop back up (reuse node)
116
+ if len(self._stack) > 1 and self._stack[-2].type_key == type_key:
117
+ self._stack.pop()
118
+ return self._stack[-1]
119
+
120
+ # Push - check existing children first
121
+ child_idx = current._child_cursor
122
+ if child_idx < len(current.children):
123
+ existing = current.children[child_idx]
124
+ if existing.type_key == type_key:
125
+ # Reuse existing child
126
+ current._child_cursor += 1
127
+ self._stack.append(existing)
128
+ return existing
129
+ else:
130
+ # Type changed - replace child, invalidate its subtree
131
+ self._clear(existing)
132
+ new_child = CacheNode(type_key)
133
+ current.children[child_idx] = new_child
134
+ current._child_cursor += 1
135
+ self._stack.append(new_child)
136
+ return new_child
137
+ else:
138
+ # New child
139
+ child = CacheNode(type_key)
140
+ current.children.append(child)
141
+ current._child_cursor += 1
142
+ self._stack.append(child)
143
+ return child
144
+
145
+ def _reset_child_cursors(self, node):
146
+ node._child_cursor = 0
147
+ for child in node.children:
148
+ self._reset_child_cursors(child)
149
+
150
+ def node_at(self, index):
151
+ return self._mapping[index]
152
+
153
+ def invalidate(self):
154
+ self._clear(self._root)
155
+
156
+ def _clear(self, node):
157
+ node.value = UNSET_VALUE
158
+ for child in node.children:
159
+ self._clear(child)
160
+
161
+ def print_tree(self, node=None, indent=0):
162
+ if node is None:
163
+ node = self._root
164
+ prefix = " " * indent
165
+ print(f"{prefix}{node.type_key}: {node.value!r}")
166
+ for child in node.children:
167
+ self.print_tree(child, indent + 1)
168
+
169
+ def print_mapping(self, labels=None):
170
+ for i, node in enumerate(self._mapping):
171
+ label = labels[i] if labels else str(i)
172
+ if node is None:
173
+ print(f" [{i}] {label:>15} → (unset)")
174
+ else:
175
+ print(f" [{i}] {label:>15} → {node.type_key} (id={id(node):#x})")
176
+
177
+ def get_mapping_as_str(self, labels=None):
178
+ lines = []
179
+ for i, node in enumerate(self._mapping):
180
+ label = labels[i] if labels else str(i)
181
+ if node is None:
182
+ lines.append(f" [{i}] {label:>15} → (unset)")
183
+ else:
184
+ lines.append(f" [{i}] {label:>15} → {node.type_key} (id={id(node):#x})")
185
+ return "\n".join(lines)
186
+
187
+ def __len__(self):
188
+ return len(self._mapping)
@@ -0,0 +1,113 @@
1
+ """
2
+ Chain executor for the unified render pipeline.
3
+
4
+ A chain is a flat list of @render_func nodes. Data flows left to right:
5
+
6
+ [cls_to_cst, cst_to_dict, draw_collection, dict_to_cst, cst_to_cls]
7
+ load side ──────────────> render <────────────── save side
8
+
9
+ Every node has the same calling convention:
10
+ changed, value = func(input_value, **kwargs)
11
+
12
+ The executor walks the chain, passing the output of each node as the
13
+ input to the next. Nodes that haven't changed return cached results
14
+ instantly. The chain stops propagating when a node returns changed=False
15
+ (no downstream work needed), UNLESS the node has never run (first frame).
16
+
17
+ Usage:
18
+ draw_any(my_class, chain=[cls_to_cst, cst_to_dict, draw_collection,
19
+ dict_to_cst, cst_to_cls])
20
+ """
21
+
22
+ from meltygui.core.conversion.path_finder import Pending
23
+
24
+
25
+ def _step(func, value, **kwargs):
26
+ """Execute one chain node. Returns (changed, value, func_name)."""
27
+ result = func(value, **kwargs)
28
+
29
+ if isinstance(result, tuple) and len(result) == 2:
30
+ changed, value = result
31
+ else:
32
+ changed = False
33
+ value = result
34
+
35
+ name = getattr(func, '__name__', None) or getattr(func, '__qualname__', repr(func))
36
+ return changed, value, name
37
+
38
+
39
+ # Nodes a real user edit flows through. If a chain ends changed=True and NONE
40
+ # of these returned True, the change came from converters alone - exactly the
41
+ # "round-trip mutated state on its own" case we want to surface. Loose by name
42
+ # (matches whatever render_func wrappers expose) and overinclusive on purpose:
43
+ # false-positets are worse than the occasional missed case.
44
+ _INTERACTION_NODES = frozenset({
45
+ "focus", "draw_collection", "draw_tuple", "draw_text", "button",
46
+ "draw_int", "draw_str", "draw_float", "draw_bool", "draw_enum_tabs",
47
+ "draw_dropdown", "draw_tuple_float",
48
+ })
49
+
50
+
51
+ def run_chain(chain, input_value, **kwargs):
52
+ """Execute a chain of render_func nodes left to right.
53
+
54
+ Each node is called as: changed, value = node(value, **kwargs)
55
+
56
+ Returns (changed, value) — the output of the last node that ran.
57
+
58
+ Propagation rules:
59
+ - A node always runs if it has never produced a result (first frame).
60
+ - After first frame, a node runs only if upstream returned changed=True.
61
+ - If a node returns a Pending, the chain stops and returns the Pending.
62
+ """
63
+ value = input_value
64
+ changed = True # first frame always runs
65
+
66
+ trace = [] # (name, changed_returned, value_type) - for round-trip diagnostics
67
+
68
+ for func in chain:
69
+ if not changed:
70
+ break
71
+
72
+ changed, value, name = _step(func, value, **kwargs)
73
+ trace.append((name, changed, type(value).__name__))
74
+
75
+ # Pending stops the chain: downstream can't run without data
76
+ if isinstance(value, Pending):
77
+ return False, value
78
+
79
+ # ── Round-trip change detection ──────────────────────────────────────────
80
+ # The chain ends changed=True iff a save-side node will (or might, via the
81
+ # save_pending latch) write back. A real user edit always flows through an
82
+ # interaction node (focus, the picker, etc.). If we get here with changed=True
83
+ # but no interaction node returned True, only converters did - meaning a
84
+ # round-trip mutated the value on its own. Logs only, behavior unchanged.
85
+ if changed:
86
+ interaction_fired = any(
87
+ ch and n in _INTERACTION_NODES for n, ch, _ in trace)
88
+ if not interaction_fired:
89
+ converter_changes = [(n, vt) for n, ch, vt in trace if ch]
90
+ if converter_changes:
91
+ _log_round_trip_change(trace, converter_changes, kwargs)
92
+
93
+ return changed, value
94
+
95
+
96
+ def _log_round_trip_change(trace, converter_changes, kwargs):
97
+ """Dump a prominent record of a converter-only changed=True flow."""
98
+ import sys
99
+ ds = kwargs.get("draw_state")
100
+ ds_id = getattr(ds, "id", None) or getattr(ds, "_tile_id", None)
101
+ ds_name = getattr(ds, "name", None)
102
+ marked = " → ".join(
103
+ (f"\033[91m{n}*\033[0m" if ch else n) for n, ch, _ in trace)
104
+ print(
105
+ f"\n\033[93;1m[CHAIN ROUND-TRIP CHANGE — no user input]\033[0m "
106
+ f"chain returned changed=True with no interaction node firing.",
107
+ file=sys.stderr)
108
+ print(f" draw_state: name={ds_name!r} id={ds_id}", file=sys.stderr)
109
+ print(f" trace (* = node returned changed=True):", file=sys.stderr)
110
+ print(f" {marked}", file=sys.stderr)
111
+ print(f" suspect converters: "
112
+ f"{[n for n, _ in converter_changes]}", file=sys.stderr)
113
+ print(f" → a downstream save may fire without user input", file=sys.stderr)
@@ -0,0 +1,145 @@
1
+ """
2
+ Converter decorator. Threads apply and cache_id through all converters.
3
+
4
+ @converter(registry=Melty)
5
+ def str_to_module(value: str) -> cst.Module:
6
+ return cst.parse_module(value)
7
+
8
+ All registered converters accept (value, apply=False, cache_id=None)
9
+ regardless of original signature. The decorator absorbs extra kwargs
10
+ for simple converters and passes them through for load/save wrappers.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import functools
16
+ import inspect
17
+ from typing import Any
18
+
19
+
20
+ def _fn_accepts(fn, param_name: str) -> bool:
21
+ """Check if fn accepts a given parameter."""
22
+ if getattr(fn, f"_accepts_{param_name}", False):
23
+ return True
24
+ try:
25
+ return param_name in inspect.signature(fn).parameters
26
+ except (ValueError, TypeError):
27
+ return False
28
+
29
+
30
+ def converter(converter_fn=None, registry: Any = None, *,
31
+ from_type: type | None = None,
32
+ to_type: type | None = None,
33
+ load_data=None,
34
+ save_data=None,
35
+ **kwargs):
36
+ if converter_fn is None:
37
+ return lambda fn: converter(
38
+ fn, registry,
39
+ from_type=from_type, to_type=to_type,
40
+ load_data=load_data, save_data=save_data,
41
+ **kwargs
42
+ )
43
+
44
+ # ── Infer types from signature ──────────────────────────────────
45
+
46
+ func_signature = inspect.signature(converter_fn)
47
+ fn_params = func_signature.parameters
48
+
49
+ inferred_to = func_signature.return_annotation
50
+ if inferred_to is inspect.Signature.empty:
51
+ inferred_to = None
52
+ if inferred_to is not None and hasattr(inferred_to, "__args__"):
53
+ inferred_to = inferred_to.__args__[0]
54
+
55
+ inferred_from = None
56
+ for param_name in ("value", "data"):
57
+ param = fn_params.get(param_name)
58
+ if param is not None and param.annotation is not inspect.Parameter.empty:
59
+ inferred_from = param.annotation
60
+ break
61
+
62
+ actual_from = from_type or inferred_from
63
+ actual_to = to_type or inferred_to
64
+
65
+ # ── Wrap with load/save if needed ───────────────────────────────
66
+
67
+ wrapped_fn = converter_fn
68
+
69
+ if load_data is not None:
70
+ if actual_from is None:
71
+ raise TypeError(
72
+ f"Converter {converter_fn.__name__} with load_data requires from_type=")
73
+ from meltygui.code.fileref import make_load_wrapper
74
+ wrapped_fn = make_load_wrapper(converter_fn, load_data, actual_from)
75
+
76
+ if save_data is not None:
77
+ if actual_to is None:
78
+ raise TypeError(
79
+ f"Converter {converter_fn.__name__} with save_data requires to_type=")
80
+ from meltygui.code.fileref import make_save_wrapper
81
+ load_cache = None
82
+ inverse_fn = kwargs.get("inverse_of")
83
+ if inverse_fn is not None and hasattr(inverse_fn, "_local_cache"):
84
+ load_cache = inverse_fn._local_cache
85
+ wrapped_fn = make_save_wrapper(converter_fn, save_data, actual_to,
86
+ load_cache=load_cache)
87
+
88
+ # ── Normalize args: all converters accept (value, apply, cache_id) ──
89
+
90
+ has_apply = _fn_accepts(wrapped_fn, "apply")
91
+ has_cache_id = _fn_accepts(wrapped_fn, "cache_id")
92
+
93
+ if not has_apply or not has_cache_id:
94
+ inner = wrapped_fn
95
+
96
+ @functools.wraps(inner)
97
+ def _normalized(value, apply=False, cache_id=None):
98
+ kw = {}
99
+ if has_apply:
100
+ kw["apply"] = apply
101
+ if has_cache_id:
102
+ kw["cache_id"] = cache_id
103
+ return inner(value, **kw)
104
+
105
+ _normalized._accepts_apply = True
106
+ _normalized._accepts_cache_id = True
107
+ # Preserve wrapper attributes
108
+ for attr in ("_watched_from_type", "_watched_to_type", "_local_cache"):
109
+ if hasattr(inner, attr):
110
+ setattr(_normalized, attr, getattr(inner, attr))
111
+ wrapped_fn = _normalized
112
+
113
+ # ── Register ────────────────────────────────────────────────────
114
+
115
+ if not hasattr(registry, '_converters') or not isinstance(registry._converters, dict):
116
+ setattr(registry, '_converters', {})
117
+
118
+ if actual_from is not None and actual_to is not None:
119
+ registry._converters[(actual_from, actual_to)] = wrapped_fn
120
+
121
+ if hasattr(registry, "_converter_to_type") and isinstance(registry._converter_to_type, dict):
122
+ registry._converter_to_type[wrapped_fn] = (actual_from, actual_to)
123
+
124
+ # ── Store flags ─────────────────────────────────────────────────
125
+
126
+ if hasattr(registry, 'converter_flags_by_type') and isinstance(registry.converter_flags_by_type, dict):
127
+ if kwargs:
128
+ registry.converter_flags_by_type[(actual_from, actual_to)] = kwargs
129
+
130
+ if hasattr(registry, 'converter_flags') and isinstance(registry.converter_flags, dict):
131
+ if kwargs:
132
+ registry.converter_flags[wrapped_fn] = kwargs
133
+ if "inverse_of" in kwargs:
134
+ inverse_fn = kwargs["inverse_of"]
135
+ if inverse_fn not in registry.converter_flags:
136
+ registry.converter_flags[inverse_fn] = {}
137
+ registry.converter_flags[inverse_fn]["inverse_of"] = wrapped_fn
138
+
139
+ if load_data is not None:
140
+ if hasattr(registry, 'converter_flags') and isinstance(registry.converter_flags, dict):
141
+ if wrapped_fn not in registry.converter_flags:
142
+ registry.converter_flags[wrapped_fn] = {}
143
+ registry.converter_flags[wrapped_fn]["stateful"] = True
144
+
145
+ return wrapped_fn
@@ -0,0 +1,67 @@
1
+ from dataclasses import dataclass, field, MISSING, make_dataclass
2
+ import types
3
+ from typing import Any
4
+
5
+ # Types considered safe to use as constants (not recreated per instance)
6
+ IMMUTABLES = (int, float, str, bool, type(None), bytes, tuple, frozenset)
7
+
8
+ def data(_cls=None, **dc_kwargs):
9
+ """
10
+ A decorator that works like @dataclass, but ensures that
11
+ all defaults are re-evaluated for every new instance.
12
+
13
+ Behavior:
14
+ - Immutable defaults (int, str, bool, None, tuple, etc.) are kept as-is.
15
+ - Mutable defaults ([], {}, set()) are turned into default_factory calls.
16
+ - Callable defaults (like list, dict, or a user function) are used as factories.
17
+ - Function call results (e.g. id = generate_id()) are re-executed for each instance.
18
+ """
19
+
20
+ def wrap(cls):
21
+ annotations = getattr(cls, "__annotations__", {})
22
+ new_fields = []
23
+
24
+ for name, typ in annotations.items():
25
+ default = getattr(cls, name, MISSING)
26
+
27
+ if default is MISSING:
28
+ # Field without a default
29
+ new_fields.append((name, typ))
30
+ else:
31
+ if isinstance(default, IMMUTABLES):
32
+ # Safe to use as a constant default
33
+ new_fields.append((name, typ, default))
34
+
35
+ elif isinstance(default, (list, dict, set)):
36
+ # Dangerous mutable literal: replace with factory
37
+ new_fields.append(
38
+ (name, typ, field(default_factory=default.__class__))
39
+ )
40
+
41
+ elif isinstance(default, types.FunctionType):
42
+ # A function type was provided: use it as default_factory
43
+ new_fields.append((name, typ, field(default_factory=default)))
44
+
45
+ else:
46
+ # A computed value (like generate_id())
47
+ # We capture the original function by closure
48
+ def factory(val=default):
49
+ # If value looks callable (like generate_id()), re-call
50
+ if callable(val):
51
+ return val()
52
+ # Else try to copy/recreate it
53
+ return type(val)(val)
54
+
55
+ new_fields.append((name, typ, field(default_factory=factory)))
56
+
57
+ # Rebuild dataclass with new defaults
58
+ new_cls = make_dataclass(
59
+ cls.__name__,
60
+ new_fields,
61
+ bases=cls.__bases__,
62
+ namespace=dict(cls.__dict__),
63
+ **dc_kwargs
64
+ )
65
+ return new_cls
66
+
67
+ return wrap if _cls is None else wrap(_cls)