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