meltygui 0.1.2__py3-none-any.whl → 0.1.8__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 (160) hide show
  1. meltygui/__init__.py +32 -51
  2. meltygui/accounts/internet_accounts.py +9 -9
  3. meltygui/chat/chat_interface.py +5 -5
  4. meltygui/chat/images.py +1 -1
  5. meltygui/chat/main.py +13 -7
  6. meltygui/code/chain_converters.py +1 -1
  7. meltygui/code/codec_registry.py +4 -2
  8. meltygui/code/embedded_jedi.py +47 -0
  9. meltygui/code/fileref.py +18 -2
  10. meltygui/code/libcst_conversion.py +60 -25
  11. meltygui/code/new_codecs.py +118 -12
  12. meltygui/code/new_converters.py +20 -8
  13. meltygui/code/project_code.py +35 -4
  14. meltygui/code/source_context.py +170 -8
  15. meltygui/code/symbol_roster.py +418 -58
  16. meltygui/completion/providers/anthropic_oauth.py +1 -1
  17. meltygui/completion/providers/anthropic_requests.py +1 -1
  18. meltygui/completion/providers/claude_usage.py +9 -9
  19. meltygui/completion/providers/codex_accounts.py +1 -1
  20. meltygui/core/automation/orchestration_core.py +4 -4
  21. meltygui/core/automation/query_core.py +2 -2
  22. meltygui/core/cache/deferred_invalidation.py +88 -0
  23. meltygui/core/cache/mask_batch.py +16 -4
  24. meltygui/core/cache/tile_cache.py +770 -607
  25. meltygui/core/conversion/dict_conversion_util.py +5 -4
  26. meltygui/core/conversion/render_host.py +1 -2
  27. meltygui/core/core_render.py +162 -191
  28. meltygui/core/diagnostics/edge_motion_guard.py +16 -10
  29. meltygui/core/diagnostics/fps_counter.py +41 -1
  30. meltygui/core/diagnostics/geometry_jitter.py +4 -1
  31. meltygui/core/diagnostics/resize_trace.py +6 -2
  32. meltygui/core/files/ios_file_observer.py +147 -0
  33. meltygui/core/files/workspace_paths.py +59 -0
  34. meltygui/core/graphics/desktop_gl.py +14 -0
  35. meltygui/core/graphics/gl_state.py +5 -3
  36. meltygui/core/graphics/metal_renderer.py +273 -0
  37. meltygui/core/graphics/metal_tiles.py +376 -0
  38. meltygui/core/graphics/overlay_renderer.py +36 -6
  39. meltygui/core/graphics/scene_target.py +1 -1
  40. meltygui/core/graphics/screenshot.py +3 -2
  41. meltygui/core/graphics/shader_func.py +39 -30
  42. meltygui/core/graphics/text_texture.py +1 -1
  43. meltygui/core/input/input_handler.py +86 -6
  44. meltygui/core/input/ios_input.py +476 -0
  45. meltygui/core/input/pynput_backend.py +5 -2
  46. meltygui/core/input/touch_input.py +232 -0
  47. meltygui/core/layout/column_core.py +48 -33
  48. meltygui/core/layout/tile_links.py +3 -5
  49. meltygui/core/layout/tile_manager_core.py +79 -17
  50. meltygui/core/layout/tile_resize.py +2 -1
  51. meltygui/core/melty.py +309 -77
  52. meltygui/core/module_map.json +2 -1
  53. meltygui/core/rendering/gui_imgui_import.py +40 -0
  54. meltygui/core/rendering/gui_imgui_input.py +188 -0
  55. meltygui/core/rendering/gui_layout_prototype.py +666 -0
  56. meltygui/core/rendering/gui_native_collision.py +156 -0
  57. meltygui/core/rendering/gui_prototype.py +110 -0
  58. meltygui/core/rendering/gui_window_prototype.py +96 -0
  59. meltygui/core/rendering/mode.py +6 -6
  60. meltygui/core/rendering/overlay.py +130 -20
  61. meltygui/core/rendering/render_funcs.py +8 -0
  62. meltygui/core/rendering/retained_gui_prototype.py +593 -0
  63. meltygui/core/rendering/view_identity.py +17 -3
  64. meltygui/core/runtime/app.py +200 -35
  65. meltygui/core/runtime/app_session.py +7 -7
  66. meltygui/core/runtime/app_settings.py +7 -7
  67. meltygui/core/runtime/gc_manager.py +1 -1
  68. meltygui/core/runtime/launch_override.py +6 -3
  69. meltygui/core/runtime/native_app.py +303 -0
  70. meltygui/core/runtime/paths.py +65 -3
  71. meltygui/core/runtime/public_exports.py +79 -0
  72. meltygui/core/runtime/toggles.py +17 -2
  73. meltygui/core/services/terminal_core.py +4 -1
  74. meltygui/core/styling/fonts.py +31 -14
  75. meltygui/core/styling/style_core.py +7 -3
  76. meltygui/core/windowing/backends/imgui_renderer.py +5 -0
  77. meltygui/core/windowing/backends/native_ios.py +197 -0
  78. meltygui/core/windowing/geometry_feed.py +2 -2
  79. meltygui/core/windowing/glfw_utils.py +22 -3
  80. meltygui/core/windowing/melty_windows.py +358 -0
  81. meltygui/core/windowing/os_frame.py +70 -20
  82. meltygui/core/windowing/surface.py +87 -169
  83. meltygui/core/windowing/surface_frame.py +181 -0
  84. meltygui/core/windowing/titlebar.py +227 -62
  85. meltygui/core/windowing/titlebar_buttons.py +179 -4
  86. meltygui/core/windowing/window_api.py +38 -3
  87. meltygui/editor/pending_save.py +100 -15
  88. meltygui/editor/text_editor.py +58 -14
  89. meltygui/examples/retained_gui_demo.py +164 -0
  90. meltygui/examples/retained_layout_demo.py +146 -0
  91. meltygui/examples/rust_gui_demo.py +308 -0
  92. meltygui/files/fast_file_explorer.py +16 -3
  93. meltygui/graphics/shader_compiler.py +9 -3
  94. meltygui/image_load.py +5 -0
  95. meltygui/model/chat_folder_model.py +13 -5
  96. meltygui/model/chat_model.py +6 -6
  97. meltygui/model/file_location_model.py +151 -4
  98. meltygui/model/file_model.py +38 -0
  99. meltygui/model/folder_icon_model.py +152 -25
  100. meltygui/model/import_graph_model.py +6 -2
  101. meltygui/model/source_snapshot_model.py +10 -2
  102. meltygui/model/ssh_auth_model.py +148 -0
  103. meltygui/model/ssh_file_model.py +459 -0
  104. meltygui/model/texture_model.py +59 -3
  105. meltygui/models/file_meta.py +107 -16
  106. meltygui/platforms/__init__.py +1 -0
  107. meltygui/platforms/ios/.gitignore +2 -0
  108. meltygui/platforms/ios/Host/App.mm +316 -0
  109. meltygui/platforms/ios/Host/Info.plist +34 -0
  110. meltygui/platforms/ios/Host/InputQueue.hpp +66 -0
  111. meltygui/platforms/ios/Host/LocalNetworkAccess.h +96 -0
  112. meltygui/platforms/ios/Host/MeltyHost.h +25 -0
  113. meltygui/platforms/ios/Host/MeltyHost.mm +808 -0
  114. meltygui/platforms/ios/Host/MeltyMetalRenderer.mm +426 -0
  115. meltygui/platforms/ios/Host/MeltyRenderer.h +32 -0
  116. meltygui/platforms/ios/Host/SSHAuthentication.h +183 -0
  117. meltygui/platforms/ios/Python/melty_ios_bootstrap.py +255 -0
  118. meltygui/platforms/ios/README.md +440 -0
  119. meltygui/platforms/ios/Renderer/MetalShaders.metal +102 -0
  120. meltygui/platforms/ios/__init__.py +9 -0
  121. meltygui/platforms/ios/__main__.py +21 -0
  122. meltygui/platforms/ios/app_icon.py +441 -0
  123. meltygui/platforms/ios/application.py +20 -0
  124. meltygui/platforms/ios/build_crypto.py +269 -0
  125. meltygui/platforms/ios/build_imgui.py +236 -0
  126. meltygui/platforms/ios/build_platform_deps.py +278 -0
  127. meltygui/platforms/ios/build_rust_deps.py +257 -0
  128. meltygui/platforms/ios/compile_shaders.py +192 -0
  129. meltygui/platforms/ios/dependencies.json +308 -0
  130. meltygui/platforms/ios/devices.py +329 -0
  131. meltygui/platforms/ios/download_numeric_wheels.py +223 -0
  132. meltygui/platforms/ios/generate.py +216 -0
  133. meltygui/platforms/ios/prepare_bundle.py +237 -0
  134. meltygui/platforms/ios/provision.py +851 -0
  135. meltygui/platforms/ios/rec2020-pq.icc +0 -0
  136. meltygui/platforms/ios/runtime.py +77 -0
  137. meltygui/platforms/ios/stage_dependencies.py +409 -0
  138. meltygui/state/new_core_model.py +109 -24
  139. meltygui/text_index.py +3 -1
  140. meltygui/view/account_view.py +2 -2
  141. meltygui/view/chat_view.py +6 -6
  142. meltygui/view/collection_view.py +6 -3
  143. meltygui/view/color_view.py +61 -118
  144. meltygui/view/control_view.py +4 -3
  145. meltygui/view/dropdown_view.py +112 -61
  146. meltygui/view/file_view.py +27 -18
  147. meltygui/view/graph_view.py +2 -2
  148. meltygui/view/header_view.py +21 -6
  149. meltygui/view/menu_view.py +4 -2
  150. meltygui/view/orchestration_view.py +4 -4
  151. meltygui/view/path_icon_view.py +7 -5
  152. meltygui/view/text_view.py +272 -41
  153. meltygui/view/texture_view.py +33 -19
  154. meltygui/view/tile_view.py +49 -27
  155. meltygui/view/trace_view.py +5 -5
  156. meltygui/view/voxel_view.py +2 -2
  157. {meltygui-0.1.2.dist-info → meltygui-0.1.8.dist-info}/METADATA +40 -8
  158. {meltygui-0.1.2.dist-info → meltygui-0.1.8.dist-info}/RECORD +160 -102
  159. {meltygui-0.1.2.dist-info → meltygui-0.1.8.dist-info}/WHEEL +0 -0
  160. {meltygui-0.1.2.dist-info → meltygui-0.1.8.dist-info}/licenses/LICENSE +0 -0
