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,281 @@
1
+ """Which window controls the desktop puts in a title bar, and on which side.
2
+
3
+ Every desktop lets the user choose the title-bar buttons (drop the maximize
4
+ button, put the controls on the left, ...), and GTK / Qt apps that draw
5
+ their own header bars follow that setting. meltygui's frameless windows draw
6
+ their own controls too (titlebar.py), so they read the same setting:
7
+
8
+ GNOME, Budgie, Hyprland / sway / ... with a dconf profile:
9
+ gsettings get org.gnome.desktop.wm.preferences button-layout
10
+ -> 'appmenu:minimize,maximize,close' (what GTK's header bars read)
11
+ Cinnamon: org.cinnamon.desktop.wm.preferences button-layout
12
+ MATE: org.mate.Marco.general button-layout
13
+ KDE: ~/.config/kwinrc [org.kde.kdecoration2] ButtonsOnLeft / ButtonsOnRight
14
+ (letters: I minimize, A maximize, X close; the rest are ignored)
15
+ XFCE: ~/.config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml button_layout
16
+ ("O|SHMC": H hide = minimize, M maximize, C close, | splits the sides)
17
+ fallback: the settings portal (org.freedesktop.portal.Settings.Read of
18
+ the GNOME key, what a sandboxed GTK app gets), then DEFAULT.
19
+
20
+ The GNOME syntax is the canonical form here — `parse_gnome` is also what
21
+ Toggles.Melty.titlebar_button_layout takes to pin a layout: "left:right",
22
+ comma-separated `minimize` / `maximize` / `close`; a string without a colon
23
+ is all LEFT buttons (mutter's and GTK's rule); other tokens (appmenu, menu,
24
+ spacer, icon) are dropped.
25
+
26
+ Stdlib only, so app.py's boot can start the probe (`start_probe`) before
27
+ meltygui's heavy imports land: the reads run on a daemon thread (a gsettings
28
+ call is a ~10 ms subprocess), `system_layout()` answers DEFAULT until the
29
+ first probe lands, and a layout change asks for a frame. `refresh()` re-reads
30
+ after a focus gain, `refresh_if_stale()` per frame at
31
+ Toggles.Melty.titlebar_button_refresh_s, so a setting changed while the app
32
+ runs is picked up without a restart.
33
+
34
+ Subprocesses go through posix_spawn (absolute executable, close_fds=False,
35
+ no cwd / preexec_fn) — the studio must never fork (CLAUDE.md).
36
+ """
37
+ from __future__ import annotations
38
+
39
+ import configparser
40
+ import os
41
+ import re
42
+ import shutil
43
+ import subprocess
44
+ import threading
45
+ import time
46
+ from collections import namedtuple
47
+
48
+ ButtonLayout = namedtuple("ButtonLayout", "left right")
49
+ KINDS = ("minimize", "maximize", "close")
50
+ DEFAULT = ButtonLayout((), ("minimize", "maximize", "close"))
51
+
52
+ _GSETTINGS = shutil.which("gsettings") or "/usr/bin/gsettings"
53
+ _GDBUS = shutil.which("gdbus") or "/usr/bin/gdbus"
54
+
55
+ _GNOME_SCHEMA = "org.gnome.desktop.wm.preferences"
56
+ _CINNAMON_SCHEMA = "org.cinnamon.desktop.wm.preferences"
57
+ _MATE_SCHEMA = "org.mate.Marco.general"
58
+
59
+ # Survives hotswap re-exec (module globals are reused): the last layout read,
60
+ # where it came from, when, and the probe thread.
61
+ _state = globals().get("_state") or {"layout": None, "source": None, "probed_at": 0.0, "thread": None}
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Parsers
66
+ # ---------------------------------------------------------------------------
67
+
68
+ def _kinds(tokens, seen):
69
+ out = []
70
+ for token in tokens:
71
+ token = token.strip().lower()
72
+ if token in KINDS and token not in seen:
73
+ seen.add(token)
74
+ out.append(token)
75
+ return tuple(out)
76
+
77
+
78
+ def parse_gnome(text):
79
+ """'appmenu:minimize,maximize,close' → ButtonLayout. No colon = all on
80
+ the left (mutter / GTK); unknown tokens dropped; a kind appears once."""
81
+ text = (text or "").strip().strip("'\"").strip()
82
+ if ":" in text:
83
+ left_text, right_text = text.split(":", 1)
84
+ else:
85
+ left_text, right_text = text, ""
86
+ seen = set()
87
+ left = _kinds(left_text.split(","), seen)
88
+ right = _kinds(right_text.split(","), seen)
89
+ return ButtonLayout(left, right)
90
+
91
+
92
+ _KDE_LETTERS = {"I": "minimize", "A": "maximize", "X": "close"}
93
+ _XFWM_LETTERS = {"H": "minimize", "M": "maximize", "C": "close"}
94
+
95
+
96
+ def _letters(text, table, seen):
97
+ out = []
98
+ for letter in (text or "").strip():
99
+ kind = table.get(letter.upper())
100
+ if kind is not None and kind not in seen:
101
+ seen.add(kind)
102
+ out.append(kind)
103
+ return tuple(out)
104
+
105
+
106
+ def parse_kde(buttons_on_left, buttons_on_right):
107
+ """kwinrc's ButtonsOnLeft / ButtonsOnRight letter strings."""
108
+ seen = set()
109
+ return ButtonLayout(_letters(buttons_on_left, _KDE_LETTERS, seen),
110
+ _letters(buttons_on_right, _KDE_LETTERS, seen))
111
+
112
+
113
+ def parse_xfwm(text):
114
+ """xfwm4's button_layout ("O|SHMC"): the bar splits the sides; without
115
+ one everything is on the left, like GNOME."""
116
+ text = (text or "").strip()
117
+ left_text, right_text = (text.split("|", 1) + [""])[:2] if "|" in text else (text, "")
118
+ seen = set()
119
+ return ButtonLayout(_letters(left_text, _XFWM_LETTERS, seen),
120
+ _letters(right_text, _XFWM_LETTERS, seen))
121
+
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Readers
125
+ # ---------------------------------------------------------------------------
126
+
127
+ def _run(args, timeout=3.0):
128
+ """stdout of a command, or None when it can't run / fails. Absolute
129
+ executable + close_fds=False → posix_spawn, never fork."""
130
+ if not args or not os.path.exists(args[0]):
131
+ return None
132
+ try:
133
+ proc = subprocess.run(args, capture_output=True, text=True, timeout=timeout, close_fds=False)
134
+ except (OSError, subprocess.SubprocessError):
135
+ return None
136
+ if proc.returncode != 0:
137
+ return None
138
+ return proc.stdout
139
+
140
+
141
+ def _config_home(environ):
142
+ return environ.get("XDG_CONFIG_HOME") or os.path.join(os.path.expanduser("~"), ".config")
143
+
144
+
145
+ def _read_gsettings(schema, run=_run):
146
+ out = run([_GSETTINGS, "get", schema, "button-layout"])
147
+ if out is None or not out.strip():
148
+ return None
149
+ return parse_gnome(out), f"gsettings {schema}"
150
+
151
+
152
+ def _read_portal(run=_run):
153
+ """org.freedesktop.portal.Settings.Read — the answer prints as
154
+ `(<<'appmenu:minimize,maximize,close'>>,)`."""
155
+ out = run([_GDBUS, "call", "--session", "--dest", "org.freedesktop.portal.Desktop",
156
+ "--object-path", "/org/freedesktop/portal/desktop",
157
+ "--method", "org.freedesktop.portal.Settings.Read", _GNOME_SCHEMA, "button-layout"])
158
+ if out is None:
159
+ return None
160
+ match = re.search(r"'([^']*)'", out)
161
+ if match is None:
162
+ return None
163
+ return parse_gnome(match.group(1)), "settings portal"
164
+
165
+
166
+ def _read_kde(environ):
167
+ path = os.path.join(_config_home(environ), "kwinrc")
168
+ if not os.path.isfile(path):
169
+ return None
170
+ parser = configparser.ConfigParser(strict=False, interpolation=None, delimiters=("=",))
171
+ parser.optionxform = str
172
+ try:
173
+ parser.read(path, encoding="utf-8")
174
+ except (configparser.Error, OSError, UnicodeDecodeError):
175
+ return None
176
+ section = "org.kde.kdecoration2"
177
+ # KWin's own defaults when the keys are absent: menu + on-all-desktops
178
+ # left, help / minimize / maximize / close right.
179
+ left = parser.get(section, "ButtonsOnLeft", fallback="MS") if parser.has_section(section) else "MS"
180
+ right = parser.get(section, "ButtonsOnRight", fallback="HIAX") if parser.has_section(section) else "HIAX"
181
+ return parse_kde(left, right), "kwinrc"
182
+
183
+
184
+ def _read_xfwm(environ):
185
+ path = os.path.join(_config_home(environ), "xfce4", "xfconf", "xfce-perchannel-xml", "xfwm4.xml")
186
+ if not os.path.isfile(path):
187
+ return None
188
+ try:
189
+ import xml.etree.ElementTree as ElementTree
190
+ root = ElementTree.parse(path).getroot()
191
+ except Exception:
192
+ return None
193
+ for node in root.iter("property"):
194
+ if node.get("name") == "button_layout":
195
+ return parse_xfwm(node.get("value", "")), "xfwm4"
196
+ return parse_xfwm("O|SHMC"), "xfwm4 default"
197
+
198
+
199
+ def detect(environ=None, run=_run):
200
+ """(ButtonLayout, source) for this desktop: the desktop's own store
201
+ first (XDG_CURRENT_DESKTOP), then the GNOME key via gsettings (most
202
+ desktops carry a dconf profile), the settings portal, and DEFAULT."""
203
+ environ = os.environ if environ is None else environ
204
+ desktops = (environ.get("XDG_CURRENT_DESKTOP") or "").lower().split(":")
205
+ probes = []
206
+ if "kde" in desktops:
207
+ probes.append(lambda: _read_kde(environ))
208
+ if "xfce" in desktops:
209
+ probes.append(lambda: _read_xfwm(environ))
210
+ if "x-cinnamon" in desktops or "cinnamon" in desktops:
211
+ probes.append(lambda: _read_gsettings(_CINNAMON_SCHEMA, run))
212
+ if "mate" in desktops:
213
+ probes.append(lambda: _read_gsettings(_MATE_SCHEMA, run))
214
+ probes.append(lambda: _read_gsettings(_GNOME_SCHEMA, run))
215
+ probes.append(lambda: _read_portal(run))
216
+ for probe in probes:
217
+ found = probe()
218
+ if found is not None:
219
+ return found
220
+ return DEFAULT, "default"
221
+
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # A probe thread + the cached answer
225
+ # ---------------------------------------------------------------------------
226
+
227
+ def _probe():
228
+ try:
229
+ layout, source = detect()
230
+ except Exception:
231
+ layout, source = DEFAULT, "error"
232
+ changed = layout != _state["layout"]
233
+ _state.update(layout=layout, source=source, probed_at=time.monotonic())
234
+ if changed:
235
+ try:
236
+ from meltygui.core.windowing.glfw_utils import request_render
237
+ request_render()
238
+ except Exception:
239
+ pass
240
+
241
+
242
+ def start_probe(force=False):
243
+ """Read the layout on a daemon thread (idempotent while one runs; a
244
+ known layout is kept unless ``force``). Returns the thread, or None."""
245
+ thread = _state["thread"]
246
+ if thread is not None and thread.is_alive():
247
+ return thread
248
+ if _state["layout"] is not None and not force:
249
+ return None
250
+ thread = threading.Thread(target=_probe, name="titlebar-buttons", daemon=True)
251
+ thread.start()
252
+ _state["thread"] = thread
253
+ return thread
254
+
255
+
256
+ def system_layout():
257
+ """The desktop's layout, DEFAULT until the first probe lands (kicked
258
+ here if nobody started it)."""
259
+ layout = _state["layout"]
260
+ if layout is None:
261
+ start_probe()
262
+ return DEFAULT
263
+ return layout
264
+
265
+
266
+ def source():
267
+ """Where the current layout came from ("gsettings …", "kwinrc", …)."""
268
+ return _state["source"]
269
+
270
+
271
+ def refresh(min_interval=2.0):
272
+ """Re-read (a focus gain: the user may have changed the setting), at
273
+ most once per ``min_interval`` seconds."""
274
+ if time.monotonic() - _state["probed_at"] >= min_interval:
275
+ start_probe(force=True)
276
+
277
+
278
+ def refresh_if_stale(interval):
279
+ """Per-frame: re-read once the last probe is ``interval`` seconds old."""
280
+ if interval and interval > 0:
281
+ refresh(min_interval=float(interval))