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,613 @@
|
|
|
1
|
+
"""Per-file display attributes (tint, icon, order …) in ONE store shared
|
|
2
|
+
by every meltygui app on the machine — the studio, melty_code_editor, the
|
|
3
|
+
folder windows — as `file_meta_store()`: a FileMetaProxy, path string ->
|
|
4
|
+
FileMeta, backed by a pickle at ~/.melty/file_meta.pkl.
|
|
5
|
+
|
|
6
|
+
Shaped like GitProxy: the store is a dict subclass and THE SAME OBJECT for
|
|
7
|
+
every holder, so reads are plain dict reads and every holder sees updates
|
|
8
|
+
live. The file I/O is the proxy's job and stays off the render thread:
|
|
9
|
+
|
|
10
|
+
* Writes are debounced (`SAVE_DELAY_S` after the last edit) and atomic
|
|
11
|
+
(tmp + os.replace) on a worker thread; an edit to an entry in place
|
|
12
|
+
(`entry["tint"] = …`) reaches the store through FileMeta's change hooks.
|
|
13
|
+
Empty entries are not written (the studio's folder scan setdefault()s
|
|
14
|
+
thousands of them; they cost nothing in memory and re-create on demand).
|
|
15
|
+
* Other processes' writes: a poller thread stats the file every
|
|
16
|
+
`POLL_S`; a new (mtime, size) marks a pending reload, which the NEXT read
|
|
17
|
+
applies on the reading thread (never a swap under a reader's iteration),
|
|
18
|
+
keeping entries this process edited since its last save (`_dirty`) and
|
|
19
|
+
then repainting: `Melty.cache.invalidate_all()` + request_render. The
|
|
20
|
+
poller also requests a render so an idle app wakes to read.
|
|
21
|
+
* The root save no longer carries the entries (FileMetaCollection is
|
|
22
|
+
`@no_save("file_meta")`); a root loaded from an older save migrates the
|
|
23
|
+
entries it still holds into the store on its on_load, once — the store
|
|
24
|
+
wins for a path it already has.
|
|
25
|
+
|
|
26
|
+
`MELTY_FILE_META=/path.pkl` overrides the location (tests, a second user).
|
|
27
|
+
"""
|
|
28
|
+
import atexit
|
|
29
|
+
import contextlib
|
|
30
|
+
import os
|
|
31
|
+
import pickle
|
|
32
|
+
import tempfile
|
|
33
|
+
import threading
|
|
34
|
+
import time
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
if os.name == "nt":
|
|
38
|
+
import msvcrt
|
|
39
|
+
else:
|
|
40
|
+
import fcntl
|
|
41
|
+
|
|
42
|
+
from meltygui.core.conversion.dict_conversion import DictConversion
|
|
43
|
+
from meltygui.core.rendering.core_decoration import defaults
|
|
44
|
+
from meltygui.core.rendering.core_decoration import no_save
|
|
45
|
+
|
|
46
|
+
SAVE_DELAY_S = 0.4
|
|
47
|
+
POLL_S = 0.5
|
|
48
|
+
_FORMAT = 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _lock_fd(fd):
|
|
52
|
+
"""Block until this process holds the exclusive cross-process lock on fd
|
|
53
|
+
(flock on POSIX; on Windows a one-byte region lock at offset 0)."""
|
|
54
|
+
if os.name != "nt":
|
|
55
|
+
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
56
|
+
return
|
|
57
|
+
while True:
|
|
58
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
59
|
+
try:
|
|
60
|
+
msvcrt.locking(fd, msvcrt.LK_LOCK, 1) # retries for ~10 s, then OSError
|
|
61
|
+
return
|
|
62
|
+
except OSError:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _unlock_fd(fd):
|
|
67
|
+
if os.name != "nt":
|
|
68
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
69
|
+
return
|
|
70
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
71
|
+
try:
|
|
72
|
+
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
|
|
73
|
+
except OSError:
|
|
74
|
+
pass # never acquired (the lock call itself raised)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def default_store_path():
|
|
78
|
+
override = os.environ.get("MELTY_FILE_META")
|
|
79
|
+
if override:
|
|
80
|
+
return Path(override).expanduser()
|
|
81
|
+
return Path.home() / ".melty" / "file_meta.pkl"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@defaults(show_tint=False, show_name=True, show_excluded=True)
|
|
85
|
+
class FileMeta(dict):
|
|
86
|
+
"""One file's attribute entry in FileMetaCollection.file_meta.
|
|
87
|
+
|
|
88
|
+
A plain dict to the framework — the codec kwargs layer merges its items,
|
|
89
|
+
the folder tree syncs it, and it serializes like any dict — but a TYPE of
|
|
90
|
+
its own, so file attributes have a home: class-level attrs declare the
|
|
91
|
+
vocabulary and its defaults (and give draw_any / @defaults / the context
|
|
92
|
+
menu a class to anchor on), while the dict itself holds only the values
|
|
93
|
+
actually attached to the file. Reading `entry.tint` falls through to the
|
|
94
|
+
class default when the file has nothing stored."""
|
|
95
|
+
|
|
96
|
+
# Black transparent = UNPAINTED: no tint on the file's views, and every
|
|
97
|
+
# consumer (tabs, trees, search rows) falls back to its own default. Get
|
|
98
|
+
# tints through painted_tint(), not entry["tint"] directly.
|
|
99
|
+
tint = (0.0, 0.0, 0.0, 0.0)
|
|
100
|
+
icon = None # glyph drawn before the file name (editor tabs); None = no icon
|
|
101
|
+
# A FOLDER marked as a project (folder_project): a root for the editor's
|
|
102
|
+
# global search, the symbol's usage graph, tabs and editability. Files
|
|
103
|
+
# never store it; read it through is_project() or project_roots().
|
|
104
|
+
project = False
|
|
105
|
+
environment = None # Project venv folder override; None means auto-detect.
|
|
106
|
+
|
|
107
|
+
# The pre-09-02 class default; stored values of it are dropped at load
|
|
108
|
+
# (folder_files._init_file_meta) so the files read as unpainted.
|
|
109
|
+
_LEGACY_DEFAULT_TINT = (0.11, 0.122, 0.14)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def painted_tint(entry):
|
|
113
|
+
"""The tint a user attached to `entry` (a FileMeta / dict / None) as
|
|
114
|
+
an rgb(a) tuple, or None when the file is unpainted — no tint, a
|
|
115
|
+
malformed one, or one with alpha 0 (the default)."""
|
|
116
|
+
tint = entry.get("tint") if isinstance(entry, dict) else None
|
|
117
|
+
if not (isinstance(tint, (tuple, list)) and len(tint) >= 3):
|
|
118
|
+
return None
|
|
119
|
+
if len(tint) >= 4 and not tint[3]:
|
|
120
|
+
return None
|
|
121
|
+
return tuple(tint)
|
|
122
|
+
|
|
123
|
+
def __getattribute__(self, name):
|
|
124
|
+
# Stored-first attribute access: `entry.tint` is the file's own value
|
|
125
|
+
# when one is attached, the class default otherwise. (A plain
|
|
126
|
+
# __getattr__ can't do this - normal lookup finds the class attr
|
|
127
|
+
# first and the dict value would never win.) Dunder/internal names
|
|
128
|
+
# skip the dict so machinery never collides with stored keys.
|
|
129
|
+
if not name.startswith("_"):
|
|
130
|
+
try:
|
|
131
|
+
return dict.__getitem__(self, name)
|
|
132
|
+
except KeyError:
|
|
133
|
+
pass
|
|
134
|
+
return super().__getattribute__(name)
|
|
135
|
+
|
|
136
|
+
# ── Merged iteration: class defaults flow like stored values ───────────
|
|
137
|
+
# The iteration protocol serves class the class together, so every
|
|
138
|
+
# consumer that iterates - draw_collection, the codec kwargs layer, the
|
|
139
|
+
# folder tree sync - sees every declared attribute on every entry, and
|
|
140
|
+
# editing a default row simply attaches the value (__setitem__). This is
|
|
141
|
+
# deliberate: the defaults ARE the entry's effective values. A consumer
|
|
142
|
+
# that ever needs only what's explicitly attached reads dict.items(entry)
|
|
143
|
+
# - the raw stored dict is always one super() away.
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def _class_defaults(cls):
|
|
147
|
+
out = {}
|
|
148
|
+
for klass in reversed(cls.__mro__):
|
|
149
|
+
if klass in (dict, object):
|
|
150
|
+
continue
|
|
151
|
+
for k, v in vars(klass).items():
|
|
152
|
+
if (k.startswith("_") or callable(v)
|
|
153
|
+
or isinstance(v, (classmethod, staticmethod, property))):
|
|
154
|
+
continue
|
|
155
|
+
out[k] = v
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
def _merged(self):
|
|
159
|
+
d = dict(type(self)._class_defaults())
|
|
160
|
+
d.update(dict.items(self))
|
|
161
|
+
return d
|
|
162
|
+
|
|
163
|
+
def __iter__(self):
|
|
164
|
+
return iter(self._merged())
|
|
165
|
+
|
|
166
|
+
def __len__(self):
|
|
167
|
+
return len(self._merged())
|
|
168
|
+
|
|
169
|
+
def keys(self):
|
|
170
|
+
return self._merged().keys()
|
|
171
|
+
|
|
172
|
+
def items(self):
|
|
173
|
+
return self._merged().items()
|
|
174
|
+
|
|
175
|
+
def values(self):
|
|
176
|
+
return self._merged().values()
|
|
177
|
+
|
|
178
|
+
def __contains__(self, k):
|
|
179
|
+
return dict.__contains__(self, k) or k in type(self)._class_defaults()
|
|
180
|
+
|
|
181
|
+
def __getitem__(self, k):
|
|
182
|
+
try:
|
|
183
|
+
return dict.__getitem__(self, k)
|
|
184
|
+
except KeyError:
|
|
185
|
+
defaults = type(self)._class_defaults()
|
|
186
|
+
if k in defaults:
|
|
187
|
+
return defaults[k]
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
def get(self, k, default=None):
|
|
191
|
+
try:
|
|
192
|
+
return self[k]
|
|
193
|
+
except KeyError:
|
|
194
|
+
return default
|
|
195
|
+
|
|
196
|
+
# ── Mutation hooks: an entry edited in-place (`entry["tint"] = ...`) tells
|
|
197
|
+
# the store that holds it (FileMetaProxy stamps `_owner` on insertion)
|
|
198
|
+
# so the shared file gets written. dict-level raw edits (`dict.pop(entry,
|
|
199
|
+
# ...)`) bypass these on purpose - you store.touch() after those.
|
|
200
|
+
_owner = None
|
|
201
|
+
_key = None
|
|
202
|
+
|
|
203
|
+
def _changed(self):
|
|
204
|
+
owner = self._owner
|
|
205
|
+
if owner is not None:
|
|
206
|
+
owner.touch(self._key)
|
|
207
|
+
|
|
208
|
+
def __setitem__(self, k, v):
|
|
209
|
+
dict.__setitem__(self, k, v)
|
|
210
|
+
self._changed()
|
|
211
|
+
|
|
212
|
+
def __delitem__(self, k):
|
|
213
|
+
dict.__delitem__(self, k)
|
|
214
|
+
self._changed()
|
|
215
|
+
|
|
216
|
+
def pop(self, *args):
|
|
217
|
+
out = dict.pop(self, *args)
|
|
218
|
+
self._changed()
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
def popitem(self):
|
|
222
|
+
out = dict.popitem(self)
|
|
223
|
+
self._changed()
|
|
224
|
+
return out
|
|
225
|
+
|
|
226
|
+
def clear(self):
|
|
227
|
+
dict.clear(self)
|
|
228
|
+
self._changed()
|
|
229
|
+
|
|
230
|
+
def update(self, *args, **kwargs):
|
|
231
|
+
dict.update(self, *args, **kwargs)
|
|
232
|
+
self._changed()
|
|
233
|
+
|
|
234
|
+
def setdefault(self, k, default=None):
|
|
235
|
+
if dict.__contains__(self, k):
|
|
236
|
+
return dict.__getitem__(self, k)
|
|
237
|
+
dict.__setitem__(self, k, default)
|
|
238
|
+
self._changed()
|
|
239
|
+
return default
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class FileMetaProxy(dict):
|
|
243
|
+
"""path string -> FileMeta, the shared per-file attribute store (see the
|
|
244
|
+
module docstring). Create one through file_meta_store()."""
|
|
245
|
+
|
|
246
|
+
def __init__(self, path=None):
|
|
247
|
+
super().__init__()
|
|
248
|
+
self.path = Path(path) if path is not None else default_store_path()
|
|
249
|
+
self._lock = threading.RLock()
|
|
250
|
+
self._disk_sig = None # (mtime_ns, size) of the file as last read / written
|
|
251
|
+
self._pending = False # the poller saw a newer sig: merge on the next read
|
|
252
|
+
self._dirty = set() # paths edited here since the last save
|
|
253
|
+
self._dirty_all = False # a clear()/whole-store edit: our copy wins entirely
|
|
254
|
+
self.generation = 0 # bumped on every local edit and every save (memo key)
|
|
255
|
+
self._timer = None
|
|
256
|
+
self._poller = None
|
|
257
|
+
self.listeners = [] # callables run(reading thread) after an external reload
|
|
258
|
+
self.load()
|
|
259
|
+
atexit.register(self.flush)
|
|
260
|
+
|
|
261
|
+
# ── file ───────────────────────────────────────────────────────────────
|
|
262
|
+
def _stat_sig(self):
|
|
263
|
+
"""Identity of the file's current contents. The inode is in it
|
|
264
|
+
because every write is a fresh temp file renamed over the path:
|
|
265
|
+
two writes inside one clock tick share mtime and can share size,
|
|
266
|
+
the inode never repeats."""
|
|
267
|
+
try:
|
|
268
|
+
st = self.path.stat()
|
|
269
|
+
except OSError:
|
|
270
|
+
return None
|
|
271
|
+
return (st.st_ino, st.st_mtime_ns, st.st_size)
|
|
272
|
+
|
|
273
|
+
@contextlib.contextmanager
|
|
274
|
+
def _file_lock(self):
|
|
275
|
+
"""Serialize writers across processes (flock on a sibling .lock) so
|
|
276
|
+
the stale check and the replace are one step."""
|
|
277
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
278
|
+
lock_path = self.path.with_suffix(self.path.suffix + ".lock")
|
|
279
|
+
fd = os.open(str(lock_path), os.O_RDWR | os.O_CREAT, 0o644)
|
|
280
|
+
try:
|
|
281
|
+
_lock_fd(fd)
|
|
282
|
+
yield
|
|
283
|
+
finally:
|
|
284
|
+
_unlock_fd(fd)
|
|
285
|
+
os.close(fd)
|
|
286
|
+
|
|
287
|
+
def _read_file(self):
|
|
288
|
+
"""{path: stored dict} from disk, {} when absent / unreadable."""
|
|
289
|
+
try:
|
|
290
|
+
with open(self.path, "rb") as fh:
|
|
291
|
+
data = pickle.load(fh)
|
|
292
|
+
except (OSError, EOFError, pickle.UnpicklingError, AttributeError,
|
|
293
|
+
ImportError, ValueError):
|
|
294
|
+
return {}
|
|
295
|
+
entries = data.get("entries") if isinstance(data, dict) else None
|
|
296
|
+
if not isinstance(entries, dict):
|
|
297
|
+
return {}
|
|
298
|
+
return {str(k): dict(v) for k, v in entries.items() if isinstance(v, dict)}
|
|
299
|
+
|
|
300
|
+
def load(self):
|
|
301
|
+
"""Replace the held entries with the file's (keeping locally dirty
|
|
302
|
+
ones). Returns True when anything changed. Runs on the caller's
|
|
303
|
+
thread — the reading thread, by design (see _sync)."""
|
|
304
|
+
with self._lock:
|
|
305
|
+
sig = self._stat_sig()
|
|
306
|
+
fresh = self._read_file() if sig is not None else {}
|
|
307
|
+
self._disk_sig = sig
|
|
308
|
+
self._pending = False
|
|
309
|
+
if self._dirty_all:
|
|
310
|
+
return False
|
|
311
|
+
keep = {p: dict.__getitem__(self, p) for p in self._dirty
|
|
312
|
+
if dict.__contains__(self, p)}
|
|
313
|
+
before = {p: dict(dict.items(e)) for p, e in dict.items(self)}
|
|
314
|
+
merged = {}
|
|
315
|
+
for path, values in fresh.items():
|
|
316
|
+
if path in self._dirty:
|
|
317
|
+
continue # Includes locally deleted entries, absent from keep.
|
|
318
|
+
entry = dict.get(self, path)
|
|
319
|
+
if not isinstance(entry, FileMeta):
|
|
320
|
+
entry = self._adopt(FileMeta(), path)
|
|
321
|
+
dict.clear(entry)
|
|
322
|
+
dict.update(entry, values)
|
|
323
|
+
merged[path] = entry
|
|
324
|
+
merged.update(keep)
|
|
325
|
+
after = {p: dict(dict.items(e)) for p, e in merged.items()}
|
|
326
|
+
if after == before:
|
|
327
|
+
return False
|
|
328
|
+
dict.clear(self)
|
|
329
|
+
dict.update(self, merged)
|
|
330
|
+
self.generation += 1
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
def _adopt(self, entry, key):
|
|
334
|
+
if isinstance(entry, FileMeta):
|
|
335
|
+
entry._owner = self
|
|
336
|
+
entry._key = key
|
|
337
|
+
return entry
|
|
338
|
+
|
|
339
|
+
def _snapshot(self):
|
|
340
|
+
with self._lock:
|
|
341
|
+
return {p: dict(dict.items(e)) for p, e in dict.items(self)
|
|
342
|
+
if isinstance(e, dict) and len(dict.keys(e))}
|
|
343
|
+
|
|
344
|
+
def _write(self):
|
|
345
|
+
entries = self._snapshot()
|
|
346
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
fd, tmp = tempfile.mkstemp(prefix=".file_meta-", suffix=".tmp",
|
|
348
|
+
dir=str(self.path.parent))
|
|
349
|
+
try:
|
|
350
|
+
with os.fdopen(fd, "wb") as fh:
|
|
351
|
+
pickle.dump({"format": _FORMAT, "entries": entries}, fh,
|
|
352
|
+
protocol=pickle.HIGHEST_PROTOCOL)
|
|
353
|
+
os.replace(tmp, self.path)
|
|
354
|
+
except OSError:
|
|
355
|
+
try:
|
|
356
|
+
os.unlink(tmp)
|
|
357
|
+
except OSError:
|
|
358
|
+
pass
|
|
359
|
+
return
|
|
360
|
+
with self._lock:
|
|
361
|
+
self._disk_sig = self._stat_sig()
|
|
362
|
+
self._dirty.clear()
|
|
363
|
+
self._dirty_all = False
|
|
364
|
+
|
|
365
|
+
def flush(self):
|
|
366
|
+
"""Write now if anything is pending (atexit; tests). Merges from
|
|
367
|
+
disk first when another process wrote meanwhile."""
|
|
368
|
+
with self._lock:
|
|
369
|
+
timer, self._timer = self._timer, None
|
|
370
|
+
dirty = bool(self._dirty or self._dirty_all)
|
|
371
|
+
if timer is not None:
|
|
372
|
+
timer.cancel()
|
|
373
|
+
if dirty:
|
|
374
|
+
with self._file_lock():
|
|
375
|
+
if self._stat_sig() != self._disk_sig:
|
|
376
|
+
self.load()
|
|
377
|
+
self._write()
|
|
378
|
+
|
|
379
|
+
def touch(self, path=None):
|
|
380
|
+
"""Mark the store (or one entry) edited here and arm the debounced
|
|
381
|
+
save. FileMeta entries call this through their change hooks."""
|
|
382
|
+
with self._lock:
|
|
383
|
+
self.generation += 1
|
|
384
|
+
if path is None:
|
|
385
|
+
self._dirty_all = True
|
|
386
|
+
else:
|
|
387
|
+
self._dirty.add(str(path))
|
|
388
|
+
if self._timer is not None:
|
|
389
|
+
self._timer.cancel()
|
|
390
|
+
self._timer = threading.Timer(SAVE_DELAY_S, self._save_timer)
|
|
391
|
+
self._timer.daemon = True
|
|
392
|
+
self._timer.start()
|
|
393
|
+
self._ensure_poller()
|
|
394
|
+
|
|
395
|
+
def _save_timer(self):
|
|
396
|
+
with self._lock:
|
|
397
|
+
self._timer = None
|
|
398
|
+
with self._file_lock():
|
|
399
|
+
stale = self._stat_sig() != self._disk_sig
|
|
400
|
+
if not stale:
|
|
401
|
+
self._merge_retries = 0
|
|
402
|
+
self._write()
|
|
403
|
+
return
|
|
404
|
+
retries = self._merge_retries = getattr(self, "_merge_retries", 0) + 1
|
|
405
|
+
if retries > 5:
|
|
406
|
+
# Nobody in this process is reading (yet): merge here,
|
|
407
|
+
# drop the lock, then write.
|
|
408
|
+
self._merge_retries = 0
|
|
409
|
+
self.load()
|
|
410
|
+
self._write()
|
|
411
|
+
return
|
|
412
|
+
# Another process wrote since we last read: writing our snapshot
|
|
413
|
+
# would overwrite its change. Merge first - load() keeps our dirty
|
|
414
|
+
# entries - on the reading thread: flag it, wake a frame, retry.
|
|
415
|
+
with self._lock:
|
|
416
|
+
self._pending = True
|
|
417
|
+
_request_render()
|
|
418
|
+
with self._lock:
|
|
419
|
+
if self._timer is None:
|
|
420
|
+
self._timer = threading.Timer(POLL_S, self._save_timer)
|
|
421
|
+
self._timer.daemon = True
|
|
422
|
+
self._timer.start()
|
|
423
|
+
|
|
424
|
+
# ── other processes ────────────────────────────────────────────────────
|
|
425
|
+
def _ensure_poller(self):
|
|
426
|
+
if self._poller is not None:
|
|
427
|
+
return
|
|
428
|
+
self._poller = threading.Thread(target=self._poll, name="file-meta-poll",
|
|
429
|
+
daemon=True)
|
|
430
|
+
self._poller.start()
|
|
431
|
+
|
|
432
|
+
def _poll(self):
|
|
433
|
+
while True:
|
|
434
|
+
time.sleep(POLL_S)
|
|
435
|
+
with self._lock:
|
|
436
|
+
if self._pending or self._timer is not None:
|
|
437
|
+
continue # already flagged or our own write is coming
|
|
438
|
+
sig = self._stat_sig()
|
|
439
|
+
changed = sig != self._disk_sig
|
|
440
|
+
if changed:
|
|
441
|
+
self._pending = True
|
|
442
|
+
if changed:
|
|
443
|
+
_request_render()
|
|
444
|
+
|
|
445
|
+
def _sync(self):
|
|
446
|
+
if self._pending and self.load():
|
|
447
|
+
self._repaint()
|
|
448
|
+
|
|
449
|
+
def _repaint(self):
|
|
450
|
+
try:
|
|
451
|
+
from meltygui.core.melty import Melty
|
|
452
|
+
cache = getattr(Melty, "cache", None)
|
|
453
|
+
if cache is not None:
|
|
454
|
+
cache.invalidate_all()
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
_request_render()
|
|
458
|
+
for fn in list(self.listeners):
|
|
459
|
+
try:
|
|
460
|
+
fn()
|
|
461
|
+
except Exception:
|
|
462
|
+
pass
|
|
463
|
+
|
|
464
|
+
# ── dict surface: reads try a pending reload first, writes mark dirty ─
|
|
465
|
+
def __getitem__(self, k):
|
|
466
|
+
self._sync()
|
|
467
|
+
return dict.__getitem__(self, k)
|
|
468
|
+
|
|
469
|
+
def get(self, k, default=None):
|
|
470
|
+
self._sync()
|
|
471
|
+
return dict.get(self, k, default)
|
|
472
|
+
|
|
473
|
+
def __contains__(self, k):
|
|
474
|
+
self._sync()
|
|
475
|
+
return dict.__contains__(self, k)
|
|
476
|
+
|
|
477
|
+
def __iter__(self):
|
|
478
|
+
self._sync()
|
|
479
|
+
return dict.__iter__(self)
|
|
480
|
+
|
|
481
|
+
def __len__(self):
|
|
482
|
+
self._sync()
|
|
483
|
+
return dict.__len__(self)
|
|
484
|
+
|
|
485
|
+
def keys(self):
|
|
486
|
+
self._sync()
|
|
487
|
+
return dict.keys(self)
|
|
488
|
+
|
|
489
|
+
def values(self):
|
|
490
|
+
self._sync()
|
|
491
|
+
return dict.values(self)
|
|
492
|
+
|
|
493
|
+
def items(self):
|
|
494
|
+
self._sync()
|
|
495
|
+
return dict.items(self)
|
|
496
|
+
|
|
497
|
+
def __setitem__(self, k, v):
|
|
498
|
+
self._sync()
|
|
499
|
+
if isinstance(v, dict) and not isinstance(v, FileMeta):
|
|
500
|
+
v = FileMeta(v)
|
|
501
|
+
with self._lock:
|
|
502
|
+
dict.__setitem__(self, k, self._adopt(v, k))
|
|
503
|
+
self.touch(k)
|
|
504
|
+
|
|
505
|
+
def __delitem__(self, k):
|
|
506
|
+
self._sync()
|
|
507
|
+
with self._lock:
|
|
508
|
+
dict.__delitem__(self, k)
|
|
509
|
+
self.touch(k)
|
|
510
|
+
|
|
511
|
+
def pop(self, k, *default):
|
|
512
|
+
self._sync()
|
|
513
|
+
with self._lock:
|
|
514
|
+
had = dict.__contains__(self, k)
|
|
515
|
+
out = dict.pop(self, k, *default)
|
|
516
|
+
if had:
|
|
517
|
+
self.touch(k)
|
|
518
|
+
return out
|
|
519
|
+
|
|
520
|
+
def popitem(self):
|
|
521
|
+
self._sync()
|
|
522
|
+
with self._lock:
|
|
523
|
+
k, v = dict.popitem(self)
|
|
524
|
+
self.touch(k)
|
|
525
|
+
return k, v
|
|
526
|
+
|
|
527
|
+
def setdefault(self, k, default=None):
|
|
528
|
+
self._sync()
|
|
529
|
+
if dict.__contains__(self, k):
|
|
530
|
+
return dict.__getitem__(self, k)
|
|
531
|
+
self[k] = default
|
|
532
|
+
return dict.__getitem__(self, k)
|
|
533
|
+
|
|
534
|
+
def update(self, *args, **kwargs):
|
|
535
|
+
for k, v in dict(*args, **kwargs).items():
|
|
536
|
+
self[k] = v
|
|
537
|
+
|
|
538
|
+
def clear(self):
|
|
539
|
+
with self._lock:
|
|
540
|
+
dict.clear(self)
|
|
541
|
+
self.touch()
|
|
542
|
+
|
|
543
|
+
def __reduce__(self):
|
|
544
|
+
# Pickled / deepcopopied as a plain dict of plain dicts: the proxy's
|
|
545
|
+
# path, timer and poller never travel, and a save that reaches it by
|
|
546
|
+
# mistake stays loadable.
|
|
547
|
+
return (dict, (self._snapshot(),))
|
|
548
|
+
|
|
549
|
+
def __repr__(self):
|
|
550
|
+
return f"FileMetaProxy({self.path}, {dict.__len__(self)} entries)"
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _request_render():
|
|
554
|
+
try:
|
|
555
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
556
|
+
request_render()
|
|
557
|
+
except Exception:
|
|
558
|
+
pass
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
_store = None
|
|
562
|
+
_store_lock = threading.Lock()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def file_meta_store():
|
|
566
|
+
"""The process's shared FileMetaProxy (created on first use)."""
|
|
567
|
+
global _store
|
|
568
|
+
if _store is None:
|
|
569
|
+
with _store_lock:
|
|
570
|
+
if _store is None:
|
|
571
|
+
_store = FileMetaProxy()
|
|
572
|
+
_store._ensure_poller()
|
|
573
|
+
return _store
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@no_save("file_meta")
|
|
577
|
+
class FileMetaCollection(DictConversion):
|
|
578
|
+
"""AppModel's handle on the shared per-file attribute store.
|
|
579
|
+
|
|
580
|
+
`file_meta` IS the process's FileMetaProxy (path string -> FileMeta:
|
|
581
|
+
`tint`, `expanded`, `order`, …), the same object file_meta_store()
|
|
582
|
+
returns, so the folder tree, the editor tabs and the codec layer keep
|
|
583
|
+
reading `root.file_meta_collection.file_meta` unchanged. It is no_save:
|
|
584
|
+
the entries live in ~/.melty/file_meta.pkl, not the root save. A root
|
|
585
|
+
loaded from an older save still carries them — on_load migrates those
|
|
586
|
+
into the store (the store wins where it already has the path) and puts
|
|
587
|
+
the proxy back."""
|
|
588
|
+
|
|
589
|
+
def __init__(self):
|
|
590
|
+
super().__init__()
|
|
591
|
+
self.name = "File Metadata"
|
|
592
|
+
self.file_meta = file_meta_store()
|
|
593
|
+
|
|
594
|
+
def on_load(self, vis=None, root=None):
|
|
595
|
+
store = file_meta_store()
|
|
596
|
+
held = self.__dict__.get("file_meta")
|
|
597
|
+
if held is store:
|
|
598
|
+
return
|
|
599
|
+
if isinstance(held, dict) and held:
|
|
600
|
+
migrated = 0
|
|
601
|
+
for path, entry in held.items():
|
|
602
|
+
if not isinstance(entry, dict) or not entry:
|
|
603
|
+
continue
|
|
604
|
+
stored = {k: v for k, v in dict.items(entry)
|
|
605
|
+
if not (isinstance(k, str) and k.startswith("__"))}
|
|
606
|
+
if not stored or path in store:
|
|
607
|
+
continue
|
|
608
|
+
store[str(path)] = FileMeta(stored)
|
|
609
|
+
migrated += 1
|
|
610
|
+
if migrated:
|
|
611
|
+
print(f"[file_meta] migrated {migrated} entries from the root save "
|
|
612
|
+
f"into {store.path}")
|
|
613
|
+
self.file_meta = store
|