meltygui/__init__.py CHANGED
@@ -22,6 +22,7 @@ from meltygui.core.styling.style import default_tint_accumulation
22
22
  from meltygui.core.styling.style import default_scalar_accumulation
23
23
 
24
24
  from meltygui.core.runtime.app import boot
25
+ from meltygui.core.runtime.app import embedded_imports
25
26
  from meltygui.core.runtime.app import glfw_window
26
27
  from meltygui.core.runtime.app import run
27
28
  from meltygui.core.runtime.app import pressed
@@ -29,68 +30,31 @@ from meltygui.core.runtime.app import content_size
29
30
  from meltygui.core.runtime.app import mark
30
31
  from meltygui.core.runtime.app import persisted
31
32
  from meltygui.core.runtime.app import after_first_frame
33
+ from meltygui.core.runtime.app import checkpoint
32
34
 
33
35
  if TYPE_CHECKING: # IDE / type checkers only; never executed
36
+ import meltygui_imgui as imgui
37
+ from meltygui.core.rendering.gui_prototype import gui, os_window
34
38
  from meltygui.state.new_core_model import DrawState
35
- from meltygui.view.text_view import draw_text
36
39
  from meltygui.model.tile_model import multi_instance
37
40
  from meltygui.view.chat_view import draw_claude_chat
