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,231 @@
1
+ """Keep live definitions canonical when their source moves between modules."""
2
+ import inspect
3
+ import gc
4
+ import sys
5
+ import types
6
+
7
+ from meltygui.core.melty import Melty
8
+
9
+
10
+ def _patch_nested_functions(previous, replacement, namespace):
11
+ """Update existing factory-created callbacks without recreating closures."""
12
+ def nested_codes(code):
13
+ result = {}
14
+ for child in code.co_consts:
15
+ if isinstance(child, types.CodeType):
16
+ if not child.co_name.startswith("<"):
17
+ result[child.co_qualname] = child
18
+ result.update(nested_codes(child))
19
+ return result
20
+
21
+ fresh = nested_codes(replacement)
22
+ if not fresh:
23
+ return lambda: None
24
+ edits = []
25
+ # Earlier factory-only swaps can leave callbacks more than one code
26
+ # generation behind. Match their defining namespace, source and qualified
27
+ # local name rather than only referrers of the immediately previous code.
28
+ for function in gc.get_objects():
29
+ if type(function) is not types.FunctionType or function.__globals__ is not namespace:
30
+ continue
31
+ old_code = function.__code__
32
+ new_code = fresh.get(old_code.co_qualname)
33
+ if (new_code is None or new_code is old_code
34
+ or old_code.co_filename != previous.co_filename):
35
+ continue
36
+ if old_code.co_freevars != new_code.co_freevars:
37
+ raise ValueError(f"cannot preserve live closure for {old_code.co_qualname}: free variables changed")
38
+ edits.append((function, old_code, new_code))
39
+ for function, _old_code, new_code in edits:
40
+ function.__code__ = new_code
41
+
42
+ def restore():
43
+ for function, old_code, _new_code in edits:
44
+ function.__code__ = old_code
45
+ return restore
46
+
47
+
48
+ def patch_function(live, replacement, *, force=False):
49
+ """Patch in place, retaining the defining namespace after a source move.
50
+
51
+ Python cannot reassign a function's __globals__. For a relocated function,
52
+ its original identity becomes a small forwarding entry point; __wrapped__
53
+ exposes the actual source implementation to inspection and later edits.
54
+ Normal edits still patch code directly and pay no forwarding cost.
55
+ """
56
+ previous = (live.__code__, live.__defaults__, live.__kwdefaults__,
57
+ dict(live.__annotations__), live.__doc__, live.__module__,
58
+ live.__qualname__, dict(live.__dict__))
59
+ previous_target = Melty.relocated_functions.get(id(live))
60
+ target = inspect.unwrap(live) if live.__dict__.get('__melty_relocated__') else live
61
+ target_previous = (target.__code__, target.__defaults__, target.__kwdefaults__,
62
+ dict(target.__annotations__), target.__doc__, target.__module__,
63
+ target.__qualname__)
64
+
65
+ if force and replacement.__globals__ is live.__globals__:
66
+ target = live
67
+ live.__dict__.pop('__wrapped__', None)
68
+ live.__dict__.pop('__melty_relocated__', None)
69
+ Melty.relocated_functions.pop(id(live), None)
70
+
71
+ restore_nested = None
72
+ if target.__globals__ is replacement.__globals__:
73
+ restore_nested = _patch_nested_functions(target.__code__, replacement.__code__, target.__globals__)
74
+ target.__code__ = replacement.__code__
75
+ target.__defaults__ = replacement.__defaults__
76
+ target.__kwdefaults__ = replacement.__kwdefaults__
77
+ target.__annotations__ = replacement.__annotations__
78
+ target.__doc__ = replacement.__doc__
79
+ target.__module__ = replacement.__module__
80
+ target.__qualname__ = replacement.__qualname__
81
+ else:
82
+ # Keep existing lexical state when the free-variable contract survives.
83
+ implementation = replacement
84
+ if target.__code__.co_freevars and target.__code__.co_freevars == replacement.__code__.co_freevars:
85
+ implementation = types.FunctionType(
86
+ replacement.__code__, replacement.__globals__, replacement.__name__,
87
+ replacement.__defaults__, target.__closure__)
88
+ implementation.__kwdefaults__ = replacement.__kwdefaults__
89
+ implementation.__annotations__ = replacement.__annotations__
90
+ implementation.__doc__ = replacement.__doc__
91
+ implementation.__qualname__ = replacement.__qualname__
92
+ Melty.relocated_functions[id(live)] = implementation
93
+ captures = [f'capture_{index}' for index in range(len(live.__closure__ or ()))]
94
+ capture_line = f' ({", ".join(captures)},)\n' if captures else ''
95
+ source = (
96
+ f'def make_entry({", ".join(captures)}):\n'
97
+ ' def entry(*args, **kwargs):\n'
98
+ + capture_line
99
+ + ' from meltygui.core.melty import Melty\n'
100
+ + f' return Melty.relocated_functions[{id(live)}](*args, **kwargs)\n'
101
+ + ' return entry\n')
102
+ namespace = {}
103
+ exec(compile(source, '<melty definition relocation>', 'exec'), namespace)
104
+ entry = namespace['make_entry'](*([None] * len(captures)))
105
+ live.__code__ = entry.__code__
106
+ live.__defaults__ = None
107
+ live.__kwdefaults__ = None
108
+ live.__wrapped__ = implementation
109
+ live.__melty_relocated__ = True
110
+ live.__annotations__ = replacement.__annotations__
111
+ live.__doc__ = replacement.__doc__
112
+ live.__module__ = replacement.__module__
113
+ live.__qualname__ = replacement.__qualname__
114
+
115
+ def restore():
116
+ if restore_nested is not None:
117
+ restore_nested()
118
+ (live.__code__, live.__defaults__, live.__kwdefaults__, annotations,
119
+ live.__doc__, live.__module__, live.__qualname__, attributes) = previous
120
+ live.__annotations__ = annotations
121
+ live.__dict__.clear()
122
+ live.__dict__.update(attributes)
123
+ if target is not live:
124
+ (target.__code__, target.__defaults__, target.__kwdefaults__, annotations,
125
+ target.__doc__, target.__module__, target.__qualname__) = target_previous
126
+ target.__annotations__ = annotations
127
+ if previous_target is None:
128
+ Melty.relocated_functions.pop(id(live), None)
129
+ else:
130
+ Melty.relocated_functions[id(live)] = previous_target
131
+ return restore
132
+
133
+
134
+ def canonicalize_definitions(replacements):
135
+ """Rebind exports and type-bearing function metadata to the live objects.
136
+
137
+ Only exact object identities are replaced, never names shared by unrelated
138
+ modules. Runtime instances and arbitrary application object graphs are not
139
+ traversed. Closure metadata matters for render_func's injection plans and
140
+ for zero-argument super() in relocated methods.
141
+ """
142
+ # Keep the original alive too: replaced tuples/signatures can otherwise
143
+ # be freed mid-walk and their ids reused by later function metadata.
144
+ seen = {}
145
+
146
+ def replace(value):
147
+ pair = replacements.get(id(value))
148
+ if pair is not None and pair[0] is value:
149
+ return pair[1]
150
+ identity = id(value)
151
+ if identity in seen:
152
+ return seen[identity][1]
153
+ seen[identity] = (value, value)
154
+ if isinstance(value, dict):
155
+ for key, item in list(value.items()):
156
+ new_key, new_item = replace(key), replace(item)
157
+ if new_key is not key:
158
+ del value[key]
159
+ if new_key is not key or new_item is not item:
160
+ value[new_key] = new_item
161
+ elif isinstance(value, list):
162
+ for index, item in enumerate(value):
163
+ value[index] = replace(item)
164
+ elif isinstance(value, tuple):
165
+ result = tuple(replace(item) for item in value)
166
+ if any(a is not b for a, b in zip(result, value)):
167
+ seen[identity] = (value, result)
168
+ return result
169
+ elif isinstance(value, inspect.Parameter):
170
+ result = value.replace(annotation=replace(value.annotation), default=replace(value.default))
171
+ seen[identity] = (value, result)
172
+ return result
173
+ elif isinstance(value, inspect.Signature):
174
+ result = value.replace(parameters=[replace(p) for p in value.parameters.values()],
175
+ return_annotation=replace(value.return_annotation))
176
+ seen[identity] = (value, result)
177
+ return result
178
+ return value
179
+
180
+ visited_functions = set()
181
+
182
+ def function_metadata(function):
183
+ if id(function) in visited_functions:
184
+ return
185
+ visited_functions.add(id(function))
186
+ function.__annotations__ = replace(function.__annotations__)
187
+ function.__defaults__ = replace(function.__defaults__)
188
+ function.__kwdefaults__ = replace(function.__kwdefaults__)
189
+ for cell in function.__closure__ or ():
190
+ try:
191
+ value = cell.cell_contents
192
+ except ValueError:
193
+ continue
194
+ replacement = replace(value)
195
+ if replacement is not value:
196
+ cell.cell_contents = replacement
197
+ if isinstance(replacement, types.FunctionType):
198
+ function_metadata(replacement)
199
+ wrapped = function.__dict__.get('__wrapped__')
200
+ if isinstance(wrapped, types.FunctionType):
201
+ function_metadata(wrapped)
202
+
203
+ def definition_metadata(value):
204
+ if isinstance(value, types.FunctionType):
205
+ function_metadata(value)
206
+ elif isinstance(value, type):
207
+ for member in vars(value).values():
208
+ if isinstance(member, types.FunctionType):
209
+ function_metadata(member)
210
+ elif isinstance(member, (staticmethod, classmethod)):
211
+ function_metadata(member.__func__)
212
+ elif isinstance(member, property):
213
+ for function in (member.fget, member.fset, member.fdel):
214
+ if function is not None:
215
+ function_metadata(function)
216
+
217
+ for fresh, live in replacements.values():
218
+ definition_metadata(fresh)
219
+ definition_metadata(live)
220
+ for module in tuple(sys.modules.values()):
221
+ if not isinstance(module, types.ModuleType):
222
+ continue
223
+ touched = False
224
+ for name, value in tuple(vars(module).items()):
225
+ pair = replacements.get(id(value))
226
+ if pair is not None and pair[0] is value:
227
+ vars(module)[name] = pair[1]
228
+ touched = True
229
+ if touched:
230
+ for value in tuple(vars(module).values()):
231
+ definition_metadata(value)
@@ -0,0 +1 @@
1
+ """Shared framework diagnostics machinery."""
@@ -0,0 +1,38 @@
1
+ from typing import Dict, Set, Tuple
2
+
3
+ from meltygui.core.rendering.window_decoration import window
4
+
5
+
6
+ @window
7
+ class AttributeChurnMonitor:
8
+ """Per-frame counter of @live attribute writes.
9
+
10
+ Reset at frame end; pair with Toggles.attrib_churn_log to dump
11
+ a top-N summary each frame the counter is non-empty.
12
+ """
13
+
14
+
15
+ attributes_changed: Set[Tuple[str, str]] = set()
16
+ attribute_change_count: Dict[Tuple[str, str], int] = {}
17
+
18
+ @classmethod
19
+ def record(cls, cls_name: str, attr_name: str) -> None:
20
+ key = (cls_name, attr_name)
21
+ cls.attributes_changed.add(key)
22
+ cls.attribute_change_count[key] = cls.attribute_change_count.get(key, 0) + 1
23
+
24
+ @classmethod
25
+ def on_frame_end(cls) -> None:
26
+ if not cls.attribute_change_count:
27
+ return
28
+
29
+ from meltygui.core.runtime.toggles import Toggles
30
+ if Toggles.attrib_churn_log:
31
+ from meltygui.core.melty import Melty
32
+ top = sorted(cls.attribute_change_count.items(), key=lambda kv: -kv[1])[:10]
33
+ line = ", ".join(f"{c}.{a}={n}" for (c, a), n in top)
34
+ print(f"[churn f{Melty.frame_count}] {line}")
35
+
36
+
37
+ cls.attributes_changed.clear()
38
+ cls.attribute_change_count.clear()
@@ -0,0 +1,39 @@
1
+ """Per-OS-window frame rate for the Toggles.show_fps titlebar readout.
2
+
3
+ Frames render on request, so an idle window presents nothing: the rate is
4
+ measured over the recent burst of consecutive frames only. A gap longer than
5
+ IDLE_GAP_S starts a new burst instead of averaging the idle time in; the
6
+ previous burst's rate stays on display until the new one has two frames.
7
+ """
8
+ import time
9
+
10
+ # Frames the rate is averaged over. Raise for a steadier, slower readout.
11
+ WINDOW_FRAMES = 60
12
+ # A pause this long between frames counts as idle, not as a slow frame.
13
+ IDLE_GAP_S = 0.25
14
+
15
+
16
+ class FpsCounter:
17
+ def __init__(self):
18
+ self.frame_starts = []
19
+ self.rate = None
20
+ self.frame_ms = 0.0
21
+
22
+ def frame_started(self):
23
+ now = time.perf_counter()
24
+ if self.frame_starts and now - self.frame_starts[-1] > IDLE_GAP_S:
25
+ self.frame_starts.clear()
26
+ self.frame_starts.append(now)
27
+ del self.frame_starts[:-WINDOW_FRAMES]
28
+ if len(self.frame_starts) >= 2:
29
+ self.rate = (len(self.frame_starts) - 1) / (now - self.frame_starts[0])
30
+ return now
31
+
32
+ def frame_finished(self, started):
33
+ self.frame_ms = (time.perf_counter() - started) * 1000.0
34
+
35
+ def label(self):
36
+ """'118 fps 7.9 ms': the latest burst's rate and the previous frame's
37
+ render-thread time. A lone frame after idle keeps the last burst's rate."""
38
+ rate = "--" if self.rate is None else f"{self.rate:.0f}"
39
+ return f"{rate} fps {self.frame_ms:.1f} ms"
@@ -0,0 +1,103 @@
1
+ """GPU frame-segment timing via GL_TIMESTAMP query objects.
2
+
3
+ CPU timers around post_frame's segments show where the RENDER THREAD blocks,
4
+ but a present stall is GPU backpressure: the CPU submits everything quickly
5
+ and swap_buffers waits for the queue to drain — the CPU split blames "swap"
6
+ while the actual cost sits in whichever segment's draw calls flooded the GPU
7
+ (UI draw data, the tile-capture pass, the filter/shadow passes, ...).
8
+ Timestamp queries mark when the GPU *executes* each boundary, so a completed
9
+ frame yields per-segment GPU milliseconds.
10
+
11
+ Non-blocking by design: results are polled a few frames late (ring of DEPTH
12
+ slots) and a slot whose result isn't ready yet is simply dropped when its
13
+ turn to be reused comes. Inert unless the perf-trace toggle is on, and it
14
+ permanently disables itself on the first GL error (headless contexts, old
15
+ drivers, missing GL 3.3 timer support)."""
16
+
17
+ import time
18
+
19
+
20
+ class GpuFrameTimer:
21
+ DEPTH = 4 # frames in flight before a result is read
22
+ MAX_STAMPS = 8 # boundaries per frame
23
+
24
+ def __init__(self):
25
+ self._gl = None
26
+ self._rings = None # DEPTH x MAX_STAMPS query IDs
27
+ self._labels = None # DEPTH x MAX_STAMPS stamp labels
28
+ self._counts = [0] * self.DEPTH
29
+ self._frames = [None] * self.DEPTH # app frame number per slot
30
+ self._slot = 0
31
+ self._dead = False
32
+ self._active = False # stamping enabled for the current frame
33
+
34
+ def begin(self, enabled, frame_no):
35
+ """Arm the current slot for this frame. GL context must be current."""
36
+ if self._dead or not enabled:
37
+ self._active = False
38
+ return
39
+ try:
40
+ if self._rings is None:
41
+ from OpenGL import GL as gl
42
+ self._gl = gl
43
+ self._rings = [[int(q) for q in gl.glGenQueries(self.MAX_STAMPS)]
44
+ for _ in range(self.DEPTH)]
45
+ self._labels = [[None] * self.MAX_STAMPS
46
+ for _ in range(self.DEPTH)]
47
+ self._counts[self._slot] = 0
48
+ self._frames[self._slot] = frame_no
49
+ self._active = True
50
+ except Exception:
51
+ self._dead = True
52
+ self._active = False
53
+
54
+ def stamp(self, label):
55
+ if not self._active:
56
+ return
57
+ try:
58
+ i = self._counts[self._slot]
59
+ if i >= self.MAX_STAMPS:
60
+ return
61
+ self._gl.glQueryCounter(self._rings[self._slot][i],
62
+ self._gl.GL_TIMESTAMP)
63
+ self._labels[self._slot][i] = label
64
+ self._counts[self._slot] = i + 1
65
+ except Exception:
66
+ self._dead = True
67
+ self._active = False
68
+
69
+ def end(self):
70
+ """Advance the ring; poll the OLDEST slot. Returns (frame_no,
71
+ {label: gpu_ms}) for a completed frame, else None. Never blocks: an
72
+ unavailable result stays queued until its slot is about to be reused,
73
+ then drops."""
74
+ if not self._active:
75
+ return None
76
+ out = None
77
+ try:
78
+ gl = self._gl
79
+ nxt = (self._slot + 1) % self.DEPTH
80
+ n = self._counts[nxt]
81
+ if n >= 2 and self._frames[nxt] is not None:
82
+ avail = gl.glGetQueryObjectiv(self._rings[nxt][n - 1],
83
+ gl.GL_QUERY_RESULT_AVAILABLE)
84
+ if avail:
85
+ ts = [int(gl.glGetQueryObjectui64v(self._rings[nxt][i],
86
+ gl.GL_QUERY_RESULT))
87
+ for i in range(n)]
88
+ segs = {}
89
+ for i in range(1, n):
90
+ segs[self._labels[nxt][i]] = (ts[i] - ts[i - 1]) / 1e6
91
+ out = (self._frames[nxt], segs)
92
+ self._frames[nxt] = None
93
+ self._counts[nxt] = 0
94
+ self._slot = nxt
95
+ except Exception:
96
+ self._dead = True
97
+ self._active = False
98
+ return None
99
+ return out
100
+
101
+
102
+ # Module singleton; survives hotswap (re-exec reuses existing context).
103
+ GPU_TIMER = globals().get("GPU_TIMER") or GpuFrameTimer()
@@ -0,0 +1,169 @@
1
+ # fastinspect.py
2
+ from __future__ import annotations
3
+ import inspect
4
+ import weakref
5
+ from types import FunctionType, MethodType
6
+ from typing import Mapping
7
+
8
+ # Mapping of function -> (signature, code_object_id)
9
+ # WeakKeyDictionary avoids memory leaks when functions go out of scope.
10
+ _SIG_CACHE: "weakref.WeakKeyDictionary[object, tuple[inspect.Signature, int | None]]" = weakref.WeakKeyDictionary()
11
+
12
+
13
+ def _unwrap_callable(obj):
14
+ """Normalize bound methods, functools.partial, and wrappers to a base callable."""
15
+ # Bound method -> underlying function (keep method object as key if you prefer)
16
+ if isinstance(obj, MethodType):
17
+ return obj.__func__
18
+ # functools.partial exposes .func
19
+ func = getattr(obj, "func", None)
20
+ if func is not None and callable(func):
21
+ return func
22
+ # Many wrappers set __wrapped__
23
+ return inspect.unwrap(obj)
24
+
25
+
26
+ def _code_id(func) -> int | None:
27
+ """Stable identifier for the current implementation of a Python function."""
28
+ code = getattr(func, "__code__", None)
29
+ return id(code) if code is not None else None # For builtins/extension funcs returns None
30
+
31
+
32
+ def get_signature(callable_obj) -> inspect.Signature:
33
+ """
34
+ Fast, safe signature fetch with caching and auto-invalidating when the function's
35
+ code object changes (i.e., after hot-reload).
36
+ """
37
+ base = _unwrap_callable(callable_obj)
38
+ current_code_id = _code_id(base)
39
+
40
+ cached = _SIG_CACHE.get(base)
41
+ if cached is not None:
42
+ sig, cached_code_id = cached
43
+ if cached_code_id == current_code_id:
44
+ return sig # up-to-date
45
+
46
+ # Compute once; this is the slow part we want to avoid doing repeatedly
47
+ sig = inspect.signature(base)
48
+
49
+ # Store in WeakKeyDictionary
50
+ _SIG_CACHE[base] = (sig, current_code_id)
51
+
52
+ # Bonus: stamp __signature__ so *future* inspect.signature(base) is O(1)
53
+ try:
54
+ setattr(base, "__signature__", sig)
55
+ except Exception:
56
+ # Not all callables allow setting attributes (e.g., many builtins)
57
+ pass
58
+
59
+ return sig
60
+
61
+
62
+ import inspect
63
+ from functools import partial
64
+ from types import FunctionType, MethodType
65
+ from typing import Mapping, Any
66
+
67
+
68
+ def set_runtime_defaults_in_place(fn: Any, new_defaults: Mapping[str, Any]) -> bool:
69
+ """
70
+ Change the *runtime* defaults of a callable so that calling it uses the new values.
71
+ Supports:
72
+ - plain Python functions
73
+ - bound/unbound methods (updates the underlying function)
74
+ - classmethod/staticmethod (via bound method's __func__)
75
+ - functools.partial (you normally want to rebuild/patch the partial; see note)
76
+
77
+ Returns True if runtime defaults were updated; False if unsupported (e.g., builtins).
78
+ """
79
+ # 1) Find the underlying Python function object we can mutate.
80
+ base = fn
81
+ # bound method -> underlying function
82
+ if isinstance(base, MethodType):
83
+ base = base.__func__
84
+ # functools.partial (defaults live on the wrapped func; partial itself has args/keywords)
85
+ if isinstance(base, partial):
86
+ base = base.func
87
+ # unwrap decorator wrappers
88
+ base = inspect.unwrap(base)
89
+
90
+ # Only pure Python functions have writable __defaults__/__kwdefaults__
91
+ if not isinstance(base, FunctionType):
92
+ return False
93
+
94
+ sig = inspect.signature(base)
95
+
96
+ # Build a new parameter list with updated defaults by name
97
+ params = []
98
+ for p in sig.parameters.values():
99
+ if p.name in new_defaults and p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD) and not isinstance(p.default, inspect.Parameter):
100
+ p = p.replace(default=new_defaults[p.name])
101
+ params.append(p)
102
+
103
+ # Positional defaults must be a trailing contiguous block
104
+ pos = [p for p in params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)]
105
+ flags = [p.default is not inspect._empty for p in pos]
106
+ try:
107
+ first = next(i for i, f in enumerate(flags) if f)
108
+ if not all(flags[i] for i in range(first, len(flags))):
109
+ # Invalid layout for runtime defaults; keep function usable, but don't set.
110
+ return False
111
+ pos_defaults = tuple(p.default if not isinstance(p.default, inspect.Parameter) else p.default for p in pos[first:]) if pos else None
112
+ except StopIteration:
113
+ pos_defaults = None
114
+
115
+ # Keyword-only defaults: free-form dict
116
+ kwonly = [p for p in params if p.kind is p.KEYWORD_ONLY]
117
+ kwdefaults = {p.name: p.default for p in kwonly if p.default is not inspect._empty} or None
118
+
119
+ # 2) Mutate the function object in place
120
+ try:
121
+ base.__defaults__ = pos_defaults
122
+ except Exception:
123
+ return False
124
+ try:
125
+ base.__kwdefaults__ = kwdefaults
126
+ except Exception:
127
+ # Some functions do not allow this; that's okay for functions without kw-only defaults
128
+ pass
129
+
130
+ return True
131
+
132
+ def set_fn_defaults(fn, new_defaults: Mapping[str, Any]) -> bool:
133
+ ok = set_runtime_defaults_in_place(fn, new_defaults)
134
+ # Update the inspect signature to match (optional but nice)
135
+ # try:
136
+ # sig = inspect.signature(fn)
137
+ # new_params = [(p.replace(default=new_defaults[p.name])
138
+ # if p.name in new_defaults and p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD)
139
+ # else p)
140
+ # for p in sig.parameters.values()]
141
+ # new_sig = inspect.Signature(new_params, return_annotation=sig.return_annotation)
142
+ # # set on the exact object and the unwrapped base for robustness
143
+ # setattr(fn, "__signature__", new_sig)
144
+ # setattr(inspect.unwrap(fn), "__signature__", new_sig)
145
+ #
146
+ # except Exception:
147
+ # pass
148
+ return ok
149
+
150
+
151
+ def get_params(callable_obj) -> Mapping[str, inspect.Parameter]:
152
+ """Convenience: directly get the .parameters mapping (ordered)."""
153
+ return get_signature(callable_obj).parameters
154
+
155
+
156
+ def invalidate(callable_obj=None):
157
+ """
158
+ Invalidate the cache for one callable (or all, if None).
159
+ Useful if you mutate __code__ in-place (rare); otherwise hot-reload creates
160
+ new function objects and the cache auto-updates by key.
161
+ """
162
+ if callable_obj is None:
163
+ _SIG_CACHE.clear()
164
+ return
165
+ base = _unwrap_callable(callable_obj)
166
+ try:
167
+ del _SIG_CACHE[base]
168
+ except KeyError:
169
+ pass
@@ -0,0 +1,105 @@
1
+ import subprocess
2
+ import threading
3
+ import time
4
+
5
+ from meltygui.core.automation.mcp_server import _LOG_RESULT_CAP
6
+ from meltygui.core.rendering.render_funcs import RenderFuncs
7
+ from meltygui.core.rendering.window_decoration import window
8
+
9
+ _SENTINEL = object()
10
+
11
+
12
+ class _MonitorMeta(type):
13
+ _brightness = None
14
+ _pending = None
15
+ _event = threading.Event()
16
+ _thread = None
17
+ _lock = threading.Lock()
18
+
19
+ def __init__(cls, *args):
20
+ super().__init__(*args)
21
+
22
+ def _worker():
23
+ while True:
24
+ cls._event.wait()
25
+ cls._event.clear()
26
+ with cls._lock:
27
+ value = cls._pending
28
+ if value is _SENTINEL:
29
+ break
30
+ try:
31
+ value = min(max(1, value), 100)
32
+ subprocess.run(["ddcutil", "setvcp", "10", str(value)])
33
+ except Exception as e:
34
+ print(f"Monitor error: {e}")
35
+
36
+ cls._thread = threading.Thread(target=_worker, daemon=True)
37
+ cls._thread.start()
38
+
39
+ def _fetch():
40
+ # result = subprocess.run(["ddcutil", "getvcp", "10"], capture_output=True, text=True)
41
+ # for part in result.stdout.split(","):
42
+ # if "current value" in part:
43
+ # cls._brightness = int(part.split("=")[1].strip())
44
+ # return
45
+ cls._brightness = 30
46
+
47
+ threading.Thread(target=_fetch, daemon=True).start()
48
+
49
+ @property
50
+ def brightness(cls):
51
+ return cls._brightness
52
+
53
+ @brightness.setter
54
+ def brightness(cls, value):
55
+ cls._brightness = value
56
+ with cls._lock:
57
+ cls._pending = value
58
+ cls._event.set()
59
+
60
+ def shutdown(cls):
61
+ with cls._lock:
62
+ cls._pending = _SENTINEL
63
+ cls._event.set()
64
+ cls._thread.join()
65
+
66
+
67
+ class Monitor(metaclass=_MonitorMeta):
68
+ pass
69
+
70
+
71
+ @window(live=True, view_func=RenderFuncs.draw_type)
72
+ class MCPServerLog:
73
+ """Append-only, in-memory record of MCP tool interactions for this launcher.
74
+
75
+ Every tool call (and its outcome) is appended to the ``logs`` class
76
+ attribute by the logging wrapper in ``start_launcher_mcp``. It's a class
77
+ attribute rather than an instance field so it's reachable from anywhere —
78
+ including ``eval_python`` — as just ``MCPServerLog.logs`` without threading
79
+ an instance around.
80
+
81
+ Each entry is a dict::
82
+
83
+ {"tool": str, "args": dict, "result": str|None,
84
+ "error": str|None, "timestamp": float}
85
+ """
86
+
87
+ logs = []
88
+ def __init__(self):
89
+ pass
90
+
91
+ @classmethod
92
+ def record(cls, tool, args, result=None, error=None):
93
+ def _cap(v):
94
+ if v is None:
95
+ return None
96
+ s = v if isinstance(v, str) else repr(v)
97
+ return s if len(s) <= _LOG_RESULT_CAP else s[:_LOG_RESULT_CAP] + "…"
98
+
99
+ cls.logs.append({
100
+ "tool": tool,
101
+ "args": dict(args),
102
+ "result": _cap(result),
103
+ "error": _cap(error),
104
+ "timestamp": time.time(),
105
+ })