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,458 @@
1
+ """A real terminal: an interactive shell running in a pseudo-terminal (PTY), with
2
+ its byte stream fed through a pyte VT100 emulator and the resulting screen grid
3
+ rendered each frame. Because the child sees a true tty (isatty() == True), full
4
+ interactive TUIs — Claude Code, vim, REPLs — launch and render here, not just
5
+ line-oriented `--print` tools.
6
+
7
+ Pipeline:
8
+ keystrokes ── encode ──► os.write(master)
9
+ os.read(master) ── pyte.ByteStream.feed ──► pyte.Screen grid ──► imgui draw
10
+
11
+ The shell owns line-editing, history, cd, prompts, job control — so this file is
12
+ just a PTY pump + a screen renderer + a key encoder, plus selection/copy on top.
13
+ """
14
+ import os
15
+ import shlex
16
+ import shutil
17
+ import signal
18
+ import struct
19
+ import subprocess
20
+ import threading
21
+ import time
22
+ import uuid
23
+
24
+ if os.name != "nt": # the pseudo-terminal pump is POSIX; Terminal reports that on Windows
25
+ import fcntl
26
+ import termios
27
+
28
+ import meltygui.core.windowing.window_api as glfw
29
+ import meltygui_imgui as imgui
30
+
31
+ from meltygui.core.melty import Melty
32
+ from meltygui.core.runtime.toggles import Toggles
33
+ from meltygui.core.windowing.glfw_utils import request_render
34
+ from meltygui.core.rendering.window_decoration import window
35
+ # Reuse the editor's GLFW-key -> character map (covers letters, digits, punctuation
36
+ # with shift pairs) to turn key events into the bytes the shell expects.
37
+ from meltygui.editor.text_editor import _KEY_CHAR_MAP
38
+
39
+
40
+ # 16-colour ANSI palette (pyte hands us names for the basic colours, 6-hex strings
41
+ # for 256/true-colour). Tuned to read well on the dark window background.
42
+
43
+ # Special keys -> the escape sequences an xterm-based terminal sends for them.
44
+ _PTY_KEYS = {
45
+ glfw.KEY_ENTER: b"\r", glfw.KEY_KP_ENTER: b"\r",
46
+ glfw.KEY_BACKSPACE: b"\x7f", glfw.KEY_TAB: b"\t", glfw.KEY_ESCAPE: b"\x1b",
47
+ glfw.KEY_UP: b"\x1b[A", glfw.KEY_DOWN: b"\x1b[B",
48
+ glfw.KEY_RIGHT: b"\x1b[C", glfw.KEY_LEFT: b"\x1b[D",
49
+ glfw.KEY_HOME: b"\x1b[H", glfw.KEY_END: b"\x1b[F",
50
+ glfw.KEY_DELETE: b"\x1b[3~", glfw.KEY_INSERT: b"\x1b[2~",
51
+ glfw.KEY_PAGE_UP: b"\x1b[5~", glfw.KEY_PAGE_DOWN: b"\x1b[6~",
52
+ }
53
+
54
+ # Keys that should auto-repeat while held: every typeable char plus the special PTY
55
+ # keys (arrows, backspace, enter, ...). Used to synthesize repeats from imgui since
56
+ # GLFW doesn't emit REPEAT actions on its own - same approach as draw_text.
57
+ _TERM_REPEATABLE_KEYS = set(_KEY_CHAR_MAP) | set(_PTY_KEYS)
58
+
59
+ # Navigation keys that take the xterm "modified" form when ctrl/alt/shift is held:
60
+ # plain `\x1b[<final>`, modified `\x1b[1;<mod><final>` (mod from _xterm_mod). This is
61
+ # how a terminal distinguishes Ctrl+Right (word-forward) from a bare Right arrow.
62
+ _CSI_FINAL = {
63
+ glfw.KEY_UP: "A", glfw.KEY_DOWN: "B", glfw.KEY_RIGHT: "C", glfw.KEY_LEFT: "D",
64
+ glfw.KEY_HOME: "H", glfw.KEY_END: "F",
65
+ }
66
+ # Editing/paging keys in the tilde form: plain `\x1b[<n>~`, modified `\x1b[<n>;<mod>~`.
67
+ _CSI_TILDE = {
68
+ glfw.KEY_INSERT: 2, glfw.KEY_DELETE: 3,
69
+ glfw.KEY_PAGE_UP: 5, glfw.KEY_PAGE_DOWN: 6,
70
+ }
71
+
72
+
73
+ def _xterm_mod(shift, alt, ctrl):
74
+ """The xterm modifier parameter: 1 + a bitmask (shift=1, alt=2, ctrl=4). Returns 0
75
+ when no modifier is held, signalling the caller to emit the plain sequence instead
76
+ of the `;<mod>` parameterized one. E.g. Ctrl+Right -> mod 5 -> `\\x1b[1;5C`."""
77
+ bits = (1 if shift else 0) | (2 if alt else 0) | (4 if ctrl else 0)
78
+ return bits + 1 if bits else 0
79
+
80
+
81
+ def _set_winsize(fd, rows, cols):
82
+ try:
83
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", rows, cols, 0, 0))
84
+ except OSError:
85
+ pass
86
+
87
+
88
+ def _inheritable_fds():
89
+ """Every fd >= 3 that would survive an exec (not FD_CLOEXEC). Python-created fds are
90
+ CLOEXEC by default (PEP 446); this catches the C libraries' (CUDA / GL / inotify)."""
91
+ fds = []
92
+ for name in os.listdir("/proc/self/fd"):
93
+ fd = int(name)
94
+ if fd < 3:
95
+ continue
96
+ try:
97
+ if not (fcntl.fcntl(fd, fcntl.F_GETFD) & fcntl.FD_CLOEXEC):
98
+ fds.append(fd)
99
+ except OSError:
100
+ pass # closed between the listdir and the fcntl
101
+ return fds
102
+
103
+
104
+ def _spawn_in_pty(argv, slave, env):
105
+ """Run `argv` on the pty `slave` as a session leader with the pty as its controlling
106
+ terminal, and return the child's pid. NEVER fork()s: `os.posix_spawn` is glibc's
107
+ vfork/CLONE_VM path — the child shares our address space until it execs, so there is
108
+ no page-table copy of the studio's huge CUDA/GL/torch mappings and NO at-fork handler
109
+ runs. The old `subprocess.Popen(..., preexec_fn=setsid+TIOCSCTTY)` had to fork() the
110
+ whole process (preexec_fn rules out posix_spawn) and the single-threaded child
111
+ deadlocked in an inherited lock before it ever reached exec — Popen then blocked on
112
+ the exec-errpipe read forever and the terminal sat on "starting…" (08-27).
113
+
114
+ Controlling tty without TIOCSCTTY: the file actions run AFTER the child's setsid, so
115
+ OPENing the slave's path (no O_NOCTTY) as the new session leader makes it the
116
+ controlling terminal — the same rule login programs rely on. The CLOSE actions are
117
+ Popen's close_fds=True: glibc ignores EBADF on an in-range fd, so an fd another
118
+ thread closed in the meantime is harmless. Signals match Popen's restore_signals."""
119
+ executable = shutil.which(argv[0]) or argv[0] # posix_spawn wants a path
120
+ actions = [(os.POSIX_SPAWN_CLOSE, fd) for fd in _inheritable_fds()]
121
+ actions += [(os.POSIX_SPAWN_OPEN, 0, os.ttyname(slave), os.O_RDWR, 0),
122
+ (os.POSIX_SPAWN_DUP2, 0, 1),
123
+ (os.POSIX_SPAWN_DUP2, 0, 2)]
124
+ return os.posix_spawn(executable, argv, env, file_actions=actions, setsid=True,
125
+ setsigmask=(), setsigdef=(signal.SIGPIPE, signal.SIGXFSZ))
126
+
127
+
128
+ _TERM_SCREEN_CLS = None
129
+
130
+
131
+ def _make_history_screen(pyte, cols, rows):
132
+ """A pyte HistoryScreen that also implements SU/SD (CSI Ps S / CSI Ps T), which
133
+ pyte omits entirely. tmux clears the screen (and redraws on scroll) by setting a
134
+ scroll region and SCROLLING UP — `\\x1b[<n>S` — not `\\x1b[2J`. Without a handler
135
+ pyte silently drops it, so the pane never clears; tmux's later writes assume a
136
+ blank pane and overprint the stale rows (mangled output) until a resize forces a
137
+ full redraw. We map S/T to index()/reverse_index() (scroll one line within the
138
+ margins, which is exactly SU/SD).
139
+
140
+ The CSI dispatch is compiled once from `pyte.Stream.csi` at stream construction, so
141
+ S/T must be registered BEFORE the ByteStream is built (start() does so right after
142
+ this call)."""
143
+ global _TERM_SCREEN_CLS
144
+ if _TERM_SCREEN_CLS is None:
145
+ from pyte.screens import Margins
146
+
147
+ class _TermScreen(pyte.HistoryScreen):
148
+ def scroll_up(self, count=1): # CSI Ps S
149
+ count = count or 1
150
+ sy, sx = self.cursor.y, self.cursor.x
151
+ _, bottom = self.margins or Margins(0, self.lines - 1)
152
+ self.cursor.y = bottom
153
+ for _ in range(count):
154
+ self.index() # scrolls the region up by one
155
+ self.cursor.y, self.cursor.x = sy, sx
156
+
157
+ def scroll_down(self, count=1): # CSI Ps T
158
+ count = count or 1
159
+ sy, sx = self.cursor.y, self.cursor.x
160
+ top, _ = self.margins or Margins(0, self.lines - 1)
161
+ self.cursor.y = top
162
+ for _ in range(count):
163
+ self.reverse_index() # scrolls the region down by one
164
+ self.cursor.y, self.cursor.x = sy, sx
165
+
166
+ if "S" not in pyte.Stream.csi:
167
+ pyte.Stream.csi = {**pyte.Stream.csi, "S": "scroll_up", "T": "scroll_down"}
168
+ _TERM_SCREEN_CLS = _TermScreen
169
+ return _TERM_SCREEN_CLS(cols, rows, history=4000, ratio=0.5)
170
+
171
+
172
+ # A new "owned" terminal names its session claude-d-<pid>-<n> so the studio's
173
+ # claude_terminals poller discovers + manages it just like an externally-launched
174
+ # claude-d session. The suffix is a random uuid, NOT pid+counter: the studio restarts
175
+ # IN-PLACE (same pid) and a module reload would reset any module-level counter to 1, so
176
+ # pid+counter handed out `claude-d-<samepid>-1` after every restart → the new window
177
+ # collided with the previous run's draw_state (stale `closed` etc.). A uuid is unique
178
+ # across restarts and immune to pid reuse.
179
+ _OWNED_SESSION_PREFIX = "claude-d-"
180
+
181
+ # Full path to tmux so subprocess can use posix_spawn (vfork) instead of fork()ing the
182
+ # studio's huge CUDA/GL/torch address space - a fork stalls the render thread (kernel
183
+ # mmap_lock + GIL held across the fork). Full path + close_fds=False is what triggers the
184
+ # posix_spawn path on this Python. See claude_terminals._list_claude_sessions.
185
+ _TMUX = shutil.which("tmux") or "/usr/bin/tmux"
186
+ _GNOME_TERMINAL = shutil.which("gnome-terminal") or "/usr/bin/gnome-terminal"
187
+
188
+ # OWNED claude-d sessions run on a DEDICATED tmux server (`-L claude-d`) started with
189
+ # `-f /dev/null` so it ignores ~/.tmux.conf (which sets `mouse on` + rebinds the arrows).
190
+ # That isolation lets the handed-off gnome window scroll/copy like a plain terminal
191
+ # without disturbing the user's `main`/`lsd` sessions - see bin/claude-d. `main`/`lsd`
192
+ # themselves run on the DEFAULT server (see _tmux_launch). `_CD_TMUX` is the common prefix
193
+ # for any claude-d tmux call; `_CD_SETUP` reapplies the plain default config (idempotent
194
+ # global `set -g`) in case the client cold-started the server.
195
+ _CD_TMUX = "tmux -f /dev/null -L claude-d"
196
+ _CD_SETUP = (
197
+ f"{_CD_TMUX} set -g mouse off 2>/dev/null; "
198
+ f"{_CD_TMUX} set -g status off 2>/dev/null; "
199
+ f"{_CD_TMUX} set -g window-size latest 2>/dev/null; "
200
+ f"{_CD_TMUX} set -g terminal-overrides ',*:smcup@:rmcup@' 2>/dev/null; ")
201
+
202
+
203
+ def _attach_argv(session):
204
+ """Argv that attaches an in-app PTY to an existing tmux session (a shared client).
205
+ `window-size latest` makes this view the active client so output wraps to it."""
206
+ return ["bash", "-c",
207
+ "tmux set -g window-size latest 2>/dev/null; "
208
+ "exec tmux attach-session -t " + shlex.quote(session)]
209
+
210
+
211
+ def _new_owned_session_name():
212
+ """Just the NAME for a brand-new owned session. Nothing is created here — the
213
+ in-app PTY creates the session itself when it starts (see _owned_launch_argv), so
214
+ a "+" click never blocks the UI thread on a synchronous `tmux new-session`."""
215
+ return _OWNED_SESSION_PREFIX + uuid.uuid4().hex[:8]
216
+
217
+
218
+ def _owned_launch_argv(session):
219
+ """Argv for the in-app PTY of a brand-new OWNED terminal: it CREATES the session
220
+ (`new-session -A` = create-or-attach) directly in the PTY fork. This is the speed
221
+ win — no separate blocking `tmux new-session -d` round-trip on the UI thread first;
222
+ the terminal launches the process itself and the session exists as soon as the PTY
223
+ is up. The gnome window is handed the session afterwards (see _handoff_to_gnome)."""
224
+ return ["bash", "-c",
225
+ _CD_SETUP +
226
+ "exec " + _CD_TMUX + " new-session -A -s " + shlex.quote(session)]
227
+
228
+
229
+ def _handoff_to_gnome(session):
230
+ """Open a gnome-terminal that attaches to an OWNED session the in-app PTY already
231
+ created, and OWNS its lifetime: its trap kills the session on window close, so
232
+ meltygui-close ↔ gnome-close stay in sync. Non-blocking (Popen). `new-session -A` (not
233
+ plain attach) is race-safe — whoever loses the create just attaches — though the PTY
234
+ forks first so it normally wins. No `exec`, or the trap is skipped (see claude-d).
235
+ `env -u TMUX` so it attaches even if the studio itself was launched inside tmux."""
236
+ q = shlex.quote(session)
237
+ inner = (_CD_SETUP +
238
+ 'trap "' + _CD_TMUX + ' kill-session -t ' + q + ' 2>/dev/null" EXIT HUP TERM INT; '
239
+ 'env -u TMUX ' + _CD_TMUX + ' new-session -A -s ' + q)
240
+ try:
241
+ # Full path + close_fds=False → posix_spawn, never a fork of the studio (see _TMUX).
242
+ subprocess.Popen([_GNOME_TERMINAL, "--", "bash", "-c", inner], close_fds=False)
243
+ except Exception:
244
+ pass
245
+
246
+
247
+ def _disable_mouse(session):
248
+ """Belt-and-suspenders `mouse off` for an owned session. The dedicated `-L claude-d`
249
+ server already runs with mouse off globally (it skips ~/.tmux.conf via `-f /dev/null`
250
+ and _CD_SETUP reasserts it), so this is mostly redundant now — but it cheaply covers
251
+ any ordering race before _CD_SETUP lands. With mouse mode ON, a click-drag in the
252
+ gnome window is grabbed by tmux's copy-mode and the selection is cleared the instant
253
+ you release — you can't select/copy text. Off, gnome-terminal does its own native
254
+ selection. Run off-thread with a short retry: an owned session is created by the
255
+ in-app PTY's `new-session -A` and may not exist the instant we ask."""
256
+ def go():
257
+ for _ in range(20):
258
+ try:
259
+ # Full path + close_fds=False → posix_spawn, not fork (see _TMUX). The
260
+ # `-f /dev/null -L claude-d` flags target the dedicated server (see _CD_TMUX).
261
+ r = subprocess.run([_TMUX, "-f", "/dev/null", "-L", "claude-d",
262
+ "set", "-t", session, "mouse", "off"],
263
+ stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
264
+ timeout=2, close_fds=False)
265
+ if r.returncode == 0:
266
+ return
267
+ except Exception:
268
+ pass
269
+ time.sleep(0.1)
270
+ threading.Thread(target=go, daemon=True).start()
271
+
272
+
273
+ from meltygui.model.terminal_model import Terminal
274
+
275
+
276
+ # --------------------------------------------------------------------------- #
277
+ # Color helpers
278
+ # --------------------------------------------------------------------------- #
279
+
280
+
281
+ # --------------------------------------------------------------------------- #
282
+ # View
283
+ # --------------------------------------------------------------------------- #
284
+
285
+
286
+ # pyte reports private DEC modes shifted left by 5; the alternate-screen modes
287
+ # (47 / 1047 / 1049) tell us a full-screen program (vim, Claude) owns the grid.
288
+ _ALT_SCREEN_MODES = {47 << 5, 1047 << 5, 1049 << 5}
289
+ # Mouse-tracking modes (1000 off / 1002 button / 1003 any) mean the program (e.g.
290
+ # tmux with `mouse on`) wants mouse events forwarded; 1006 is the SGR form.
291
+ _MOUSE_MODES = {1000 << 5, 1002 << 5, 1003 << 5}
292
+ _SGR_MOUSE = 1006 << 5
293
+ # tmux's copy-mode WheelUpPane scrolls this many lines per forwarded wheel escape
294
+ # (its `bind-keys -X -N 5 scroll-up` default), with a smaller line count between escapes.
295
+ _TMUX_WHEEL_LINES = 5
296
+
297
+
298
+ def _mouse_seq(modes, btn, col0, row0):
299
+ """Encode a mouse event for the program. SGR (1006) when offered — it has no
300
+ coordinate limit — else the legacy X10 form (clamped to a byte)."""
301
+ c, r = col0 + 1, row0 + 1
302
+ if _SGR_MOUSE in modes:
303
+ return ("\x1b[<%d;%d;%dM" % (btn, c, r)).encode()
304
+ return b"\x1b[M" + bytes((32 + btn, 32 + min(c, 223), 32 + min(r, 223)))
305
+
306
+
307
+ # --- clickable file:line links (jump to IDE, like draw_text's jump-to button) ---
308
+ from meltygui.core.runtime.paths import application_root
309
+ _PROJECT_ROOT = str(application_root())
310
+ # Python traceback `File "path", line N`, or an absolute/~/./relative `path.ext:line`.
311
+
312
+
313
+ def _resolve_path(p):
314
+ p = os.path.expanduser(p.strip())
315
+ if os.path.isabs(p):
316
+ return p
317
+ cand = os.path.join(_PROJECT_ROOT, p) # resolve relatives against the project
318
+ return cand if os.path.exists(cand) else p
319
+
320
+
321
+ from meltygui.view.terminal_view import draw_terminal_screen
322
+
323
+
324
+ def _forward_keys(term, vs):
325
+ # Keystrokes come from the loop's event queue (Melty.frame_key_events: ordered
326
+ # (glfw_key, mods) per PRESS/REPEAT this frame). GLFW doesn't send REPEAT actions
327
+ # on every platform, so - like draw_text - supplement the queue with imgui's
328
+ # synthesized auto-repeat (io.key_repeat_delay/rate) for any held key, skipping
329
+ # those GLFW already reported this frame so a held key never double-inputs. While a
330
+ # repeatable key is down, keep the loop rendering so imgui's repeat cadence (which
331
+ # is once per frame) keeps firing instead of stalling on wait_events.
332
+ frame_keys = list(Melty.frame_key_events)
333
+ io = imgui.get_io()
334
+ glfw_this_frame = {k for k, _m in frame_keys}
335
+ repeat_mods = ((glfw.MOD_SHIFT if io.key_shift else 0)
336
+ | (glfw.MOD_CONTROL if io.key_ctrl else 0)
337
+ | (glfw.MOD_ALT if getattr(io, 'key_alt', False) else 0))
338
+ any_down = False
339
+ for rk in _TERM_REPEATABLE_KEYS:
340
+ if imgui.is_key_down(rk):
341
+ any_down = True
342
+ if rk not in glfw_this_frame and imgui.is_key_pressed(rk, repeat=True):
343
+ frame_keys.append((rk, repeat_mods))
344
+ if any_down:
345
+ request_render()
346
+ if not frame_keys:
347
+ return
348
+
349
+ def send(data):
350
+ vs.scroll = 0 # any input to the program snaps the view back to the bottom
351
+ term.write(data)
352
+
353
+ for fk, fmods in frame_keys:
354
+ ctrl = fmods & glfw.MOD_CONTROL
355
+ shift = fmods & glfw.MOD_SHIFT
356
+ alt = fmods & glfw.MOD_ALT
357
+ # Ctrl+Shift+C / V are copy / paste (Ctrl+C alone is SIGINT, sent as ^C below).
358
+ if ctrl and shift and fk == glfw.KEY_C:
359
+ _copy_selection(term, vs)
360
+ continue
361
+ if ctrl and shift and fk == glfw.KEY_V:
362
+ clip = imgui.get_clipboard_text()
363
+ if clip:
364
+ send(clip.encode())
365
+ continue
366
+ # Navigation keys carry their modifiers in the xterm "modified" CSI form, so the
367
+ # program sees Ctrl+Arrow (word jump), Alt+Arrow, Shift+Arrow as distinct from a
368
+ # bare arrow; mod==0 (no modifier) falls back to the plain sequence.
369
+ mod = _xterm_mod(shift, alt, ctrl)
370
+ if fk in _CSI_FINAL:
371
+ final = _CSI_FINAL[fk]
372
+ send((f"\x1b[1;{mod}{final}" if mod else f"\x1b[{final}").encode())
373
+ continue
374
+ if fk in _CSI_TILDE:
375
+ n = _CSI_TILDE[fk]
376
+ send((f"\x1b[{n};{mod}~" if mod else f"\x1b[{n}~").encode())
377
+ continue
378
+ if fk in _PTY_KEYS: # enter, tab, backspace, escape: Alt adds ESC (meta),
379
+ seq = _PTY_KEYS[fk] # so e.g. Alt+Backspace = `\x1b\x7f` (delete word).
380
+ send(b"\x1b" + seq if alt else seq)
381
+ continue
382
+ cm = _KEY_CHAR_MAP.get(fk)
383
+ if cm is None:
384
+ continue
385
+ ch = cm[1] if shift else cm[0]
386
+ if ctrl:
387
+ o = ord(ch.upper())
388
+ if 64 <= o <= 95: # Ctrl+@..Ctrl+_ -> control byte (Ctrl+C=^C, Ctrl+D=^D, ...)
389
+ b = bytes([o - 64])
390
+ elif ch == ' ':
391
+ b = b"\x00"
392
+ else:
393
+ continue
394
+ send(b"\x1b" + b if alt else b) # Ctrl+Alt+key -> ESC + control byte
395
+ continue
396
+ if alt: # Alt+key -> ESC prefix (meta), e.g. Alt+f /
397
+ send(b"\x1b" + ch.encode()) # Alt+b for readline-like navigation.
398
+ continue
399
+ send(ch.encode())
400
+
401
+
402
+ def _copy_selection(term, vs):
403
+ from meltygui.state.terminal_state import _norm
404
+ lo, hi = _norm(vs.sel_anchor, vs.sel_active)
405
+ if lo is None or lo == hi:
406
+ return
407
+ with term.lock:
408
+ disp = list(term.screen.display)
409
+ lines = []
410
+ for r in range(lo[0], hi[0] + 1):
411
+ if 0 <= r < len(disp):
412
+ s = lo[1] if r == lo[0] else 0
413
+ e = hi[1] if r == hi[0] else len(disp[r])
414
+ lines.append(disp[r][s:e].rstrip())
415
+ if lines:
416
+ imgui.set_clipboard_text("\n".join(lines))
417
+
418
+
419
+ def _tmux_launch(session):
420
+ """Argv that attaches to (creating if needed) a durable tmux session. The shells
421
+ and processes live in the tmux server, so they survive studio restarts and are
422
+ shared with any external `tmux attach -t <session>`.
423
+
424
+ `window-size latest` (tmux's default) sizes the pane to the ACTIVE client. With
425
+ `largest`, a wider/stale client makes the pane wider than this view, so long lines
426
+ don't wrap here — they clip at our right edge and clickable links lose their
427
+ `:line`. `latest` means typing/scrolling here makes this the active client, so
428
+ output wraps to our width and links stay whole."""
429
+ return ["bash", "-c",
430
+ "tmux set -g window-size latest 2>/dev/null; "
431
+ "exec tmux new-session -A -s " + shlex.quote(session)]
432
+
433
+
434
+ # Two durable, attachable terminals. `main` is the app's shell; `lsd` is reserved
435
+ # for the studio/launcher session (will hold the app's console once the app is
436
+ # wrapped in tmux). test_instance kept as an alias so old tooling still works.
437
+ terminal_instance = Terminal(_tmux_launch("main"), tmux_session="main")
438
+ session_instance = Terminal(_tmux_launch("lsd"), tmux_session="lsd")
439
+ test_instance = terminal_instance
440
+
441
+
442
+ def _draw_terminal_window(term, ds, name):
443
+ # get_content_rect() is the body below the window header (and above any footer);
444
+ # abs_window_rect starts at the very top, so using it would paint under the title.
445
+ left, top, right, bottom = ds.get_content_rect()
446
+ imgui.set_cursor_screen_pos((left, top))
447
+ draw_terminal_screen(term, name=name,
448
+ width=max(1.0, right - left), height=max(1.0, bottom - top))
449
+ return False, term
450
+
451
+
452
+ # Output subscribers and declared input events invalidate the affected screens.
453
+ from meltygui.view.terminal_view import draw_terminal
454
+ draw_terminal = window(tint=(0.05, 0.06, 0.07), bg_offset=-1, max_bg_value=0.08, disable_scroll=True, input_value=terminal_instance)(draw_terminal)
455
+
456
+
457
+ from meltygui.view.terminal_view import draw_session_terminal
458
+ draw_session_terminal = window(tint=(0.09, 0.07, 0.077), input_value=session_instance)(draw_session_terminal)
@@ -0,0 +1,78 @@
1
+ """Injected terminal integration, owned by the screen's render lifecycle."""
2
+ import weakref
3
+ from meltygui.core.melty import Melty
4
+ from meltygui.core.windowing.glfw_utils import request_render
5
+
6
+
7
+ class _TerminalOutputBridge:
8
+ """Let an already-running pre-migration reader notify current subscribers.
9
+
10
+ Hotswap replaces function definitions, not executing Python frames. Those
11
+ older frames still call ``self._ds.invalidate()`` until their PTY exits.
12
+ This transition object contains no draw state and is never used by fresh
13
+ terminals or by the current reader implementation.
14
+ """
15
+
16
+ def __init__(self, terminal):
17
+ self._terminal = weakref.ref(terminal)
18
+
19
+ def invalidate(self):
20
+ terminal = self._terminal()
21
+ if terminal is not None:
22
+ terminal._notify_changed()
23
+
24
+
25
+ class TerminalRuntime:
26
+ def __init__(self):
27
+ self._owner_ds = None
28
+ self._terminal = None
29
+ self._appeared = False
30
+
31
+ def prepare(self, terminal, cols, rows):
32
+ if terminal is not self._terminal:
33
+ if self._terminal is not None:
34
+ self._terminal.unsubscribe(self.output_changed)
35
+ self._terminal = terminal
36
+ terminal.subscribe(self.output_changed)
37
+ if '_ds' in vars(terminal):
38
+ terminal._ds = _TerminalOutputBridge(terminal)
39
+ self._appeared = False
40
+ if not self._appeared:
41
+ self._appeared = True
42
+ if self._owner_ds.closable:
43
+ self.focus()
44
+ terminal.start(cols, rows)
45
+ if terminal.screen is not None:
46
+ terminal.resize(cols, rows)
47
+
48
+ def output_changed(self):
49
+ # Invalidating the screen also dirties cached ancestors. Weak model
50
+ # subscriptions allow several views of one PTY without retaining either.
51
+ self._owner_ds.invalidate_up(force=True)
52
+ try:
53
+ request_render()
54
+ except Exception:
55
+ # A reader can publish while the window backend is starting/stopping.
56
+ pass
57
+
58
+ @property
59
+ def focused(self):
60
+ return Melty.text_focused_ds is self._owner_ds
61
+
62
+ def focus(self):
63
+ previous = Melty.text_focused_ds
64
+ if previous is not None and previous is not self._owner_ds:
65
+ previous.invalidate()
66
+ Melty.text_focused_ds = self._owner_ds
67
+
68
+ def forward_keys(self, terminal, view_state):
69
+ if self.focused:
70
+ from meltygui.core.services.terminal_core import _forward_keys
71
+ _forward_keys(terminal, view_state)
72
+
73
+ def open_link(self, path, line):
74
+ import threading
75
+ from meltygui.core.services.terminal_core import _resolve_path
76
+ from meltygui.utils.jump_to_code import open_in_intellij
77
+ threading.Thread(target=open_in_intellij, args=(_resolve_path(path),),
78
+ kwargs={"line_number": line}, daemon=True).start()
@@ -0,0 +1 @@
1
+ """Shared framework styling machinery."""
@@ -0,0 +1,46 @@
1
+ """Color core functions and supporting definitions."""
2
+
3
+
4
+
5
+ def _style_policy_source(owner, field):
6
+ """Match style inheritance, including policy-only (nonpainting) parents."""
7
+ from meltygui.core.styling.style import default_tint_accumulation
8
+ from meltygui.core.styling.style import default_scalar_accumulation
9
+ seen = set()
10
+ current = owner
11
+ while current is not None and id(current) not in seen:
12
+ seen.add(id(current))
13
+ style = current.locate_style
14
+ if style is None:
15
+ style = current.locate_tint
16
+ fn = getattr(style, field, None)
17
+ if fn is not None:
18
+ return fn, current
19
+ current = current._parent
20
+ return (default_tint_accumulation if field == "tint_fn" else default_scalar_accumulation), None
21
+
22
+
23
+ def _add_style_policy(owner, field):
24
+ """Stamp the effective policy through locate, like Inputs' + value seed."""
25
+ from meltygui.core.windowing.glfw_utils import request_render
26
+
27
+ from meltygui.core.styling.style import Style
28
+ from meltygui.state.core_undo import UndoManager
29
+ original = owner.locate_style
30
+ attr = "locate_style" if original is not None else "locate_tint"
31
+ if original is None:
32
+ original = owner.locate_tint
33
+ if getattr(original, field, None) is not None:
34
+ return
35
+ fn, _ = _style_policy_source(owner, field)
36
+ style = original if isinstance(original, Style) else Style(
37
+ original or (0, 0, 0), absolute=original is not None)
38
+ metadata = style.__getnewargs_ex__()[1]
39
+ metadata[field] = fn
40
+ updated = Style(tuple(style), **metadata)
41
+ setter = lambda value: setattr(owner, attr, value)
42
+ UndoManager.record(owner, original, updated, setter=setter,
43
+ key="add_style_policy_" + field, label="Add style policy")
44
+ setter(updated)
45
+ owner.invalidate()
46
+ request_render()