arrayview 0.26.2__py3-none-any.whl → 0.26.3__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.
@@ -0,0 +1,285 @@
1
+ """Data-only launch routing primitives and environment snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from enum import Enum
7
+ import json
8
+ import os
9
+ import socket
10
+ import sys
11
+ import urllib.request
12
+
13
+ _LOOPBACK_HOST = "localhost"
14
+ _PING_TIMEOUT_SECONDS = 0.2
15
+
16
+ _SNAPSHOT_ENV_KEYS = (
17
+ "ARRAYVIEW_WINDOW",
18
+ "ARRAYVIEW_WINDOW_ID",
19
+ "DISPLAY",
20
+ "TERM_PROGRAM",
21
+ "VSCODE_AGENT_FOLDER",
22
+ "VSCODE_INJECTION",
23
+ "VSCODE_IPC_HOOK_CLI",
24
+ "WAYLAND_DISPLAY",
25
+ "SSH_CLIENT",
26
+ "SSH_CONNECTION",
27
+ )
28
+
29
+
30
+ class _StrEnum(str, Enum):
31
+ def __str__(self) -> str:
32
+ return self.value
33
+
34
+
35
+ class Invocation(_StrEnum):
36
+ CLI = "cli"
37
+ PYTHON = "python"
38
+ JUPYTER = "jupyter"
39
+ JULIA = "julia"
40
+ STDIO = "stdio"
41
+ CODEX = "codex"
42
+
43
+
44
+ class Environment(_StrEnum):
45
+ TERMINAL = "terminal"
46
+ VSCODE_LOCAL = "vscode_local"
47
+ VSCODE_REMOTE = "vscode_remote"
48
+ SSH = "ssh"
49
+ JUPYTER = "jupyter"
50
+ JULIA = "julia"
51
+
52
+
53
+ class Transport(_StrEnum):
54
+ HTTP = "http"
55
+ STDIO_FILE = "stdio_file"
56
+ STDIO_SHM = "stdio_shm"
57
+ NONE = "none"
58
+
59
+
60
+ class ServerOwner(_StrEnum):
61
+ EXISTING = "existing"
62
+ SPAWNED_DAEMON = "spawned_daemon"
63
+ IN_PROCESS = "in_process"
64
+ PERSISTENT = "persistent"
65
+ EXTERNAL = "external"
66
+
67
+
68
+ class Display(_StrEnum):
69
+ NATIVE = "native"
70
+ BROWSER = "browser"
71
+ VSCODE = "vscode"
72
+ INLINE = "inline"
73
+ NONE = "none"
74
+
75
+
76
+ class Registration(_StrEnum):
77
+ HTTP_LOAD = "http_load"
78
+ DAEMON_STARTUP = "daemon_startup"
79
+ IN_PROCESS_SESSION = "in_process_session"
80
+ STDIO_REGISTER = "stdio_register"
81
+ RELAY = "relay"
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class ServerSnapshot:
86
+ port: int
87
+ port_busy: bool
88
+ arrayview_server_alive: bool
89
+ server_pid: int | None = None
90
+ server_hostname: str | None = None
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class LaunchEnvironmentSnapshot:
95
+ invocation: Invocation
96
+ requested_window: str | None
97
+ environment: Environment
98
+ platform: str
99
+ env_vars: dict[str, str]
100
+ config_default: str | None
101
+ native_backend: str | None
102
+ server: ServerSnapshot
103
+ in_jupyter: bool
104
+ in_julia: bool
105
+ in_vscode_terminal: bool
106
+ is_vscode_remote: bool
107
+ in_vscode_tunnel: bool
108
+ ssh_connection: bool
109
+ ssh_client: bool
110
+ hostname: str
111
+
112
+ def to_dict(self) -> dict:
113
+ return asdict(self)
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class LaunchRequest:
118
+ port: int
119
+ requested_window: str | None = None
120
+
121
+
122
+ @dataclass(frozen=True)
123
+ class LaunchPlan:
124
+ invocation: Invocation
125
+ environment: Environment
126
+ transport: Transport
127
+ server_owner: ServerOwner
128
+ display: Display
129
+ registration: Registration
130
+ fallback_display: Display | None = None
131
+ fallback_allowed: bool = False
132
+
133
+
134
+ def snapshot_launch_environment(
135
+ port: int,
136
+ invocation: Invocation | str,
137
+ requested_window: str | None = None,
138
+ ) -> LaunchEnvironmentSnapshot:
139
+ """Capture launch-relevant facts without importing the server stack."""
140
+ inv = _coerce_invocation(invocation)
141
+ env_vars = _snapshot_env_vars()
142
+ in_jupyter = _platform_bool("_in_jupyter")
143
+ in_julia = _platform_bool("_is_julia_env")
144
+ in_vscode_terminal = _platform_bool("_in_vscode_terminal")
145
+ is_vscode_remote = _platform_bool("_is_vscode_remote")
146
+ in_vscode_tunnel = _platform_bool("_in_vscode_tunnel")
147
+ ssh_connection = bool(os.environ.get("SSH_CONNECTION"))
148
+ ssh_client = bool(os.environ.get("SSH_CLIENT"))
149
+ environment = _classify_environment(
150
+ in_jupyter=in_jupyter,
151
+ in_julia=in_julia,
152
+ in_vscode_terminal=in_vscode_terminal,
153
+ is_vscode_remote=is_vscode_remote,
154
+ ssh_connection=ssh_connection,
155
+ ssh_client=ssh_client,
156
+ )
157
+
158
+ return LaunchEnvironmentSnapshot(
159
+ invocation=inv,
160
+ requested_window=requested_window,
161
+ environment=environment,
162
+ platform=sys.platform,
163
+ env_vars=env_vars,
164
+ config_default=_config_window_default(environment.value),
165
+ native_backend=_native_window_gui(),
166
+ server=_server_snapshot(port),
167
+ in_jupyter=in_jupyter,
168
+ in_julia=in_julia,
169
+ in_vscode_terminal=in_vscode_terminal,
170
+ is_vscode_remote=is_vscode_remote,
171
+ in_vscode_tunnel=in_vscode_tunnel,
172
+ ssh_connection=ssh_connection,
173
+ ssh_client=ssh_client,
174
+ hostname=socket.gethostname(),
175
+ )
176
+
177
+
178
+ def _coerce_invocation(value: Invocation | str) -> Invocation:
179
+ if isinstance(value, Invocation):
180
+ return value
181
+ return Invocation(value)
182
+
183
+
184
+ def _snapshot_env_vars() -> dict[str, str]:
185
+ return {key: os.environ[key] for key in _SNAPSHOT_ENV_KEYS if key in os.environ}
186
+
187
+
188
+ def _platform_bool(name: str) -> bool:
189
+ try:
190
+ from arrayview import _platform
191
+
192
+ return bool(getattr(_platform, name)())
193
+ except Exception:
194
+ return False
195
+
196
+
197
+ def _native_window_gui() -> str | None:
198
+ try:
199
+ from arrayview._platform import _native_window_gui as native_window_gui
200
+
201
+ return native_window_gui()
202
+ except Exception:
203
+ return None
204
+
205
+
206
+ def _config_window_default(environment: str) -> str | None:
207
+ try:
208
+ from arrayview._config import load_config
209
+
210
+ cfg = load_config()
211
+ except Exception:
212
+ return None
213
+ window_cfg = cfg.get("window", {})
214
+ if not isinstance(window_cfg, dict):
215
+ return None
216
+ env_key = "vscode" if environment in {"vscode_local", "vscode_remote"} else environment
217
+ value = window_cfg.get(env_key) or window_cfg.get("default")
218
+ if isinstance(value, str):
219
+ return value.strip().lower() or None
220
+ return None
221
+
222
+
223
+ def _classify_environment(
224
+ *,
225
+ in_jupyter: bool,
226
+ in_julia: bool,
227
+ in_vscode_terminal: bool,
228
+ is_vscode_remote: bool,
229
+ ssh_connection: bool,
230
+ ssh_client: bool,
231
+ ) -> Environment:
232
+ if in_jupyter:
233
+ return Environment.JUPYTER
234
+ if in_julia:
235
+ return Environment.JULIA
236
+ if is_vscode_remote:
237
+ return Environment.VSCODE_REMOTE
238
+ if in_vscode_terminal:
239
+ return Environment.VSCODE_LOCAL
240
+ if ssh_connection or ssh_client:
241
+ return Environment.SSH
242
+ return Environment.TERMINAL
243
+
244
+
245
+ def _server_snapshot(port: int) -> ServerSnapshot:
246
+ payload = _ping_arrayview_server(port)
247
+ return ServerSnapshot(
248
+ port=port,
249
+ port_busy=_port_busy(port),
250
+ arrayview_server_alive=payload is not None,
251
+ server_pid=_int_or_none(payload.get("pid")) if payload else None,
252
+ server_hostname=_str_or_none(payload.get("hostname")) if payload else None,
253
+ )
254
+
255
+
256
+ def _port_busy(port: int) -> bool:
257
+ try:
258
+ with socket.create_connection(
259
+ (_LOOPBACK_HOST, port), timeout=_PING_TIMEOUT_SECONDS
260
+ ):
261
+ return True
262
+ except OSError:
263
+ return False
264
+
265
+
266
+ def _ping_arrayview_server(port: int) -> dict | None:
267
+ url = f"http://{_LOOPBACK_HOST}:{port}/ping"
268
+ try:
269
+ with urllib.request.urlopen(url, timeout=_PING_TIMEOUT_SECONDS) as resp:
270
+ if resp.status != 200:
271
+ return None
272
+ payload = json.loads(resp.read().decode("utf-8"))
273
+ except Exception:
274
+ return None
275
+ if payload.get("ok") is True and payload.get("service") == "arrayview":
276
+ return payload
277
+ return None
278
+
279
+
280
+ def _int_or_none(value: object) -> int | None:
281
+ return value if isinstance(value, int) else None
282
+
283
+
284
+ def _str_or_none(value: object) -> str | None:
285
+ return value if isinstance(value, str) else None
arrayview/_launcher.py CHANGED
@@ -33,6 +33,7 @@ from arrayview._platform import (
33
33
  _is_vscode_remote,
34
34
  _in_vscode_tunnel,
35
35
  _can_native_window,
36
+ _native_window_gui,
36
37
  _find_vscode_ipc_hook,
37
38
  _is_julia_env,
38
39
  _in_julia_jupyter,
@@ -249,6 +250,7 @@ def _open_webview(
249
250
 
250
251
  icon_path = _get_icon_png_path() or ""
251
252
  inline_html_b64 = None
253
+ gui_name = _native_window_gui() or ""
252
254
 
253
255
  if shell_port is not None:
254
256
  shell_html = _build_inline_shell_html(url, shell_port)
@@ -258,14 +260,14 @@ def _open_webview(
258
260
  if inline_html_b64:
259
261
  script_lines = [
260
262
  "import sys, base64, webview",
261
- "u, w, h, icon, html_b64, ready_file = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5], sys.argv[6]",
263
+ "u, w, h, icon, html_b64, ready_file, gui = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5], sys.argv[6], sys.argv[7]",
262
264
  "html = base64.b64decode(html_b64.encode()).decode()",
263
265
  "class Api:",
264
266
  " def set_title(self, title):",
265
267
  " try: webview.windows[0].set_title(str(title)[:240])",
266
268
  " except Exception: pass",
267
269
  "win = webview.create_window('ArrayView', html=html, width=w, height=h, background_color='#0c0c0c', js_api=Api())",
268
- "kw = {'gui': 'qt'} if sys.platform.startswith('linux') else {}",
270
+ "kw = {'gui': gui} if gui else {}",
269
271
  "def _start_func():",
270
272
  " if ready_file:",
271
273
  " try:",
@@ -298,6 +300,7 @@ def _open_webview(
298
300
  icon_path,
299
301
  inline_html_b64,
300
302
  ready_file or "",
303
+ gui_name,
301
304
  ],
302
305
  stdout=subprocess.DEVNULL,
303
306
  stderr=subprocess.PIPE if capture_stderr else subprocess.DEVNULL,
@@ -306,13 +309,13 @@ def _open_webview(
306
309
  # URL mode — direct load (used when shell_port not provided)
307
310
  script_lines = [
308
311
  "import sys, webview",
309
- "u, w, h, icon, ready_file = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5]",
312
+ "u, w, h, icon, ready_file, gui = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4], sys.argv[5], sys.argv[6]",
310
313
  "class Api:",
311
314
  " def set_title(self, title):",
312
315
  " try: webview.windows[0].set_title(str(title)[:240])",
313
316
  " except Exception: pass",
314
317
  "win = webview.create_window('ArrayView', u, width=w, height=h, background_color='#0c0c0c', js_api=Api())",
315
- "kw = {'gui': 'qt'} if sys.platform.startswith('linux') else {}",
318
+ "kw = {'gui': gui} if gui else {}",
316
319
  "def _start_func():",
317
320
  " if ready_file:",
318
321
  " try:",
@@ -344,6 +347,7 @@ def _open_webview(
344
347
  str(win_h),
345
348
  icon_path,
346
349
  ready_file or "",
350
+ gui_name,
347
351
  ],
348
352
  stdout=subprocess.DEVNULL,
349
353
  stderr=subprocess.PIPE if capture_stderr else subprocess.DEVNULL,
@@ -368,6 +372,7 @@ def _open_webview_with_fallback(
368
372
  sockets_before = (
369
373
  _session_mod.VIEWER_SOCKETS
370
374
  ) # capture count so we detect a NEW connection
375
+ shell_sockets_before = len(_session_mod.SHELL_SOCKETS)
371
376
 
372
377
  def _read_stderr():
373
378
  try:
@@ -390,14 +395,18 @@ def _open_webview_with_fallback(
390
395
  _open_browser(url, floating=floating)
391
396
  return
392
397
 
393
- # Phase 2: process is alive — wait up to 8 s for a NEW viewer WebSocket to connect.
394
- # We compare against sockets_before so an already-open browser tab doesn't
395
- # falsely confirm that the native window launched successfully.
398
+ # Phase 2: process is alive — wait up to 8 s for a NEW viewer or shell
399
+ # WebSocket. The native shell connection proves pywebview is usable;
400
+ # the embedded viewer iframe can legitimately connect later.
396
401
  import arrayview._session as _sm
397
402
 
398
403
  for _ in range(80):
399
404
  time.sleep(0.1)
400
- if _sm.VIEWER_SOCKETS > sockets_before:
405
+ shell_connected = (
406
+ shell_port is not None
407
+ and len(_sm.SHELL_SOCKETS) > shell_sockets_before
408
+ )
409
+ if _sm.VIEWER_SOCKETS > sockets_before or shell_connected:
401
410
  _vprint("[ArrayView] Native window connected successfully", flush=True)
402
411
  if sys.platform == "darwin":
403
412
  subprocess.Popen(
@@ -530,6 +539,21 @@ def _server_viewer_connections_seen(port: int, timeout: float = 0.5) -> int:
530
539
  return 0
531
540
 
532
541
 
542
+ def _server_shell_sockets_open(port: int, timeout: float = 0.5) -> int:
543
+ """Return the daemon's current native shell WebSocket count."""
544
+ url = f"http://{_LOOPBACK_HOST}:{port}/ping"
545
+ try:
546
+ with urllib.request.urlopen(url, timeout=timeout) as resp:
547
+ if resp.status != 200:
548
+ return 0
549
+ payload = json.loads(resp.read().decode("utf-8"))
550
+ if payload.get("ok") is True and payload.get("service") == "arrayview":
551
+ return int(payload.get("shell_sockets") or 0)
552
+ except Exception:
553
+ pass
554
+ return 0
555
+
556
+
533
557
  def _wait_for_viewer_connection(
534
558
  port: int,
535
559
  *,
@@ -545,6 +569,23 @@ def _wait_for_viewer_connection(
545
569
  return False
546
570
 
547
571
 
572
+ def _wait_for_native_shell_or_viewer_connection(
573
+ port: int,
574
+ *,
575
+ viewer_before: int,
576
+ timeout: float = 8.0,
577
+ ) -> bool:
578
+ """Wait until pywebview proves it is alive via shell or viewer WebSocket."""
579
+ deadline = time.monotonic() + timeout
580
+ while time.monotonic() < deadline:
581
+ if _server_viewer_connections_seen(port, timeout=0.3) > viewer_before:
582
+ return True
583
+ if _server_shell_sockets_open(port, timeout=0.3) > 0:
584
+ return True
585
+ time.sleep(0.1)
586
+ return False
587
+
588
+
548
589
  def _terminate_native_process(proc: subprocess.Popen | None) -> None:
549
590
  if proc is None or proc.poll() is not None:
550
591
  return
@@ -554,6 +595,64 @@ def _terminate_native_process(proc: subprocess.Popen | None) -> None:
554
595
  return
555
596
 
556
597
 
598
+ def _open_cli_native_shell_after_server(
599
+ *,
600
+ port: int,
601
+ sid: str,
602
+ name: str,
603
+ compare_sids: "_CompareSids | None",
604
+ win_w: int,
605
+ win_h: int,
606
+ ) -> bool:
607
+ """Open a CLI native shell and return whether it is usable."""
608
+ url_shell = _shell_url(port, sid, name, compare_sids=compare_sids)
609
+ viewer_before = _server_viewer_connections_seen(port)
610
+ opened, proc = _open_webview_cli_tracked(url_shell, win_w, win_h)
611
+ if opened and _wait_for_native_shell_or_viewer_connection(
612
+ port, viewer_before=viewer_before
613
+ ):
614
+ return True
615
+ if opened:
616
+ _vprint(
617
+ "[ArrayView] Native window did not connect to the backend; falling back to browser",
618
+ flush=True,
619
+ )
620
+ _terminate_native_process(proc)
621
+ return False
622
+
623
+
624
+ def _activate_early_cli_native_shell(
625
+ *,
626
+ port: int,
627
+ sid: str,
628
+ name: str,
629
+ proc: subprocess.Popen | None,
630
+ ) -> bool:
631
+ """Attach an already-started preload shell to a spawned daemon session."""
632
+ viewer_before = _server_viewer_connections_seen(port)
633
+ try:
634
+ notify_result = _notify_existing_session(
635
+ port,
636
+ sid,
637
+ name,
638
+ url=_viewer_path(sid),
639
+ wait=True,
640
+ )
641
+ notified = bool(notify_result.get("notified"))
642
+ except Exception:
643
+ notified = False
644
+ if notified and _wait_for_native_shell_or_viewer_connection(
645
+ port, viewer_before=viewer_before
646
+ ):
647
+ return True
648
+ _vprint(
649
+ "[ArrayView] Native window did not connect to the backend; falling back to browser",
650
+ flush=True,
651
+ )
652
+ _terminate_native_process(proc)
653
+ return False
654
+
655
+
557
656
  # ── Server Port Utilities ─────────────────────────────────────────
558
657
 
559
658
 
@@ -862,23 +961,21 @@ def _open_cli_existing_server_view(
862
961
  _vprint(f"Injected into existing window (port {port})")
863
962
  return
864
963
  if notify_webview and not notified:
865
- url_shell = _shell_url(port, sid, name, compare_sids=compare_sids)
866
- seen_before = _server_viewer_connections_seen(port)
867
- opened, proc = _open_webview_cli_tracked(url_shell, 1200, 800)
868
- if not opened or not _wait_for_viewer_connection(port, before=seen_before):
869
- if opened:
870
- _vprint(
871
- "[ArrayView] Native window did not connect to the backend; falling back to browser",
872
- flush=True,
873
- )
874
- _terminate_native_process(proc)
964
+ native_ready = _open_cli_native_shell_after_server(
965
+ port=port,
966
+ sid=sid,
967
+ name=name,
968
+ compare_sids=compare_sids,
969
+ win_w=1200,
970
+ win_h=800,
971
+ )
972
+ if not native_ready:
875
973
  _vprint("[ArrayView] Falling back to browser", flush=True)
876
974
  _print_viewer_location(url)
877
975
  _open_browser(
878
976
  url,
879
977
  blocking=True,
880
978
  title=f"ArrayView: {name}",
881
- filepath=base_file,
882
979
  floating=floating,
883
980
  )
884
981
  return
@@ -889,7 +986,6 @@ def _open_cli_existing_server_view(
889
986
  blocking=True,
890
987
  force_vscode=(window_mode == "vscode"),
891
988
  title=f"ArrayView: {name}",
892
- filepath=base_file,
893
989
  floating=floating,
894
990
  )
895
991
 
@@ -1056,7 +1152,6 @@ def _handle_cli_spawned_daemon(
1056
1152
 
1057
1153
  early_webview_opened = False
1058
1154
  early_webview_proc = None
1059
- early_webview_notified = False
1060
1155
  early_webview_connected = False
1061
1156
  if (
1062
1157
  use_webview
@@ -1083,29 +1178,12 @@ def _handle_cli_spawned_daemon(
1083
1178
  sys.exit(1)
1084
1179
 
1085
1180
  if early_webview_opened:
1086
- seen_before = _server_viewer_connections_seen(port)
1087
- try:
1088
- notify_result = _notify_existing_session(
1089
- port,
1090
- sid,
1091
- name,
1092
- url=_viewer_path(sid),
1093
- wait=True,
1094
- )
1095
- early_webview_notified = bool(notify_result.get("notified"))
1096
- except Exception:
1097
- early_webview_notified = False
1098
- if early_webview_notified:
1099
- early_webview_connected = _wait_for_viewer_connection(
1100
- port,
1101
- before=seen_before,
1102
- )
1103
- if not early_webview_connected:
1104
- _vprint(
1105
- "[ArrayView] Native window did not connect to the backend; falling back to browser",
1106
- flush=True,
1107
- )
1108
- _terminate_native_process(early_webview_proc)
1181
+ early_webview_connected = _activate_early_cli_native_shell(
1182
+ port=port,
1183
+ sid=sid,
1184
+ name=name,
1185
+ proc=early_webview_proc,
1186
+ )
1109
1187
 
1110
1188
  should_retry_webview = use_webview and not (
1111
1189
  early_webview_opened and not early_webview_connected
@@ -1154,16 +1232,15 @@ def _open_cli_spawned_view(
1154
1232
  if _should_notify_webview(use_webview, overlay_sid):
1155
1233
  if webview_already_opened:
1156
1234
  return
1157
- url_shell = _shell_url(port, sid, name, compare_sids=compare_sids)
1158
- seen_before = _server_viewer_connections_seen(port)
1159
- opened, proc = _open_webview_cli_tracked(url_shell, 1400, 900)
1160
- if not opened or not _wait_for_viewer_connection(port, before=seen_before):
1161
- if opened:
1162
- _vprint(
1163
- "[ArrayView] Native window did not connect to the backend; falling back to browser",
1164
- flush=True,
1165
- )
1166
- _terminate_native_process(proc)
1235
+ native_ready = _open_cli_native_shell_after_server(
1236
+ port=port,
1237
+ sid=sid,
1238
+ name=name,
1239
+ compare_sids=compare_sids,
1240
+ win_w=1400,
1241
+ win_h=900,
1242
+ )
1243
+ if not native_ready:
1167
1244
  _vprint("[ArrayView] Falling back to browser", flush=True)
1168
1245
  _print_viewer_location(url)
1169
1246
  _open_browser(
arrayview/_platform.py CHANGED
@@ -415,18 +415,23 @@ def _can_native_window() -> bool:
415
415
  # Plain SSH (no VS Code): the display is on the client machine, not here.
416
416
  if os.environ.get("SSH_CLIENT") or os.environ.get("SSH_CONNECTION"):
417
417
  return False
418
+ return _native_window_gui() is not None
419
+
420
+
421
+ def _native_window_gui() -> str | None:
422
+ """Return the pywebview GUI backend name to use, or None if unavailable."""
418
423
  if importlib.util.find_spec("webview") is None:
419
- return False
424
+ return None
420
425
  if sys.platform in ("darwin", "win32"):
421
- return True
422
- # Linux/BSD: need a display server AND pywebview's GUI bindings
426
+ return ""
427
+ # Linux/BSD: need a display server AND pywebview's GUI bindings.
423
428
  if not (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
424
- return False
425
-
426
- return (
427
- importlib.util.find_spec("qtpy") is not None
428
- or importlib.util.find_spec("gi") is not None
429
- )
429
+ return None
430
+ if importlib.util.find_spec("qtpy") is not None:
431
+ return "qt"
432
+ if importlib.util.find_spec("gi") is not None:
433
+ return "gtk"
434
+ return None
430
435
 
431
436
 
432
437
  # ---------------------------------------------------------------------------
arrayview/_server.py CHANGED
@@ -154,6 +154,7 @@ def ping():
154
154
  "hostname": socket.gethostname(),
155
155
  "viewer_sockets": _session_mod.VIEWER_SOCKETS,
156
156
  "viewer_connections_seen": _session_mod.VIEWER_CONNECTIONS_SEEN,
157
+ "shell_sockets": len(_session_mod.SHELL_SOCKETS),
157
158
  }
158
159
 
159
160
 
arrayview/_viewer.html CHANGED
@@ -11985,6 +11985,8 @@
11985
11985
  rectRoiMode = true;
11986
11986
  _roiShape = 'rect';
11987
11987
  _roiVisible = true;
11988
+ hideAllPixelInfo();
11989
+ _resetDimbarHover();
11988
11990
  canvas.style.cursor = 'crosshair';
11989
11991
  if (qmriActive) qmriViews.forEach(v => { v.canvas.style.cursor = 'crosshair'; });
11990
11992
  showStatus('ROI mode: rect · drag to draw');
@@ -11995,6 +11997,7 @@
11995
11997
 
11996
11998
  function _deactivateRoiInteraction({ clear = false } = {}) {
11997
11999
  rectRoiMode = false;
12000
+ _roiSuppressPixelInfo = false;
11998
12001
  _roiShape = 'rect';
11999
12002
  _roiFreehandPts = [];
12000
12003
  _floodFillTolerance = 0.1;
@@ -14518,6 +14521,11 @@
14518
14521
  }
14519
14522
  function showPixelInfo(el, payload, cx, cy) {
14520
14523
  if (!el || !_pixelInfoVisible) return;
14524
+ if (rectRoiMode && _roiSuppressPixelInfo) {
14525
+ el.style.display = 'none';
14526
+ delete el.dataset.pixelInfoKey;
14527
+ return;
14528
+ }
14521
14529
  const key = (payload && typeof payload === 'object')
14522
14530
  ? `${payload.valueText ?? ''}|${payload.rawX ?? ''}|${payload.rawY ?? ''}`
14523
14531
  : String(payload ?? '');
@@ -14530,6 +14538,7 @@
14530
14538
  }
14531
14539
 
14532
14540
  let _pixelHoverThrottle = false;
14541
+ let _roiSuppressPixelInfo = false;
14533
14542
  const _pixelFmt = (v, contextValues = null) => formatScalarValue(v, { contextValues, maxDecimals: 7, minDecimals: 3 });
14534
14543
  function _activeNumericContext(view = null) {
14535
14544
  const vals = view && view.displayState
@@ -14538,6 +14547,17 @@
14538
14547
  return vals.filter(Number.isFinite);
14539
14548
  }
14540
14549
  function fetchPixelInfo(opts) {
14550
+ if (rectRoiMode) {
14551
+ const roiIdx = _roiHitIndexForCanvasEvent(opts.event, opts.canvas, opts.imgW, opts.imgH);
14552
+ _roiSuppressPixelInfo = _roiHasStats(roiIdx);
14553
+ if (_roiSuppressPixelInfo) {
14554
+ if (opts.displayEl) {
14555
+ opts.displayEl.style.display = 'none';
14556
+ delete opts.displayEl.dataset.pixelInfoKey;
14557
+ }
14558
+ return false;
14559
+ }
14560
+ }
14541
14561
  const indices = Array.isArray(opts.indices) ? opts.indices : [];
14542
14562
  if (!indices.length || indices.some(v => !Number.isFinite(v))) return false;
14543
14563
  const rect = opts.canvas.getBoundingClientRect();
@@ -16259,6 +16279,20 @@
16259
16279
  }
16260
16280
  return false;
16261
16281
  }
16282
+ function _roiHasStats(idx) {
16283
+ return idx >= 0 && !!(_rois[idx] && (_rois[idx].stats || _rois[idx].qmriStats));
16284
+ }
16285
+ function _roiHitIndexForCanvasEvent(e, cv, imgW, imgH) {
16286
+ if (!rectRoiMode || !cv || !imgW || !imgH) return -1;
16287
+ const rect = cv.getBoundingClientRect();
16288
+ if (!rect.width || !rect.height) return -1;
16289
+ const cx = (e.clientX - rect.left) * imgW / rect.width;
16290
+ const cy = (e.clientY - rect.top) * imgH / rect.height;
16291
+ for (let i = _rois.length - 1; i >= 0; i--) {
16292
+ if (_pointInRoi(_rois[i], cx, cy)) return i;
16293
+ }
16294
+ return -1;
16295
+ }
16262
16296
  function _updateRoiPanel() {
16263
16297
  renderIsland();
16264
16298
  }
@@ -16533,25 +16567,27 @@
16533
16567
  for (let i = _rois.length - 1; i >= 0; i--) {
16534
16568
  if (_pointInRoi(_rois[i], cx, cy)) { hitIdx = i; break; }
16535
16569
  }
16536
- const piEl = document.getElementById('main-pixel-info');
16537
- if (hitIdx < 0 || (!_rois[hitIdx].stats && !_rois[hitIdx].qmriStats)) { ttip.style.display = 'none'; if (_pixelInfoVisible) piEl.style.display = ''; return; }
16570
+ _roiSuppressPixelInfo = _roiHasStats(hitIdx);
16571
+ if (!_roiSuppressPixelInfo) { ttip.style.display = 'none'; return; }
16538
16572
  const s = _rois[hitIdx].stats;
16539
16573
  const q = _rois[hitIdx].qmriStats;
16540
16574
  const c = _roiColors[hitIdx % _roiColors.length];
16541
16575
  if (Array.isArray(q) && q.length && q[0].stats) {
16542
- const ttipQ = document.getElementById('roi-stats-tooltip');
16576
+ const ttipQ = _getRoiRowTooltip();
16543
16577
  if (ttipQ) {
16544
16578
  ttipQ.innerHTML = _buildRoiTooltipContent(hitIdx);
16545
- ttipQ.style.display = 'block';
16546
16579
  _positionRoiRowTooltip({clientX: e.clientX, clientY: e.clientY});
16580
+ ttipQ.classList.add('visible');
16547
16581
  }
16548
- if (_pixelInfoVisible) piEl.style.display = 'none';
16582
+ const piEl = document.getElementById('main-pixel-info');
16583
+ if (_pixelInfoVisible && piEl) piEl.style.display = 'none';
16549
16584
  return;
16550
16585
  }
16551
16586
  const fmt = v => { const a = Math.abs(v); if (a === 0) return '0'; if (a >= 1e4 || (a < 1e-2 && a > 0)) return v.toExponential(3); return parseFloat(v.toPrecision(4)).toString(); };
16552
16587
  ttip.innerHTML = `<span style="color:${c.stroke};font-weight:bold">${hitIdx+1}</span> ${fmt(s.mean)} \u00B1 ${fmt(s.std)}`;
16553
16588
  ttip.style.display = 'block';
16554
- if (_pixelInfoVisible) piEl.style.display = 'none';
16589
+ const piEl = document.getElementById('main-pixel-info');
16590
+ if (_pixelInfoVisible && piEl) piEl.style.display = 'none';
16555
16591
  const par = ttip.offsetParent;
16556
16592
  if (par) {
16557
16593
  const pr = par.getBoundingClientRect();
@@ -16690,8 +16726,10 @@
16690
16726
  roiDragging = false;
16691
16727
  _drawAllRois(); // clear in-progress preview
16692
16728
  }
16729
+ _roiSuppressPixelInfo = false;
16693
16730
  const ttip = document.getElementById('roi-hover-tooltip');
16694
16731
  if (ttip) ttip.style.display = 'none';
16732
+ _hideRoiRowTooltip();
16695
16733
  const piEl = document.getElementById('main-pixel-info');
16696
16734
  if (piEl) piEl.style.display = 'none';
16697
16735
  });
@@ -23209,7 +23247,8 @@
23209
23247
  for (let ii = _rois.length - 1; ii >= 0; ii--) {
23210
23248
  if (_pointInRoi(_rois[ii], cx, cy)) { hitIdx = ii; break; }
23211
23249
  }
23212
- if (hitIdx < 0 || (!_rois[hitIdx].stats && !_rois[hitIdx].qmriStats)) {
23250
+ _roiSuppressPixelInfo = _roiHasStats(hitIdx);
23251
+ if (!_roiSuppressPixelInfo) {
23213
23252
  const t = document.getElementById('roi-hover-tooltip');
23214
23253
  if (t) t.style.display = 'none';
23215
23254
  if (_pixelInfoVisible && view.pixelInfoEl) view.pixelInfoEl.style.display = 'none';
@@ -23217,11 +23256,11 @@
23217
23256
  }
23218
23257
  const q = _rois[hitIdx].qmriStats;
23219
23258
  if (Array.isArray(q) && q.length && q[0].stats) {
23220
- const tq = document.getElementById('roi-stats-tooltip');
23259
+ const tq = _getRoiRowTooltip();
23221
23260
  if (tq) {
23222
23261
  tq.innerHTML = _buildRoiTooltipContent(hitIdx);
23223
- tq.classList.add('visible');
23224
23262
  _positionRoiRowTooltip({clientX: e.clientX, clientY: e.clientY});
23263
+ tq.classList.add('visible');
23225
23264
  }
23226
23265
  if (_pixelInfoVisible && view.pixelInfoEl) view.pixelInfoEl.style.display = 'none';
23227
23266
  return;
@@ -23237,6 +23276,7 @@
23237
23276
  }
23238
23277
  });
23239
23278
  cv.addEventListener('mouseleave', () => {
23279
+ _roiSuppressPixelInfo = false;
23240
23280
  const tq = document.getElementById('roi-stats-tooltip');
23241
23281
  if (tq) tq.classList.remove('visible');
23242
23282
  });
@@ -283,65 +283,23 @@ def _open_direct_via_shm(
283
283
  title: str | None = None,
284
284
  floating: bool = False,
285
285
  ) -> bool:
286
- """Write a direct-mode signal file with shared memory parameters.
286
+ """Compatibility shim for the canonical SHM direct-mode helper."""
287
+ import warnings
287
288
 
288
- Places the array in POSIX shared memory so the extension-spawned subprocess
289
- can read it without any disk I/O.
290
- """
291
- import atexit
292
- import sys as _sys
293
- from multiprocessing.shared_memory import SharedMemory
294
-
295
- import numpy as np
296
-
297
- arr = np.ascontiguousarray(data)
298
- shm = SharedMemory(create=True, size=arr.nbytes)
299
- np.ndarray(arr.shape, dtype=arr.dtype, buffer=shm.buf)[:] = arr
300
-
301
- # The subprocess will unlink the SHM after reading it. Unregister from
302
- # Python's resource tracker so it doesn't warn about "leaked" SHM at exit.
303
- from multiprocessing import resource_tracker
304
- try:
305
- resource_tracker.unregister(f"/{shm.name}", "shared_memory")
306
- except Exception:
307
- pass
308
-
309
- # Keep the shm alive until the subprocess reads it (or this process exits).
310
- _ACTIVE_SHM.append(shm)
289
+ from arrayview._vscode_shm import _open_direct_via_shm as _canonical_open_direct_via_shm
311
290
 
312
- def _cleanup():
313
- try:
314
- shm.close()
315
- except Exception:
316
- pass
317
- try:
318
- shm.unlink()
319
- except Exception:
320
- pass
321
-
322
- atexit.register(_cleanup)
323
-
324
- payload: dict = {
325
- "action": "open-preview",
326
- "mode": "direct",
327
- "shm": {
328
- "name": shm.name,
329
- "shape": ",".join(str(int(s)) for s in arr.shape),
330
- "dtype": str(arr.dtype),
331
- },
332
- "arrayName": name,
333
- "pythonPath": _sys.executable,
334
- "maxAgeMs": _VSCODE_SIGNAL_MAX_AGE_MS,
335
- }
336
- if title:
337
- payload["title"] = title
338
- if floating:
339
- payload["floating"] = True
340
- return _write_vscode_signal(payload, skip_compat=True)
341
-
342
-
343
- # Shared memory blocks kept alive until process exit or subprocess reads them.
344
- _ACTIVE_SHM: list = []
291
+ warnings.warn(
292
+ "arrayview._vscode_signal._open_direct_via_shm is deprecated; "
293
+ "use arrayview._vscode_shm._open_direct_via_shm",
294
+ DeprecationWarning,
295
+ stacklevel=2,
296
+ )
297
+ return _canonical_open_direct_via_shm(
298
+ data,
299
+ name=name,
300
+ title=title,
301
+ floating=floating,
302
+ )
345
303
 
346
304
 
347
305
  def _schedule_remote_open_retries(
@@ -772,3 +730,19 @@ def _write_vscode_signal(payload: dict, delay: float = 0.0, skip_compat: bool =
772
730
  return True
773
731
  except Exception:
774
732
  return False
733
+
734
+
735
+ def __getattr__(name: str):
736
+ if name == "_ACTIVE_SHM":
737
+ import warnings
738
+
739
+ from arrayview._vscode_shm import _ACTIVE_SHM
740
+
741
+ warnings.warn(
742
+ "arrayview._vscode_signal._ACTIVE_SHM is deprecated; "
743
+ "use arrayview._vscode_shm._ACTIVE_SHM",
744
+ DeprecationWarning,
745
+ stacklevel=2,
746
+ )
747
+ return _ACTIVE_SHM
748
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.26.2
3
+ Version: 0.26.3
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -9,10 +9,11 @@ arrayview/_diff.py,sha256=mSIo1ArNxkrB0v2iUMy3dezJOlpos0xNu1V_umNUMhU,5677
9
9
  arrayview/_icon.png,sha256=a8RTAbvweLhh3hzD5eGg76Peq-KSDcS6YrCphRb_HEs,2748
10
10
  arrayview/_imaging.py,sha256=MQX1uy4m1lofOeoLDhCp6cHO5p_V5fXm4qlSu9Ykn-c,534
11
11
  arrayview/_io.py,sha256=z9gFRI7OneCnm5jBTLwdkDARrHF-LyqHspksYQx1lTg,11585
12
- arrayview/_launcher.py,sha256=oToBEnso80kbGNT0G9XB7xeRd7XWs_Hi-F53wLe-68E,136733
12
+ arrayview/_launch_plan.py,sha256=kiWitWvKe2Wetj_pS1M8OtaSqZ11aWF6KDDD6IPnKIM,7393
13
+ arrayview/_launcher.py,sha256=WhS-rgV0fwmeldUQfcs68AkG1alD04-UpmH93_ws_50,138470
13
14
  arrayview/_lifecycle.py,sha256=oFD05gNYAu6OBIUFcKmPfT-_Hp03KaI2oK8komUXtJY,937
14
15
  arrayview/_overlays.py,sha256=C-qDZFBOgwJQL84XxFoMAX3qyfNa2EI5N-AlOvXeoTI,1634
15
- arrayview/_platform.py,sha256=xyEksBuhg4rmHE2IKxIXt0o2J_yCbfEJfR_01micXOo,18231
16
+ arrayview/_platform.py,sha256=gWZ5_yPN1B_kqiUewRl0lKjOZmB6jGw7X71hmFxJ_PQ,18428
16
17
  arrayview/_render.py,sha256=UCUx8YD8Q38GDTajHhnmEMUUUali-8PLvoBOHvUmaYc,28684
17
18
  arrayview/_routes_analysis.py,sha256=ipws39Q0e6ZtYsxXk4ZcbRKlD7LPAt_Iehf_kDjo1Yo,16272
18
19
  arrayview/_routes_export.py,sha256=Ha8mMO2E9TlRL5x25-ADV8_8xUiC-jRz9N1oWwgKOsg,3898
@@ -26,23 +27,23 @@ arrayview/_routes_state.py,sha256=TmxC__NPPsvg59jM2D_FRc_oPv1wWbRmUr6t6ukVj64,11
26
27
  arrayview/_routes_vectorfield.py,sha256=P8avWU8jDH173YQ4Kfpzs8SZz5gIL4ki0-Gc6pGA9w4,7383
27
28
  arrayview/_routes_websocket.py,sha256=7mKy52OyCSRIGcsKx02VWGY1qTxGjB22RVeS2Ev833A,16092
28
29
  arrayview/_segmentation.py,sha256=aENWHmefrKDScXhcZL1X3KgYXE784dpMkmFFo83qnrw,7377
29
- arrayview/_server.py,sha256=HpgoSxW76Rp8q_BnBBtuHdrpmdzHfxVRphSoydtTkR8,7733
30
+ arrayview/_server.py,sha256=x9_B3Su5BGxfpEho3RknXeG06_QIrvMIhmSU0SaEWKU,7791
30
31
  arrayview/_session.py,sha256=OD3f2Q2gubZt1ftoIwfPX3SfKmCvPYy_FbplmmSnLSg,17427
31
32
  arrayview/_shell.html,sha256=xzHIB-kRX12okubZoqdrgsWDY8CCLteg5jtXxDQru8U,11624
32
33
  arrayview/_stdio_server.py,sha256=ECwUGHuO6KUx4TlQPPzqFZoL48HOTFc0n2ckCPo69Mc,33954
33
34
  arrayview/_synthetic_mri.py,sha256=zb613sIke-yljrILKYn_f-1SB6T4XsB4f916STgAp4M,6756
34
35
  arrayview/_torch.py,sha256=13sqIMCUu_-b9SQ9KfDCExDz2UMStL87EDVjnbJxAEo,7536
35
36
  arrayview/_vectorfield.py,sha256=eDqMCMEGjO8Jdt-6Auh01cz3WD9dbF7xYUWD9xHAf1E,8115
36
- arrayview/_viewer.html,sha256=3-IDDxLCkeMbuIp31dR1ae8hi0ulq2QX96P8QbUyA0E,1538008
37
+ arrayview/_viewer.html,sha256=GuK-5KpSOpAvrkMiEiQZ1xs1v-Au8HyYQgc2-eQm0iE,1539699
37
38
  arrayview/_vscode.py,sha256=laTHzOL6YnUsZYFX0fAR3aAMBFpPLL6cF9KArBl3nAI,988
38
39
  arrayview/_vscode_browser.py,sha256=EzYEl4ZdxD_njqIfHywqw3ovy3p2SaWX9c03Kzd_Xh0,7458
39
40
  arrayview/_vscode_extension.py,sha256=v17e4IVJkTpHmuJMhuL2VPPWwbmsCCDWnbEzdONwH3I,12953
40
41
  arrayview/_vscode_shm.py,sha256=Rm5DapeorNwhnLnVz5fkXcctkQqTzii9uLxnOxxIMU4,2238
41
- arrayview/_vscode_signal.py,sha256=rSDPEjFeX4dl2PnlBMlVsFO_lmRhgJ7eHlNmNDY4h9U,33840
42
+ arrayview/_vscode_signal.py,sha256=xy7HcQdLrbcUSDLuDX5pERzwrfHtKfaJ2nry_Su76tI,33072
42
43
  arrayview/arrayview-opener.vsix,sha256=uxIm_06uI9RC7N54SpckwhkjIh0yKLXDv613hvJTgV0,19098
43
44
  arrayview/gsap.min.js,sha256=VGihm4idI0E1uqrWezWsiScB9F_jMosn7qs6pPSkeac,72304
44
- arrayview-0.26.2.dist-info/METADATA,sha256=sazLVlN3xN1zUgSTLeUfNDe26FsnvIVDaZ7chzzuSEI,2161
45
- arrayview-0.26.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
46
- arrayview-0.26.2.dist-info/entry_points.txt,sha256=FQSz3M11B-zaVLDyXofKUDvMhHASz7OjmoLgI4nn7ko,105
47
- arrayview-0.26.2.dist-info/licenses/LICENSE,sha256=34OivjM9faB2QaPjZpMRTIiSx31V3U9cEyQ6F2k-bH0,1062
48
- arrayview-0.26.2.dist-info/RECORD,,
45
+ arrayview-0.26.3.dist-info/METADATA,sha256=Fp9WT0LXPGkC7qk8uUxly06qYvYxH9050iIFKhK_73Y,2161
46
+ arrayview-0.26.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
47
+ arrayview-0.26.3.dist-info/entry_points.txt,sha256=FQSz3M11B-zaVLDyXofKUDvMhHASz7OjmoLgI4nn7ko,105
48
+ arrayview-0.26.3.dist-info/licenses/LICENSE,sha256=34OivjM9faB2QaPjZpMRTIiSx31V3U9cEyQ6F2k-bH0,1062
49
+ arrayview-0.26.3.dist-info/RECORD,,