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,289 @@
1
+ """File-tree I/O and metadata adaptation for mutable dictionary values.
2
+
3
+ These operations do not own windows, pollers, or render state. The core host
4
+ coordinates when disk snapshots and edits pass through them.
5
+ """
6
+ import os
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+ from meltygui.core.conversion.bubbling import install_bubbling
11
+
12
+ def _scan(folder):
13
+ """Disk → the held shape: {name: Path} for files, {name: {…}} for dirs."""
14
+ out = {}
15
+ try:
16
+ children = sorted(folder.iterdir())
17
+ except OSError:
18
+ return out
19
+ for p in children:
20
+ if p.name.startswith(".") or p.name == "__pycache__":
21
+ continue
22
+ out[p.name] = _scan(p) if p.is_dir() else p
23
+ return out
24
+
25
+
26
+
27
+ def _create(path, value, pending):
28
+ """A key the user ADDED → put it on disk. A held dict → mkdir (its children
29
+ create themselves from the same plan); a held str → a file with those
30
+ contents; a held Path that still exists elsewhere → a MOVE (a dragged or
31
+ renamed key is the old Path re-appearing under a new name). A STALE held
32
+ Path (its file already gone) pairs by basename to a pending delete — the
33
+ two halves of a move whose delete side was planned first (e.g. undoing a
34
+ cross-folder drag re-inserts the OLD Path while the file sits at the NEW
35
+ one) — and moves that file instead of writing an empty husk."""
36
+ try:
37
+ if isinstance(value, dict):
38
+ path.mkdir(exist_ok=True)
39
+ return
40
+ if isinstance(value, Path):
41
+ if value.exists() and value != path:
42
+ value.replace(path) # rename() refuses an existing target on Windows
43
+ pending.discard(value)
44
+ return
45
+ if not path.exists():
46
+ # value == path covers undo: the OLD Path re-inserted at its
47
+ # old home while the file sits at the move's target.
48
+ twin = next((p for p in sorted(pending)
49
+ if p.name == value.name and p.is_file()), None)
50
+ if twin is not None:
51
+ twin.replace(path)
52
+ pending.discard(twin)
53
+ return
54
+ if not path.exists():
55
+ path.write_text(value if isinstance(value, str) else "")
56
+ except OSError:
57
+ pass
58
+
59
+
60
+
61
+ def _delete(path):
62
+ """A key the user DELETED → remove from disk (rmtree for a folder).
63
+ Toggles.FileSafety.block_file_delete gates ALL disk deletes (read live);
64
+ the poller re-discovers the surviving file and restores its key."""
65
+ from meltygui.core.runtime.toggles import Toggles
66
+ if Toggles.FileSafety.block_file_delete:
67
+ print(f"[folder_files] delete blocked (Toggles.FileSafety.block_file_delete): {path}")
68
+ return
69
+ try:
70
+ shutil.rmtree(path) if path.is_dir() else path.unlink(missing_ok=True)
71
+ except OSError:
72
+ pass
73
+
74
+
75
+
76
+ def _reconcile(store, disk, folder, seen):
77
+ """Mirror `store` (the held tree) ⇄ `disk` (the poller's snapshot of
78
+ `folder`), in place, in two phases. Phase 1 (_collect) walks the whole
79
+ tree gathering creates and deletes, stamping every planned effect straight
80
+ into `disk` so the snapshot never lags our own writes — the poller only
81
+ ever reports EXTERNAL changes. Phase 2 executes EVERY create before ANY
82
+ delete, tree-wide: a file dragged between folders is renamed (a MOVE)
83
+ before the old key's delete fires no matter which folder the walk visits
84
+ first. (The old per-folder ordering destroyed the file's content whenever
85
+ the source folder reconciled before the target.) A delete consumed by a
86
+ move pairing is skipped; the rest run last, so a moved folder's old
87
+ skeleton is removed only after its children have been renamed out."""
88
+ creates, deletes = [], []
89
+ _collect(store, disk, folder, seen, creates, deletes)
90
+ pending = set(deletes)
91
+ for path, value in creates:
92
+ _create(path, value, pending)
93
+ for path in deletes:
94
+ if path in pending:
95
+ _delete(path)
96
+
97
+
98
+
99
+ def _collect(store, disk, folder, seen, creates, deletes):
100
+ """Phase 1 of _reconcile: the recursive walk. A path the store has never
101
+ SEEN is disk's to give (new on disk → hold it); a path it HAS seen is the
102
+ user's to take away (key deleted → plan a delete, key added → plan a
103
+ create). Store/disk/seen bookkeeping happens here — only the disk side
104
+ effects are deferred to the plan."""
105
+ for name, value in list(store.items()):
106
+ if name == "__overrides__": # view metadata, not a file
107
+ continue
108
+ if name not in disk and (folder / name) not in seen: # user added
109
+ creates.append((folder / name, value))
110
+ disk[name] = {} if isinstance(value, dict) else folder / name
111
+ if not isinstance(value, dict):
112
+ store[name] = disk[name]
113
+ for name in sorted(set(disk) | set(store)):
114
+ if name == "__overrides__":
115
+ continue
116
+ path = folder / name
117
+ if name not in store:
118
+ if path in seen: # user deleted
119
+ deletes.append(path)
120
+ disk.pop(name)
121
+ seen.discard(path)
122
+ continue
123
+ store[name] = {} if isinstance(disk[name], dict) else disk[name] # new on disk
124
+ elif name not in disk: # vanished from disk
125
+ store.pop(name)
126
+ seen.discard(path)
127
+ continue
128
+ seen.add(path)
129
+ if isinstance(store[name], dict):
130
+ sub = disk[name] if isinstance(disk[name], dict) else {}
131
+ _collect(store[name], sub, path, seen, creates, deletes)
132
+
133
+
134
+
135
+ def _file_meta(root=None):
136
+ """The shared path→params store (file_meta.file_meta_store()) — what
137
+ AppModel.file_meta_collection.file_meta is too. `root` is accepted for
138
+ the on_load callers and ignored: the store exists before any model."""
139
+ from meltygui.models.file_meta import file_meta_store
140
+ return file_meta_store()
141
+
142
+
143
+
144
+ def _apply_meta(tree, folder, meta):
145
+ """Meta → tree, recursively: stamp __overrides__ entries and sort keys by
146
+ stored `order`. All writes are inbound state, not user edits — dunder-key
147
+ stores are raw (no dirty mark) and reorders use raw dict ops — so applying
148
+ never dirties the host or triggers a save. Returns True if anything
149
+ changed (caller invalidates the subtree so cached rows repaint)."""
150
+ changed = False
151
+ names = [n for n in tree if n != "__overrides__"]
152
+ desired, orders = {}, {}
153
+ for n in names:
154
+ entry = meta.get(str(folder / n))
155
+ if not isinstance(entry, dict):
156
+ continue
157
+ params = {k: v for k, v in entry.items()
158
+ if k not in ("order", "project", "environment") and not (isinstance(k, str) and k.startswith("__"))
159
+ # unpainted (alpha-0) tint: no override, the row keeps its own
160
+ and not (k == "tint" and isinstance(v, (tuple, list))
161
+ and len(v) >= 4 and not v[3])}
162
+ if params:
163
+ desired[f"__{n}__"] = params
164
+ if isinstance(entry.get("order"), (int, float)):
165
+ orders[n] = entry["order"]
166
+ current = tree.get("__overrides__")
167
+ if desired:
168
+ if current != desired:
169
+ # Wrap entries in the host's bubbling for the raw dunder store,
170
+ # or later UI edits to an existing entry would show but not save
171
+ # (see _LazyOverrideEntry's docstring).
172
+ broot = getattr(tree, "_bubble_root", None)
173
+ if broot is not None:
174
+ desired = install_bubbling(desired, broot)
175
+ tree["__overrides__"] = desired
176
+ changed = True
177
+ elif isinstance(current, dict) and current:
178
+ dict.pop(tree, "__overrides__", None)
179
+ changed = True
180
+ if orders:
181
+ want = sorted(names, key=lambda n: (orders.get(n, float("inf")), n))
182
+ if names != want:
183
+ for n in want:
184
+ dict.__setitem__(tree, n, dict.pop(tree, n))
185
+ changed = True
186
+ for n in names:
187
+ child = tree.get(n)
188
+ if isinstance(child, dict):
189
+ changed |= _apply_meta(child, folder / n, meta)
190
+ return changed
191
+
192
+
193
+
194
+ def _collect_meta(tree, folder, meta):
195
+ """Tree → meta, recursively: read each child's __overrides__ params back
196
+ into the persisted store, and capture drag reordering as `order` stamps.
197
+ Order is stamped only once a folder's key order diverges from the natural
198
+ sorted order (or was stamped before) — an untouched folder saves nothing."""
199
+ names = [n for n in tree if n != "__overrides__"]
200
+ ovs = tree.get("__overrides__")
201
+ ovs = ovs if isinstance(ovs, dict) else {}
202
+ stamp_order = (names != sorted(names)
203
+ or any(isinstance(meta.get(str(folder / n)), dict)
204
+ and "order" in meta[str(folder / n)] for n in names))
205
+ for i, n in enumerate(names):
206
+ path = str(folder / n)
207
+ entry_src = ovs.get(f"__{n}__")
208
+ entry = {k: v for k, v in entry_src.items()
209
+ if not (isinstance(k, str) and k.startswith("__"))} \
210
+ if isinstance(entry_src, dict) else {}
211
+ old = meta.get(path)
212
+ if stamp_order:
213
+ entry["order"] = i
214
+ elif isinstance(old, dict) and "order" in old:
215
+ entry["order"] = old["order"]
216
+ if entry:
217
+ if old != entry:
218
+ from meltygui.models.file_meta import FileMeta
219
+ meta[path] = FileMeta(entry)
220
+ elif old is not None:
221
+ meta.pop(path, None)
222
+ child = tree.get(n)
223
+ if isinstance(child, dict):
224
+ _collect_meta(child, folder / n, meta)
225
+
226
+
227
+ def initialize_file_metadata(vis, root):
228
+ skip_suffixes = {".pyc"}
229
+ from meltygui.models.file_meta import FileMeta
230
+ from meltygui.core.runtime.toggles import Toggles
231
+ meta = _file_meta(root)
232
+ if meta is None:
233
+ return
234
+ # Upgrade entries deserialized as plain dicts (older saves / from_dict)
235
+ # to FileMeta, keeping their stored values.
236
+ # Retroactive (09-02): stored tints that were never a user's pick - the
237
+ # old bluish-grey class default, the tab tint's fallback colour (which got
238
+ # written back onto 184 files), or an alpha-0 default — are dropped, so
239
+ # those files read as unpainted (FileMeta.tint, black background).
240
+ unpainted = {tuple(round(c, 3) for c in FileMeta._LEGACY_DEFAULT_TINT[:3]),
241
+ tuple(round(c, 3) for c in Toggles.CodeEditor.tab_tint_fallback[:3])}
242
+ for key, entry in list(meta.items()):
243
+ if isinstance(entry, dict) and not isinstance(entry, FileMeta):
244
+ meta[key] = FileMeta(entry)
245
+ stored = dict.get(meta[key], "tint") if isinstance(meta[key], dict) else None
246
+ if stored is None:
247
+ continue
248
+ if (FileMeta.painted_tint({"tint": stored}) is None
249
+ or tuple(round(c, 3) for c in stored[:3]) in unpainted):
250
+ dict.pop(meta[key], "tint", None)
251
+ meta.touch(key) # raw dict op: tell the shared store
252
+ from meltygui.core.runtime.paths import PACKAGE_ROOT
253
+ module_root = PACKAGE_ROOT # .../src
254
+ for p in module_root.rglob("*"):
255
+ rel = p.relative_to(module_root).parts
256
+ if any(part == "__pycache__" or part.startswith(".") for part in rel):
257
+ continue
258
+ if p.suffix in skip_suffixes:
259
+ continue
260
+ meta.setdefault(str(p), FileMeta())
261
+
262
+
263
+
264
+ def list_directory(directory, show_hidden=False):
265
+ """The rows of `directory`: [(Path, is_dir)] — folders first, then files,
266
+ each case-insensitive by name. An unreadable directory lists empty."""
267
+ try:
268
+ entries = list(os.scandir(directory))
269
+ except OSError:
270
+ return []
271
+ folders, files = [], []
272
+ for entry in entries:
273
+ if not show_hidden and entry.name.startswith("."):
274
+ continue
275
+ try:
276
+ is_dir = entry.is_dir()
277
+ except OSError:
278
+ is_dir = False
279
+ (folders if is_dir else files).append(entry.name)
280
+ key = str.casefold
281
+ return ([(Path(directory) / n, True) for n in sorted(folders, key=key)]
282
+ + [(Path(directory) / n, False) for n in sorted(files, key=key)])
283
+
284
+
285
+ def _dir_mtime_ns(directory):
286
+ try:
287
+ return os.stat(directory).st_mtime_ns
288
+ except OSError:
289
+ return -1
@@ -0,0 +1,390 @@
1
+ import torch
2
+
3
+
4
+ import os
5
+ import pwd
6
+ import subprocess
7
+ import getpass
8
+
9
+
10
+ def format_name(self, input_str):
11
+ """
12
+ Convert various string formats to properly capitalized space-separated string.
13
+ Examples:
14
+ test_string -> Test String
15
+ testString -> Test String
16
+ TestString -> Test String
17
+ test-string -> Test String
18
+ """
19
+ # First replace any hyphens with underscores for consistent handling
20
+ input_str = input_str.replace('-', '_')
21
+
22
+ # Split on underscores if they exist
23
+ if '_' in input_str:
24
+ words = input_str.split('_')
25
+ else:
26
+ # Handle camelCase by adding space before capital letters
27
+ words = []
28
+ current_word = input_str[0]
29
+ for char in input_str[1:]:
30
+ if char.isupper():
31
+ words.append(current_word)
32
+ current_word = char
33
+ else:
34
+ current_word += char
35
+ words.append(current_word)
36
+
37
+ # Capitalize each word and join with spaces
38
+ return ' '.join(word.capitalize() for word in words)
39
+
40
+ def create_models_folder(password_callback=None, subfolder="latent-descent"):
41
+ """
42
+ Creates a folder at /models and a subfolder inside it, both owned by the current user in Linux.
43
+
44
+ Args:
45
+ password_callback (callable, optional): A function that returns the sudo password.
46
+ If None, will use getpass to prompt the user.
47
+ subfolder (str, optional): Name of the subfolder to create inside /models.
48
+ Defaults to "sub".
49
+
50
+ Returns:
51
+ bool: True if successful, False otherwise
52
+ """
53
+ # Check if both directories already exist
54
+ if os.path.exists('/models') and os.path.exists(f'/models/{subfolder}'):
55
+ return True
56
+
57
+ try:
58
+ # Get current username
59
+ current_user = pwd.getpwuid(os.getuid()).pw_name
60
+
61
+ # Get password using the callback or default to getpass
62
+ if password_callback is None:
63
+ password = getpass.getpass("Enter sudo password: ")
64
+ else:
65
+ password = password_callback()
66
+
67
+ # Create the commands to run
68
+ commands = [
69
+ ["mkdir", "-p", f"/models/{subfolder}"], # -p will create parent dir if needed
70
+ ["chown", f"{current_user}:{current_user}", "/models"],
71
+ ["chown", f"{current_user}:{current_user}", f"/models/{subfolder}"],
72
+ ["chmod", "755", "/models"],
73
+ ["chmod", "755", f"/models/{subfolder}"]
74
+ ]
75
+
76
+ # Run each command with sudo using the provided password
77
+ for cmd in commands:
78
+ process = subprocess.Popen(
79
+ ["sudo", "-S"] + cmd,
80
+ stdin=subprocess.PIPE,
81
+ stdout=subprocess.PIPE,
82
+ stderr=subprocess.PIPE,
83
+ universal_newlines=True
84
+ )
85
+ stdout, stderr = process.communicate(input=password + "\n")
86
+
87
+ if process.returncode != 0:
88
+ print(f"Command failed: sudo {' '.join(cmd)}")
89
+ print(f"Error: {stderr}")
90
+ return False
91
+
92
+ print(f"Successfully created /models directory and /{subfolder} subdirectory owned by {current_user}")
93
+ return True
94
+
95
+ except Exception as e:
96
+ print(f"Unexpected error: {e}")
97
+ return False
98
+
99
+ def print_ascii_tensor(tensors, border=True, indices=None, spacing=2, names=None):
100
+ """
101
+ Prints an ASCII representation of one or more PyTorch tensors horizontally.
102
+ - Zeros are replaced with '#' symbols
103
+ - Single-digit numbers are shown as is
104
+ - Multi-digit positive numbers are shown as '+'
105
+ - Multi-digit negative numbers are shown as '-'
106
+
107
+ For tensors with more than 2 dimensions, this function will use the last two dimensions
108
+ by default, or you can specify which indices to use for higher dimensions.
109
+
110
+ Args:
111
+ tensors (torch.Tensor or list): A PyTorch tensor or list of tensors
112
+ border (bool): Whether to add an ASCII border around each tensor (default: True)
113
+ indices (tuple or None): Specific indices to use for dimensions beyond the last two.
114
+ For a 4D tensor, this would be a tuple of 2 indices.
115
+ spacing (int): Number of spaces between tensors (default: 2)
116
+ names (list or None): Optional list of names for each tensor. If provided, must match
117
+ the number of tensors. Names will be displayed above each tensor.
118
+
119
+ Example:
120
+ >>> x = torch.tensor([[1, 0, 0], [1, 1, 0], [1, 1, 1]])
121
+ >>> y = torch.tensor([[0, 0, 2], [0, 3, 2], [4, 3, 2]])
122
+ >>> print_ascii_tensor([x, y], names=["Identity", "Values"])
123
+ Identity Values
124
+ +-----+ +-----+
125
+ | 1 # # | | # # 2 |
126
+ | 1 1 # | | # 3 2 |
127
+ | 1 1 1 | | 4 3 2 |
128
+ +-----+ +-----+
129
+ """
130
+ # Convert single tensor to list for uniform processing
131
+ if isinstance(tensors, torch.Tensor):
132
+ tensors = [tensors]
133
+
134
+ # Validate names if provided
135
+ if names is not None:
136
+ if len(names) != len(tensors):
137
+ raise ValueError(f"Number of names ({len(names)}) doesn't match number of tensors ({len(tensors)})")
138
+
139
+ # Process each tensor into a list of string rows
140
+ all_tensor_rows = []
141
+ max_heights = []
142
+ tensor_widths = []
143
+
144
+ for tensor_idx, tensor in enumerate(tensors):
145
+ # Handle tensors with more than 2 dimensions
146
+ tensor_dim = tensor.dim()
147
+ if tensor_dim > 2:
148
+ # For tensors with more than 2 dimensions, extract the 2D slice to display
149
+ if indices is None:
150
+ # Default: use first indices for all but the last two dimensions
151
+ slice_indices = tuple([0] * (tensor_dim - 2))
152
+ else:
153
+ # Use provided indices
154
+ if len(indices) != tensor_dim - 2:
155
+ raise ValueError(f"Expected {tensor_dim - 2} indices but got {len(indices)}")
156
+ slice_indices = indices
157
+
158
+ # Extract the 2D slice from the tensor
159
+ tensor_slice = tensor
160
+ for idx in slice_indices:
161
+ tensor_slice = tensor_slice[idx]
162
+
163
+ tensor = tensor_slice
164
+
165
+ # Ensure the tensor is 2D at this point
166
+ if tensor.dim() != 2:
167
+ raise ValueError("Each tensor must have at least 2 dimensions")
168
+
169
+ # Convert tensor to CPU and get its values as a numpy array
170
+ tensor_np = tensor.cpu().numpy()
171
+
172
+ # Convert tensor to string representation
173
+ rows = []
174
+ max_width = 0
175
+
176
+ for row in tensor_np:
177
+ row_str = ""
178
+ for val in row:
179
+ if val == 0:
180
+ row_str += "0 "
181
+ else:
182
+ # Convert to integer if it's a whole number
183
+ if float(val).is_integer():
184
+ val = int(val)
185
+
186
+ # Display single-digit numbers as is, use symbols for multi-digit numbers
187
+ if -9 <= val <= 9:
188
+ row_str += f"{val} "
189
+ elif val > 0:
190
+ row_str += "+ "
191
+ else: # val < 0
192
+ row_str += "- "
193
+
194
+ rows.append(row_str.rstrip()) # Remove trailing space
195
+ max_width = max(max_width, len(row_str.rstrip()))
196
+
197
+ # Add border if needed
198
+ tensor_rows = []
199
+ if border:
200
+ # Create top border
201
+ border_line = "+" + "-" * (max_width + 2) + "+"
202
+ tensor_rows.append(border_line)
203
+
204
+ # Add each row with side borders
205
+ for row in rows:
206
+ # Calculate padding to ensure the right border aligns perfectly
207
+ padding = max_width - len(row)
208
+ tensor_rows.append(f"| {row}{' ' * padding} |")
209
+
210
+ # Create bottom border
211
+ tensor_rows.append(border_line)
212
+ else:
213
+ # Use rows without border
214
+ tensor_rows = rows
215
+
216
+ all_tensor_rows.append(tensor_rows)
217
+ max_heights.append(len(tensor_rows))
218
+
219
+ # Record width of this tensor's rows
220
+ if len(tensor_rows) > 0:
221
+ tensor_widths.append(len(tensor_rows[0]))
222
+ else:
223
+ tensor_widths.append(0)
224
+
225
+ # Print tensor names if provided
226
+ if names is not None:
227
+ name_line = ""
228
+ for tensor_idx, name in enumerate(names):
229
+ # Center name over tensor width
230
+ tensor_width = tensor_widths[tensor_idx]
231
+
232
+ # If name is longer than tensor, truncate or allow overflow
233
+ if len(name) > tensor_width:
234
+ # Let's allow overflow for readability
235
+ centered_name = name
236
+ else:
237
+ # Center the name
238
+ padding = (tensor_width - len(name)) // 2
239
+ centered_name = " " * padding + name
240
+
241
+ name_line += centered_name
242
+
243
+ # Add spacing between tensors (except after the last one)
244
+ if tensor_idx < len(tensors) - 1:
245
+ name_line += " " * spacing
246
+
247
+ print(name_line)
248
+
249
+ # Find the maximum height across all tensors
250
+ max_height = max(max_heights)
251
+
252
+ # Print all tensors horizontally
253
+ for row_idx in range(max_height):
254
+ row_str = ""
255
+ for tensor_idx, tensor_rows in enumerate(all_tensor_rows):
256
+ # If this tensor has fewer rows than the max height, print empty space
257
+ if row_idx < len(tensor_rows):
258
+ row_str += tensor_rows[row_idx]
259
+ else:
260
+ # Add empty space for the width of this tensor's representation
261
+ if len(tensor_rows) > 0: # Make sure tensor has at least one row
262
+ row_str += " " * len(tensor_rows[0])
263
+
264
+ # Add spacing between tensors (except after the last one)
265
+ if tensor_idx < len(all_tensor_rows) - 1:
266
+ row_str += " " * spacing
267
+
268
+ print(row_str)
269
+ print("\n")
270
+
271
+ def split_string_by_token(tokenizer, text):
272
+ if tokenizer is None:
273
+ return text
274
+
275
+ encoding = tokenizer(text, return_tensors="pt", return_attention_mask=True,
276
+ add_special_tokens=False)
277
+ input_ids = encoding['input_ids']
278
+ # iterate over input ids
279
+ sequence_length = input_ids.shape[1]
280
+ batch_size = input_ids.shape[0]
281
+ batches = []
282
+ for b in range(batch_size):
283
+ decoded_tokens = []
284
+ for s in range(sequence_length):
285
+ token = input_ids[b, s]
286
+ token_text = tokenizer.decode(token)
287
+ decoded_tokens.append(token_text)
288
+ batches.append(decoded_tokens)
289
+ return batches
290
+
291
+
292
+ def format_time(seconds):
293
+ """
294
+ Convert seconds to a human-readable time string with associated color.
295
+
296
+ Args:
297
+ seconds (float): Time in seconds
298
+
299
+ Returns:
300
+ tuple: (formatted_time_string, color_tuple)
301
+ where color_tuple is (r, g, b) values from 0-1
302
+ """
303
+ # Handle negative time
304
+ if seconds is None:
305
+ return "Unknown time", (1.0, 0.0, 0.0)
306
+
307
+ if seconds < 0:
308
+ time_str, color = format_time(-seconds)
309
+ return f"-{time_str}", color
310
+
311
+ # Very small time periods - golden yellow (1.0, 0.84, 0)
312
+ golden_yellow = (1.0, 0.84, 0.0)
313
+ if seconds < 1:
314
+ return f"0 seconds", golden_yellow
315
+
316
+ # Seconds - golden yellow (1.0, 0.84, 0)
317
+ if seconds < 60:
318
+ return f"{seconds:.0f} seconds", golden_yellow
319
+
320
+ # Minutes - warm red (0.86, 0.24, 0.2)
321
+ warm_red = (0.96, 0.44, 0.4)
322
+ minutes = seconds / 60
323
+ if minutes < 60:
324
+ return f"{minutes:.1f} minutes", warm_red
325
+
326
+ # Hours - greenish (0.29, 0.71, 0.31)
327
+ greenish = (0.29, 0.71, 0.31)
328
+ hours = minutes / 60
329
+ if hours < 24:
330
+ return f"{hours:.1f} hours", greenish
331
+
332
+ # Days - greenish (0.29, 0.71, 0.31)
333
+ days = hours / 24
334
+ if days < 7:
335
+ return f"{days:.1f} days", greenish
336
+
337
+ # Weeks - greenish (0.29, 0.71, 0.31)
338
+ weeks = days / 7
339
+ if weeks < 4.35: # Approximate weeks in a month
340
+ return f"{weeks:.1f} weeks", greenish
341
+
342
+ # Months - greenish (0.29, 0.71, 0.31)
343
+ months = days / 30.44 # Average days in a month
344
+ if months < 12:
345
+ return f"{months:.1f} months", greenish
346
+
347
+ # Years - yellow orange (0.94, 0.59, 0.2)
348
+ yellow_orange = (0.94, 0.59, 0.2)
349
+ years = days / 365.25
350
+ if years < 10:
351
+ return f"{years:.1f} years", yellow_orange
352
+
353
+ # Decades - dark bluish grey (0.27, 0.35, 0.43)
354
+ dark_bluish_grey = (0.27, 0.35, 0.43)
355
+ if years < 100:
356
+ return f"{years:.1f} years", dark_bluish_grey
357
+
358
+ if years < 1_000:
359
+ return f"{years:.1f} years", dark_bluish_grey
360
+
361
+ if years < 1_000_000:
362
+ return f"{years / 1_000:.1f} thousand years", dark_bluish_grey
363
+
364
+ # Millions of years - dark red (0.55, 0.12, 0.12)
365
+ dark_red = (0.55, 0.12, 0.12)
366
+ if years < 1_000_000_000:
367
+ return f"{years / 1_000_000:.1f} million years", dark_red
368
+
369
+ # Billions of years and beyond - blue (0.12, 0.31, 0.71)
370
+ blue = (0.12, 0.31, 0.71)
371
+
372
+ # Use descriptive strings instead of numerical values for billion+ years
373
+ if years < 4.5e9: # Age of Earth ~4.5 billion years
374
+ return "Age of the Earth", blue
375
+
376
+ if years < 5e9: # Approximate time until Sun begins to expand significantly
377
+ return "Time until the Sun begins expanding", blue
378
+
379
+ if years < 7.6e9: # Time until Sun engulfs Earth's orbit
380
+ return "Time until the Sun engulfs Earth", blue
381
+
382
+ if years < 1e14: # Time until all stars burn out
383
+ return "Era of stellar extinction", blue
384
+
385
+ if years < 1e40: # Deep time
386
+ return "Approaching heat death of the universe", blue
387
+
388
+ return "Beyond heat death of the universe", blue
389
+
390
+