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,439 @@
|
|
|
1
|
+
"""draw_fast_file_explorer — a shortcuts column beside a flat directory
|
|
2
|
+
listing, immediate mode.
|
|
3
|
+
|
|
4
|
+
changed, picked = draw_fast_file_explorer(current_dir, name="files")
|
|
5
|
+
if changed:
|
|
6
|
+
current_dir = picked if Path(picked).is_dir() else current_dir # a file: open it
|
|
7
|
+
|
|
8
|
+
`input_value` is the directory shown. The view returns ``(True, path)`` the
|
|
9
|
+
frame the user NAVIGATES — a double-click on a folder, Ctrl+Up to the
|
|
10
|
+
parent, a crumb of the path strip, a shortcut, Enter on a selected row — or
|
|
11
|
+
double-clicks a FILE (the caller opens it however it likes); the caller
|
|
12
|
+
writes a directory back and passes it in next frame. Nothing of the listing
|
|
13
|
+
is kept beyond a scan memoized on the directory's mtime, refreshed by a
|
|
14
|
+
FileWatch emitter on the directory (one at a time, retired on navigation).
|
|
15
|
+
|
|
16
|
+
Two views: `draw_file_listing` is the path strip + rows (the fast_dock /
|
|
17
|
+
file_tree model: names straight to the window draw list, no per-row widgets
|
|
18
|
+
or draw_states, events as parameters, the blit cache while idle, its own
|
|
19
|
+
scroll); `draw_fast_file_explorer` puts it in a ColumnLayout (columns.py,
|
|
20
|
+
shared draggable edge, persisted) next to the shortcuts — the XDG user
|
|
21
|
+
directories, home and the root. Each row wears the file's OWN tint from the
|
|
22
|
+
shared file-meta store (`FileMeta.painted_tint`, what the studio's tabs and
|
|
23
|
+
trees paint) on its name and, stronger, on its icon — no row background;
|
|
24
|
+
the selection and hover washes use the tint through the editor tab's colour recipe —
|
|
25
|
+
and its icon from the meta entry, else the codec's, else the folder / file
|
|
26
|
+
glyph. Every row — the shortcuts too — leads with its tint control: a
|
|
27
|
+
painted row shows its `draw_tuple_fast` chip (click = the colour-picker
|
|
28
|
+
popover, the studio's tab-bar chip), an unpainted one a faint paint-brush
|
|
29
|
+
button (shown only on the selected row / the current shortcut) whose click
|
|
30
|
+
stamps `default_tint` in and opens the picker. The store is only
|
|
31
|
+
WRITTEN for a row the user paints (a meta entry per browsed file would
|
|
32
|
+
bloat ~/.melty/file_meta.pkl); clearing the colour in the picker drops the
|
|
33
|
+
tint again, and the entry with it when nothing else is attached.
|
|
34
|
+
|
|
35
|
+
Rows drag to reorder (the code editor's tab bar model, `DragDrop.on_drag`
|
|
36
|
+
per row + one `on_drop`): a landed drop stamps every row of the directory
|
|
37
|
+
with an `order` in the meta store — the studio's folder-tree convention
|
|
38
|
+
(folder_files._collect_meta), so the two agree on a folder's order — and
|
|
39
|
+
the listing sorts by those stamps (unstamped rows keep the natural
|
|
40
|
+
folders-first order after the stamped ones). A folder that is itself
|
|
41
|
+
painted washes the whole listing in its tint (`folder_bg_boost`), the rows
|
|
42
|
+
on top of it. `context_menu={label: callable}` is the wrapper's right-click
|
|
43
|
+
menu (the hdr-viewer's), except that the explorer's callables receive ONE
|
|
44
|
+
argument: the path of the row under the right-click (a right-press
|
|
45
|
+
selects it), or the directory when the click landed on no row.
|
|
46
|
+
|
|
47
|
+
Type to search (`type_to_search`, on by default): while nothing else owns
|
|
48
|
+
the keyboard — no text editor, find box, menu or popover — the listing holds
|
|
49
|
+
meltygui's text-focus slot (the menu bar's trick), so every keystroke reaches it
|
|
50
|
+
without the pointer having to hover it, and typing searches the directory
|
|
51
|
+
shown. The keys come from the GLFW callback queue (Melty.frame_key_events, the
|
|
52
|
+
editor's source: nothing is dropped on a slow frame). Each keystroke re-ranks
|
|
53
|
+
the rows against the query — a name prefix, then a word start, a substring,
|
|
54
|
+
finally the letters in order — selects the best, scrolls it into view (centred
|
|
55
|
+
when it was out of sight) and flashes it with Melty.emphasize; every other
|
|
56
|
+
match shows the matched letters highlighted and the rest of the listing dims
|
|
57
|
+
so the matches stand out. A pill at the bottom right shows the query and
|
|
58
|
+
"n of m". Up / Down (and Tab / Shift+Tab) step through the matches, Enter
|
|
59
|
+
opens the selected one, Backspace edits, Ctrl+Backspace clears, Ctrl+V pastes,
|
|
60
|
+
Esc clears the search (a second Esc the selection). A navigation clears it.
|
|
61
|
+
"""
|
|
62
|
+
import os
|
|
63
|
+
import re
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
|
|
66
|
+
import meltygui.core.windowing.window_api as glfw
|
|
67
|
+
import meltygui_imgui as imgui
|
|
68
|
+
|
|
69
|
+
from meltygui.hdr_color import pack_color
|
|
70
|
+
from meltygui.core.melty import Melty
|
|
71
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
72
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
73
|
+
from meltygui.code.new_codecs import extension_to_codec
|
|
74
|
+
from meltygui.core.cache.tile_cache import add_shadow
|
|
75
|
+
from meltygui.core.cache.tile_cache import clear_glows
|
|
76
|
+
from meltygui.core.layout.header_runtime import _brightness_clamp_fn
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def row_icon(path, is_dir, entry, folder_icon, file_icon):
|
|
80
|
+
"""The glyph before a row's name: the file-meta icon, else the codec
|
|
81
|
+
registered for the extension (no content sniff — that reads file heads,
|
|
82
|
+
one per row), else the folder / file glyph."""
|
|
83
|
+
icon = entry.get("icon") if isinstance(entry, dict) else None
|
|
84
|
+
if icon:
|
|
85
|
+
return icon
|
|
86
|
+
if is_dir:
|
|
87
|
+
return folder_icon
|
|
88
|
+
codec = extension_to_codec.get(path.suffix.lower())
|
|
89
|
+
codec_icon = getattr(codec, "icon", None) if codec is not None else None
|
|
90
|
+
return codec_icon or file_icon
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def row_tint_bg():
|
|
94
|
+
"""A memoized `(tint, boost) -> packed row background` through the
|
|
95
|
+
editor tab's colour recipe (style-manager mix under
|
|
96
|
+
Toggles.CodeEditor.tab_active_bg_* + the brightness clamp), so a bright
|
|
97
|
+
tint still leaves the name readable. `boost` adds to the mix value AND
|
|
98
|
+
the clamp ceiling: the selected row is the same tint, brighter."""
|
|
99
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
100
|
+
bg_theme_factor = 0.1
|
|
101
|
+
style_manager = Melty.style_manager
|
|
102
|
+
brightness_clamp = _brightness_clamp_fn()
|
|
103
|
+
bg_memo = {}
|
|
104
|
+
|
|
105
|
+
rgb_memo = {}
|
|
106
|
+
|
|
107
|
+
def row_rgb(tint, boost=0.0):
|
|
108
|
+
"""The row background as an (r, g, b) tuple."""
|
|
109
|
+
tint = tuple(tint[:3])
|
|
110
|
+
memo_key = (tint, boost)
|
|
111
|
+
rgb = rgb_memo.get(memo_key)
|
|
112
|
+
if rgb is None:
|
|
113
|
+
mixed = style_manager.make_color_rgb(
|
|
114
|
+
tint[0], tint[1], tint[2],
|
|
115
|
+
value=Toggles.CodeEditor.tab_active_bg_brightness + boost,
|
|
116
|
+
factor=bg_theme_factor,
|
|
117
|
+
saturation_scale=Toggles.CodeEditor.tab_active_bg_saturation,
|
|
118
|
+
alpha=1.0)
|
|
119
|
+
mixed = brightness_clamp(mixed[0], mixed[1], mixed[2], 0.0,
|
|
120
|
+
Toggles.CodeEditor.tab_active_bg_max_brightness + boost)
|
|
121
|
+
rgb = rgb_memo[memo_key] = (mixed[0], mixed[1], mixed[2])
|
|
122
|
+
return rgb
|
|
123
|
+
|
|
124
|
+
def row_bg(tint, boost=0.0):
|
|
125
|
+
memo_key = (tuple(tint[:3]), boost)
|
|
126
|
+
bg = bg_memo.get(memo_key)
|
|
127
|
+
if bg is None:
|
|
128
|
+
bg = bg_memo[memo_key] = pack_color(*row_rgb(tint, boost), 1.0)
|
|
129
|
+
return bg
|
|
130
|
+
row_bg.rgb = row_rgb
|
|
131
|
+
return row_bg
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
_TEXT_TINT_MEMO = globals().get("_TEXT_TINT_MEMO", {})
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def tinted_text(base, tint, mix=0.3):
|
|
138
|
+
"""The row's text colour: `base` (r, g, b, a) pulled `mix` of the way
|
|
139
|
+
toward `tint`'s hue at the base's brightness, so the name reads in the
|
|
140
|
+
row's colour without losing contrast. Memoized (packed) per pair."""
|
|
141
|
+
key = (base, tuple(tint[:3]), mix)
|
|
142
|
+
col = _TEXT_TINT_MEMO.get(key)
|
|
143
|
+
if col is None:
|
|
144
|
+
r, g, b = float(tint[0]), float(tint[1]), float(tint[2])
|
|
145
|
+
# Lift the tint to the base's brightness before mixing, so a dark
|
|
146
|
+
# tint colours the name rather than dimming it.
|
|
147
|
+
base_lum = max(base[0], base[1], base[2])
|
|
148
|
+
tint_lum = max(r, g, b, 1e-6)
|
|
149
|
+
scale = base_lum / tint_lum
|
|
150
|
+
r, g, b = min(1.0, r * scale), min(1.0, g * scale), min(1.0, b * scale)
|
|
151
|
+
col = _TEXT_TINT_MEMO[key] = pack_color(
|
|
152
|
+
base[0] + (r - base[0]) * mix, base[1] + (g - base[1]) * mix,
|
|
153
|
+
base[2] + (b - base[2]) * mix, base[3])
|
|
154
|
+
return col
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def chip_swatch(tint, bg_rgb, mix=0.55):
|
|
158
|
+
"""The tint chip's painted colour: `tint` pulled `mix` of the way toward
|
|
159
|
+
the row background it sits on, so the chip reads as a subtle marker
|
|
160
|
+
rather than a saturated block (the picker still edits the real tint)."""
|
|
161
|
+
r, g, b = float(tint[0]), float(tint[1]), float(tint[2])
|
|
162
|
+
return (r + (bg_rgb[0] - r) * mix, g + (bg_rgb[1] - g) * mix, b + (bg_rgb[2] - b) * mix,
|
|
163
|
+
float(tint[3]) if len(tint) == 4 else 1.0)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def tint_control(draw_state, key, tint, x, y, size, text_y, hovered, default_tint,
|
|
167
|
+
swatch=None, show_brush=True, setter=None, brush_color=None):
|
|
168
|
+
"""One row's tint control at (x, y): the `draw_tuple_fast` chip when
|
|
169
|
+
`tint` is painted, else the paint-brush button. `key` is the store
|
|
170
|
+
path; `hovered` says the pointer is on the row (the brush brightens
|
|
171
|
+
under it). One view_id for chip and brush, so the brush's click can
|
|
172
|
+
hand the popover to the chip that replaces it next frame;
|
|
173
|
+
priority_delta=4 outranks the row's own left_mouse_down param
|
|
174
|
+
(registered at 3). `swatch` is the colour the chip paints (see
|
|
175
|
+
`chip_swatch`); None paints the tint itself. `show_brush` False draws
|
|
176
|
+
(and registers) no brush for an unpainted row — the listing shows it
|
|
177
|
+
only on the selected row. ``setter(value)`` writes the tint somewhere
|
|
178
|
+
other than the file-meta store (the chat window's conversations); None
|
|
179
|
+
clears. `brush_color` optionally supplies the exact icon RGB (the chat
|
|
180
|
+
sidebar matches its expand arrow). Returns True when the store was written."""
|
|
181
|
+
if setter is None:
|
|
182
|
+
raise TypeError("tint_control requires a supplied value setter")
|
|
183
|
+
write = setter
|
|
184
|
+
# [tint=(0.55, 0.72, 0.95)]
|
|
185
|
+
brush_icon = f"\uf1fc"
|
|
186
|
+
brush_col = pack_color(*brush_color, 1.0) if brush_color is not None else pack_color(1.0, 1.0, 1.0, 0.22)
|
|
187
|
+
brush_hover_col = brush_col if brush_color is not None else pack_color(1.0, 1.0, 1.0, 0.9)
|
|
188
|
+
from meltygui.view.collection_view import draw_tuple_fast
|
|
189
|
+
|
|
190
|
+
view_id = f"tint_{key}"
|
|
191
|
+
if tint:
|
|
192
|
+
cursor = imgui.get_cursor_screen_pos()
|
|
193
|
+
changed, new_tint = draw_tuple_fast(
|
|
194
|
+
tint, draw_state, view_id=view_id, x=x, y=y, size=size, priority_delta=4,
|
|
195
|
+
setter=write, swatch=swatch)
|
|
196
|
+
imgui.set_cursor_screen_pos(cursor)
|
|
197
|
+
if changed:
|
|
198
|
+
write(new_tint if isinstance(new_tint, tuple) else None)
|
|
199
|
+
if not isinstance(new_tint, tuple):
|
|
200
|
+
# The picker's delete button: the chip that opened the popover
|
|
201
|
+
# is a brush next frame and never runs draw_tuple_fast
|
|
202
|
+
# again, so let go of the popover here or it stays open.
|
|
203
|
+
if Melty.popover_focused_ds is not None:
|
|
204
|
+
Melty.popover_focused_ds = None
|
|
205
|
+
draw_state._tint_edit_key = None
|
|
206
|
+
draw_state.invalidate()
|
|
207
|
+
request_render()
|
|
208
|
+
return changed
|
|
209
|
+
if not show_brush:
|
|
210
|
+
return False
|
|
211
|
+
mouse_x, mouse_y = imgui.get_mouse_pos()
|
|
212
|
+
brush_hovered = hovered and x <= mouse_x < x + size and y <= mouse_y < y + size
|
|
213
|
+
brush_w = imgui.calc_text_size(brush_icon).x
|
|
214
|
+
imgui.get_window_draw_list().add_text(x + (size - brush_w) * 0.5, text_y,
|
|
215
|
+
brush_hover_col if brush_hovered else brush_col,
|
|
216
|
+
brush_icon)
|
|
217
|
+
if draw_state.on_action("left_mouse_down", view_id=view_id, rect=(x, y, x + size, y + size),
|
|
218
|
+
priority_delta=4) is None:
|
|
219
|
+
return False
|
|
220
|
+
# Stamp the brush in (the first write as a browsed file) and open the
|
|
221
|
+
# picker to the chip that appears next frame by the same slot handoff
|
|
222
|
+
# draw_tuple_fast's own click does - the opening click is graced, and
|
|
223
|
+
# the chip, seeing itself owner with the slot on the host, draws the
|
|
224
|
+
# picker and moves the slot to the pop window.
|
|
225
|
+
write(default_tint)
|
|
226
|
+
Melty.popover_focused_ds = draw_state
|
|
227
|
+
draw_state._tint_edit_key = view_id
|
|
228
|
+
Melty._popover_open_frame = Melty.frame_count
|
|
229
|
+
draw_state.invalidate()
|
|
230
|
+
request_render()
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
_XDG_LINE = re.compile(r'^\s*XDG_(\w+)_DIR\s*=\s*"?(.*?)"?\s*$')
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def shortcut_directories(home=None):
|
|
238
|
+
"""The shortcuts column: home, the XDG user directories that exist
|
|
239
|
+
(`~/.config/user-dirs.dirs`, the conventional names when the file is
|
|
240
|
+
missing) and the filesystem root. [(label, Path)], home first."""
|
|
241
|
+
home = Path(home) if home is not None else Path.home()
|
|
242
|
+
names = ["DESKTOP", "DOCUMENTS", "DOWNLOAD", "PICTURES", "MUSIC", "VIDEOS"]
|
|
243
|
+
fallback = {"DESKTOP": "Desktop", "DOCUMENTS": "Documents", "DOWNLOAD": "Downloads",
|
|
244
|
+
"PICTURES": "Pictures", "MUSIC": "Music", "VIDEOS": "Videos"}
|
|
245
|
+
dirs = {name: home / fallback[name] for name in names}
|
|
246
|
+
config = home / ".config" / "user-dirs.dirs"
|
|
247
|
+
try:
|
|
248
|
+
for line in config.read_text().splitlines():
|
|
249
|
+
match = _XDG_LINE.match(line)
|
|
250
|
+
if match and match.group(1) in dirs:
|
|
251
|
+
dirs[match.group(1)] = Path(match.group(2).replace("$HOME", str(home)))
|
|
252
|
+
except OSError:
|
|
253
|
+
pass
|
|
254
|
+
out = [("Home", home)]
|
|
255
|
+
for name in names:
|
|
256
|
+
path = dirs[name]
|
|
257
|
+
if path.is_dir() and path != home:
|
|
258
|
+
out.append((path.name, path))
|
|
259
|
+
out.append(("Computer", Path(os.sep)))
|
|
260
|
+
return out
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ── type-to-search ──────────────────────────────────────────────────────────
|
|
264
|
+
_SEARCH_SEPARATORS = " _-.,()[]{}+&@'\""
|
|
265
|
+
# Keys that keep repeating while held (imgui's synthesized auto-repeat):
|
|
266
|
+
# GLFW's REPEAT events are noisy or absent on Wayland.
|
|
267
|
+
_SEARCH_REPEAT_KEYS = (glfw.KEY_BACKSPACE, glfw.KEY_UP, glfw.KEY_DOWN, glfw.KEY_TAB)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def search_match(name, query):
|
|
271
|
+
"""How `name` matches `query`, case-insensitively: ``(rank, spans)`` —
|
|
272
|
+
rank 0 a prefix of the name, 1 the start of a word inside it (after a
|
|
273
|
+
space, dot, dash, underscore ...), 2 a substring, 3 a subsequence (the
|
|
274
|
+
query's characters in order, anything between) — or None. `spans` are
|
|
275
|
+
the [start, end) character ranges of `name` the query landed on, what
|
|
276
|
+
the listing highlights."""
|
|
277
|
+
if not query:
|
|
278
|
+
return None
|
|
279
|
+
n, q = name.lower(), query.lower()
|
|
280
|
+
at = n.find(q)
|
|
281
|
+
if at == 0:
|
|
282
|
+
return 0, [(0, len(q))]
|
|
283
|
+
if at > 0:
|
|
284
|
+
word_at = at if n[at - 1] in _SEARCH_SEPARATORS else -1
|
|
285
|
+
pos = at
|
|
286
|
+
while word_at < 0:
|
|
287
|
+
pos = n.find(q, pos + 1)
|
|
288
|
+
if pos < 0:
|
|
289
|
+
break
|
|
290
|
+
if n[pos - 1] in _SEARCH_SEPARATORS:
|
|
291
|
+
word_at = pos
|
|
292
|
+
if word_at >= 0:
|
|
293
|
+
return 1, [(word_at, word_at + len(q))]
|
|
294
|
+
return 2, [(at, at + len(q))]
|
|
295
|
+
spans, pos = [], 0
|
|
296
|
+
for ch in q:
|
|
297
|
+
pos = n.find(ch, pos)
|
|
298
|
+
if pos < 0:
|
|
299
|
+
return None
|
|
300
|
+
if spans and spans[-1][1] == pos:
|
|
301
|
+
spans[-1] = (spans[-1][0], pos + 1)
|
|
302
|
+
else:
|
|
303
|
+
spans.append((pos, pos + 1))
|
|
304
|
+
pos += 1
|
|
305
|
+
return 3, spans
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def search_hits(rows, query):
|
|
309
|
+
"""The rows of `rows` ([(Path, is_dir)]) matching `query`:
|
|
310
|
+
``([(row index, rank, spans)] in listing order, position of the best)``
|
|
311
|
+
— the best is the lowest rank, the earliest in the listing among equals
|
|
312
|
+
(folders lead it, so a folder beats a file at the same rank). No match:
|
|
313
|
+
``([], None)``."""
|
|
314
|
+
hits = []
|
|
315
|
+
for i, (path, _is_dir) in enumerate(rows):
|
|
316
|
+
match = search_match(path.name, query)
|
|
317
|
+
if match is not None:
|
|
318
|
+
hits.append((i, match[0], match[1]))
|
|
319
|
+
if not hits:
|
|
320
|
+
return hits, None
|
|
321
|
+
return hits, min(range(len(hits)), key=lambda k: (hits[k][1], k))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def search_keys():
|
|
325
|
+
"""This frame's keystrokes, in typed order, for a view that owns the
|
|
326
|
+
keyboard: the GLFW callback queue (Melty.frame_key_events, every press
|
|
327
|
+
and repeat since the last frame) plus imgui's synthesized auto-repeat
|
|
328
|
+
for the keys that should keep firing while held. Keeps frames coming
|
|
329
|
+
while one of those is down so the repeat cadence is sampled."""
|
|
330
|
+
keys = list(Melty.frame_key_events)
|
|
331
|
+
seen = {k for k, _m in keys}
|
|
332
|
+
io = imgui.get_io()
|
|
333
|
+
mods = ((glfw.MOD_SHIFT if io.key_shift else 0)
|
|
334
|
+
| (glfw.MOD_CONTROL if io.key_ctrl else 0)
|
|
335
|
+
| (glfw.MOD_ALT if getattr(io, "key_alt", False) else 0))
|
|
336
|
+
for key in _SEARCH_REPEAT_KEYS:
|
|
337
|
+
if imgui.is_key_down(key):
|
|
338
|
+
request_render()
|
|
339
|
+
if key not in seen and imgui.is_key_pressed(key, repeat=True):
|
|
340
|
+
keys.append((key, mods))
|
|
341
|
+
return keys
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def claim_keyboard(draw_state):
|
|
345
|
+
"""Take meltygui's text-focus slot for `draw_state` when it is free (or held
|
|
346
|
+
by an earlier draw_state of the same tile — a cache rebuild), the way the
|
|
347
|
+
menu bar does while a menu is open: begin_frame then re-runs this view on
|
|
348
|
+
every key event, hovered or not, and the bare-key global hotkeys (E, the
|
|
349
|
+
invalidate tracker) stay muted. Returns True when the view has the
|
|
350
|
+
keyboard this frame; an open popover (a colour picker, the context menu)
|
|
351
|
+
keeps it off so the two never read the same arrows."""
|
|
352
|
+
holder = Melty.text_focused_ds
|
|
353
|
+
if holder is None or (holder is not draw_state
|
|
354
|
+
and getattr(holder, "_tile_id", None) == draw_state._tile_id):
|
|
355
|
+
Melty.text_focused_ds = draw_state
|
|
356
|
+
Melty._text_focus_grant_frame = Melty.frame_count
|
|
357
|
+
holder = draw_state
|
|
358
|
+
return holder is draw_state and Melty.popover_focused_ds is None
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def search_typed(query, keys):
|
|
362
|
+
"""Apply this frame's `keys` ([(glfw key, mods)]) to the search `query`.
|
|
363
|
+
Returns ``(query, step, activate, parent, escape)``: the edited query,
|
|
364
|
+
the Up / Down / Tab steps (net, + is down), Enter, Ctrl+Up and a bare
|
|
365
|
+
Esc that landed on an EMPTY query (the caller's "clear the selection").
|
|
366
|
+
Alt / Super chords and Ctrl chords other than Backspace (clear) and V
|
|
367
|
+
(paste) are left alone — they are shortcuts, not typing."""
|
|
368
|
+
from meltygui.editor.text_editor import _KEY_CHAR_MAP
|
|
369
|
+
step, activate, parent, escape = 0, False, False, False
|
|
370
|
+
for key, mods in keys:
|
|
371
|
+
if mods & (glfw.MOD_ALT | glfw.MOD_SUPER):
|
|
372
|
+
continue
|
|
373
|
+
ctrl, shift = bool(mods & glfw.MOD_CONTROL), bool(mods & glfw.MOD_SHIFT)
|
|
374
|
+
if key == glfw.KEY_ESCAPE:
|
|
375
|
+
if query:
|
|
376
|
+
query = ""
|
|
377
|
+
else:
|
|
378
|
+
escape = True
|
|
379
|
+
elif key == glfw.KEY_BACKSPACE:
|
|
380
|
+
query = "" if ctrl else query[:-1]
|
|
381
|
+
elif key in (glfw.KEY_ENTER, glfw.KEY_KP_ENTER):
|
|
382
|
+
activate = True
|
|
383
|
+
elif key == glfw.KEY_UP:
|
|
384
|
+
if ctrl:
|
|
385
|
+
parent = True
|
|
386
|
+
else:
|
|
387
|
+
step -= 1
|
|
388
|
+
elif key == glfw.KEY_DOWN:
|
|
389
|
+
if not ctrl:
|
|
390
|
+
step += 1
|
|
391
|
+
elif key == glfw.KEY_TAB:
|
|
392
|
+
if query and not ctrl:
|
|
393
|
+
step += -1 if shift else 1
|
|
394
|
+
elif ctrl:
|
|
395
|
+
if key == glfw.KEY_V:
|
|
396
|
+
lines = (imgui.get_clipboard_text() or "").strip().splitlines()
|
|
397
|
+
query += lines[0].strip() if lines else ""
|
|
398
|
+
else:
|
|
399
|
+
pair = _KEY_CHAR_MAP.get(key)
|
|
400
|
+
if pair is not None:
|
|
401
|
+
query += pair[1] if shift else pair[0]
|
|
402
|
+
return query, step, activate, parent, escape
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# ── the listing ─────────────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _scroll_row_into_view(draw_state, index, row_h, rows_top, centre=False, content_h=None):
|
|
409
|
+
"""Nudge the window's scroll the minimal amount so row `index` (at
|
|
410
|
+
`rows_top` + index × row_h in content coordinates) is fully visible.
|
|
411
|
+
`centre`: a row that is out of sight lands in the middle of the view
|
|
412
|
+
instead of at its edge (a search jump), the scroll clamped to
|
|
413
|
+
`content_h` when given; a row already in view is left alone."""
|
|
414
|
+
view_h = draw_state.abs_clipped_height - draw_state.header_height - draw_state.footer_height
|
|
415
|
+
if view_h <= 0:
|
|
416
|
+
return
|
|
417
|
+
sx, sy = draw_state.scroll_offset
|
|
418
|
+
row_top = rows_top + index * row_h
|
|
419
|
+
row_bottom = row_top + row_h
|
|
420
|
+
new_sy = sy
|
|
421
|
+
if centre and (row_bottom > sy + view_h or row_top < sy):
|
|
422
|
+
new_sy = row_top - (view_h - row_h) * 0.5
|
|
423
|
+
if content_h is not None:
|
|
424
|
+
new_sy = min(new_sy, max(0.0, content_h - view_h))
|
|
425
|
+
else:
|
|
426
|
+
if row_bottom > new_sy + view_h:
|
|
427
|
+
new_sy = row_bottom - view_h
|
|
428
|
+
if row_top < new_sy:
|
|
429
|
+
new_sy = row_top
|
|
430
|
+
new_sy = max(0.0, new_sy)
|
|
431
|
+
if new_sy != sy:
|
|
432
|
+
draw_state.scroll_offset = (sx, new_sy)
|
|
433
|
+
draw_state.invalidate()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
# ── the shortcuts column ────────────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ── the explorer: shortcuts + listing ───────────────────────────────────────
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// LSD Window Geometry
|
|
2
|
+
//
|
|
3
|
+
// A Wayland client can neither position its window nor learn where it is;
|
|
4
|
+
// only the compositor knows. This extension runs inside GNOME Shell and
|
|
5
|
+
// publishes what the studio needs over the session bus:
|
|
6
|
+
//
|
|
7
|
+
// org.latentdescent.WindowGeometry at /org/latentdescent/WindowGeometry
|
|
8
|
+
// GetWindows(pid) -> the frame (xdg window geometry) and buffer (whole
|
|
9
|
+
// surface) rects of every window owned by ``pid``
|
|
10
|
+
// (0 = all windows), in logical screen pixels
|
|
11
|
+
// GetMonitors() -> every monitor's geometry, work area and scale
|
|
12
|
+
// Watch(pid) / Unwatch(pid)
|
|
13
|
+
// signal Geometry(window) -> a watched window moved or resized
|
|
14
|
+
// signal Removed(id, pid) -> a watched window closed
|
|
15
|
+
// signal MonitorsChanged() -> monitors / work areas changed
|
|
16
|
+
// property Version -> protocol version (bump on any change)
|
|
17
|
+
//
|
|
18
|
+
// The studio side lives in src/lsd/gl_gui/installation_helper.py (install /
|
|
19
|
+
// uninstall / status) and reads the feed for its OS-edge physics.
|
|
20
|
+
|
|
21
|
+
import Gio from 'gi://Gio';
|
|
22
|
+
import GLib from 'gi://GLib';
|
|
23
|
+
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
|
24
|
+
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
|
25
|
+
|
|
26
|
+
const BUS_NAME = 'org.latentdescent.WindowGeometry';
|
|
27
|
+
const OBJECT_PATH = '/org/latentdescent/WindowGeometry';
|
|
28
|
+
// Bump whenever the interface or the dict keys change; the studio compares
|
|
29
|
+
// it against the extension files it ships to offer a reinstall.
|
|
30
|
+
const PROTOCOL_VERSION = 1;
|
|
31
|
+
|
|
32
|
+
const IFACE_XML = `
|
|
33
|
+
<node>
|
|
34
|
+
<interface name="org.latentdescent.WindowGeometry">
|
|
35
|
+
<method name="GetWindows">
|
|
36
|
+
<arg type="i" name="pid" direction="in"/>
|
|
37
|
+
<arg type="aa{sv}" name="windows" direction="out"/>
|
|
38
|
+
</method>
|
|
39
|
+
<method name="GetMonitors">
|
|
40
|
+
<arg type="aa{sv}" name="monitors" direction="out"/>
|
|
41
|
+
</method>
|
|
42
|
+
<method name="Watch">
|
|
43
|
+
<arg type="i" name="pid" direction="in"/>
|
|
44
|
+
</method>
|
|
45
|
+
<method name="Unwatch">
|
|
46
|
+
<arg type="i" name="pid" direction="in"/>
|
|
47
|
+
</method>
|
|
48
|
+
<signal name="Geometry">
|
|
49
|
+
<arg type="a{sv}" name="window"/>
|
|
50
|
+
</signal>
|
|
51
|
+
<signal name="Removed">
|
|
52
|
+
<arg type="t" name="id"/>
|
|
53
|
+
<arg type="i" name="pid"/>
|
|
54
|
+
</signal>
|
|
55
|
+
<signal name="MonitorsChanged"/>
|
|
56
|
+
<property name="Version" type="u" access="read"/>
|
|
57
|
+
</interface>
|
|
58
|
+
</node>`;
|
|
59
|
+
|
|
60
|
+
function windowInfo(win) {
|
|
61
|
+
// frame = the xdg window geometry (what the compositor places and
|
|
62
|
+
// constrains — the studio sets it to its content rect); buffer = the
|
|
63
|
+
// whole surface including any transparent shadow margin.
|
|
64
|
+
const frame = win.get_frame_rect();
|
|
65
|
+
const buffer = win.get_buffer_rect();
|
|
66
|
+
const v = (type, value) => new GLib.Variant(type, value);
|
|
67
|
+
return {
|
|
68
|
+
id: v('t', win.get_id()),
|
|
69
|
+
pid: v('i', win.get_pid()),
|
|
70
|
+
wm_class: v('s', win.get_wm_class() ?? ''),
|
|
71
|
+
title: v('s', win.get_title() ?? ''),
|
|
72
|
+
x: v('i', frame.x),
|
|
73
|
+
y: v('i', frame.y),
|
|
74
|
+
width: v('i', frame.width),
|
|
75
|
+
height: v('i', frame.height),
|
|
76
|
+
buffer_x: v('i', buffer.x),
|
|
77
|
+
buffer_y: v('i', buffer.y),
|
|
78
|
+
buffer_width: v('i', buffer.width),
|
|
79
|
+
buffer_height: v('i', buffer.height),
|
|
80
|
+
monitor: v('i', win.get_monitor()),
|
|
81
|
+
maximized: v('b', win.get_maximized() !== 0),
|
|
82
|
+
fullscreen: v('b', win.is_fullscreen()),
|
|
83
|
+
focused: v('b', win.has_focus()),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function monitorInfo(index) {
|
|
88
|
+
const display = global.display;
|
|
89
|
+
const geometry = display.get_monitor_geometry(index);
|
|
90
|
+
const workspace = global.workspace_manager.get_active_workspace();
|
|
91
|
+
const work = workspace.get_work_area_for_monitor(index);
|
|
92
|
+
const v = (type, value) => new GLib.Variant(type, value);
|
|
93
|
+
return {
|
|
94
|
+
index: v('i', index),
|
|
95
|
+
x: v('i', geometry.x),
|
|
96
|
+
y: v('i', geometry.y),
|
|
97
|
+
width: v('i', geometry.width),
|
|
98
|
+
height: v('i', geometry.height),
|
|
99
|
+
work_x: v('i', work.x),
|
|
100
|
+
work_y: v('i', work.y),
|
|
101
|
+
work_width: v('i', work.width),
|
|
102
|
+
work_height: v('i', work.height),
|
|
103
|
+
scale: v('d', display.get_monitor_scale(index)),
|
|
104
|
+
primary: v('b', index === display.get_primary_monitor()),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default class LsdWindowGeometry extends Extension {
|
|
109
|
+
enable() {
|
|
110
|
+
this._watched = new Set(); // pids whose windows emit Geometry
|
|
111
|
+
this._handlers = new Map(); // MetaWindow -> [signal ids]
|
|
112
|
+
|
|
113
|
+
this._dbus = Gio.DBusExportedObject.wrapJSObject(IFACE_XML, this);
|
|
114
|
+
this._dbus.export(Gio.DBus.session, OBJECT_PATH);
|
|
115
|
+
this._nameId = Gio.bus_own_name(
|
|
116
|
+
Gio.BusType.SESSION, BUS_NAME,
|
|
117
|
+
Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT | Gio.BusNameOwnerFlags.REPLACE,
|
|
118
|
+
null, null, null);
|
|
119
|
+
|
|
120
|
+
this._createdId = global.display.connect(
|
|
121
|
+
'window-created', (_display, win) => this._track(win));
|
|
122
|
+
this._workareasId = global.display.connect(
|
|
123
|
+
'workareas-changed', () => this._emitMonitorsChanged());
|
|
124
|
+
this._monitorsId = Main.layoutManager.connect(
|
|
125
|
+
'monitors-changed', () => this._emitMonitorsChanged());
|
|
126
|
+
|
|
127
|
+
// Windows already open when the extension is enabled never emit
|
|
128
|
+
// 'window-created' for us — attach to them directly.
|
|
129
|
+
for (const actor of global.get_window_actors())
|
|
130
|
+
this._track(actor.meta_window);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
disable() {
|
|
134
|
+
if (this._createdId) {
|
|
135
|
+
global.display.disconnect(this._createdId);
|
|
136
|
+
this._createdId = 0;
|
|
137
|
+
}
|
|
138
|
+
if (this._workareasId) {
|
|
139
|
+
global.display.disconnect(this._workareasId);
|
|
140
|
+
this._workareasId = 0;
|
|
141
|
+
}
|
|
142
|
+
if (this._monitorsId) {
|
|
143
|
+
Main.layoutManager.disconnect(this._monitorsId);
|
|
144
|
+
this._monitorsId = 0;
|
|
145
|
+
}
|
|
146
|
+
for (const [win, ids] of this._handlers) {
|
|
147
|
+
for (const id of ids)
|
|
148
|
+
win.disconnect(id);
|
|
149
|
+
}
|
|
150
|
+
this._handlers.clear();
|
|
151
|
+
this._watched.clear();
|
|
152
|
+
if (this._nameId) {
|
|
153
|
+
Gio.bus_unown_name(this._nameId);
|
|
154
|
+
this._nameId = 0;
|
|
155
|
+
}
|
|
156
|
+
if (this._dbus) {
|
|
157
|
+
this._dbus.unexport();
|
|
158
|
+
this._dbus = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---- D-Bus interface -------------------------------------------------
|
|
163
|
+
|
|
164
|
+
get Version() {
|
|
165
|
+
return PROTOCOL_VERSION;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
GetWindows(pid) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const actor of global.get_window_actors()) {
|
|
171
|
+
const win = actor.meta_window;
|
|
172
|
+
if (!win)
|
|
173
|
+
continue;
|
|
174
|
+
if (pid !== 0 && win.get_pid() !== pid)
|
|
175
|
+
continue;
|
|
176
|
+
out.push(windowInfo(win));
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
GetMonitors() {
|
|
182
|
+
const out = [];
|
|
183
|
+
const count = global.display.get_n_monitors();
|
|
184
|
+
for (let index = 0; index < count; index++)
|
|
185
|
+
out.push(monitorInfo(index));
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
Watch(pid) {
|
|
190
|
+
this._watched.add(pid);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
Unwatch(pid) {
|
|
194
|
+
this._watched.delete(pid);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- window tracking -------------------------------------------------
|
|
198
|
+
|
|
199
|
+
_track(win) {
|
|
200
|
+
if (!win || this._handlers.has(win))
|
|
201
|
+
return;
|
|
202
|
+
// Every window type: the studio's own windows are NORMAL, but the
|
|
203
|
+
// feed is generic (a popup of ours could be asked for too).
|
|
204
|
+
const changed = () => this._onChanged(win);
|
|
205
|
+
const ids = [
|
|
206
|
+
win.connect('position-changed', changed),
|
|
207
|
+
win.connect('size-changed', changed),
|
|
208
|
+
win.connect('unmanaged', () => this._untrack(win)),
|
|
209
|
+
];
|
|
210
|
+
this._handlers.set(win, ids);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_untrack(win) {
|
|
214
|
+
const ids = this._handlers.get(win);
|
|
215
|
+
if (ids) {
|
|
216
|
+
for (const id of ids)
|
|
217
|
+
win.disconnect(id);
|
|
218
|
+
this._handlers.delete(win);
|
|
219
|
+
}
|
|
220
|
+
if (this._dbus && this._watched.has(win.get_pid())) {
|
|
221
|
+
this._dbus.emit_signal('Removed',
|
|
222
|
+
new GLib.Variant('(ti)', [win.get_id(), win.get_pid()]));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_onChanged(win) {
|
|
227
|
+
if (!this._dbus || !this._watched.has(win.get_pid()))
|
|
228
|
+
return;
|
|
229
|
+
this._dbus.emit_signal('Geometry',
|
|
230
|
+
new GLib.Variant('(a{sv})', [windowInfo(win)]));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_emitMonitorsChanged() {
|
|
234
|
+
if (this._dbus)
|
|
235
|
+
this._dbus.emit_signal('MonitorsChanged', null);
|
|
236
|
+
}
|
|
237
|
+
}
|
meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="WEB_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
4
|
+
<exclude-output />
|
|
5
|
+
<content url="file://$MODULE_DIR$" />
|
|
6
|
+
<orderEntry type="inheritedJdk" />
|
|
7
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
8
|
+
</component>
|
|
9
|
+
</module>
|