arrayview 0.12.2__tar.gz → 0.13.0__tar.gz

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 (72) hide show
  1. {arrayview-0.12.2 → arrayview-0.13.0}/PKG-INFO +1 -1
  2. {arrayview-0.12.2 → arrayview-0.13.0}/pyproject.toml +1 -1
  3. {arrayview-0.12.2 → arrayview-0.13.0}/scripts/release.sh +1 -1
  4. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_launcher.py +79 -18
  5. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_stdio_server.py +278 -1
  6. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_vscode.py +60 -9
  7. arrayview-0.13.0/src/arrayview/arrayview-opener.vsix +0 -0
  8. {arrayview-0.12.2 → arrayview-0.13.0}/uv.lock +1 -1
  9. {arrayview-0.12.2 → arrayview-0.13.0}/vscode-extension/extension.js +151 -39
  10. arrayview-0.13.0/vscode-extension/package.json +53 -0
  11. arrayview-0.12.2/src/arrayview/arrayview-opener.vsix +0 -0
  12. arrayview-0.12.2/vscode-extension/package.json +0 -30
  13. {arrayview-0.12.2 → arrayview-0.13.0}/.claude/skills/invocation-consistency/SKILL.md +0 -0
  14. {arrayview-0.12.2 → arrayview-0.13.0}/.claude/skills/modes-consistency/SKILL.md +0 -0
  15. {arrayview-0.12.2 → arrayview-0.13.0}/.claude/skills/ui-consistency-audit/SKILL.md +0 -0
  16. {arrayview-0.12.2 → arrayview-0.13.0}/.claude/skills/viewer-ui-checklist/SKILL.md +0 -0
  17. {arrayview-0.12.2 → arrayview-0.13.0}/.claude/skills/visual-bug-fixing/SKILL.md +0 -0
  18. {arrayview-0.12.2 → arrayview-0.13.0}/.github/workflows/docs.yml +0 -0
  19. {arrayview-0.12.2 → arrayview-0.13.0}/.github/workflows/python-publish.yml +0 -0
  20. {arrayview-0.12.2 → arrayview-0.13.0}/.gitignore +0 -0
  21. {arrayview-0.12.2 → arrayview-0.13.0}/.python-version +0 -0
  22. {arrayview-0.12.2 → arrayview-0.13.0}/AGENTS.md +0 -0
  23. {arrayview-0.12.2 → arrayview-0.13.0}/CONTRIBUTING.md +0 -0
  24. {arrayview-0.12.2 → arrayview-0.13.0}/LICENSE +0 -0
  25. {arrayview-0.12.2 → arrayview-0.13.0}/README.md +0 -0
  26. {arrayview-0.12.2 → arrayview-0.13.0}/docs/comparing.md +0 -0
  27. {arrayview-0.12.2 → arrayview-0.13.0}/docs/configuration.md +0 -0
  28. {arrayview-0.12.2 → arrayview-0.13.0}/docs/display.md +0 -0
  29. {arrayview-0.12.2 → arrayview-0.13.0}/docs/index.md +0 -0
  30. {arrayview-0.12.2 → arrayview-0.13.0}/docs/loading.md +0 -0
  31. {arrayview-0.12.2 → arrayview-0.13.0}/docs/logo.png +0 -0
  32. {arrayview-0.12.2 → arrayview-0.13.0}/docs/measurement.md +0 -0
  33. {arrayview-0.12.2 → arrayview-0.13.0}/docs/remote.md +0 -0
  34. {arrayview-0.12.2 → arrayview-0.13.0}/docs/stylesheets/extra.css +0 -0
  35. {arrayview-0.12.2 → arrayview-0.13.0}/docs/viewing.md +0 -0
  36. {arrayview-0.12.2 → arrayview-0.13.0}/matlab/arrayview.m +0 -0
  37. {arrayview-0.12.2 → arrayview-0.13.0}/mkdocs.yml +0 -0
  38. {arrayview-0.12.2 → arrayview-0.13.0}/plans/webview/LOG.md +0 -0
  39. {arrayview-0.12.2 → arrayview-0.13.0}/scripts/demo.py +0 -0
  40. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/ARCHITECTURE.md +0 -0
  41. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/__init__.py +0 -0
  42. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/__main__.py +0 -0
  43. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_app.py +0 -0
  44. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_config.py +0 -0
  45. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_icon.png +0 -0
  46. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_io.py +0 -0
  47. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_platform.py +0 -0
  48. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_render.py +0 -0
  49. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_segmentation.py +0 -0
  50. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_server.py +0 -0
  51. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_session.py +0 -0
  52. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_shell.html +0 -0
  53. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_torch.py +0 -0
  54. {arrayview-0.12.2 → arrayview-0.13.0}/src/arrayview/_viewer.html +0 -0
  55. {arrayview-0.12.2 → arrayview-0.13.0}/tests/conftest.py +0 -0
  56. {arrayview-0.12.2 → arrayview-0.13.0}/tests/make_vectorfield_test_arrays.py +0 -0
  57. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_api.py +0 -0
  58. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_browser.py +0 -0
  59. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_cli.py +0 -0
  60. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_command_reachability.py +0 -0
  61. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_config.py +0 -0
  62. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_interactions.py +0 -0
  63. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_large_arrays.py +0 -0
  64. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_mode_consistency.py +0 -0
  65. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_mode_matrix.py +0 -0
  66. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_mode_roundtrip.py +0 -0
  67. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_nifti_meta.py +0 -0
  68. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_rgb_pixel_art.py +0 -0
  69. {arrayview-0.12.2 → arrayview-0.13.0}/tests/test_torch.py +0 -0
  70. {arrayview-0.12.2 → arrayview-0.13.0}/tests/ui_audit.py +0 -0
  71. {arrayview-0.12.2 → arrayview-0.13.0}/tests/visual_smoke.py +0 -0
  72. {arrayview-0.12.2 → arrayview-0.13.0}/vscode-extension/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.12.2
3
+ Version: 0.13.0
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.12.2"
7
+ version = "0.13.0"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -66,7 +66,7 @@ run() {
66
66
  }
67
67
 
68
68
  # --- Generate release notes ---
69
- CLAUDE_BIN="${CLAUDE_BIN:-/Users/oscar/.local/bin/claude}"
69
+ CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude 2>/dev/null || echo claude)}"
70
70
  PREV_TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "")
