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,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()
|