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.
Files changed (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. 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
+ }
@@ -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>