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,468 @@
1
+ """File import graph — which files import which files.
2
+
3
+ Built from the meltygui syntax scanner (`melty_scan.scan` → `iter_imports`),
4
+ never from `ast`: the same tokenize-based front end the meltygui syntax trees
5
+ use, so the graph reads exactly the statements the trees do. Every `.py`
6
+ under the root is scanned once (`build_import_graph`), each import resolved
7
+ to a FILE under the same root:
8
+
9
+ import a.b.c → root/a/b/c.py | root/a/b/c/__init__.py
10
+ from a.b import x, y → the module a.b, PLUS a/b/x.py | a/b/x/__init__.py
11
+ when the imported name is itself a submodule
12
+ from ..m import x → `level` dots up from the importing file's folder
13
+
14
+ Absolute imports resolve against the tree root and its ANCESTORS (the
15
+ repo keeps `src/…` on sys.path from the repo root, so a tree rooted at
16
+ `src/` still reads `from src.lsd… import …`), like sys.path; a target is
17
+ kept only when it lies inside the tree root. Anything that doesn't land on
18
+ a project file (stdlib, site-packages, a name that is a symbol rather than
19
+ a module) is dropped. The result is an
20
+ `ImportGraph` with both directions. Building runs on a background thread
21
+ (`start_build`), the file tree polls `ImportGraphBuild.result`.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import math
27
+ import os
28
+ import pickle
29
+ import threading
30
+ import time
31
+ from pathlib import Path
32
+
33
+
34
+ from meltygui.text_index import _SKIP_DIRS
35
+ from meltygui.core.windowing.glfw_utils import request_render
36
+ from meltygui.code.melty_scan import Import
37
+ from meltygui.code.melty_scan import ImportFrom
38
+ from meltygui.code.melty_scan import ScanError
39
+ from meltygui.code.melty_scan import alias
40
+ from meltygui.code.melty_scan import scan_imports
41
+ from meltygui.code.melty_scan import _k
42
+
43
+
44
+ class ImportGraph:
45
+ """`imports[file]` = files it imports, `imported_by[file]` = files that
46
+ import it (both `set[Path]`, keys = resolved absolute paths).
47
+ `errors` = files the scanner refused (path → message)."""
48
+
49
+ def __init__(self, root):
50
+ self.root = Path(root).resolve()
51
+ self.imports: dict[Path, set[Path]] = {}
52
+ self.imported_by: dict[Path, set[Path]] = {}
53
+ self.errors: dict[Path, str] = {}
54
+ self.built_at = 0.0
55
+ self.seconds = 0.0
56
+ self.layout: dict[Path, tuple[float, float]] = {} # node → (x, y) in [-1, 1]
57
+ self.columns = 0 # layered layout depth (layout_graph)
58
+ self.layers: list[list[Path]] = [] # column → nodes in vertical order
59
+
60
+ def add(self, importer, imported):
61
+ if importer == imported:
62
+ return
63
+ self.imports.setdefault(importer, set()).add(imported)
64
+ self.imported_by.setdefault(imported, set()).add(importer)
65
+
66
+ @property
67
+ def file_count(self):
68
+ return len(self.imports)
69
+
70
+ @property
71
+ def edge_count(self):
72
+ return sum(len(v) for v in self.imports.values())
73
+
74
+ @property
75
+ def max_importers(self):
76
+ """The largest importer count of any file (0 for an empty graph) —
77
+ the normalizer for usage-driven visuals."""
78
+ if not self.imported_by:
79
+ return 0
80
+ return max(len(v) for v in self.imported_by.values())
81
+
82
+ def usage(self, path):
83
+ """Importer count of `path` on a 0..1 log scale against the graph's
84
+ most-used file — a few importers already lift a file noticeably,
85
+ the long tail of heavily used ones spreads over the top half."""
86
+ top = self.max_importers
87
+ if top <= 0:
88
+ return 0.0
89
+ n = len(self.importers_of(path))
90
+ return math.log1p(n) / math.log1p(top) if n else 0.0
91
+
92
+ def imports_of(self, path):
93
+ return self.imports.get(Path(path), set())
94
+
95
+ def importers_of(self, path):
96
+ return self.imported_by.get(Path(path), set())
97
+
98
+
99
+ def project_python_files(root):
100
+ """Every `.py` under `root`, skipping the trigram index's skip set, venvs,
101
+ dot-directories and __pycache__ — the same pruning the studio's other
102
+ project walks use."""
103
+ root = Path(root)
104
+ out = []
105
+ stack = [root]
106
+ while stack:
107
+ folder = stack.pop()
108
+ try:
109
+ entries = sorted(folder.iterdir(), key=lambda p: p.name)
110
+ except OSError:
111
+ continue
112
+ for p in entries:
113
+ name = p.name
114
+ if p.is_dir():
115
+ if name in _SKIP_DIRS or name.startswith(".") or "venv" in name:
116
+ continue
117
+ stack.append(p)
118
+ elif p.suffix == ".py":
119
+ out.append(p)
120
+ return out
121
+
122
+
123
+ # How many ancestors of the tree root absolute imports may resolve against
124
+ # (root first, then its parent, ...) - the repo root sits two above `src/`.
125
+ IMPORT_ROOT_DEPTH = 3
126
+
127
+
128
+ def import_roots(root):
129
+ root = Path(root).resolve()
130
+ roots = [root]
131
+ for _ in range(IMPORT_ROOT_DEPTH - 1):
132
+ if roots[-1].parent == roots[-1]:
133
+ break
134
+ roots.append(roots[-1].parent)
135
+ return roots
136
+
137
+
138
+ def _module_file_under(base, parts):
139
+ """The file a dotted module `parts` names under `base`, or None."""
140
+ if not parts:
141
+ return None
142
+ target = base.joinpath(*parts)
143
+ candidate = target.with_suffix(".py")
144
+ if candidate.is_file():
145
+ return candidate
146
+ candidate = target / "__init__.py"
147
+ if candidate.is_file():
148
+ return candidate
149
+ return None
150
+
151
+
152
+ def _module_file(root, parts, roots=None, memo=None):
153
+ """`parts` resolved against the import roots in order (first hit wins,
154
+ like sys.path); only a file inside the tree `root` counts. `memo`
155
+ (one dict per build) caches the answer per (search bases, parts): the
156
+ same module is imported from dozens of files, and every probe is a
157
+ stat — a warm build was 290 ms of stats without it."""
158
+ root = Path(root)
159
+ search = tuple(roots or import_roots(root))
160
+ key = (search, tuple(parts))
161
+ if memo is not None and key in memo:
162
+ return memo[key]
163
+ found = None
164
+ for base in search:
165
+ f = _module_file_under(base, parts)
166
+ if f is not None:
167
+ found = f if (f == root or root in f.parents) else None
168
+ break
169
+ if memo is not None:
170
+ memo[key] = found
171
+ return found
172
+
173
+
174
+ def resolve_import(root, importer, node, roots=None, memo=None):
175
+ """The project files one Import / ImportFrom node of `importer` refers
176
+ to (a list; empty when nothing under `root` matches)."""
177
+ root = Path(root).resolve()
178
+ roots = roots or import_roots(root)
179
+ found = []
180
+ if _k(node) == "Import":
181
+ for a in node.names:
182
+ f = _module_file(root, a.name.split("."), roots, memo)
183
+ if f is not None:
184
+ found.append(f)
185
+ return found
186
+ # ImportFrom: the base package is the importer's folder `level` dots up
187
+ # (level 1 is its own folder) for a relative import, the root otherwise.
188
+ if node.level:
189
+ # Relative: anchored on the importer's folder, no root search.
190
+ base = importer.parent
191
+ for _ in range(node.level - 1):
192
+ base = base.parent
193
+ module_parts = node.module.split(".") if node.module else []
194
+ search = [base]
195
+ else:
196
+ module_parts = node.module.split(".")
197
+ search = roots
198
+ module_file = _module_file(root, module_parts, search, memo) if module_parts else None
199
+ if module_file is not None:
200
+ found.append(module_file)
201
+ for a in node.names:
202
+ if a.name == "*":
203
+ continue
204
+ f = _module_file(root, module_parts + [a.name], search, memo)
205
+ if f is not None:
206
+ found.append(f)
207
+ return found
208
+
209
+
210
+ # ── per-file import cache: (mtime, size) + the file's import records ─────────
211
+ # Invalidation is content-free (stat only - never a digest); a hit costs one
212
+ # stat, a miss one tokenize pass (scan_imports). Persisted in ~/.lsd so the
213
+ # first build of a session is as fast as a rebuild. Records are plain tuples:
214
+ # ("Import", None, 0, ((name, asname), ...)) / ("ImportFrom", module, level, names)
215
+
216
+ CACHE_VERSION = 1
217
+
218
+
219
+ def cache_path():
220
+ return Path.home() / ".lsd" / "import_graph_cache.pkl"
221
+
222
+
223
+ def load_cache(path=None):
224
+ path = path or cache_path()
225
+ try:
226
+ with open(path, "rb") as f:
227
+ data = pickle.load(f)
228
+ if isinstance(data, dict) and data.get("version") == CACHE_VERSION:
229
+ return data["files"]
230
+ except (OSError, pickle.PickleError, EOFError, KeyError, TypeError, ValueError):
231
+ pass
232
+ return {}
233
+
234
+
235
+ def save_cache(files, path=None):
236
+ path = path or cache_path()
237
+ try:
238
+ path.parent.mkdir(parents=True, exist_ok=True)
239
+ tmp = path.with_suffix(".tmp")
240
+ with open(tmp, "wb") as f:
241
+ pickle.dump({"version": CACHE_VERSION, "files": files}, f, protocol=pickle.HIGHEST_PROTOCOL)
242
+ os.replace(tmp, path)
243
+ except OSError:
244
+ pass
245
+
246
+
247
+ def _records(nodes):
248
+ return tuple((_k(n), getattr(n, "module", None), getattr(n, "level", 0),
249
+ tuple((a.name, a.asname) for a in n.names)) for n in nodes)
250
+
251
+
252
+ def _nodes(records):
253
+ out = []
254
+ for kind, module, level, names in records:
255
+ aliases = [alias(name, asname) for name, asname in names]
256
+ out.append(Import(aliases) if kind == "Import" else ImportFrom(module, aliases, level))
257
+ return out
258
+
259
+
260
+ def build_import_graph(root, files=None, read_text=None, cache=None, persist=True):
261
+ """Scan every project file (or `files`) and resolve its imports. Pure:
262
+ no Melty state, safe on a background thread. `read_text(path)` lets the
263
+ caller serve pending (unsaved) text (a file so served bypasses the
264
+ cache); `cache` = a files dict (load_cache) — None loads the persisted
265
+ one; `persist` writes it back when anything was rescanned."""
266
+ root = Path(root).resolve()
267
+ started = time.monotonic()
268
+ graph = ImportGraph(root)
269
+ roots = import_roots(root)
270
+ if files is None:
271
+ files = project_python_files(root)
272
+ if cache is None:
273
+ cache = load_cache() if persist else {}
274
+ dirty = False
275
+ memo = {}
276
+ for path in files:
277
+ path = Path(path)
278
+ graph.imports.setdefault(path, set())
279
+ key = str(path)
280
+ try:
281
+ if read_text is None:
282
+ stat = path.stat()
283
+ stamp = (stat.st_mtime_ns, stat.st_size)
284
+ hit = cache.get(key)
285
+ if hit is not None and hit[0] == stamp:
286
+ records = hit[1]
287
+ else:
288
+ # A scan failure is cached too (with its message), else an
289
+ # unparsable file would be rescanned on every build.
290
+ try:
291
+ records = _records(scan_imports(path.read_text(encoding="utf-8", errors="replace")))
292
+ except ScanError as e:
293
+ records = str(e)
294
+ cache[key] = (stamp, records)
295
+ dirty = True
296
+ else:
297
+ records = _records(scan_imports(read_text(path)))
298
+ except ScanError as e:
299
+ graph.errors[path] = str(e)
300
+ continue
301
+ except OSError as e:
302
+ graph.errors[path] = str(e)
303
+ continue
304
+ if isinstance(records, str):
305
+ graph.errors[path] = records
306
+ continue
307
+ for node in _nodes(records):
308
+ for target in resolve_import(root, path, node, roots, memo):
309
+ graph.add(path, target)
310
+ if dirty and persist:
311
+ save_cache(cache)
312
+ graph.seconds = time.monotonic() - started
313
+ graph.built_at = time.time()
314
+ return graph
315
+
316
+
317
+ # ── layout: layered, left to right ───────────────────────────────────────────
318
+
319
+ def layout_graph(graph, sweeps=8, seed=0):
320
+ """Stamp `graph.layout` — one (x, y) in [-1, 1] per file — as a LAYERED
321
+ left-to-right drawing (the Sugiyama idea, kept simple):
322
+
323
+ column 0 = the roots: files nothing imports (isolated files too)
324
+ column c = one past the deepest column among the file's importers,
325
+ so `toggles.py` — imported from everywhere — lands at the
326
+ far right and every import line runs left → right.
327
+
328
+ Cycles (the repo has them) are broken by usage rank — an edge from a
329
+ more-used file to a less-used one is ignored for the column assignment
330
+ only — so a hub can never be pulled left by a cycle. Within a column, nodes are ordered
331
+ by `sweeps` barycenter passes over their neighbours' rows (down then up),
332
+ which keeps lines short and mostly uncrossed; rows share one pitch, so a
333
+ tall column fills the height and a short one sits centred. Deterministic."""
334
+ nodes = sorted(set(graph.imports) | set(graph.imported_by))
335
+ n = len(nodes)
336
+ if n == 0:
337
+ graph.layout = {}
338
+ return graph.layout
339
+ index = {p: i for i, p in enumerate(nodes)}
340
+ # edges importer → imported (order grows along an import)
341
+ out = [[] for _ in range(n)]
342
+ for a, targets in graph.imports.items():
343
+ for b in targets:
344
+ if a in index and b in index and a != b:
345
+ out[index[a]].append(index[b])
346
+ for lst in out:
347
+ lst.sort()
348
+
349
+ # ── cycle breaking: rank files by USAGE (importer count, ties → the file
350
+ # with more imports first) and keep, for the column assignment only,
351
+ # the edges that run from a less-used file to a more-used one. That is a
352
+ # DAG by construction, and it decides the cycles the way the picture
353
+ # should read: the hubs everything imports can only ever move RIGHT.
354
+ rank = sorted(range(n), key=lambda v: (len(graph.importers_of(nodes[v])),
355
+ -len(graph.imports_of(nodes[v])), nodes[v].name))
356
+ position = {v: i for i, v in enumerate(rank)}
357
+ dag_out = [[w for w in out[v] if position[w] > position[v]] for v in range(n)]
358
+ indeg = [0] * n
359
+ for v in range(n):
360
+ for w in dag_out[v]:
361
+ indeg[w] += 1
362
+
363
+ # ── columns: longest path from the roots (Kahn order) ──
364
+ column = [0] * n
365
+ ready = [v for v in range(n) if indeg[v] == 0]
366
+ order = []
367
+ while ready:
368
+ v = ready.pop(0)
369
+ order.append(v)
370
+ for w in dag_out[v]:
371
+ column[w] = max(column[w], column[v] + 1)
372
+ indeg[w] -= 1
373
+ if indeg[w] == 0:
374
+ ready.append(w)
375
+ columns = max(column) + 1
376
+ layers = [[] for _ in range(columns)]
377
+ for v in range(n):
378
+ layers[column[v]].append(v)
379
+
380
+ # ── rows: barycenter sweeps over ALL neighbours (both edge directions) ──
381
+ neighbours = [set() for _ in range(n)]
382
+ for v in range(n):
383
+ for w in out[v]:
384
+ neighbours[v].add(w)
385
+ neighbours[w].add(v)
386
+ row = [0.0] * n
387
+ for layer in layers:
388
+ for i, v in enumerate(layer):
389
+ row[v] = float(i)
390
+
391
+ def sweep(layer_indices):
392
+ for c in layer_indices:
393
+ layer = layers[c]
394
+ keyed = []
395
+ for v in layer:
396
+ adjacent = [row[w] for w in neighbours[v] if column[w] != c]
397
+ bary = sum(adjacent) / len(adjacent) if adjacent else row[v]
398
+ keyed.append((bary, nodes[v].name, v))
399
+ keyed.sort()
400
+ layers[c] = [v for _b, _name, v in keyed]
401
+ for i, v in enumerate(layers[c]):
402
+ row[v] = float(i)
403
+
404
+ for _ in range(sweeps):
405
+ sweep(range(1, columns))
406
+ sweep(range(columns - 2, -1, -1))
407
+
408
+ # ── normalize: x by column, y by row with one shared pitch ──
409
+ tallest = max(len(layer) for layer in layers)
410
+ pitch = 2.0 / max(1, tallest)
411
+ graph.layout = {}
412
+ for c, layer in enumerate(layers):
413
+ x = -1.0 + 2.0 * c / max(1, columns - 1) if columns > 1 else 0.0
414
+ offset = (len(layer) - 1) * 0.5
415
+ for i, v in enumerate(layer):
416
+ graph.layout[nodes[v]] = (float(x), float((i - offset) * pitch))
417
+ graph.columns = columns
418
+ # The column/row ORDER itself, for views that draw draw in pixels
419
+ # (import_graph_view lays labelled boxes out from it, rather of the
420
+ # normalized coordinates above).
421
+ graph.layers = [[nodes[v] for v in layer] for layer in layers]
422
+ return graph.layout
423
+
424
+
425
+ # ── the shared current graph: built by either window, read by both ─────────────
426
+ # Hotswap-guarded module state (the file re-execs on hotswap; the graph
427
+ # survives). Data, not UI state - each window's own injected state keeps
428
+ # its selection / camera.
429
+ _CURRENT = globals().get("_CURRENT", {"graph": None})
430
+
431
+
432
+ def current():
433
+ """The last graph built in this session (any window), or None."""
434
+ return _CURRENT["graph"]
435
+
436
+
437
+ def set_current(graph):
438
+ _CURRENT["graph"] = graph
439
+
440
+
441
+ class ImportGraphBuild:
442
+ """One background build: `running` while the thread works, then
443
+ `result` (an ImportGraph) or `error`."""
444
+
445
+ def __init__(self, root):
446
+ self.root = Path(root)
447
+ self.running = True
448
+ self.result = None
449
+ self.error = None
450
+ self._thread = threading.Thread(target=self._run, name="import-graph",
451
+ daemon=True)
452
+ self._thread.start()
453
+
454
+ def _run(self):
455
+ try:
456
+ graph = build_import_graph(self.root)
457
+ layout_graph(graph)
458
+ set_current(graph)
459
+ self.result = graph
460
+ except Exception as e: # surfaced on the UI's status line
461
+ self.error = f"{type(e).__name__}: {e}"
462
+ finally:
463
+ self.running = False
464
+ request_render()
465
+
466
+
467
+ def start_build(root):
468
+ return ImportGraphBuild(root)
@@ -0,0 +1,15 @@
1
+ """Layout model functions and supporting definitions."""
2
+
3
+
4
+
5
+ class Columns(dict):
6
+ """Marker dict: values render side by side (draw_columns is its default
7
+ renderer), so column layouts nest by data:
8
+ Columns({"a": ..., "b": Columns({...})})."""
9
+
10
+
11
+ class Rows(dict):
12
+ """Marker dict: values render stacked top to bottom (draw_rows is its
13
+ default renderer) with draggable edges between them — the row-axis twin
14
+ of Columns, and the two nest freely by data:
15
+ Rows({"top": Columns({...}), "bottom": ...})."""