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.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- 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)
|