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