71
71
  NOTES=""
72
72
 
@@ -2102,24 +2102,67 @@ def arrayview():
2102
2102
  from arrayview._io import load_data
2103
2103
  from arrayview._session import SESSIONS, Session
2104
2104
 
2105
- for file_path in args.files:
2106
- data = load_data(file_path)
2107
- session = Session(
2108
- data=data,
2109
- filepath=file_path,
2110
- name=_Path(file_path).name,
2105
+ # First file is the main array
2106
+ base_path = args.files[0]
2107
+ data = load_data(base_path)
2108
+ session = Session(
2109
+ data=data,
2110
+ filepath=base_path,
2111
+ name=_Path(base_path).name,
2112
+ )
2113
+ if getattr(args, "rgb", False):
2114
+ _setup_rgb(session)
2115
+
2116
+ # Attach vector field if provided
2117
+ if getattr(args, "vectorfield", None):
2118
+ from arrayview._server import _configure_vectorfield
2119
+
2120
+ vf_data = load_data(args.vectorfield)
2121
+ _configure_vectorfield(
2122
+ session, vf_data,
2123
+ getattr(args, "vectorfield_components_dim", None),
2111
2124
  )
2112
- if getattr(args, "rgb", False):
2113
- _setup_rgb(session)
2114
- SESSIONS[session.sid] = session
2115
- info = json.dumps(
2116
- {
2117
- "sid": session.sid,
2118
- "name": session.name,
2119
- "shape": [int(s) for s in session.shape],
2120
- }
2125
+
2126
+ SESSIONS[session.sid] = session
2127
+
2128
+ # Load overlay as a separate session
2129
+ overlay_sid = None
2130
+ if getattr(args, "overlay", None):
2131
+ ov_data = load_data(args.overlay)
2132
+ ov_session = Session(
2133
+ data=ov_data,
2134
+ filepath=args.overlay,
2135
+ name="overlay",
2121
2136
  )
2122
- print(f"SESSION:{info}", file=sys.stderr)
2137
+ SESSIONS[ov_session.sid] = ov_session
2138
+ overlay_sid = ov_session.sid
2139
+
2140
+ # Load compare files (additional positional args + --compare)
2141
+ compare_sids = []
2142
+ compare_paths = list(args.files[1:])
2143
+ if getattr(args, "compare", None):
2144
+ compare_paths.append(args.compare)
2145
+ for cp in compare_paths:
2146
+ cmp_data = load_data(cp)
2147
+ cmp_session = Session(
2148
+ data=cmp_data,
2149
+ filepath=cp,
2150
+ name=_Path(cp).name,
2151
+ )
2152
+ SESSIONS[cmp_session.sid] = cmp_session
2153
+ compare_sids.append(cmp_session.sid)
2154
+
2155
+ info = json.dumps(
2156
+ {
2157
+ "sid": session.sid,
2158
+ "name": session.name,
2159
+ "shape": [int(s) for s in session.shape],
2160
+ "has_vectorfield": session.vfield is not None,
2161
+ "overlay_sid": overlay_sid,
2162
+ "compare_sids": compare_sids or None,
2163
+ }
2164
+ )
2165
+ print(f"SESSION:{info}", file=sys.stderr)
2123
2166
  run_stdio_server()
2124
2167
  return
2125
2168
 
@@ -2662,9 +2705,27 @@ def arrayview():
2662
2705
  # WebSocket server entirely. The extension spawns a Python subprocess
2663
2706
  # with --mode stdio and bridges via postMessage — no ports needed.
2664
2707
  is_remote = _is_vscode_remote()
2665
- if is_remote and not args.overlay and not compare_files and not getattr(args, 'vectorfield', None):
2708
+ if is_remote:
2666
2709
  _ensure_vscode_extension()
2667
- _open_direct_via_signal_file(base_file, title=f"ArrayView: {name}")
2710
+ # Build generic extra CLI args so new flags work without touching the
2711
+ # VS Code extension — they're forwarded verbatim to the subprocess.
2712
+ extra_args: list[str] = []
2713
+ if args.vectorfield:
2714
+ extra_args += ["--vectorfield", os.path.abspath(args.vectorfield)]
2715
+ if vfield_components_dim is not None:
2716
+ extra_args += ["--vectorfield-components-dim", str(vfield_components_dim)]
2717
+ if args.overlay:
2718
+ extra_args += ["--overlay", os.path.abspath(args.overlay)]
2719
+ if args.rgb:
2720
+ extra_args.append("--rgb")
2721
+ # Compare files: pass as additional positional args
2722
+ for cf in compare_files:
2723
+ extra_args.append(cf)
2724
+ _open_direct_via_signal_file(
2725
+ base_file,
2726
+ title=f"ArrayView: {name}",
2727
+ extra_args=extra_args or None,
2728
+ )
2668
2729
  return
2669
2730
 
2670
2731
  sid = uuid.uuid4().hex
@@ -24,7 +24,10 @@ import numpy as np
24
24
 
25
25
  from arrayview._io import load_data, load_data_with_meta
26
26
  from arrayview._render import (
27
+ LUTS,
27
28
  _composite_overlay_mask,
29
+ _compute_vmin_vmax,
30
+ _ensure_lut,
28
31
  _extract_overlay_mask,
29
32
  _init_luts,
30
33
  _overlay_is_label_map,
@@ -32,6 +35,7 @@ from arrayview._render import (
32
35
  apply_complex_mode,
33
36
  extract_projection,
34
37
  extract_slice,
38
+ mosaic_shape,
35
39
  render_mosaic,
36
40
  render_projection_rgba,
37
41
  render_rgb_rgba,
@@ -485,6 +489,258 @@ def _handle_lebesgue(sid: str, params: dict[str, str]) -> None:
485
489
  })
486
490
 
487
491
 
492
+ def _handle_vectorfield_fetch(sid: str, params: dict) -> None:
493
+ """Return downsampled vector field arrows for a 2-D view."""
494
+ session = SESSIONS.get(sid)
495
+ if not session or session.vfield is None:
496
+ _write_json({"error": "no vectorfield"})
497
+ return
498
+ dim_x = int(params.get("dim_x", 0))
499
+ dim_y = int(params.get("dim_y", 0))
500
+ indices = params.get("indices", "")
501
+ idx_tuple = tuple(int(x) for x in indices.split(",")) if indices else ()
502
+ t_index = int(params.get("t_index", 0))
503
+ density_offset = int(params.get("density_offset", 0))
504
+ result = _compute_vfield_arrows(session, dim_x, dim_y, idx_tuple, t_index, density_offset)
505
+ if result is None:
506
+ _write_json({"error": "vectorfield computation failed"})
507
+ return
508
+ _write_json({
509
+ "arrows": result["arrows"].tolist(),
510
+ "scale": result["scale"],
511
+ "stride": result["stride"],
512
+ })
513
+
514
+
515
+ def _handle_pixel(sid: str, params: dict) -> None:
516
+ """Return the raw value at a single pixel."""
517
+ session = SESSIONS.get(sid)
518
+ if not session:
519
+ _write_json({"error": "session not found"})
520
+ return
521
+ if session.rgb_axis is not None:
522
+ _write_json({"value": None})
523
+ return
524
+ dim_x = int(params.get("dim_x", 0))
525
+ dim_y = int(params.get("dim_y", 0))
526
+ indices = params.get("indices", "")
527
+ idx_tuple = tuple(int(x) for x in indices.split(",")) if indices else ()
528
+ px = int(params.get("px", 0))
529
+ py = int(params.get("py", 0))
530
+ complex_mode = int(params.get("complex_mode", 0))
531
+ raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
532
+ data = apply_complex_mode(raw, complex_mode)
533
+ h, w = data.shape
534
+ if 0 <= py < h and 0 <= px < w:
535
+ v = data[py, px]
536
+ val = None if np.isnan(v) or np.isinf(v) else float(v)
537
+ else:
538
+ val = None
539
+ _write_json({"value": val})
540
+
541
+
542
+ # ---------------------------------------------------------------------------
543
+ # Diff / Compare helpers
544
+ # ---------------------------------------------------------------------------
545
+
546
+ def _render_normalized(session, dim_x, dim_y, idx_tuple, dr, complex_mode, log_scale):
547
+ """Extract a slice and normalize to [0, 1] float32."""
548
+ raw = extract_slice(session, dim_x, dim_y, list(idx_tuple))
549
+ data, vmin, vmax = _prepare_display(session, raw, complex_mode, dr, log_scale)
550
+ if vmax > vmin:
551
+ normalized = np.clip((data - vmin) / (vmax - vmin), 0, 1)
552
+ else:
553
+ normalized = np.zeros_like(data)
554
+ return normalized.astype(np.float32)
555
+
556
+
557
+ def _render_normalized_mosaic(session, dim_x, dim_y, dim_z, idx_tuple, dr, complex_mode, log_scale):
558
+ """Return (float32 normalized mosaic [0,1], nan_mask)."""
559
+ n = session.shape[dim_z]
560
+ idx_list = list(idx_tuple)
561
+ frames_raw = [
562
+ extract_slice(
563
+ session, dim_x, dim_y,
564
+ [i if j == dim_z else idx_list[j] for j in range(len(session.shape))],
565
+ )
566
+ for i in range(n)
567
+ ]
568
+ frames = [apply_complex_mode(f, complex_mode) for f in frames_raw]
569
+ if log_scale:
570
+ frames = [np.log1p(np.abs(f)).astype(np.float32) for f in frames]
571
+ all_data = np.stack(frames)
572
+ if log_scale:
573
+ vmin = float(np.percentile(all_data, 1))
574
+ vmax = float(np.percentile(all_data, 99))
575
+ else:
576
+ vmin, vmax = _compute_vmin_vmax(session, all_data, dr, complex_mode)
577
+ rows, cols = mosaic_shape(n)
578
+ H, W = frames[0].shape
579
+ GAP = 2
580
+ total_h = rows * H + (rows - 1) * GAP
581
+ total_w = cols * W + (cols - 1) * GAP
582
+ grid = np.full((total_h, total_w), np.nan, dtype=np.float32)
583
+ for k in range(n):
584
+ r, c = divmod(k, cols)
585
+ r0, c0 = r * (H + GAP), c * (W + GAP)
586
+ grid[r0 : r0 + H, c0 : c0 + W] = all_data[k]
587
+ nan_mask = np.isnan(grid)
588
+ if vmax > vmin:
589
+ normalized = np.clip(np.where(nan_mask, 0.0, (grid - vmin) / (vmax - vmin)), 0, 1)
590
+ else:
591
+ normalized = np.zeros_like(grid)
592
+ return normalized.astype(np.float32), nan_mask
593
+
594
+
595
+ def _compute_diff(session_a, session_b, dim_x, dim_y, indices, dim_z, dr, complex_mode, log_scale, diff_mode):
596
+ """Shared diff logic for both diff image and diff histogram handlers.
597
+
598
+ Returns (raw_diff, vmin, vmax, colormap, nan_mask_or_None).
599
+ """
600
+ idx_tuple = tuple(int(x) for x in indices.split(",")) if isinstance(indices, str) else indices
601
+ ndim_a = len(session_a.shape)
602
+ ndim_b = len(session_b.shape)
603
+ idx_a = idx_tuple[:ndim_a]
604
+ idx_b = idx_tuple[:ndim_b]
605
+ nan_mask = None
606
+
607
+ if dim_z >= 0:
608
+ a, nan_mask_a = _render_normalized_mosaic(session_a, dim_x, dim_y, dim_z, idx_a, dr, complex_mode, log_scale)
609
+ b, nan_mask_b = _render_normalized_mosaic(session_b, dim_x, dim_y, dim_z, idx_b, dr, complex_mode, log_scale)
610
+ nan_mask = nan_mask_a | nan_mask_b
611
+ else:
612
+ a = _render_normalized(session_a, dim_x, dim_y, idx_a, dr, complex_mode, log_scale)
613
+ b = _render_normalized(session_b, dim_x, dim_y, idx_b, dr, complex_mode, log_scale)
614
+
615
+ # Resize b to match a if shapes differ
616
+ if a.shape != b.shape:
617
+ b_img = _pil_image().fromarray((b * 255).astype(np.uint8), mode="L")
618
+ b_img = b_img.resize((a.shape[1], a.shape[0]), _pil_image().BILINEAR)
619
+ b = np.array(b_img, dtype=np.float32) / 255.0
620
+
621
+ if diff_mode == 1:
622
+ raw = a - b
623
+ vmin, vmax = -1.0, 1.0
624
+ colormap = "RdBu_r"
625
+ elif diff_mode == 2:
626
+ raw = np.abs(a - b)
627
+ vmax = float(raw.max()) or 1.0
628
+ vmin = 0.0
629
+ colormap = "afmhot"
630
+ else:
631
+ raw = np.abs(a - b) / np.maximum(np.abs(a), 1e-6)
632
+ raw = np.clip(raw, 0.0, 2.0).astype(np.float32)
633
+ vmax = float(raw.max()) or 1.0
634
+ vmin = 0.0
635
+ colormap = "afmhot"
636
+
637
+ return raw, vmin, vmax, colormap, nan_mask
638
+
639
+
640
+ def _handle_diff(sid_a: str, sid_b: str, params: dict) -> None:
641
+ """Render a diff image and return as base64 JPEG with metadata headers."""
642
+ import base64
643
+ import io
644
+
645
+ session_a = SESSIONS.get(sid_a)
646
+ session_b = SESSIONS.get(sid_b)
647
+ if not session_a or not session_b:
648
+ _write_json({"error": "session not found"})
649
+ return
650
+
651
+ dim_x = int(params.get("dim_x", 0))
652
+ dim_y = int(params.get("dim_y", 0))
653
+ indices = params.get("indices", "")
654
+ dim_z = int(params.get("dim_z", -1))
655
+ dr = int(params.get("dr", 1))
656
+ complex_mode = int(params.get("complex_mode", 0))
657
+ log_scale = params.get("log_scale", "false").lower() == "true"
658
+ diff_mode = int(params.get("diff_mode", 1))
659
+ diff_colormap = params.get("diff_colormap", "")
660
+ _vmin_ov = params.get("vmin_override")
661
+ _vmax_ov = params.get("vmax_override")
662
+ vmin_override = float(_vmin_ov) if _vmin_ov is not None else None
663
+ vmax_override = float(_vmax_ov) if _vmax_ov is not None else None
664
+
665
+ try:
666
+ raw, vmin, vmax, colormap, nan_mask = _compute_diff(
667
+ session_a, session_b, dim_x, dim_y, indices,
668
+ dim_z, dr, complex_mode, log_scale, diff_mode,
669
+ )
670
+ except Exception as e:
671
+ _write_json({"error": str(e)})
672
+ return
673
+
674
+ if vmin_override is not None:
675
+ vmin = vmin_override
676
+ if vmax_override is not None:
677
+ vmax = vmax_override
678
+ if diff_colormap and _ensure_lut(diff_colormap):
679
+ colormap = diff_colormap
680
+
681
+ if vmax > vmin:
682
+ normalized = np.clip((raw - vmin) / (vmax - vmin), 0, 1)
683
+ else:
684
+ normalized = np.zeros_like(raw)
685
+ _ensure_lut(colormap)
686
+ lut = LUTS.get(colormap, LUTS["gray"])
687
+ rgba = lut[(normalized * 255).astype(np.uint8)]
688
+ if nan_mask is not None and nan_mask.shape == rgba.shape[:2]:
689
+ rgba[nan_mask] = [22, 22, 22, 255]
690
+
691
+ img = _pil_image().fromarray(rgba[:, :, :3], mode="RGB")
692
+ buf = io.BytesIO()
693
+ img.save(buf, format="JPEG", quality=90)
694
+
695
+ _write_json({
696
+ "_binary": True,
697
+ "data": base64.b64encode(buf.getvalue()).decode("ascii"),
698
+ "headers": {
699
+ "X-ArrayView-Vmin": str(vmin),
700
+ "X-ArrayView-Vmax": str(vmax),
701
+ "X-ArrayView-Colormap": colormap,
702
+ },
703
+ })
704
+
705
+
706
+ def _handle_diff_histogram(sid_a: str, sid_b: str, params: dict) -> None:
707
+ """Compute histogram of the diff between two sessions."""
708
+ session_a = SESSIONS.get(sid_a)
709
+ session_b = SESSIONS.get(sid_b)
710
+ if not session_a or not session_b:
711
+ _write_json({"error": "session not found"})
712
+ return
713
+
714
+ dim_x = int(params.get("dim_x", 0))
715
+ dim_y = int(params.get("dim_y", 0))
716
+ indices = params.get("indices", "")
717
+ dim_z = int(params.get("dim_z", -1))
718
+ dr = int(params.get("dr", 1))
719
+ complex_mode = int(params.get("complex_mode", 0))
720
+ log_scale = params.get("log_scale", "false").lower() == "true"
721
+ diff_mode = int(params.get("diff_mode", 1))
722
+ bins = int(params.get("bins", 64))
723
+
724
+ try:
725
+ raw, _, _, _, _ = _compute_diff(
726
+ session_a, session_b, dim_x, dim_y, indices,
727
+ dim_z, dr, complex_mode, log_scale, diff_mode,
728
+ )
729
+ except Exception as e:
730
+ _write_json({"error": str(e)})
731
+ return
732
+
733
+ vmin = float(raw.min())
734
+ vmax = float(raw.max())
735
+ counts, edges = np.histogram(raw.ravel(), bins=bins)
736
+ _write_json({
737
+ "counts": counts.tolist(),
738
+ "edges": edges.tolist(),
739
+ "vmin": vmin,
740
+ "vmax": vmax,
741
+ })
742
+
743
+
488
744
  def _handle_fetch_proxy(msg: dict) -> None:
489
745
  """Handle proxied fetch requests from the viewer."""
490
746
  endpoint = msg.get("endpoint", "")
@@ -516,6 +772,14 @@ def _handle_fetch_proxy(msg: dict) -> None:
516
772
  _write_json({"status": "started"})
517
773
  elif route == "preload_status" and sid:
518
774
  _write_json({"done": 0, "total": 0, "skipped": True})
775
+ elif route == "vectorfield" and sid:
776
+ _handle_vectorfield_fetch(sid, params)
777
+ elif route == "pixel" and sid:
778
+ _handle_pixel(sid, params)
779
+ elif route == "diff" and len(parts) >= 3:
780
+ _handle_diff(parts[1], parts[2], params)
781
+ elif route == "diff-histogram" and len(parts) >= 3:
782
+ _handle_diff_histogram(parts[1], parts[2], params)
519
783
  else:
520
784
  _write_error(f"unsupported endpoint: {endpoint}")
521
785
 
@@ -648,7 +912,20 @@ def _handle_get_viewer_html(msg: dict) -> None:
648
912
 
649
913
  sid = msg.get("sid", "")
650
914
 
651
- query_val = json.dumps(f"?sid={sid}&transport=postMessage") if sid else "null"
915
+ # Build query string from session info — the extension forwards the full
916
+ # SESSION: payload so new features (overlay_sid, compare, etc.) work
917
+ # without touching extension code.
918
+ qs = f"?sid={sid}&transport=postMessage"
919
+ overlay_sid = msg.get("overlay_sid")
920
+ if overlay_sid:
921
+ qs += f"&overlay_sid={overlay_sid}"
922
+ compare_sids = msg.get("compare_sids")
923
+ if compare_sids:
924
+ sids = compare_sids if isinstance(compare_sids, list) else [s.strip() for s in str(compare_sids).split(",") if s.strip()]
925
+ if sids:
926
+ qs += f"&compare_sid={sids[0]}"
927
+ qs += f"&compare_sids={','.join(sids)}"
928
+ query_val = json.dumps(qs) if sid else "null"
652
929
 
653
930
  _theme_names = ["dark", "light", "solarized", "nord"]
654
931
  _cfg_theme = get_viewer_theme()
@@ -58,7 +58,7 @@ def _vscode_app_bundle() -> str | None:
58
58
 
59
59
  _VSCODE_EXT_INSTALLED = False # cached so we only check once per process
60
60
  _VSCODE_EXT_FRESH_INSTALL = False # True if we just installed it this session
61
- _VSCODE_EXT_VERSION = "0.9.0" # current bundled extension version
61
+ _VSCODE_EXT_VERSION = "0.13.0" # current bundled extension version
62
62
  _VSCODE_SIGNAL_FILENAME = "open-request-v0900.json"
63
63
  _VSCODE_COMPAT_SIGNAL_FILENAMES: tuple[str, ...] = ("open-request-v0800.json",)
64
64
  _VSCODE_PORT_SETTINGS_SETTLE_SECONDS = 2.0
@@ -152,14 +152,42 @@ def _remove_old_extension_versions(current_version: str) -> None:
152
152
  _vprint(f"[ArrayView] could not remove {entry}: {exc}", flush=True)
153
153
 
154
154
 
155
- def _extension_on_disk(version: str) -> bool:
156
- """Return True if the extension directory for *version* exists on disk."""
155
+ def _extension_on_disk(version: str, vsix_path: str | None = None) -> bool:
156
+ """Return True if the extension directory for *version* exists on disk.
157
+
158
+ When *vsix_path* is given, also verifies that the installed extension
159
+ matches the bundled VSIX by comparing a content hash stored at install
160
+ time. This catches rebuilds during development where the version stays
161
+ the same but the VSIX content changed.
162
+ """
163
+ import hashlib
164
+
157
165
  for base in (
158
166
  os.path.expanduser("~/.vscode/extensions"),
159
167
  os.path.expanduser("~/.vscode-server/extensions"),
160
168
  ):
161
- if os.path.isdir(os.path.join(base, f"arrayview.arrayview-opener-{version}")):
169
+ ext_dir = os.path.join(base, f"arrayview.arrayview-opener-{version}")
170
+ if not os.path.isdir(ext_dir):
171
+ continue
172
+ if vsix_path is None:
173
+ return True
174
+ # Compare content hash
175
+ try:
176
+ vsix_hash = hashlib.md5(open(vsix_path, "rb").read()).hexdigest()
177
+ except OSError:
178
+ return True # can't read VSIX, assume installed is fine
179
+ hash_file = os.path.join(ext_dir, ".vsix_hash")
180
+ try:
181
+ installed_hash = open(hash_file).read().strip()
182
+ except OSError:
183
+ installed_hash = None
184
+ if installed_hash == vsix_hash:
162
185
  return True
186
+ _vprint(
187
+ f"[ArrayView] VSIX content changed (installed={installed_hash}, bundled={vsix_hash}) — reinstalling",
188
+ flush=True,
189
+ )
190
+ return False
163
191
  return False
164
192
 
165
193
 
@@ -190,11 +218,11 @@ def _ensure_vscode_extension() -> bool:
190
218
  _vprint("[ArrayView] could not read version from bundled VSIX", flush=True)
191
219
  return False
192
220
 
193
- # Fast path: correct version already installed — no reinstall needed.
194
- # Reinstalling with --force triggers an extension-host reload, which
195
- # creates a ~10-15s gap during which the signal file can be missed.
221
+ # Fast path: correct version and content already installed — no reinstall
222
+ # needed. Reinstalling with --force triggers an extension-host reload,
223
+ # which creates a ~10-15s gap during which the signal file can be missed.
196
224
  _remove_old_extension_versions(ext_version)
197
- if _extension_on_disk(ext_version):
225
+ if _extension_on_disk(ext_version, vsix_path):
198
226
  _VSCODE_EXT_INSTALLED = True
199
227
  _vprint(
200
228
  f"[ArrayView] extension v{ext_version} already installed — skipping reinstall",
@@ -228,6 +256,21 @@ def _ensure_vscode_extension() -> bool:
228
256
  )
229
257
  if r.returncode == 0 and not install_failed:
230
258
  _patch_vscode_extension_metadata(ext_version)
259
+ # Write content hash so future runs can detect VSIX rebuilds
260
+ # without a version bump (common during development).
261
+ try:
262
+ import hashlib
263
+ vsix_hash = hashlib.md5(open(vsix_path, "rb").read()).hexdigest()
264
+ for base in (
265
+ os.path.expanduser("~/.vscode/extensions"),
266
+ os.path.expanduser("~/.vscode-server/extensions"),
267
+ ):
268
+ ext_dir = os.path.join(base, f"arrayview.arrayview-opener-{ext_version}")
269
+ if os.path.isdir(ext_dir):
270
+ with open(os.path.join(ext_dir, ".vsix_hash"), "w") as f:
271
+ f.write(vsix_hash)
272
+ except Exception:
273
+ pass # non-critical
231
274
  _VSCODE_EXT_INSTALLED = True
232
275
  _VSCODE_EXT_FRESH_INSTALL = True
233
276
  return True
@@ -566,7 +609,9 @@ def _open_via_signal_file(
566
609
 
567
610
 
568
611
  def _open_direct_via_signal_file(
569
- filepath: str, title: str | None = None
612
+ filepath: str,
613
+ title: str | None = None,
614
+ extra_args: list[str] | None = None,
570
615
  ) -> bool:
571
616
  """Write a direct-mode signal file for the VS Code extension.
572
617
 
@@ -574,6 +619,10 @@ def _open_direct_via_signal_file(
574
619
  subprocess (``python -m arrayview --mode stdio <filepath>``) and hosts
575
620
  the viewer HTML directly in a webview panel with postMessage transport.
576
621
  No ports, no WebSocket, no authentication — just IPC.
622
+
623
+ *extra_args* are forwarded verbatim to the subprocess command line so
624
+ that new CLI flags (``--vectorfield``, ``--overlay``, ``--rgb``, …)
625
+ work without touching the extension.
577
626
  """
578
627
  import sys as _sys
579
628
 
@@ -586,6 +635,8 @@ def _open_direct_via_signal_file(
586
635
  }
587
636
  if title:
588
637
  payload["title"] = title
638
+ if extra_args:
639
+ payload["extraArgs"] = extra_args
589
640
  return _write_vscode_signal(payload, skip_compat=True)
590
641
 
591
642
 
@@ -44,7 +44,7 @@ wheels = [
44
44
 
45
45
  [[package]]
46
46
  name = "arrayview"
47
- version = "0.12.2"
47
+ version = "0.13.0"
48
48
  source = { editable = "." }
49
49
  dependencies = [
50
50
  { name = "fastapi" },
@@ -74,9 +74,10 @@ const EXT_PPIDS = _getAncestorPids(process.pid, 8);
74
74
  // bridges length-prefixed binary responses from stdout / JSON requests on stdin.
75
75
  // ---------------------------------------------------------------------------
76
76
  class PythonBridge {
77
- constructor(filePath, onSessionReady, pythonPath, shmParams) {
77
+ constructor(filePath, onSessionReady, pythonPath, shmParams, extraArgs) {
78
78
  this.filePath = filePath;
79
79
  this.shmParams = shmParams || null; // {name, shape, dtype}
80
+ this.extraArgs = extraArgs || []; // additional CLI args (e.g. --vectorfield)
80
81
  this.process = null;
81
82
  this.sid = null;
82
83
  this._buffer = Buffer.alloc(0);
@@ -89,38 +90,85 @@ class PythonBridge {
89
90
  return new Promise((resolve, reject) => {
90
91
  this._startResolve = resolve;
91
92
  this._startReject = reject;
92
- this._spawn(this.pythonPath || 'python3');
93
+ // Candidate order:
94
+ // 1. Explicit pythonPath (from signal file — always correct)
95
+ // 2. Workspace .venv (covers uv sync / editable installs during dev)
96
+ // 3. System python3/python (might have arrayview via pip)
97
+ // 4. uv run --with arrayview (ephemeral env from PyPI — always works)
98
+ if (this.pythonPath) {
99
+ this._candidates = [this.pythonPath];
100
+ } else {
101
+ this._candidates = ['python3', 'python', 'uv run --with arrayview python'];
102
+ // Prepend workspace .venv if it exists
103
+ const folders = vscode.workspace.workspaceFolders;
104
+ if (folders) {
105
+ for (const f of folders) {
106
+ const venvPy = path.join(f.uri.fsPath, '.venv', 'bin', 'python');
107
+ if (fs.existsSync(venvPy)) {
108
+ this._candidates.unshift(venvPy);
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ this._tryNextCandidate();
93
115
  });
94
116
  }
95
117
 
118
+ _tryNextCandidate() {
119
+ if (this._candidates.length === 0) {
120
+ const msg = 'Python with arrayview not found. Install with: pip install arrayview (or uv pip install arrayview)';
121
+ log(`PYTHON: all candidates exhausted`);
122
+ if (this._startReject) { this._startReject(new Error(msg)); this._startReject = null; }
123
+ return;
124
+ }
125
+ const cmd = this._candidates.shift();
126
+ this._spawn(cmd);
127
+ }
128
+
96
129
  _spawn(cmd) {
97
130
  const { spawn } = require('child_process');
98
- let args;
131
+ let arrayviewArgs;
99
132
  if (this.shmParams) {
100
- args = ['-m', 'arrayview', '--mode', 'stdio',
133
+ arrayviewArgs = ['-m', 'arrayview', '--mode', 'stdio',
101
134
  '--shm-name', this.shmParams.name,
102
135
  '--shm-shape', this.shmParams.shape,
103
136
  '--shm-dtype', this.shmParams.dtype];
104
137
  if (this.shmParams.arrayName) {
105
- args.push('--name', this.shmParams.arrayName);
138
+ arrayviewArgs.push('--name', this.shmParams.arrayName);
106
139
  }
107
140
  } else {
108
- args = ['-m', 'arrayview', '--mode', 'stdio', this.filePath];
141
+ arrayviewArgs = ['-m', 'arrayview', '--mode', 'stdio', this.filePath];
142
+ }
143
+ if (this.extraArgs.length > 0) {
144
+ arrayviewArgs.push(...this.extraArgs);
145
+ }
146
+
147
+ // Support compound commands like "uv run python"
148
+ let spawnCmd, spawnArgs;
149
+ const parts = cmd.split(/\s+/);
150
+ if (parts.length > 1) {
151
+ spawnCmd = parts[0];
152
+ spawnArgs = [...parts.slice(1), ...arrayviewArgs];
153
+ } else {
154
+ spawnCmd = cmd;
155
+ spawnArgs = arrayviewArgs;
109
156
  }
110
- log(`PYTHON: spawning ${cmd} ${args.join(' ')}`);
111
- this.process = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
157
+
158
+ log(`PYTHON: spawning ${spawnCmd} ${spawnArgs.join(' ')}`);
159
+ this.process = spawn(spawnCmd, spawnArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
160
+
161
+ this._currentCmd = cmd;
112
162
 
113
163
  this.process.on('error', (err) => {
114
- if (err.code === 'ENOENT' && cmd === 'python3' && !this.pythonPath) {
115
- log('PYTHON: python3 not found, trying python');
116
- this._spawn('python');
164
+ if (err.code === 'ENOENT' && this._candidates.length > 0) {
165
+ log(`PYTHON: ${spawnCmd} not found, trying next candidate`);
166
+ this._tryNextCandidate();
117
167
  return;
118
168
  }
119
169
  log(`PYTHON: spawn error: ${err.message}`);
120
170
  if (this._startReject) {
121
- this._startReject(new Error(
122
- cmd === 'python' ? 'Python not found. Install Python and arrayview (pip install arrayview).' : err.message
123
- ));
171
+ this._startReject(new Error(err.message));
124
172
  this._startReject = null;
125
173
  }
126
174
  });
@@ -133,6 +181,7 @@ class PythonBridge {
133
181
  try {
134
182
  const info = JSON.parse(line.slice(8));
135
183
  this.sid = info.sid;
184
+ this.sessionInfo = info; // preserve full session info (overlay_sid, etc.)
136
185
  if (this.onSessionReady) this.onSessionReady(info);
137
186
  if (this._startResolve) {
138
187
  this._startResolve(info);
@@ -152,7 +201,13 @@ class PythonBridge {
152
201
  });
153
202
 
154
203
  this.process.on('exit', (code) => {
155
- log(`PYTHON: exited with code ${code}`);
204
+ log(`PYTHON: ${this._currentCmd} exited with code ${code}`);
205
+ // If we haven't started yet and there are more candidates, try next
206
+ if (code !== 0 && this._startResolve && this._candidates && this._candidates.length > 0) {
207
+ log(`PYTHON: trying next candidate after exit code ${code}`);
208
+ this._tryNextCandidate();
209
+ return;
210
+ }
156
211
  // Reject any pending request callbacks
157
212
  for (const cb of this._pendingCallbacks) {
158
213
  cb(Buffer.from(JSON.stringify({ error: `process exited with code ${code}` })));
@@ -202,36 +257,28 @@ class PythonBridge {
202
257
  // ---------------------------------------------------------------------------
203
258
  // Direct webview: embeds the viewer HTML directly (no iframe/server)
204
259
  // ---------------------------------------------------------------------------
205
- async function openDirectWebview(filePath, title, pythonPath, shmParams) {
206
- const label = title || (filePath ? path.basename(filePath) : (shmParams && shmParams.arrayName) || 'Array');
207
260
 
208
- const viewColumn = vscode.window.activeTextEditor
209
- ? vscode.ViewColumn.Beside
210
- : vscode.ViewColumn.Active;
211
-
212
- const panel = vscode.window.createWebviewPanel(
213
- 'arrayview.viewer',
214
- `ArrayView: ${label}`,
215
- { viewColumn, preserveFocus: false },
216
- {
217
- enableScripts: true,
218
- retainContextWhenHidden: true,
219
- }
220
- );
221
-
222
- // Spawn Python subprocess and wait for session to be ready
261
+ /**
262
+ * Wire up a webview panel to a PythonBridge: set HTML, bridge messages,
263
+ * clean up on dispose. Shared by openDirectWebview and the custom editor.
264
+ */
265
+ async function setupArrayViewPanel(panel, filePath, pythonPath, shmParams, extraArgs) {
223
266
  const bridge = new PythonBridge(filePath, (sessionInfo) => {
224
267
  log(`SESSION READY: sid=${sessionInfo.sid} name=${sessionInfo.name}`);
225
- }, pythonPath, shmParams);
268
+ }, pythonPath, shmParams, extraArgs);
226
269
  const sessionInfo = await Promise.race([
227
270
  bridge.start(),
228
271
  new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout waiting for Python session (30s)')), 30000)),
229
272
  ]);
230
273
 
231
- // Get the rendered viewer HTML from the Python subprocess
274
+ // Get the rendered viewer HTML from the Python subprocess.
275
+ // Forward the full sessionInfo so the stdio server can build the correct
276
+ // query string (overlay_sid, etc.) without the extension needing to know
277
+ // about each feature.
232
278
  const htmlPayload = await bridge.sendRequest({
233
279
  type: 'get-viewer-html',
234
280
  sid: bridge.sid,
281
+ ...(bridge.sessionInfo || {}),
235
282
  });
236
283
 
237
284
  let viewerHtml;
@@ -239,13 +286,10 @@ async function openDirectWebview(filePath, title, pythonPath, shmParams) {
239
286
  const response = JSON.parse(htmlPayload.toString());
240
287
  viewerHtml = response.html;
241
288
  } catch (e) {
242
- vscode.window.showErrorMessage(`ArrayView: Failed to get viewer HTML: ${e.message}`);
243
289
  bridge.destroy();
244
- panel.dispose();
245
- return;
290
+ throw new Error(`Failed to get viewer HTML: ${e.message}`);
246
291
  }
247
292
 
248
- // Set the webview HTML
249
293
  panel.webview.html = viewerHtml;
250
294
 
251
295
  // Bridge messages between webview and Python subprocess
@@ -383,9 +427,66 @@ async function openDirectWebview(filePath, title, pythonPath, shmParams) {
383
427
  }
384
428
  });
385
429
 
430
+ log(`DIRECT: setup complete for ${filePath}`);
431
+ return bridge;
432
+ }
433
+
434
+ async function openDirectWebview(filePath, title, pythonPath, shmParams, extraArgs) {
435
+ const label = title || (filePath ? `ArrayView: ${path.basename(filePath)}` : (shmParams && shmParams.arrayName) || 'Array');
436
+
437
+ const viewColumn = vscode.window.activeTextEditor
438
+ ? vscode.ViewColumn.Beside
439
+ : vscode.ViewColumn.Active;
440
+
441
+ const panel = vscode.window.createWebviewPanel(
442
+ 'arrayview.viewer',
443
+ label,
444
+ { viewColumn, preserveFocus: false },
445
+ {
446
+ enableScripts: true,
447
+ retainContextWhenHidden: true,
448
+ }
449
+ );
450
+
451
+ try {
452
+ await setupArrayViewPanel(panel, filePath, pythonPath, shmParams, extraArgs);
453
+ } catch (e) {
454
+ panel.dispose();
455
+ throw e;
456
+ }
386
457
  log(`DIRECT: opened "${label}" for ${filePath}`);
387
458
  }
388
459
 
460
+ // ---------------------------------------------------------------------------
461
+ // Custom editor provider: "Open With..." from Explorer
462
+ // ---------------------------------------------------------------------------
463
+ class ArrayViewEditorProvider {
464
+ static viewType = 'arrayview.arrayEditor';
465
+
466
+ openCustomDocument(uri, _openContext, _token) {
467
+ // Minimal document — just wraps the URI. No data loading here;
468
+ // PythonBridge handles everything in resolveCustomEditor.
469
+ return { uri, dispose: () => {} };
470
+ }
471
+
472
+ async resolveCustomEditor(document, webviewPanel, _token) {
473
+ const filePath = document.uri.fsPath;
474
+ log(`CUSTOM-EDITOR: resolveCustomEditor for ${filePath}`);
475
+ webviewPanel.webview.options = { enableScripts: true };
476
+ webviewPanel.webview.html = `<html><body style="background:#1e1e1e;color:#ccc;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;font-family:system-ui">
477
+ <div>Loading ${path.basename(filePath)}…</div></body></html>`;
478
+ try {
479
+ await setupArrayViewPanel(webviewPanel, filePath);
480
+ log(`CUSTOM-EDITOR: setup complete for ${filePath}`);
481
+ } catch (e) {
482
+ log(`CUSTOM-EDITOR: error: ${e.message}\n${e.stack}`);
483
+ webviewPanel.webview.html = `<html><body style="color:#c00;padding:2em;font-family:monospace">
484
+ <h2>ArrayView failed to open</h2><pre>${e.message}</pre>
485
+ <p>Check Output → "ArrayView" for details.</p></body></html>`;
486
+ }
487
+ }
488
+ }
489
+
389
490
  let version = 'unknown';
390
491
  let isProcessingSignal = false;
391
492
  let lastHandledRequestId = null;
@@ -715,7 +816,10 @@ async function processSignalData(data) {
715
816
  try { fs.unlinkSync(path.join(SIGNAL_DIR, compat)); } catch (_) {}
716
817
  }
717
818
  const shmParams = data.shm ? { ...data.shm, arrayName: data.arrayName } : null;
718
- await openDirectWebview(data.filepath, data.title, data.pythonPath, shmParams);
819
+ // Forward extra CLI args generically — Python builds this list from
820
+ // its argparse namespace so new flags work without extension changes.
821
+ const extraArgs = Array.isArray(data.extraArgs) ? data.extraArgs : [];
822
+ await openDirectWebview(data.filepath, data.title, data.pythonPath, shmParams, extraArgs);
719
823
  return;
720
824
  }
721
825
 
@@ -901,6 +1005,14 @@ function activate(context) {
901
1005
  });
902
1006
  context.subscriptions.push(openFileCmd);
903
1007
 
1008
+ // Register custom editor provider for "Open With..." support
1009
+ const editorProvider = vscode.window.registerCustomEditorProvider(
1010
+ ArrayViewEditorProvider.viewType,
1011
+ new ArrayViewEditorProvider(),
1012
+ { webviewOptions: { retainContextWhenHidden: true } }
1013
+ );
1014
+ context.subscriptions.push(editorProvider);
1015
+
904
1016
  // Start SSH relay listener for zero-config remote array viewing
905
1017
  startRelayServer();
906
1018
  context.subscriptions.push({ dispose: () => {
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "arrayview-opener",
3
+ "displayName": "ArrayView Opener",
4
+ "description": "Opens ArrayView in VS Code and can promote tunnel ports to public preview",
5
+ "version": "0.13.0",
6
+ "publisher": "arrayview",
7
+ "license": "MIT",
8
+ "icon": "icon.png",
9
+ "engines": { "vscode": "^1.80.0" },
10
+ "extensionKind": ["workspace", "ui"],
11
+ "categories": ["Other"],
12
+ "activationEvents": ["*"],
13
+ "main": "./extension.js",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/oscarvanderheide/arrayview.git"
17
+ },
18
+ "files": [
19
+ "extension.js",
20
+ "package.json",
21
+ "icon.png",
22
+ "LICENSE"
23
+ ],
24
+ "contributes": {
25
+ "commands": [
26
+ {
27
+ "command": "arrayview.openFile",
28
+ "title": "Open in ArrayView"
29
+ }
30
+ ],
31
+ "customEditors": [
32
+ {
33
+ "viewType": "arrayview.arrayEditor",
34
+ "displayName": "ArrayView",
35
+ "selector": [
36
+ { "filenamePattern": "*.npy" },
37
+ { "filenamePattern": "*.npz" },
38
+ { "filenamePattern": "*.nii" },
39
+ { "filenamePattern": "*.nii.gz" },
40
+ { "filenamePattern": "*.h5" },
41
+ { "filenamePattern": "*.hdf5" },
42
+ { "filenamePattern": "*.mat" },
43
+ { "filenamePattern": "*.tif" },
44
+ { "filenamePattern": "*.tiff" },
45
+ { "filenamePattern": "*.pt" },
46
+ { "filenamePattern": "*.pth" },
47
+ { "filenamePattern": "*.zarr.zip" }
48
+ ],
49
+ "priority": "default"
50
+ }
51
+ ]
52
+ }
53
+ }
@@ -1,30 +0,0 @@
1
- {
2
- "name": "arrayview-opener",
3
- "displayName": "ArrayView Opener",
4
- "description": "Opens ArrayView in VS Code and can promote tunnel ports to public preview",
5
- "version": "0.11.0",
6
- "publisher": "arrayview",
7
- "license": "MIT",
8
- "engines": { "vscode": "^1.80.0" },
9
- "extensionKind": ["workspace", "ui"],
10
- "categories": ["Other"],
11
- "activationEvents": ["*"],
12
- "main": "./extension.js",
13
- "repository": {
14
- "type": "git",
15
- "url": "https://github.com/oscarvanderheide/arrayview.git"
16
- },
17
- "files": [
18
- "extension.js",
19
- "package.json",
20
- "LICENSE"
21
- ],
22
- "contributes": {
23
- "commands": [
24
- {
25
- "command": "arrayview.openFile",
26
- "title": "ArrayView: Open File"
27
- }
28
- ]
29
- }
30
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes