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,329 @@
1
+ """Terminal model functions and supporting definitions."""
2
+ from meltygui.core.rendering.core_decoration import defaults
3
+ import os
4
+ import select
5
+ import threading
6
+ import time
7
+ import weakref
8
+
9
+
10
+ @defaults(tint=(0.31, 0.37, 0.66))
11
+ class Terminal:
12
+ """A PTY-backed terminal session plus its pyte screen.
13
+ `launch_cmd` is the argv spawned in the PTY — by default a `tmux attach`/create,
14
+ so the processes live in a durable tmux server (they survive studio restarts and
15
+ are shared with any external `tmux attach`). The reader thread feeds bytes into
16
+ pyte under `lock`; the render thread reads the screen grid under the same `lock`."""
17
+
18
+ def __init__(self, launch_cmd=None, tmux_session=None):
19
+ # No args (the "+" add button calls Terminal()) -> create a brand-new OWNED
20
+ # terminal: the in-app PTY itself creates the tmux session (new-session -A) when
21
+ # it starts, then hands it off to its own gnome-terminal window (claude-d style).
22
+ # No blocking `tmux new-session -d` on the UI thread first. With a launch_cmd/
23
+ # session given (main/lsd/discovered claude-d) we just attach, as before.
24
+ from meltygui.core.services.terminal_core import _new_owned_session_name
25
+ from meltygui.core.services.terminal_core import _owned_launch_argv
26
+
27
+ owned = launch_cmd is None and tmux_session is None
28
+ if owned:
29
+ tmux_session = _new_owned_session_name()
30
+ launch_cmd = _owned_launch_argv(tmux_session)
31
+ self.launch_cmd = launch_cmd or [os.environ.get("SHELL", "/bin/bash"), "-i"]
32
+ self.tmux_session = tmux_session # session name (if tmux attached)
33
+ # add_to_collection keys a dict by `.id`, so a "+"-added terminal lands in the
34
+ # store UNDER its session name; the studio poller then finds it already there
35
+ # and doesn't create a duplicate.
36
+ self.id = tmux_session
37
+ self.master_fd = None
38
+ self.pid = None # the PTY child (a bash → tmux client); reaped by the reader
39
+ self.screen = None
40
+ self.stream = None
41
+ self.lock = threading.Lock()
42
+ self.started = False
43
+ self.size = (0, 0) # (cols, rows) currently applied to the PTY
44
+ self.error = None
45
+ self._listeners = [] # weak bound callbacks; the model never owns views
46
+ self._last_sig = None # content signature at the last render - the reader
47
+ # only invalidates/wakes when this actually changes
48
+ self._reader_alive = False # True while the reader thread runs; is_dead() reads it
49
+ # For a brand-new OWNED terminal: the session the PTY creates, handed off to its
50
+ # own gnome-terminal window once the PTY is up (in start()). None = attach-only.
51
+ self._owned_session = tmux_session if owned else None
52
+
53
+ def subscribe(self, callback):
54
+ """Observe output without retaining the owning view or runtime service."""
55
+ listener = weakref.WeakMethod(callback)
56
+ with self.lock:
57
+ # Existing PTYs keep their instances across hotswap; __init__ is
58
+ # deliberately not rerun when observer storage is introduced.
59
+ self._listeners = [item for item in vars(self).setdefault('_listeners', [])
60
+ if item() is not None]
61
+ if listener not in self._listeners:
62
+ self._listeners.append(listener)
63
+
64
+ def unsubscribe(self, callback):
65
+ with self.lock:
66
+ self._listeners = [item for item in vars(self).setdefault('_listeners', [])
67
+ if item() is not None and item() != callback]
68
+
69
+ def _notify_changed(self):
70
+ with self.lock:
71
+ callbacks = [item() for item in vars(self).setdefault('_listeners', [])]
72
+ self._listeners = [item for item in self._listeners if item() is not None]
73
+ for callback in callbacks:
74
+ if callback is not None:
75
+ callback()
76
+
77
+ def snapshot(self):
78
+ """Copy screen rows under the PTY lock for a consistent render snapshot."""
79
+ with self.lock:
80
+ screen = self.screen
81
+ if screen is None:
82
+ return None
83
+ def row_cells(line):
84
+ return tuple(line.get(x, screen.default_char) for x in range(screen.columns))
85
+
86
+ return (screen.lines, screen.columns,
87
+ (screen.cursor.x, screen.cursor.y, screen.cursor.hidden),
88
+ set(screen.mode), [row_cells(line) for line in screen.history.top],
89
+ [row_cells(screen.buffer[y]) for y in range(screen.lines)])
90
+
91
+ def is_dead(self):
92
+ """True once the reader thread has exited — the PTY (and thus its tmux session)
93
+ ended. The studio io drops a terminal on THIS (immediate) rather than waiting
94
+ for the ~1s poller snapshot, so a just-created terminal isn't briefly dropped."""
95
+ return self.started and not self._reader_alive
96
+
97
+ def start(self, cols, rows):
98
+ if self.started:
99
+ return
100
+ self.started = True
101
+ self.size = (cols, rows)
102
+ # Spawn the PTY + tmux attach OFF the main thread. Launching the studio with many
103
+ # discovered terminals would spawn a the processes (bash → tmux attach) plus
104
+ # openpty on a SINGLE render frame - a big launch hitch that scales with the
105
+ # number of sessions. Claim _reader_alive now so is_dead() can't fire in the gap
106
+ # before the bg thread runs; the thread clears it on exit / spawn failure. screen
107
+ # stays None until ready, so draw_terminal_screen shows a placeholder meanwhile.
108
+ self._reader_alive = True
109
+ threading.Thread(target=self._start_and_read, args=(cols, rows), daemon=True).start()
110
+
111
+ def _start_and_read(self, cols, rows):
112
+ from meltygui.core.services.terminal_core import _OWNED_SESSION_PREFIX
113
+ from meltygui.core.services.terminal_core import _disable_mouse
114
+ from meltygui.core.services.terminal_core import _handoff_to_gnome
115
+ from meltygui.core.services.terminal_core import _make_history_screen
116
+ from meltygui.core.services.terminal_core import _set_winsize
117
+ from meltygui.core.services.terminal_core import _spawn_in_pty
118
+
119
+ if os.name == "nt":
120
+ self.error = "The terminal runs a shell in a POSIX pseudo-terminal; Windows is not supported yet"
121
+ self._reader_alive = False
122
+ self._notify_changed()
123
+ return
124
+ try:
125
+ import pyte
126
+ except ImportError:
127
+ self.error = "pyte not installed — run: pip install pyte"
128
+ self._reader_alive = False
129
+ self._notify_changed()
130
+ return
131
+ master = slave = None
132
+ try:
133
+ screen = _make_history_screen(pyte, cols, rows)
134
+ stream = pyte.ByteStream(screen)
135
+ master, slave = os.openpty()
136
+ _set_winsize(master, rows, cols)
137
+ # Unset TMUX so a `tmux attach` child can launch even when the studio is
138
+ # itself launched inside tmux (otherwise tmux refuses to nest).
139
+ env = dict(os.environ, TERM="xterm-256color",
140
+ COLUMNS=str(cols), LINES=str(rows))
141
+ env.pop("TMUX", None)
142
+ # posix_spawn, not fork - see _spawn_in_pty. The child inherits our cwd.
143
+ # The slave stays open here until the spawn returns (the child has opened
144
+ # its own by then - posix_spawn resumes here only after the exec).
145
+ pid = _spawn_in_pty(self.launch_cmd, slave, env)
146
+ os.close(slave)
147
+ slave = None
148
+ # Publish the live objects under the lock so the render thread either sees a
149
+ # fully-wired terminal or still-None (placeholder), never a half-built one.
150
+ with self.lock:
151
+ self.screen = screen
152
+ self.stream = stream
153
+ self.master_fd = master
154
+ self.pid = pid
155
+ # The PTY has forked (owned sessions: created via new-session -A): hand off to
156
+ # its own gnome-terminal window (a second client that owns the lifetime), and
157
+ # disable tmux mouse for claude-d sessions so normal text selection works.
158
+ if self._owned_session is not None:
159
+ _handoff_to_gnome(self._owned_session)
160
+ if self.tmux_session and self.tmux_session.startswith(_OWNED_SESSION_PREFIX):
161
+ _disable_mouse(self.tmux_session)
162
+ except Exception as e: # surface a spawn failure in the render
163
+ for fd in (master, slave):
164
+ if fd is not None:
165
+ os.close(fd)
166
+ self.master_fd = None
167
+ self.error = f"pty start failed: {e}"
168
+ self._reader_alive = False
169
+ self._notify_changed()
170
+ return
171
+ self._read_loop()
172
+
173
+ def _screen_signature(self):
174
+ """A hash of everything we DRAW — every cell (text + colors), the cursor, and
175
+ the scrollback depth. Lets _read_loop invalidate/wake ONLY when a read actually
176
+ changed what's on screen: a terminal that emits bytes with no visible effect
177
+ (a query/response, a cursor report, reprinted-identical output) must not force a
178
+ re-render. pyte's Char is a hashable namedtuple, so we hash the rows directly."""
179
+ sc = self.screen
180
+ if sc is None:
181
+ return None
182
+ cols = sc.columns
183
+ buf = sc.buffer
184
+ rows = tuple(tuple(buf[y][x] for x in range(cols)) for y in range(sc.lines))
185
+ return hash((rows, sc.cursor.x, sc.cursor.y, sc.cursor.hidden, len(sc.history.top)))
186
+
187
+ def _read_loop(self):
188
+ fd = self.master_fd
189
+ self._notify_changed()
190
+
191
+ try:
192
+ while True:
193
+ try:
194
+ r, _, _ = select.select([fd], [], [], 0.2)
195
+ if fd not in r:
196
+ continue
197
+ data = os.read(fd, 65536)
198
+ except OSError:
199
+ break
200
+ if not data:
201
+ break
202
+ with self.lock:
203
+ self.stream.feed(data)
204
+ # Coalesce a burst into ONE re-render. A flood of logs arrives as many
205
+ # 64KB chunks; invalidating per chunk re-renders + re-blits the whole
206
+ # window each time (the real cost; the pyte feed itself is cheap). So
207
+ # keep feeding whatever was ALREADY queued before invalidating once. The
208
+ # time/byte budget keeps continuous output updating (~100Hz) instead of
209
+ # the drain ever starving the render. Each chunk feeds under its own lock
210
+ # so the render can still interleave.
211
+ ended = False
212
+ deadline = time.monotonic() + 0.008
213
+ read_total = len(data)
214
+ while read_total < (1 << 20) and time.monotonic() < deadline:
215
+ try:
216
+ r, _, _ = select.select([fd], [], [], 0)
217
+ if fd not in r:
218
+ break
219
+ more = os.read(fd, 65536)
220
+ except OSError:
221
+ ended = True
222
+ break
223
+ if not more:
224
+ ended = True
225
+ break
226
+ with self.lock:
227
+ self.stream.feed(more)
228
+ read_total += len(more)
229
+ # Only invalidate + wake the loop if this read actually CHANGED what we
230
+ # draw. The session pane (the studio's own console) dribbles bytes that
231
+ # don't alter the visible output; invalidating per read re-rendered the app
232
+ # every frame. Only notify subscribers when the screen changes.
233
+ with self.lock:
234
+ sig = self._screen_signature()
235
+ if sig != self._last_sig:
236
+ self._notify_changed()
237
+ self._last_sig = sig
238
+ if ended:
239
+ break
240
+ finally:
241
+ self._reap()
242
+ with self.lock:
243
+ self.master_fd = None
244
+ os.close(fd)
245
+ self._reader_alive = False # reader exited -> is_dead = True -> io drops us
246
+ self._notify_changed()
247
+
248
+ def _reap(self):
249
+ """Collect the PTY child's exit status so it doesn't linger as a zombie. The
250
+ master hit EOF/EIO, which normally means the child is gone; a child that merely
251
+ closed its tty and lives on is left alone after a short bounded wait."""
252
+ pid, self.pid = self.pid, None
253
+ if pid is None:
254
+ return
255
+ for _ in range(20):
256
+ try:
257
+ if os.waitpid(pid, os.WNOHANG)[0] == pid:
258
+ return
259
+ except ChildProcessError:
260
+ return
261
+ time.sleep(0.05)
262
+
263
+ def write(self, data):
264
+ fd = self.master_fd
265
+ if fd is None or not data:
266
+ return
267
+ try:
268
+ os.write(fd, data)
269
+ except OSError:
270
+ pass
271
+
272
+ def resize(self, cols, rows):
273
+ """Resize the PTY + emulated screen immediately — no coalescing, so every size
274
+ the window sweeps through during a drag is applied as fast as it changes.
275
+ _resize_screen keeps each individual resize correct."""
276
+ from meltygui.core.services.terminal_core import _set_winsize
277
+
278
+ if cols < 2 or rows < 2 or (cols, rows) == self.size:
279
+ return
280
+ self.size = (cols, rows)
281
+ with self.lock:
282
+ if self.screen is not None:
283
+ self._resize_screen(rows, cols)
284
+ if self.master_fd is not None:
285
+ _set_winsize(self.master_fd, rows, cols)
286
+
287
+ def _resize_screen(self, new_rows, new_cols):
288
+ """Resize the emulated screen so the result is correct no matter how often
289
+ this fires (a drag can call it many times). pyte's own resize is unfit for a
290
+ scrolling shell: on shrink it drops the top rows outright (lost), and on grow
291
+ it pads blanks at the BOTTOM, leaving content stuck at the top — so a later
292
+ shrink then pushes that content (prompt included) into history and the shell
293
+ reprints a fresh prompt → stacked prompts with window-sized gaps.
294
+
295
+ Instead we rebuild around the cursor (the prompt) line: take scrollback +
296
+ everything down to the cursor, drop the stale blanks below it, and re-lay it
297
+ BOTTOM-anchored (cursor on the last row, overflow back into history). The
298
+ shell's SIGWINCH redraw then always lands on the same bottom row and nothing
299
+ accumulates. Short content stays top-anchored, like a real terminal."""
300
+ from meltygui.core.services.terminal_core import _ALT_SCREEN_MODES
301
+
302
+ screen = self.screen
303
+ old_rows = screen.lines
304
+ # A full-screen program (vim, less, Claude) owns the alt-screen grid and
305
+ # redraws itself on SIGWINCH; don't second-guess its layout.
306
+ if _ALT_SCREEN_MODES & set(screen.mode):
307
+ screen.resize(new_rows, new_cols)
308
+ return
309
+
310
+ cur_y = min(screen.cursor.y, old_rows - 1)
311
+ above = list(screen.history.top) + [screen.buffer[y] for y in range(cur_y + 1)]
312
+ screen.resize(new_rows, new_cols)
313
+
314
+ if len(above) >= new_rows: # content fills the screen -> bottom-anchor
315
+ rows_content = above[-new_rows:]
316
+ overflow = above[:len(above) - new_rows]
317
+ cursor_row = new_rows - 1
318
+ else: # little content -> keep it at the top
319
+ rows_content = above
320
+ overflow = []
321
+ cursor_row = len(above) - 1
322
+
323
+ screen.history.top.clear()
324
+ screen.history.top.extend(overflow)
325
+ for y in range(new_rows):
326
+ screen.buffer[y] = rows_content[y] if y < len(rows_content) else screen.buffer.default_factory()
327
+ screen.cursor.y = max(0, cursor_row)
328
+ screen.cursor.x = min(screen.cursor.x, new_cols - 1)
329
+ screen.dirty.update(range(new_rows))
@@ -0,0 +1,146 @@
1
+ """Integer-like texture values with lazy, context-owned GPU storage."""
2
+ from functools import total_ordering
3
+ import operator
4
+
5
+ import OpenGL.GL as gl
6
+ from meltygui.core.graphics.gl_state import GLTexture, tight_unpack
7
+
8
+ from meltygui.core.graphics.gl_state import GLState, current_context, is_gl_thread
9
+
10
+
11
+ @total_ordering
12
+ class TextureId:
13
+ """A texture ID proxy. Resolve it only where an OpenGL ID is needed.
14
+
15
+ Subclasses provide ``target`` and ``_upload(state)``. Constructing a value
16
+ does no GL work; int/index conversion uploads it in the current context.
17
+ The numeric ID may change after an edit, so this mutable proxy is unhashable.
18
+ """
19
+ __hash__ = None
20
+
21
+ def __init__(self, target):
22
+ self.target = int(target)
23
+ self._states = {}
24
+
25
+ def _state_for_context(self, context):
26
+ if context not in self._states:
27
+ state = GLState()
28
+ state._context = context
29
+ self._states[context] = state
30
+ return self._states[context]
31
+
32
+ def _state(self):
33
+ context = current_context()
34
+ if context is None or not is_gl_thread():
35
+ raise RuntimeError('A texture ID needs the current rendering context')
36
+ return self._state_for_context(context)
37
+
38
+ def _upload(self, state):
39
+ raise NotImplementedError
40
+
41
+ def __int__(self):
42
+ return self._upload(self._state()).texture_id
43
+
44
+ def __index__(self):
45
+ return int(self)
46
+
47
+ def __bool__(self):
48
+ return bool(int(self))
49
+
50
+ def __eq__(self, other):
51
+ try:
52
+ other_id = operator.index(other)
53
+ except TypeError:
54
+ return NotImplemented
55
+ return int(self) == other_id
56
+
57
+ def __lt__(self, other):
58
+ try:
59
+ other_id = operator.index(other)
60
+ except TypeError:
61
+ return NotImplemented
62
+ return int(self) < other_id
63
+
64
+ @property
65
+ def texture_id(self):
66
+ return int(self)
67
+
68
+ @property
69
+ def _as_parameter_(self):
70
+ """ctypes/PyOpenGL accept the proxy directly as an integer argument."""
71
+ return int(self)
72
+
73
+ def adopt(self, state, old_key, key='texture'):
74
+ """Move an existing allocation into this value without re-uploading it."""
75
+ record = state._resources.pop(old_key, None)
76
+ if record is not None:
77
+ target = self._state_for_context(state._context)
78
+ target.drop(key)
79
+ target._resources[key] = record
80
+
81
+ def release(self):
82
+ for state in self._states.values():
83
+ state.release()
84
+ self._states.clear()
85
+
86
+ def __repr__(self):
87
+ return f'{type(self).__name__}(target={self.target:#x})'
88
+
89
+
90
+ def _upload_cuda_image(gl_state, out):
91
+ """Transfer only the finished 2-D pixels; never the source tensor."""
92
+ import torch
93
+ H, W = int(out.shape[0]), int(out.shape[1])
94
+ host = gl_state.get("cuda_host",
95
+ lambda: torch.empty(H, W, 4, dtype=torch.float16).pin_memory(),
96
+ deps=(W, H))
97
+ # D2H on torch's (legacy default) stream orders after the kernel on
98
+ # the same device's null stream — no explicit synchronize.
99
+ host.copy_(out)
100
+
101
+ def create():
102
+ tex_id = _scalar_int(gl.glGenTextures(1))
103
+ gl.glBindTexture(gl.GL_TEXTURE_2D, tex_id)
104
+ # cuda_march writes linear premultiplied fp16 — the same light
105
+ # the GL pass writes — so the image rides into the fp16 scene
106
+ # (hdr_color.py) unclamped: no encode, no 8-bit ceiling.
107
+ gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA16F, W, H, 0,
108
+ gl.GL_RGBA, gl.GL_HALF_FLOAT, None)
109
+ for pn, pv in ((gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST),
110
+ (gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST),
111
+ (gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE),
112
+ (gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)):
113
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, pn, pv)
114
+ gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
115
+ return GLTexture(tex_id, gl.GL_TEXTURE_2D, (H, W), gl.GL_RGBA16F)
116
+
117
+ img = gl_state.get("cuda_image", create,
118
+ lambda tx: gl.glDeleteTextures([tx.texture_id]),
119
+ deps=(W, H))
120
+ gl.glBindTexture(gl.GL_TEXTURE_2D, img.texture_id)
121
+ with tight_unpack():
122
+ gl.glTexSubImage2D(gl.GL_TEXTURE_2D, 0, 0, 0, W, H, gl.GL_RGBA,
123
+ gl.GL_HALF_FLOAT, host.numpy())
124
+ gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
125
+ return img
126
+
127
+
128
+ def _scalar_int(v):
129
+ try:
130
+ return int(v)
131
+ except TypeError:
132
+ return int(v[0])
133
+
134
+
135
+ def _cached_volume_texture(gl_state, vol_key, keys=("volume_cuda", "volume", "cuda_view")):
136
+ """The already-uploaded volume texture for `vol_key`, or None. Checks
137
+ both upload paths (interop CudaVolume wraps its GLTexture as .texture);
138
+ a hit means draw_voxels skips slice_volume AND the upload outright."""
139
+ for key in keys:
140
+ rec = gl_state.peek(key)
141
+ if rec is None:
142
+ continue
143
+ tex = getattr(rec, "texture", rec)
144
+ if getattr(tex, "_vol_key", None) == vol_key:
145
+ return tex
146
+ return None
@@ -0,0 +1,24 @@
1
+ """App-owned tiled layouts; renderer references persist through DictConversion."""
2
+ from meltygui.core.conversion.dict_conversion import DictConversion
3
+
4
+
5
+ class Tile(DictConversion):
6
+ """An editor selection and its input; view-local state is injected separately."""
7
+
8
+ def __init__(self, name="tile", tint=(0.3, 0.3, 0.3),
9
+ render_func=None, input_value=None):
10
+ super().__init__()
11
+ self.name = name
12
+ self.tint = tint
13
+ self.render_func = render_func
14
+ self.input_value = input_value
15
+
16
+
17
+ class Split(DictConversion):
18
+ """Same-axis cells and their shared interior divider edges, in window pixels."""
19
+
20
+ def __init__(self, axis="x", children=None, edges=None):
21
+ super().__init__()
22
+ self.axis = axis
23
+ self.children = list(children or [])
24
+ self.edges = list(edges or [])