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 @@
1
+ """Shared framework services machinery."""
@@ -0,0 +1,10 @@
1
+ """Account core functions and supporting definitions."""
2
+
3
+
4
+
5
+ def _cleanup_accounts(draw_state):
6
+ from meltygui.accounts.internet_accounts import KINDS
7
+ from meltygui.accounts.internet_accounts import accounts
8
+
9
+ for entry in list(accounts.values()):
10
+ KINDS[entry["kind"]].close(entry)
@@ -0,0 +1,19 @@
1
+ """Chat core functions and supporting definitions."""
2
+
3
+
4
+
5
+ def _cleanup_chat(draw_state):
6
+ """The window is going: close idle sessions. One with a turn streaming
7
+ stays up (its worker and process keep going, the session file fills in)
8
+ — the studio reuses it when the window reopens; an app waits for it
9
+ before exiting (melty_claude's `finish_turns`)."""
10
+ import meltygui.accounts.internet_accounts as internet_accounts
11
+
12
+ for account in internet_accounts.accounts.values():
13
+ proxy = account.get("_chat_proxy")
14
+ if proxy is None:
15
+ continue
16
+ if any(chat["running"] for chat in proxy.values()):
17
+ continue
18
+ account.pop("_chat_proxy", None)
19
+ proxy.close()
@@ -0,0 +1,346 @@
1
+ """
2
+ Claude terminals — discovered tmux sessions rendered through a RenderHost.
3
+
4
+ `claude-d` launches Claude Code in a tmux session named `claude-d-<pid>`. This
5
+ module surfaces every such session as a live terminal in the studio, using the
6
+ RenderHost mechanism exactly as `modifies_playground` does:
7
+
8
+ discover() → view_func(dict) → apply() (claude_terminals_io)
9
+
10
+ `claude_terminals_io` is the plain stateful wrapper — it knows nothing about
11
+ RenderHost. It reconciles a persistent `{session: Terminal}` dict against the live
12
+ `claude-d-*` tmux sessions, hands that dict to its `view_func`, and (hypothetically)
13
+ applies any edits the view made back to the sessions on the way out. `RenderHost`
14
+ wraps it so the program just sees "a dict full of terminal windows"; the host owns
15
+ the Melty window and calls the wrapper each frame it re-renders.
16
+
17
+ A background poller re-runs discovery when the set of sessions changes (the
18
+ wrapper's body is blit-cached, so it wouldn't otherwise notice a session that
19
+ appeared/vanished with no terminal output to invalidate it).
20
+ """
21
+
22
+ import itertools
23
+ import os
24
+ import shlex
25
+ import shutil
26
+ import subprocess
27
+ import threading
28
+
29
+ from meltygui.core.runtime.lifecycle import module_is_live
30
+ import time
31
+ from pathlib import Path
32
+
33
+ import meltygui_imgui as imgui
34
+
35
+ from meltygui.core.rendering.modes import Modes
36
+ from meltygui.core.rendering.render_funcs import RenderFuncs
37
+ from meltygui.core.windowing.glfw_utils import request_render
38
+ from meltygui.core.conversion.render_host import RenderHost
39
+ from meltygui.core.core_render import render_func
40
+ from meltygui.core.rendering.core_decoration import Core
41
+ from meltygui.core.rendering.window_decoration import window
42
+ from meltygui.model.terminal_model import Terminal
43
+
44
+ _SESSION_PREFIX = "claude-d-"
45
+
46
+ # claude-d runs on a DEDICATED tmux server (`-L claude-d`) started with `-f /dev/null`
47
+ # so it ignores ~/.tmux.conf (which sets `mouse on` + rebinds the arrows). This isolation
48
+ # is what lets the gnome window scroll/copy like a plain terminal without touching the
49
+ # tm's `main`/`lsd` server - only bash/claude-d. EVERY tmux call here must target that
50
+ # same server or it would look at the wrong (default) server and find nothing.
51
+ _TMUX = "tmux -f /dev/null -L claude-d"
52
+ # List form for subprocess argv. ABSOLUTE binary path so (with close_fds=False)
53
+ # CPython spawns via posix_spawn, not fork - fork from this process copies the
54
+ # huge engine/GL/torch address space with the GIL held and blocks the render thread.
55
+ _TMUX_BIN = shutil.which("tmux") or "/usr/bin/tmux"
56
+ _TMUX_ARGS = [_TMUX_BIN, "-f", "/dev/null", "-L", "claude-d"]
57
+ # Reapply the plain terminal config from any client (idempotent global `set -g`); needed
58
+ # in case THIS client cold-starts the server before gnome/claude-d does.
59
+ _TMUX_SETUP = (
60
+ f"{_TMUX} set -g mouse off 2>/dev/null; "
61
+ f"{_TMUX} set -g status off 2>/dev/null; "
62
+ f"{_TMUX} set -g window-size latest 2>/dev/null; "
63
+ f"{_TMUX} set -g terminal-overrides ',*:smcup@:rmcup@' 2>/dev/null; ")
64
+
65
+
66
+ def _list_claude_sessions():
67
+ """The live tmux sessions started by `claude-d` (by name prefix). [] on any
68
+ failure (no tmux server, none running)."""
69
+ try:
70
+ out = subprocess.run(_TMUX_ARGS + ["list-sessions", "-F", "#{session_name}"],
71
+ capture_output=True, text=True, timeout=2).stdout
72
+ except Exception:
73
+ return []
74
+ return sorted(s for s in out.split() if s.startswith(_SESSION_PREFIX))
75
+
76
+
77
+ def _attach_cmd(session):
78
+ """Argv that attaches the in-app PTY to an existing claude-d session (a second,
79
+ shared client — the session was created by `claude-d`, we just view/drive it).
80
+ `window-size latest` makes our view the active client so output wraps to us."""
81
+ return ["bash", "-c",
82
+ _TMUX_SETUP +
83
+ "exec " + _TMUX + " attach-session -t " + shlex.quote(session)]
84
+
85
+ def _kill_session(session):
86
+ """Kill a claude-d tmux session off-thread. This ends the `claude-d` process
87
+ running it (its EXIT/HUP trap fires) → the gnome window closes, and the session
88
+ leaves the tmux server so the poller drops it from `_live_sessions`."""
89
+
90
+ def go():
91
+ try:
92
+ subprocess.run(_TMUX_ARGS + ["kill-session", "-t", session],
93
+ stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, timeout=2)
94
+ except Exception:
95
+ pass
96
+
97
+ threading.Thread(target=go, daemon=True).start()
98
+
99
+
100
+ from meltygui.core.runtime.paths import application_root
101
+ _REPO_ROOT = application_root()
102
+ _studio_session_counter = itertools.count(1)
103
+
104
+
105
+ def launch_claude_session(prompt_text=None):
106
+ """Start a NEW detached claude-d tmux session running Claude Code and, once
107
+ its input box is up, TYPE `prompt_text` into it — literally, no Enter, so
108
+ the user reviews/extends the prompt before sending. The ~1s poller discovers
109
+ the session like any external `claude-d` one and the studio window grows a
110
+ terminal for it. Unlike `bin/claude-d` there's no gnome window: the session
111
+ lives on the dedicated socket only, and closing the in-app window kills it
112
+ via the normal `_kill_session` path. Returns the session name immediately;
113
+ everything runs off-thread (subprocess uses the absolute tmux path +
114
+ close_fds=False → posix_spawn, see _TMUX_BIN)."""
115
+ session = f"{_SESSION_PREFIX}studio-{os.getpid()}-{next(_studio_session_counter)}"
116
+
117
+ def run(args, **kw):
118
+ kw.setdefault("close_fds", False)
119
+ kw.setdefault("timeout", 10)
120
+ kw.setdefault("stdout", subprocess.DEVNULL)
121
+ kw.setdefault("stderr", subprocess.DEVNULL)
122
+ return subprocess.run(args, **kw)
123
+
124
+ def go():
125
+ # `TMUX` unset so this works even when the studio itself was launched
126
+ # from inside a tmux session (tmux refuses to nest otherwise).
127
+ env = {k: v for k, v in os.environ.items() if k != "TMUX"}
128
+ try:
129
+ run(_TMUX_ARGS + ["new-session", "-d", "-s", session, "-c", str(_REPO_ROOT),
130
+ "claude --dangerously-skip-permissions"], env=env)
131
+ except Exception as e:
132
+ print(f"launch_claude_session: failed to start {session}: {e}")
133
+ return
134
+ # Plain-terminal config, same as bin/claude-d (idempotent `set -g`) - in
135
+ # case this cold-starts the dedicated server before bin/claude-d does.
136
+ for opt in (["set", "-g", "mouse", "off"],
137
+ ["set", "-g", "status", "off"],
138
+ ["set", "-g", "window-size", "latest"],
139
+ ["set", "-g", "terminal-overrides", ",*:smcup@:rmcup@"]):
140
+ try:
141
+ run(_TMUX_ARGS + opt)
142
+ except Exception:
143
+ pass
144
+ if not prompt_text:
145
+ return
146
+
147
+ # Wait for Claude Code's input box before typing, so the text lands in
148
+ # the prompt rather than the boot screen. The box's prompt chrome is
149
+ # "" (U+276F); accept ASCII ">" too in case the glyph changes.
150
+ deadline = time.time() + 30.0
151
+ while time.time() < deadline:
152
+ try:
153
+ out = run(_TMUX_ARGS + ["capture-pane", "-p", "-t", session],
154
+ stdout=subprocess.PIPE, text=True).stdout or ""
155
+ except Exception:
156
+ out = ""
157
+ if "❯" in out or ">" in out:
158
+ break
159
+ time.sleep(0.5)
160
+ time.sleep(0.5) # let the TUI finish wiring its key handling
161
+ try:
162
+ # -l = literal keys: the prompt is TYPED, not sent (no Enter).
163
+ run(_TMUX_ARGS + ["send-keys", "-t", session, "-l", prompt_text])
164
+ except Exception as e:
165
+ print(f"launch_claude_session: failed to type prompt into {session}: {e}")
166
+
167
+ threading.Thread(target=go, daemon=True, name="claude-session-launch").start()
168
+ return session
169
+
170
+
171
+ def open_claude_terminals_window():
172
+ """Open + front the studio's Claude Terminals window (render thread only —
173
+ same open pattern as screenshot.process_captures)."""
174
+ from meltygui.core.melty import Melty
175
+ from meltygui.core.graphics.screenshot import _find_window
176
+ mw = _find_window("draw_claude_terminals")
177
+ if mw is None:
178
+ return
179
+ mw.draw_state.closed = False
180
+ if isinstance(mw.window_args, dict):
181
+ mw.window_args["closed"] = False
182
+ Melty.move_window_to_front(mw.draw_state)
183
+
184
+
185
+ # ── the stateful wrapper: discover -> view_func(dict) -> apply ──────────────────
186
+ # Shaped like code_file_io: stateful work, ONE nested view_func call, stateful work.
187
+ # This does not know about RenderHost - the host hands it `view_func` and consumes
188
+ # whatever dict it passes through.
189
+ @render_func(use_cache=True, selectable=False, show_bg=False)
190
+ def claude_terminals_io(input_value, draw_state, view_func=None, external_change=False, **kwargs):
191
+ # ── IN: reconcile the HELD dict IN PLACE against the live sessions. Reconcile
192
+ # `input_value` (the host's held {session: Terminal} it hands us) - NOT a separate
193
+ # draw_state store - so adds/drops happen in the SAME object the @window reads. A
194
+ # separate store leaves the host's materialized ["value"] a stale COPY: if the
195
+ # first materialize runs before the poller fills _live_sessions, ["value"] is stuck
196
+ # empty forever and NO claude windows ever show. First frame the host is empty
197
+ # (input_value None) → start fresh; view_func materializes it as the held value.
198
+ store = input_value if isinstance(input_value, dict) else {}
199
+ # Sessions we've seen in the store at least once. Distinguishes a brand-NEW session
200
+ # (add it) from one the user just CLOSED - its window X deleted the key from the held
201
+ # dict (draw_collection, by reference), but its tmux session is still live. Marking
202
+ # EVERY current store key covers "+"-created terminals too (they add themselves).
203
+ seen = getattr(draw_state, "_seen_sessions", None)
204
+ if seen is None:
205
+ seen = draw_state._seen_sessions = set()
206
+ # Sessions we've already DEALT WITH (PTY ended on its own, or we just killed one) and
207
+ # are now waiting for the ~1s poller to drop from `live`. The live loop skips these so
208
+ # a session lingering in a stale snapshot is neither re-killed every frame nor
209
+ # re-attached as "new" - both spin a loop, visible (since the relauncher restarts a
210
+ # killed session under a new pid → new name) and as windows "added over and over".
211
+ dead = getattr(draw_state, "_dead_sessions", None)
212
+ if dead is None:
213
+ dead = draw_state._dead_sessions = set()
214
+
215
+
216
+ # ── Drop a Terminal when its PTY actually ENDED (reader EOF) - immediate, not tied to
217
+ # the poller snapshot, so a just-created "+" terminal isn't briefly dropped. A PTY
218
+ # that ended on its OWN is not a user event: forget it in `seen` (so the live loop
219
+ # below does NOT mistake it for a closed window and kill it) and park it in `dead`.
220
+ for k, term in list(store.items()):
221
+ if getattr(term, "is_dead", None) and term.is_dead():
222
+ store.pop(k)
223
+ seen.discard(k)
224
+ dead.add(k)
225
+
226
+ for k in store:
227
+
228
+ seen.add(k)
229
+ live = _live_sessions
230
+ for s in live:
231
+ if s in store or s in dead:
232
+ continue # held, or already dealt with - wait for the poller drop
233
+ if s in seen:
234
+ # OUT (change by reference): the user closed this terminal's window, so it's
235
+ # missing from the held dict but its tmux session is still alive. Kill the session
236
+ # (ends claude-d → closes the gnome window) and park it in `dead` so we don't
237
+ # re-fire the kill every frame until the poller drops it from `live`.
238
+ _kill_session(s)
239
+ dead.add(s)
240
+ else:
241
+ # New external session -> add a Terminal (writer/reader start on render).
242
+ store[s] = Terminal(_attach_cmd(s), tmux_session=s)
243
+ seen.add(s)
244
+ live_set = set(live)
245
+ seen &= live_set | set(store) # forget sessions that are neither live nor still held
246
+ dead &= live_set # forget dealt-with sessions once the poller drops them
247
+
248
+ # ── VIEW: hand the dict to the host's view_func (it materializes + renders) ──
249
+ edited, value = view_func(input_value=store, external_change=False, **kwargs)
250
+
251
+ if store is not None:
252
+ imgui.text(f"Found {len(store)} terminals")
253
+ return edited, value
254
+
255
+
256
+ # ── the proxy: RenderHost wraps the wrapper; standalone -> renders in its own
257
+ # window via draw_main. To the program it's just a dict whose value is the
258
+ # {session: Terminal} map the wrapper materialized.
259
+ claude_proxy = RenderHost(io_function=claude_terminals_io, input_value=None,
260
+ name="Claude Terminals")
261
+
262
+
263
+ terminal_loop_running = False
264
+
265
+ # ── the renderer: draw each discovered terminal stacked in the host window ──────
266
+ @window(input_value=claude_proxy, tint=(0.112029,0.04592592,0.267), width=300, height=100)
267
+ @render_func(show_bg=False, use_cache=True, selectable=False)
268
+ def draw_claude_terminals(input_value, draw_state, **kwargs):
269
+ global terminal_loop_running
270
+ if not terminal_loop_running:
271
+ threading.Thread(target=_poll_loop, daemon=True, name="claude-sessions-poller").start()
272
+ terminal_loop_running = True
273
+
274
+ imgui.dummy(1, 20)
275
+
276
+
277
+ # input_value is the proxy. The {session: Terminal} dict is held ONE LEVEL DOWN
278
+ # under value_key ("value") - draw_collection on the proxy itself would only see
279
+ # the single {"value": ...} key (and render that _BubblingDict, not the
280
+ # Terminals). Pull the held dict out and draw its terminals.
281
+ windows = input_value.get("value") if isinstance(input_value, dict) else {}
282
+ if windows is None:
283
+ windows = {}
284
+
285
+ # Keep the collection owner for session-list changes; each screen subscribes
286
+ # independently to PTY output through its injected runtime service.
287
+ global _window_ds
288
+ _window_ds = draw_state
289
+
290
+
291
+ # draw_collection makes each terminal a value-key draw_state; view_func renders each
292
+ # VALUE with draw_terminal_screen (the inline screen renderer). We pass view_func
293
+ # rather than relying on is_default_for=Terminal → draw_terminal, because the
294
+ # default is the @window bound to terminal_screen with a hardcoded screen name so
295
+ # every terminal would collide on one draw_state and show the wrong content.
296
+ dict_changed, new_val, draw_state = RenderFuncs.draw_collection(windows, return_extras=True, show_add_delete=True,
297
+ close_triggers_delete=True, is_tree=False, wrap=True,
298
+ name="New Terminal", disable_scroll=True,
299
+ new_item_type=Terminal, temp=True, shadow=False,
300
+ child_kwargs={"mode": Modes.TERMINAL_WINDOW, "swoosh":False,
301
+ "disable_scroll": True})
302
+
303
+ imgui.text(f"{len(windows)} windows")
304
+
305
+ # for window_ds in draw_state._children.values():
306
+ # imgui.text(f"{window_ds.name} {window_ds.closed} {window_ds._kwargs.get('initial', {})}")
307
+
308
+
309
+ # for window_ds in draw_state._children.values():
310
+ # imgui.text(f"{window_ds.name} {window_ds.closed} {window_ds._kwargs.get('initial', {})}")
311
+
312
+ return False, None
313
+
314
+
315
+ # ── discovery poller: the wrapper's body is blit-cached, so it wouldn't notice a
316
+ # session that appeared/vanished with no output to invalidate it. Poll tmux and,
317
+ # on a CHANGE to the session set, re-run the io_function (discovery) AND re-render
318
+ # the @window so a new/removed terminal shows up.
319
+ _live_sessions = []
320
+ _window_ds = None # draw_claude_terminals' draw_state, stashed each render
321
+
322
+
323
+ def _poll_loop():
324
+ global _live_sessions
325
+ last = None
326
+ while module_is_live(globals()): # exits when a restart purges this module
327
+ if Core.melty.frame_count < 4:
328
+ time.sleep(2)
329
+ # Resilient: this thread starts at startup - BEFORE GLFW is initialized - so an
330
+ # early request_render() raises "GLFW library is not initialized" and (without
331
+ # this guard) the whole poller thread exits, locking _live_sessions so new
332
+ # sessions are never discovered. Swallow per-iteration errors and keep polling;
333
+ # once GLFW is up, request_render works.
334
+ try:
335
+ cur = _list_claude_sessions()
336
+ if cur != last:
337
+ last = cur
338
+ _live_sessions = []
339
+ for ds in (getattr(claude_proxy, "_wrapper_draw_state", None),
340
+ getattr(claude_proxy, "_draw_state", None), _window_ds):
341
+ if ds is not None:
342
+ ds.invalidate()
343
+ request_render()
344
+ except Exception:
345
+ pass
346
+ time.sleep(1.0)