38
- from meltygui.view.texture_view import draw_texture
39
41
  from meltygui.core.rendering.render_dispatch import draw_any
40
- from meltygui.view.control_view import draw_button
41
- from meltygui.view.control_view import draw_str
42
- from meltygui.view.control_view import draw_float
43
- from meltygui.view.control_view import draw_int
44
- from meltygui.view.control_view import draw_int_slider
45
- from meltygui.view.control_view import draw_enum
46
- from meltygui.view.dropdown_view import draw_dropdown
47
- from meltygui.view.inspection_view import draw_view_func_selector
48
- from meltygui.view.color_view import draw_color_picker
49
- from meltygui.view.collection_view import draw_collection_as_tabs
50
- from meltygui.view.layout_view import draw_columns
51
- from meltygui.view.layout_view import draw_rows
52
42
  from meltygui.view.menu_view import draw_menu_bar
53
- from meltygui.view.file_view import draw_file_selector
54
- from meltygui.view.file_view import draw_fast_file_explorer
55
- from meltygui.view.file_view import draw_shortcuts
56
43
  from meltygui.view.file_view import draw_breadcrumbs
57
- from meltygui.core.files.file_core import draw_folder_files
58
- from meltygui.view.terminal_view import draw_terminal
59
44
 
60
- _NCV = 'meltygui.core.rendering.render_dispatch'
61
- _VIEWS = {
45
+ # Non-render helpers keep explicit locations; @render_func views are discovered.
46
+ _LAZY_EXPORTS = {
47
+ 'columns': ('meltygui.core.rendering.gui_layout_prototype', 'columns'),
48
+ 'rows': ('meltygui.core.rendering.gui_layout_prototype', 'rows'),
49
+ 'gui': ('meltygui.core.rendering.gui_prototype', 'gui'),
50
+ 'os_window': ('meltygui.core.rendering.gui_prototype', 'os_window'),
62
51
  'DrawState': ('meltygui.state.new_core_model', 'DrawState'),
63
- 'draw_voxels': ('meltygui.view.voxel_view', 'draw_voxels'),
64
- 'draw_voxels_opengl': ('meltygui.view.voxel_view', 'draw_voxels_opengl'),
65
- 'draw_voxels_cuda': ('meltygui.view.voxel_view', 'draw_voxels_cuda'),
66
- 'draw_tensor_slices': ('meltygui.view.tensor_view', 'draw_tensor_slices'),
67
- 'draw_tensor_error': ('meltygui.view.tensor_view', 'draw_tensor_error'),
68
- 'draw_line_graph': ('meltygui.view.graph_view', 'draw_line_graph'),
69
52
  'render_func': ('meltygui.core.core_render', 'render_func'),
70
53
  'multi_instance': ('meltygui.model.tile_model', 'multi_instance'),
71
54
  'draw_claude_chat': ('meltygui.view.chat_view', 'draw_claude_chat'),
72
- 'draw_text': ('meltygui.view.text_view', 'draw_text'),
73
- 'draw_texture': ('meltygui.view.texture_view', 'draw_texture'),
74
- 'draw_any': (_NCV, 'draw_any'),
75
- 'draw_button': ('meltygui.view.control_view', 'draw_button'),
76
- 'draw_str': ('meltygui.view.control_view', 'draw_str'),
77
- 'draw_float': ('meltygui.view.control_view', 'draw_float'),
78
- 'draw_int': ('meltygui.view.control_view', 'draw_int'),
79
- 'draw_int_slider': ('meltygui.view.control_view', 'draw_int_slider'),
80
- 'draw_enum': ('meltygui.view.control_view', 'draw_enum'),
81
- 'draw_dropdown': ('meltygui.view.dropdown_view', 'draw_dropdown'),
82
- 'draw_view_func_selector': ('meltygui.view.inspection_view', 'draw_view_func_selector'),
83
- 'draw_color_picker': ('meltygui.view.color_view', 'draw_color_picker'),
84
- 'draw_collection_as_tabs': ('meltygui.view.collection_view', 'draw_collection_as_tabs'),
85
- 'draw_columns': ('meltygui.view.layout_view', 'draw_columns'),
86
- 'draw_rows': ('meltygui.view.layout_view', 'draw_rows'),
55
+ 'draw_any': ('meltygui.core.rendering.render_dispatch', 'draw_any'),
87
56
  'draw_menu_bar': ('meltygui.view.menu_view', 'draw_menu_bar'),
88
- 'draw_file_selector': ('meltygui.view.file_view', 'draw_file_selector'),
89
- 'draw_fast_file_explorer': ('meltygui.view.file_view', 'draw_fast_file_explorer'),
90
- 'draw_shortcuts': ('meltygui.view.file_view', 'draw_shortcuts'),
91
57
  'draw_breadcrumbs': ('meltygui.view.file_view', 'draw_breadcrumbs'),
92
- 'draw_folder_files': ('meltygui.core.files.file_core', 'draw_folder_files'),
93
- 'draw_terminal': ('meltygui.view.terminal_view', 'draw_terminal'),
94
58
  }
95
59
 
96
60
 
@@ -104,7 +68,13 @@ def __getattr__(name):
104
68
  if name == 'window_api':
105
69
  import meltygui.core.windowing.window_api as window_api
106
70
  return window_api
107
- spec = _VIEWS.get(name)
71
+ if name == '__all__':
72
+ value = list(dict.fromkeys([*_PUBLIC_NAMES, *_LAZY_EXPORTS, *_render_exports()]))
73
+ globals()[name] = value
74
+ return value
75
+ spec = _LAZY_EXPORTS.get(name)
76
+ if spec is None:
77
+ spec = _render_exports().get(name)
108
78
  if spec is None:
109
79
  raise AttributeError(name)
110
80
  import importlib
@@ -115,6 +85,17 @@ def __getattr__(name):
115
85
  return value
116
86
 
117
87
 
118
- __all__ = ['Style', 'boot', 'glfw_window', 'run', 'pressed', 'content_size', 'mark', 'persisted', 'after_first_frame',
119
- 'toggles', 'window_api', 'imgui',
120
- *_VIEWS]
88
+ _PUBLIC_NAMES = [
89
+ 'Style', 'boot', 'embedded_imports', 'glfw_window', 'run', 'pressed',
90
+ 'content_size', 'mark', 'persisted', 'after_first_frame', 'checkpoint',
91
+ 'toggles', 'window_api', 'imgui',
92
+ ]
93
+
94
+
95
+ def _render_exports():
96
+ from meltygui.core.runtime.public_exports import render_exports
97
+ return render_exports()
98
+
99
+
100
+ def __dir__():
101
+ return sorted(set(globals()) | set(_PUBLIC_NAMES) | set(_LAZY_EXPORTS) | set(_render_exports()))
@@ -353,7 +353,7 @@ class AnthropicKind(AccountKind):
353
353
  source = self._source(account)
354
354
  if source is None:
355
355
  return ("needs_login", "not signed in")
356
- return ("ready", f"{source} · verified" if account.get("_validated") else source)
356
+ return ("ready", f"{source} verified" if account.get("_validated") else source)
357
357
 
358
358
  # -- Claude plan usage (through Claude Code's login) ----------------------
359
359
  # The plan's rate-limit windows (session, weekly all-models, weekly
@@ -662,7 +662,7 @@ class AnthropicKind(AccountKind):
662
662
  same_day = fetched[:3] == time.localtime()[:3]
663
663
  stamp = "as of " + time.strftime("%H:%M:%S" if same_day else "%m-%d %H:%M", fetched)
664
664
  if account.get("_usage_loading"):
665
- stamp += " · refreshing…"
665
+ stamp += " refreshing…"
666
666
  out.append(("stamp", stamp))
667
667
  return out
668
668
 
@@ -690,13 +690,13 @@ class AnthropicKind(AccountKind):
690
690
  client.models.list(limit=1)
691
691
  client.close()
692
692
  account["_validated"] = True
693
- account["_status"] = ("ready", f"{source} · verified")
693
+ account["_status"] = ("ready", f"{source} verified")
694
694
  except ImportError:
695
695
  account["_status"] = ("error", "anthropic package not installed")
696
696
  except Exception as error:
697
697
  account["_validated"] = False
698
698
  message = getattr(error, "message", None) or str(error)
699
- account["_status"] = ("error", f"{source} · {message[:90]}")
699
+ account["_status"] = ("error", f"{source} {message[:90]}")
700
700
  accounts_changed()
701
701
 
702
702
  # -- browser sign-in ---------------------------------------------------
@@ -880,7 +880,7 @@ class CodexKind(AccountKind):
880
880
  return ("needs_login", "not signed in")
881
881
  if identity.get("type") != "chatgpt":
882
882
  return ("warning", "ChatGPT sign-in required for plan usage")
883
- return ("ready", " · ".join(filter(None, (
883
+ return ("ready", " ".join(filter(None, (
884
884
  identity.get("email") or "ChatGPT", identity.get("planType")))))
885
885
 
886
886
  def probe(self, account):
@@ -1025,7 +1025,7 @@ class CodexKind(AccountKind):
1025
1025
  if fetched:
1026
1026
  stamp = "as of " + time.strftime("%m-%d %H:%M:%S", time.localtime(fetched))
1027
1027
  if account.get("_usage_loading"):
1028
- stamp += " · refreshing…"
1028
+ stamp += " refreshing…"
1029
1029
  out.append(("stamp", stamp))
1030
1030
  out.append(("note", "Shared with Codex desktop / CLI" if account["id"] == "codex"
1031
1031
  else "Separate Codex account"))
@@ -1136,7 +1136,7 @@ class OllamaKind(AccountKind):
1136
1136
  models = ollama.list_models(client)
1137
1137
  except Exception as error:
1138
1138
  account["_models"] = []
1139
- return ("error", f"{host} · {str(error)[:60]}")
1139
+ return ("error", f"{host} {str(error)[:60]}")
1140
1140
  account["_models"] = models
1141
1141
  try:
1142
1142
  account["_gpus"] = ollama.gpu_inventory() # best-effort GPU names for the device menu
@@ -1148,10 +1148,10 @@ class OllamaKind(AccountKind):
1148
1148
  for key in ("coder", "codellama", "starcoder", "codestral", "deepseek-coder"))]
1149
1149
  text = f"{len(models)} models"
1150
1150
  if loaded:
1151
- text += f" · {len(loaded)} loaded on " + ", ".join(
1151
+ text += f" {len(loaded)} loaded on " + ", ".join(
1152
1152
  sorted({model['where'] or '?' for model in loaded}))
1153
1153
  if not fim_like:
1154
- text += " · no FIM model (pull qwen2.5-coder)"
1154
+ text += " no FIM model (pull qwen2.5-coder)"
1155
1155
  return ("ready", text)
1156
1156
 
1157
1157
  def actions(self, account):
@@ -387,7 +387,7 @@ def source_initials(label):
387
387
  def source_label(account_id, kind, accounts):
388
388
  """A tab's text: the provider, plus the account when the kind has several."""
389
389
  if len(accounts.of_kind(kind.name)) > 1:
390
- return f'{kind.chat_label} · {accounts[account_id]["label"]}'
390
+ return f'{kind.chat_label} {accounts[account_id]["label"]}'
391
391
  return kind.chat_label
392
392
 
393
393
 
@@ -530,10 +530,10 @@ def _draw_image(ref, x, y, max_width, box_height, caption_height, tint, *, name=
530
530
  rendered_size = (image_state.width, image_state.height)
531
531
  width, height = rendered_size
532
532
  elif entry is not None and entry.status == "failed":
533
- caption = (caption + " · " if caption else "") + "could not decode: " + str(entry.error)
533
+ caption = (caption + " " if caption else "") + "could not decode: " + str(entry.error)
534
534
  else:
535
535
  draw_list.add_rect_filled(x, y, x + width, y + height, _color(tint, 0.12), rounding=Melty.px(6))
536
- caption = (caption + " · " if caption else "") + "decoding…"
536
+ caption = (caption + " " if caption else "") + "decoding…"
537
537
  _title(caption or ref.label, x, y + height, max_width, caption_height, tint, brightness=0.7, ellipsis=True)
538
538
  return rendered_size
539
539
 
@@ -560,10 +560,10 @@ def transcript_entries(messages, state, key):
560
560
  if len(entries) == 1:
561
561
  preview = _message_preview(entries[0][1])
562
562
  if preview:
563
- summary += " · " + preview
563
+ summary += " " + preview
564
564
  failures = sum(_message_failed(message) for _, message in entries)
565
565
  if failures:
566
- summary += f" · {failures} failed"
566
+ summary += f" {failures} failed"
567
567
  group = Message("actionGroup", details={"label": summary})
568
568
  yield identifier, group
569
569
  if getattr(state, "action_groups", {}).get(key + ":" + identifier, False):
meltygui/chat/images.py CHANGED
@@ -224,4 +224,4 @@ def image_label(ref, entry=None):
224
224
  parts.append("%d×%d" % entry.size)
225
225
  if entry.hdr:
226
226
  parts.append(f"HDR, peak {entry.peak_nits:.0f} nits")
227
- return " · ".join(parts)
227
+ return " ".join(parts)
meltygui/chat/main.py CHANGED
@@ -30,6 +30,7 @@ The service must remain alive; rebooting or killing the service stops its work.
30
30
  import os
31
31
  import pathlib
32
32
  import socket
33
+ import sys
33
34
  import threading
34
35
 
35
36
  import meltygui
@@ -169,12 +170,16 @@ def main(argv=None):
169
170
  parser.error(f'not a directory: {project}')
170
171
  state = dict(new_project=str(project) if project else None,
171
172
  default_project=str(pathlib.Path.cwd()))
172
- instance = os.environ.get('MELTY_CLAUDE_INSTANCE')
173
- socket_path = (pathlib.Path(os.environ.get('XDG_RUNTIME_DIR') or '/tmp')
174
- / f"{APP_ID}-{os.getuid()}{('-' + instance) if instance else ''}.sock")
175
- if hand_over_to_running_instance(socket_path):
176
- return 0
177
- close_instance = serve_instance(socket_path)
173
+ close_instance = None
174
+ # iOS owns the app lifecycle; the desktop activation socket under /tmp
175
+ # is outside the app sandbox and must not be created there.
176
+ if sys.platform != 'ios':
177
+ instance = os.environ.get('MELTY_CLAUDE_INSTANCE')
178
+ socket_path = (pathlib.Path(os.environ.get('XDG_RUNTIME_DIR') or '/tmp')
179
+ / f"{APP_ID}-{os.getuid()}{('-' + instance) if instance else ''}.sock")
180
+ if hand_over_to_running_instance(socket_path):
181
+ return 0
182
+ close_instance = serve_instance(socket_path)
178
183
  try:
179
184
  # Window registration and runtime state belong to this invocation.
180
185
  @glfw_window(name='Claude Code', width=1180, height=820, app_id=APP_ID,
@@ -196,7 +201,8 @@ def main(argv=None):
196
201
  meltygui.run()
197
202
  finally:
198
203
  try:
199
- close_instance()
204
+ if close_instance is not None:
205
+ close_instance()
200
206
  finally:
201
207
  disconnect_chats()
202
208
  return 0
@@ -1320,7 +1320,7 @@ def focus(input_value, path=(), default=None, kind=None, draw_state=None, unique
1320
1320
  else:
1321
1321
  target = None
1322
1322
  base = kind or str(draw_state.name)
1323
- label = f"{base} · {target}" if target else base
1323
+ label = f"{base} {target}" if target else base
1324
1324
 
1325
1325
  imgui.same_line()
1326
1326
  imgui.text(label)
@@ -17,7 +17,8 @@ def file_icon_for_path(path):
17
17
  # File browsers also work before any editor has initialized the codecs.
18
18
  from meltygui.code import new_codecs
19
19
 
20
- codec = extension_to_codec.get(Path(path).suffix.lower())
20
+ from meltygui.model.file_location_model import file_path
21
+ codec = extension_to_codec.get(file_path(path).suffix.lower())
21
22
  return codec.icon_for_path(path) if codec is not None else None
22
23
 
23
24
 
@@ -36,5 +37,6 @@ def file_badge_for_path(path):
36
37
  from pathlib import Path
37
38
  from meltygui.code import new_codecs
38
39
 
39
- codec = extension_to_codec.get(Path(path).suffix.lower())
40
+ from meltygui.model.file_location_model import file_path
41
+ codec = extension_to_codec.get(file_path(path).suffix.lower())
40
42
  return codec.badge_for_path(path) if codec is not None else None
@@ -0,0 +1,47 @@
1
+ """Jedi introspection inside a running app, without process-global path swaps.
2
+
3
+ Jedi's usual same-process helpers temporarily replace sys.path. Its analysis
4
+ worker then races imports on the render thread and application workers. Source
5
+ lookups can pass their search path directly; compiled modules must use the
6
+ embedded application's actual environment.
7
+ """
8
+ import importlib
9
+ import sys
10
+ import warnings
11
+
12
+ from jedi.api.environment import InterpreterEnvironment
13
+ from jedi.inference.compiled import access
14
+ from jedi.inference.compiled.subprocess import InferenceStateSameProcess, functions
15
+
16
+
17
+ class EmbeddedProcess(InferenceStateSameProcess):
18
+ def get_module_info(self, sys_path=None, full_name=None, **kwargs):
19
+ if kwargs.get('path') is None and sys_path is not None:
20
+ kwargs['path'] = sys_path
21
+ # Jedi's namespace-package result normally gets this from a global
22
+ # lookup. Passing an explicit root path must retain that name.
23
+ full_name = full_name or kwargs['string']
24
+ try:
25
+ return functions._find_module(full_name=full_name, **kwargs)
26
+ except ImportError:
27
+ return None, None
28
+
29
+ def load_module(self, dotted_name, sys_path):
30
+ # Project-specific Python sources are resolved statically above. Native
31
+ # introspection uses this interpreter's modules, never another venv's
32
+ # binaries or a temporary replacement of the application's sys.path.
33
+ try:
34
+ module = importlib.import_module(dotted_name)
35
+ except Exception:
36
+ warnings.warn(f'Module {dotted_name} is not importable in the embedded app.',
37
+ stacklevel=2)
38
+ return None
39
+ return access.create_access_path(self._inference_state_weakref(), module)
40
+
41
+
42
+ class EmbeddedEnvironment(InterpreterEnvironment):
43
+ def get_inference_state_subprocess(self, inference_state):
44
+ return EmbeddedProcess(inference_state)
45
+
46
+ def get_sys_path(self):
47
+ return list(sys.path)
meltygui/code/fileref.py CHANGED
@@ -66,6 +66,9 @@ def add_editable_root(path) -> Path | None:
66
66
  or the PROJECT ROOT of a file `path` (see project_root_of). Idempotent;
67
67
  a library install (site-packages, a venv) is never registered. Returns
68
68
  the root registered, or None when refused."""
69
+ from meltygui.model.file_location_model import is_remote
70
+ if is_remote(path):
71
+ return None
69
72
  try:
70
73
  p = Path(path).expanduser().resolve()
71
74
  except (OSError, ValueError):
@@ -122,7 +125,7 @@ def is_editable_source(source_file) -> bool:
122
125
  except (OSError, ValueError):
123
126
  return False
124
127
  ok = True
125
- if {"site-packages", "dist-packages"} & set(p.parts):
128
+ if _ios_bundle_source(p) or {"site-packages", "dist-packages"} & set(p.parts):
126
129
  ok = False
127
130
  else:
128
131
  ok = any(p.is_relative_to(root) for root in _EDITABLE_ROOTS)
@@ -136,6 +139,12 @@ _LIBRARY_PARTS = frozenset({"site-packages", "dist-packages", "venv", ".venv",
136
139
  "node_modules"})
137
140
 
138
141
 
142
+ def _ios_bundle_source(path: Path) -> bool:
143
+ # Signed iOS bundles can report writable POSIX modes/access(), while the
144
+ # sandbox rejects writes. This includes app source registered as a project.
145
+ return sys.platform == "ios" and any(part.endswith(".app") for part in path.parts)
146
+
147
+
139
148
  def writable_file_refusal(path) -> str | None:
140
149
  """Why the PLAIN-FILE codecs (TextFileCodec and friends) will not edit
141
150
  `path` — a short human reason — or None when they will.
@@ -150,10 +159,16 @@ def writable_file_refusal(path) -> str | None:
150
159
  gate exists to prevent. (It used to require $HOME instead of write
151
160
  permission, and a refused file showed as a tab that never loaded.)
152
161
  A path that does not exist yet is judged by its directory."""
162
+ from meltygui.model.file_location_model import file_path, is_remote
163
+ if is_remote(path):
164
+ hit = _LIBRARY_PARTS & set(file_path(path).parts)
165
+ return f'inside a library install ({sorted(hit)[0]}/)' if hit else None
153
166
  try:
154
167
  p = Path(path).resolve()
155
168
  except (OSError, ValueError) as e:
156
169
  return f"path cannot be resolved ({e})"
170
+ if _ios_bundle_source(p):
171
+ return "inside a read-only iOS app bundle"
157
172
  hit = _LIBRARY_PARTS & set(p.parts)
158
173
  if hit:
159
174
  return f"inside a library install ({sorted(hit)[0]}/)"
@@ -198,7 +213,8 @@ class Address:
198
213
  end = None
199
214
 
200
215
  def __init__(self, path, start=None, end=None, source=None, watcher_ds=None):
201
- self.path = Path(path).resolve()
216
+ from meltygui.model.file_location_model import file_path
217
+ self.path = file_path(path).resolve()
202
218
  self.start = start
203
219
  self.end = end
204
220
  self.source = source
@@ -558,11 +558,12 @@ _xref_cache: dict[_Path, tuple[float, dict[str, list[UsageRef]]]] = {}
558
558
 
559
559
  DISABLE_JEDI = False
560
560
 
561
- # ── Jedi subprocess pool ──────────────────────────────────────
562
- # Runs jedi in a child process so CPU-intensive parso parsing
563
- # doesn't hold the main process GIL.
561
+ # ── Jedi workers ──────────────────────────────────────────────
562
+ # Desktop analysis runs in child processes to avoid the render thread's GIL.
563
+ # iOS uses one thread in the embedded interpreter: processes are unavailable,
564
+ # and concurrent Jedi jobs must not race its shared parser/inference caches.
564
565
 
565
- from concurrent.futures import ProcessPoolExecutor as _PPE
566
+ from concurrent.futures import Executor as _Executor
566
567
  from concurrent.futures import Future as _Future
567
568
 
568
569
  # Two pools, same forkserver context. "index" (4 workers) runs the heavy
@@ -572,7 +573,7 @@ from concurrent.futures import Future as _Future
572
573
  # every interactive job lands on the SAME worker process, so parso's per-process
573
574
  # parse cache stays warm for the file being edited (~10-30ms per completion vs
574
575
  # ~1s re-parsing cold on whichever index worker happened to be idle).
575
- _jedi_pools: dict[str, _PPE] = {}
576
+ _jedi_pools: dict[str, _Executor] = {}
576
577
  _jedi_pool_lock = threading.Lock()
577
578
  _jedi_mp_ctx = None
578
579
 
@@ -595,6 +596,8 @@ def _get_jedi_mp_ctx():
595
596
  COW-share that ~0.4 GB base instead of each re-importing it. Built once and cached:
596
597
  the server persists across restarts, so rebuilt pools fork from the same clean base."""
597
598
  global _jedi_mp_ctx
599
+ if sys.platform == 'ios':
600
+ raise RuntimeError('iOS Jedi analysis uses the embedded interpreter, not multiprocessing')
598
601
  if _jedi_mp_ctx is None:
599
602
  import multiprocessing as _mp
600
603
  if "forkserver" in _mp.get_all_start_methods():
@@ -611,7 +614,15 @@ def _get_jedi_mp_ctx():
611
614
  return _jedi_mp_ctx
612
615
 
613
616
 
614
- def _get_jedi_pool(kind: str = "index") -> _PPE:
617
+ def _get_jedi_pool(kind: str = "index") -> _Executor:
618
+ if sys.platform == 'ios':
619
+ from concurrent.futures import ThreadPoolExecutor
620
+ with _jedi_pool_lock:
621
+ pool = _jedi_pools.get('ios')
622
+ if pool is None or pool._shutdown:
623
+ pool = ThreadPoolExecutor(max_workers=1, thread_name_prefix='melty-jedi-ios')
624
+ _jedi_pools['ios'] = pool
625
+ return pool
615
626
  # A studio restart-in-place ends the session, which fires concurrent.futures'
616
627
  # atexit (_python_exit) even though THIS process keeps running. That sets the
617
628
  # module-level _global_shutdown flag and kills the worker processes, so EVERY
@@ -619,14 +630,15 @@ def _get_jedi_pool(kind: str = "index") -> _PPE:
619
630
  # silently disabling jedi + every off-GIL task. Clear that stale signal (the
620
631
  # process is not actually exiting) and rebuild BOTH pools.
621
632
  import concurrent.futures.process as _cfp
633
+ from concurrent.futures import ProcessPoolExecutor
622
634
  with _jedi_pool_lock:
623
635
  if getattr(_cfp, "_global_shutdown", False):
624
636
  _cfp._global_shutdown = False
625
637
  _jedi_pools.clear()
626
638
  pool = _jedi_pools.get(kind)
627
639
  if pool is None or getattr(pool, "_shutdown_thread", False):
628
- pool = _PPE(max_workers=1 if kind == "ac" else 4,
629
- mp_context=_get_jedi_mp_ctx())
640
+ pool = ProcessPoolExecutor(max_workers=1 if kind == "ac" else 4,
641
+ mp_context=_get_jedi_mp_ctx())
630
642
  _jedi_pools[kind] = pool
631
643
  return pool
632
644
 
@@ -647,6 +659,10 @@ def _submit_interactive(worker, *args) -> _Future:
647
659
  forkserver — which preloads this module, >1s of imports — so pool get +
648
660
  submit happen on a short-lived daemon thread and the pool future's result is
649
661
  mirrored into the returned Future (same done()/add_done_callback contract)."""
662
+ if sys.platform == 'ios':
663
+ # Thread startup does not import/spawn a second interpreter. Returning
664
+ # its own Future also preserves cancellation of queued device work.
665
+ return _get_jedi_pool('ac').submit(worker, *args)
650
666
  out = _Future()
651
667
 
652
668
  def _bg():
@@ -679,7 +695,7 @@ def _jedi_project(file_path=None):
679
695
  held = _jedi_projects.get(project.key)
680
696
  if held is None:
681
697
  held = _jedi_projects[project.key] = jedi.Project(
682
- path=project.root, environment_path=project.environment,
698
+ path=project.root, environment_path=None if sys.platform == 'ios' else project.environment,
683
699
  added_sys_path=list(project.source_paths))
684
700
  return held
685
701
 
@@ -688,7 +704,15 @@ def _jedi_script(file_path, code=None):
688
704
  """jedi.Script on the file’s owning project. `code` (in-memory source) overrides
689
705
  the on-disk file so unsaved edits are analyzed; path still drives resolution."""
690
706
  import jedi
691
- return jedi.Script(code=code, path=str(file_path), project=_jedi_project(file_path))
707
+ kwargs = {}
708
+ if sys.platform == 'ios':
709
+ # Threading alone is insufficient: Script's default Environment probes
710
+ # an executable and runs compiled-module introspection in a subprocess.
711
+ # Keep lookups inside bundled CPython without replacing sys.path while
712
+ # the app's rendering/decoder threads are importing their own modules.
713
+ from meltygui.code.embedded_jedi import EmbeddedEnvironment
714
+ kwargs['environment'] = EmbeddedEnvironment()
715
+ return jedi.Script(code=code, path=str(file_path), project=_jedi_project(file_path), **kwargs)
692
716
 
693
717
 
694
718
  def shutdown_jedi_pool():
@@ -696,8 +720,8 @@ def shutdown_jedi_pool():
696
720
  pools = list(_jedi_pools.values())
697
721
  _jedi_pools.clear()
698
722
  for pool in pools:
699
- # Kill worker processes first because shutdown(cancel_futures=True) only
700
- # cancels pending futures, not ones already running in a subprocess.
723
+ # Desktop workers can be killed. An iOS job already executing in a
724
+ # thread must finish; shutdown cancels its queued requests only.
701
725
  for pid, proc in list(getattr(pool, '_processes', {}).items()):
702
726
  try:
703
727
  proc.kill()
@@ -809,7 +833,11 @@ def _completion_namespace():
809
833
  if _completion_ns_cache is None:
810
834
  import importlib
811
835
  ns = {}
812
- for alias, mod in _COMPLETION_MODULES.items():
836
+ modules = _COMPLETION_MODULES
837
+ if sys.platform == 'ios':
838
+ modules = dict(modules, imgui='meltygui_imgui',
839
+ glfw='meltygui.core.windowing.window_api')
840
+ for alias, mod in modules.items():
813
841
  try:
814
842
  ns[alias] = importlib.import_module(mod)
815
843
  except Exception:
@@ -827,8 +855,7 @@ def _jedi_complete_worker(code: str, line: int, col: int, path: str = None):
827
855
  module/class/function/instance/param/keyword/statement/property/path)."""
828
856
  import jedi
829
857
  try:
830
- kw = {"path": path, "project": _jedi_project(path)} if path else {}
831
- script = (jedi.Script(code, **kw) if path else
858
+ script = (_jedi_script(path, code) if path else
832
859
  jedi.Interpreter(code, [_completion_namespace()]))
833
860
  comps = script.complete(line, col)
834
861
  except Exception:
@@ -904,8 +931,7 @@ def _jedi_signatures_worker(code: str, line: int, col: int, path: str = None):
904
931
  'x', 'y=0', '*args')."""
905
932
  import jedi
906
933
  try:
907
- kw = {"path": path, "project": _jedi_project(path)} if path else {}
908
- script = (jedi.Script(code, **kw) if path else
934
+ script = (_jedi_script(path, code) if path else
909
935
  jedi.Interpreter(code, [_completion_namespace()]))
910
936
  sigs = script.get_signatures(line, col)
911
937
  except Exception:
@@ -3704,7 +3730,7 @@ _FRAME_STAMP_STALE_S = 2.0 # a frame stamp this old = frame aborted mid-draw
3704
3730
 
3705
3731
  def _park_while_frame(max_park_s=_FRAME_PARK_MAX_S):
3706
3732
  """Sleep a background thread while the render thread is INSIDE a frame
3707
- (Melty._frame_draw_start set at frame start, cleared at post_frame end).
3733
+ (Melty._frame_draw_start set by the surface, restored in its finally).
3708
3734
 
3709
3735
  Why: post_frame's capture pass is hundreds of ctypes GL calls, each
3710
3736
  releasing and re-acquiring the GIL. Under a CPU-bound background
@@ -3725,9 +3751,13 @@ def _park_while_frame(max_park_s=_FRAME_PARK_MAX_S):
3725
3751
  cur = threading.current_thread()
3726
3752
  if cur is threading.main_thread():
3727
3753
  return 0.0
3728
- import meltygui.core.graphics.gl_state as gl_state
3729
- glt = getattr(gl_state, "_gl_thread", None) # read, don't claim
3730
- if glt is None or cur is glt:
3754
+ from meltygui.core.windowing import glfw_utils
3755
+ render_id = glfw_utils._render_thread_id
3756
+ # Native Metal hosts have a render thread but no GL-state owner. Read an
3757
+ # existing GL module only for the studio's older dedicated render loop.
3758
+ gl_state = sys.modules.get("meltygui.core.graphics.gl_state")
3759
+ glt = getattr(gl_state, "_gl_thread", None)
3760
+ if cur.ident == render_id or cur is glt or (render_id is None and glt is None):
3731
3761
  return 0.0
3732
3762
  t0 = time.monotonic()
3733
3763
  if t0 - fs >= _FRAME_STAMP_STALE_S:
@@ -3783,9 +3813,14 @@ def _yield_to_ui():
3783
3813
  cur = threading.current_thread()
3784
3814
  if cur is threading.main_thread():
3785
3815
  return
3786
- import meltygui.core.graphics.gl_state as gl_state
3787
- glt = getattr(gl_state, "_gl_thread", None) # read, don't claim (is_current_thread claims)
3788
- if glt is None or cur is glt:
3816
+ # Native Metal has a registered render thread but no OpenGL thread. Use
3817
+ # the same ownership check as _park_while_frame so its workers also defer
3818
+ # between frames while a gesture or momentum animation remains active.
3819
+ from meltygui.core.windowing import glfw_utils
3820
+ render_id = glfw_utils._render_thread_id
3821
+ gl_state = sys.modules.get("meltygui.core.graphics.gl_state")
3822
+ glt = getattr(gl_state, "_gl_thread", None)
3823
+ if cur.ident == render_id or cur is glt or (render_id is None and glt is None):
3789
3824
  return
3790
3825
  _t0 = time.monotonic()
3791
3826
  while Toggles.yield_to_ui:
@@ -9721,4 +9756,4 @@ def shutdown_symbol_index_daemon():
9721
9756
  # try:
9722
9757
  # SymbolIndexCache.rebuild()
9723
9758
  # except Exception:
9724
- # pass
9759
+ # pass