From 91866e5a510df3d76a438fe3fd07d364a2d587c1 Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Tue, 23 Jun 2026 09:20:24 +0900 Subject: [PATCH 01/12] Add Snap packaging recipe for manual builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strict-confinement Snap built with `snapcraft pack --use-lxd`. Uses the gnome extension for the Qt6 desktop runtime and reuses the .desktop entry and icon already produced for the AppImage. Plugs home and removable-media cover opening .npy/.npz files the user picks. Manual build only for now; auto-publish (Store name, CI upload, version single-sourcing via adopt-info) is a follow-up. Known limitation: double-click file association does not work — snapd does not register a snap's custom MIME types (application/x-npy/-npz) with the host, so the launcher cannot route .npy files in. Launch via the menu icon and use File > Open instead. Co-Authored-By: Claude Opus 4.8 --- snap/snapcraft.yaml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 snap/snapcraft.yaml diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000..8309936 --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,45 @@ +name: npyquick +base: core22 +version: '0.1.3' +summary: Quick GUI viewer for .npy / .npz arrays +description: | + npyquick is a lightweight desktop viewer for NumPy .npy and .npz files. + Open an array and inspect it as an image, a table, or a cross-section plot — + no Python install or Jupyter needed. + +license: GPL-3.0-or-later +grade: stable +confinement: strict + +apps: + npyquick: + command: bin/npyquick + # The gnome extension wires in the GTK/Qt desktop runtime (fonts, themes, + # icons, Wayland/X11) so we don't hand-assemble Qt's desktop dependencies. + extensions: [gnome] + common-id: io.github.liukdiihmieu.npyquick + desktop: usr/share/applications/io.github.liukdiihmieu.npyquick.desktop + plugs: + # npyquick opens .npy files the user picks from anywhere on disk. + - home + - removable-media + +parts: + npyquick: + plugin: python + source: . + stage-packages: + # Qt6's xcb platform plugin hard-requires libxcb-cursor; it's a frequent + # "works in dev, blank window in the snap" gotcha. Revisit after a build. + - libxcb-cursor0 + + # Reuse the desktop entry and icon already produced for the AppImage. + desktop: + plugin: dump + source: packaging/appimage + organize: + io.github.liukdiihmieu.npyquick.desktop: usr/share/applications/io.github.liukdiihmieu.npyquick.desktop + npyquick-512.png: usr/share/icons/hicolor/512x512/apps/io.github.liukdiihmieu.npyquick.png + stage: + - usr/share/applications + - usr/share/icons From 6bfb437e8d0183f9e047e25f5bcb314a1c5bf7aa Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Tue, 23 Jun 2026 10:05:33 +0900 Subject: [PATCH 02/12] Provide Snap desktop entry and icon via snap/gui The launcher could not resolve the bundled icon because the reused AppImage desktop entry references a themed icon name, which snapd does not map to an in-snap icon. Switch to the native snap/gui/ convention: a snap-specific npyquick.desktop with Icon=${SNAP}/meta/gui/npyquick.png (an absolute in-snap path) and the icon copied to snap/gui/npyquick.png. snapd auto-detects these from meta/gui, so the apps.desktop key and the dump part are no longer needed. Co-Authored-By: Claude Opus 4.8 --- snap/gui/npyquick.desktop | 12 ++++++++++++ snap/gui/npyquick.png | Bin 0 -> 1050910 bytes snap/snapcraft.yaml | 16 ++++------------ 3 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 snap/gui/npyquick.desktop create mode 100644 snap/gui/npyquick.png diff --git a/snap/gui/npyquick.desktop b/snap/gui/npyquick.desktop new file mode 100644 index 0000000..c3349c5 --- /dev/null +++ b/snap/gui/npyquick.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +Name=npyquick +GenericName=NumPy Array Viewer +Comment=Quick viewer for NumPy .npy and .npz files +Exec=npyquick %f +Icon=${SNAP}/meta/gui/npyquick.png +Terminal=false +Categories=Science;Utility; +MimeType=application/x-npy;application/x-npz; +Keywords=numpy;npy;npz;array;viewer; +StartupNotify=true diff --git a/snap/gui/npyquick.png b/snap/gui/npyquick.png new file mode 100644 index 0000000000000000000000000000000000000000..d3a9f04c4446b2c68f95215c39b4e87f4f114a8c GIT binary patch literal 1050910 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 K{r;2-ulfJzC{f!0 literal 0 HcmV?d00001 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 8309936..beec1f9 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -18,7 +18,10 @@ apps: # icons, Wayland/X11) so we don't hand-assemble Qt's desktop dependencies. extensions: [gnome] common-id: io.github.liukdiihmieu.npyquick - desktop: usr/share/applications/io.github.liukdiihmieu.npyquick.desktop + # The .desktop entry and icon come from snap/gui/ (copied to meta/gui/ at + # build; snapd auto-detects them). The desktop file's Icon= points at + # ${SNAP}/meta/gui/npyquick.png — an absolute in-snap path, which is what + # the launcher needs to resolve a bundled icon. plugs: # npyquick opens .npy files the user picks from anywhere on disk. - home @@ -32,14 +35,3 @@ parts: # Qt6's xcb platform plugin hard-requires libxcb-cursor; it's a frequent # "works in dev, blank window in the snap" gotcha. Revisit after a build. - libxcb-cursor0 - - # Reuse the desktop entry and icon already produced for the AppImage. - desktop: - plugin: dump - source: packaging/appimage - organize: - io.github.liukdiihmieu.npyquick.desktop: usr/share/applications/io.github.liukdiihmieu.npyquick.desktop - npyquick-512.png: usr/share/icons/hicolor/512x512/apps/io.github.liukdiihmieu.npyquick.png - stage: - - usr/share/applications - - usr/share/icons From 218c81a1e2fc2f7689f8988d65c7878a35b26e62 Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Tue, 23 Jun 2026 10:22:15 +0900 Subject: [PATCH 03/12] docs update --- snap/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 snap/README.md diff --git a/snap/README.md b/snap/README.md new file mode 100644 index 0000000..2729a09 --- /dev/null +++ b/snap/README.md @@ -0,0 +1,26 @@ +# npyquick (Snap) + +Snap packaging for npyquick. Build locally with: + +```sh +snapcraft pack --use-lxd +``` + +## Opening files + +The Snap is strictly confined, so it reads files in your home directory (and +removable media once that interface is connected). To open an array: + +- **Drag and drop** a `.npy` or `.npz` file onto the window — the quickest way. +- Or use **File ▸ Open**. + +### Why double-click doesn't open npyquick + +Double-clicking a `.npy` / `.npz` in the file manager will not launch npyquick. +A Snap cannot register new file types with the host system, so the desktop +doesn't recognise these extensions as belonging to npyquick. This is a +limitation of how Snaps integrate with the system, not a setting we can flip +in the package (it is unrelated to the sandbox — the same gap exists without +strict confinement). + +Open npyquick from the application menu, then drag a file in or use File ▸ Open. From 3553360b32b2965735d415d19d3b34e930f5da8c Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Tue, 23 Jun 2026 10:32:21 +0900 Subject: [PATCH 04/12] Fix inaccurate comments in snapcraft.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback: the home plug comment claimed files could be opened from "anywhere on disk", which is wrong under strict confinement — clarify it covers the home directory and that /mnt, /media, /run/media need the removable-media interface connected. Also reword the gnome extension comment: it provides desktop integration pieces (fonts, themes, icons, Wayland/X11), while PySide6/Qt itself comes from the Python dependencies. Co-Authored-By: Claude Opus 4.8 --- snap/snapcraft.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index beec1f9..d5d0c19 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -14,8 +14,9 @@ confinement: strict apps: npyquick: command: bin/npyquick - # The gnome extension wires in the GTK/Qt desktop runtime (fonts, themes, - # icons, Wayland/X11) so we don't hand-assemble Qt's desktop dependencies. + # The gnome extension provides common desktop integration pieces + # such as fonts, themes, icons, Wayland/X11, and related runtime setup. + # PySide6/Qt itself still comes from the Python dependencies. extensions: [gnome] common-id: io.github.liukdiihmieu.npyquick # The .desktop entry and icon come from snap/gui/ (copied to meta/gui/ at @@ -23,7 +24,9 @@ apps: # ${SNAP}/meta/gui/npyquick.png — an absolute in-snap path, which is what # the launcher needs to resolve a bundled icon. plugs: - # npyquick opens .npy files the user picks from anywhere on disk. + # Allow opening user data from the home directory. + # Files under /mnt, /media, or /run/media require the removable-media + # interface to be connected manually. - home - removable-media From 24df3038dacea0288b97804b4239256ce2d87389 Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Tue, 23 Jun 2026 19:45:37 +0900 Subject: [PATCH 05/12] Stage the xcb/xkb libs the Qt6 xcb plugin needs on X11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The snap ran on Wayland but crashed on X11 with "Could not load the Qt platform plugin xcb". Qt's message blames libxcb-cursor0, but that lib was already present and resolvable; `ldd` on libqxcb.so inside the snap showed the real gaps were libxkbcommon-x11, libxcb-icccm, libxcb-keysyms, libxcb-shape and libxcb-xkb — needed by libQt6XcbQpa and not supplied by the gnome platform. Add the corresponding packages to stage-packages. Verified on an X11 host: the window opens and ldd reports no missing libraries. Co-Authored-By: Claude Opus 4.8 --- snap/snapcraft.yaml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index d5d0c19..70364b2 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -35,6 +35,15 @@ parts: plugin: python source: . stage-packages: - # Qt6's xcb platform plugin hard-requires libxcb-cursor; it's a frequent - # "works in dev, blank window in the snap" gotcha. Revisit after a build. + # System libraries the Qt6 xcb (X11) platform plugin links against. The + # gnome platform supplies some xcb libs but not all of these; without them + # the app runs on Wayland but crashes on X11 with "Could not load the Qt + # platform plugin xcb". The missing set was confirmed via `ldd libqxcb.so` + # on an X11 host. (Qt's error singles out xcb-cursor, but the real gaps are + # the xkb/icccm/keysyms/shape ones below.) - libxcb-cursor0 + - libxcb-icccm4 + - libxcb-keysyms1 + - libxcb-shape0 + - libxcb-xkb1 + - libxkbcommon-x11-0 From 74005ef4183edfc2ac5c0ec3d46e92c9511fc0ad Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Tue, 23 Jun 2026 21:18:21 +0900 Subject: [PATCH 06/12] Adopt Snap store metadata from the AppStream metainfo file Drop the hand-written version/summary/description from snapcraft.yaml and adopt them via adopt-info + parse-info from the AppStream metainfo already maintained for the AppImage, so the two stay in one place. A new `metadata` part ships that file at usr/share/metainfo/ inside the snap. Keep the explicit icon (AppStream has no ) and the contact/issues/source-code links (AppStream URLs are not adopted). Recompress the store/launcher icon from ~1 MB to ~13 KB to stay well under the store's 256 KB limit. Store listing note: `snapcraft upload` only bumps the revision; the public page's summary/description/icon are updated with `snapcraft upload-metadata --force`, while links and screenshots are dashboard-only. Co-Authored-By: Claude Opus 4.8 --- snap/gui/npyquick.png | Bin 1050910 -> 13584 bytes snap/snapcraft.yaml | 31 +++++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/snap/gui/npyquick.png b/snap/gui/npyquick.png index d3a9f04c4446b2c68f95215c39b4e87f4f114a8c..4ab275d285f47af934839cc668834f517774ea5d 100644 GIT binary patch literal 13584 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 TZcWStM+36bN-xWwn*{tf^xL_Z literal 1050910 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 K{r;2-ulfJzC{f!0 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 70364b2..bcf8bfe 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,11 +1,16 @@ name: npyquick base: core22 -version: '0.1.3' -summary: Quick GUI viewer for .npy / .npz arrays -description: | - npyquick is a lightweight desktop viewer for NumPy .npy and .npz files. - Open an array and inspect it as an image, a table, or a cross-section plot — - no Python install or Jupyter needed. +# title, version, summary and description are adopted from the AppStream +# metainfo file (see the `metadata` part), keeping them in one place shared +# with the AppImage build instead of hand-maintaining them here. +adopt-info: metadata +# Store/launcher icon. AppStream has no element, so set it explicitly. +# Store icon limits: <=512x512, 1:1, <=256 KB. +icon: snap/gui/npyquick.png +# Store listing links (AppStream URLs are not adopted, so keep these). +contact: https://github.com/LiukDiihMieu/npyquick/issues +issues: https://github.com/LiukDiihMieu/npyquick/issues +source-code: https://github.com/LiukDiihMieu/npyquick license: GPL-3.0-or-later grade: stable @@ -47,3 +52,17 @@ parts: - libxcb-shape0 - libxcb-xkb1 - libxkbcommon-x11-0 + + # Adopt store metadata (title/version/summary/description) from the AppStream + # metainfo file, reusing the one written for the AppImage. Also ships it at + # the standard usr/share/metainfo/ path so the installed snap carries proper + # AppStream metadata. + metadata: + plugin: dump + source: packaging/appimage + organize: + io.github.liukdiihmieu.npyquick.appdata.xml: usr/share/metainfo/io.github.liukdiihmieu.npyquick.appdata.xml + stage: + - usr/share/metainfo + parse-info: + - usr/share/metainfo/io.github.liukdiihmieu.npyquick.appdata.xml From 8bb323961e990a8c54c9c58c613b80d413306006 Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Thu, 25 Jun 2026 14:54:25 +0900 Subject: [PATCH 07/12] Explain Snap-sandbox-blocked file opens instead of a raw error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When running confined as a Snap, opening a file outside the sandbox's reach (strict confinement only grants the home folder and removable media) failed with a bare "No such file or directory", which is confusing — the file plainly exists and the pip/AppImage build opens it fine. The common trigger is a symlink in home pointing to another mount (e.g. ~/Lab -> /data on another drive): confinement resolves the symlink to its real path, which the home interface doesn't cover. load_file now detects this case ($SNAP set + the resolved path outside $SNAP_REAL_HOME and the removable-media roots) and shows a short explanation: move the file under home / use the pip or AppImage build, or — for external drives — connect the removable-media interface. The hint is gated on $SNAP so the pip and AppImage builds are unaffected. Covered by tests in test_app_workflow.py. Co-Authored-By: Claude Opus 4.8 --- src/npyquick/app.py | 22 ++++++++++++++++++++- tests/test_app_workflow.py | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/npyquick/app.py b/src/npyquick/app.py index 7a444f0..34defbe 100644 --- a/src/npyquick/app.py +++ b/src/npyquick/app.py @@ -406,11 +406,31 @@ def open_file(self) -> None: if path: self.load_file(path) + @staticmethod + def _snap_sandbox_hint(path: str) -> str | None: + """When running as a confined Snap, explain why a file the sandbox + can't reach (anything outside the home folder and removable media) + failed to open. Returns None when not running as a Snap or when the + path is in reach, so the original error is shown instead. + """ + if not os.environ.get("SNAP"): + return None + real = os.path.realpath(path) + home = os.environ.get("SNAP_REAL_HOME") or os.path.expanduser("~") + if real == home or real.startswith(home + os.sep): + return None + if real.startswith(("/media/", "/run/media/", "/mnt/")): + return ( + "Can't open this file — if it's on a USB/external drive, run: " + "snap connect npyquick:removable-media" + ) + return "Can't open this file — the Snap only reaches your home folder and removable media." + def load_file(self, path: str) -> bool: try: self._model.load(path) except Exception as exc: - self._sb.showMessage(f"Error loading {path}: {exc}") + self._sb.showMessage(self._snap_sandbox_hint(path) or f"Error loading {path}: {exc}") return False self._current_path = path diff --git a/tests/test_app_workflow.py b/tests/test_app_workflow.py index e6e6f20..c62c547 100644 --- a/tests/test_app_workflow.py +++ b/tests/test_app_workflow.py @@ -8,6 +8,8 @@ import numpy as np +from npyquick.app import MainWindow + # --------------------------------------------------------------------------- # load_file() error handling: a bad file must not clobber prior state. @@ -42,6 +44,44 @@ def test_corrupt_npy_load_preserves_previously_loaded_array( np.testing.assert_array_equal(main_window._model.array, prior) +# --------------------------------------------------------------------------- +# load_file() Snap-sandbox hint: when confined as a Snap, a path the sandbox +# can't reach gets a clear explanation instead of a raw "No such file" error. +# --------------------------------------------------------------------------- + +def test_sandbox_hint_absent_outside_snap(monkeypatch): + monkeypatch.delenv("SNAP", raising=False) + assert MainWindow._snap_sandbox_hint("/data/x.npy") is None + + +def test_sandbox_hint_absent_for_paths_under_home(monkeypatch): + monkeypatch.setenv("SNAP", "/snap/npyquick/x1") + monkeypatch.setenv("SNAP_REAL_HOME", "/home/alice") + assert MainWindow._snap_sandbox_hint("/home/alice/lab/x.npy") is None + + +def test_sandbox_hint_for_other_drive(monkeypatch): + monkeypatch.setenv("SNAP", "/snap/npyquick/x1") + monkeypatch.setenv("SNAP_REAL_HOME", "/home/alice") + msg = MainWindow._snap_sandbox_hint("/data/experiments/x.npy") + assert msg is not None and "removable media" in msg and "snap connect" not in msg + + +def test_sandbox_hint_for_removable_media(monkeypatch): + monkeypatch.setenv("SNAP", "/snap/npyquick/x1") + monkeypatch.setenv("SNAP_REAL_HOME", "/home/alice") + msg = MainWindow._snap_sandbox_hint("/media/alice/usb/x.npy") + assert msg is not None and "snap connect npyquick:removable-media" in msg + + +def test_load_file_uses_sandbox_hint_when_confined(main_window, monkeypatch): + """A failed load of an out-of-sandbox path reports the hint, not the raw error.""" + monkeypatch.setenv("SNAP", "/snap/npyquick/x1") + monkeypatch.setenv("SNAP_REAL_HOME", "/home/alice") + main_window.load_file("/data/experiments/missing.npy") + assert "only reaches your home folder" in main_window._sb.currentMessage() + + # --------------------------------------------------------------------------- # Export Plot menu shape per view type, plus correct rebuild on transitions. # --------------------------------------------------------------------------- From 769acd8c994be87c7d865ad8ada2b9cce3d10215 Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Thu, 25 Jun 2026 15:58:08 +0900 Subject: [PATCH 08/12] Gate the Snap-sandbox hint on actual unreachability The hint previously fired for any failure on a path that resolved outside home/removable-media, which could mask the real error of a reachable file. Only return the hint when the path is genuinely unreachable from inside the Snap (os.path.exists is false); a visible-but-failing file keeps its real error. Also refine the wording: add sudo to the removable-media connect command (a normal user needs it), and say the package "can only access files in your home folder and connected removable-media locations" so it doesn't imply removable media is reachable without connecting the interface. Add tests for a home symlink that resolves onto another drive and for a visible external path not overriding the real error. Co-Authored-By: Claude Opus 4.8 --- src/npyquick/app.py | 18 ++++++++++++------ tests/test_app_workflow.py | 33 ++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/npyquick/app.py b/src/npyquick/app.py index 34defbe..7a49e2e 100644 --- a/src/npyquick/app.py +++ b/src/npyquick/app.py @@ -409,22 +409,28 @@ def open_file(self) -> None: @staticmethod def _snap_sandbox_hint(path: str) -> str | None: """When running as a confined Snap, explain why a file the sandbox - can't reach (anything outside the home folder and removable media) - failed to open. Returns None when not running as a Snap or when the - path is in reach, so the original error is shown instead. + can't reach failed to open. Returns None unless we are a Snap *and* + the path is genuinely unreachable from inside confinement — so a + visible-but-failing file (e.g. corrupt) keeps its real error. """ if not os.environ.get("SNAP"): return None + # Reachable from inside the Snap -> the failure has another cause. + if os.path.exists(path): + return None real = os.path.realpath(path) home = os.environ.get("SNAP_REAL_HOME") or os.path.expanduser("~") if real == home or real.startswith(home + os.sep): - return None + return None # a missing file under home is just a normal "not found" if real.startswith(("/media/", "/run/media/", "/mnt/")): return ( "Can't open this file — if it's on a USB/external drive, run: " - "snap connect npyquick:removable-media" + "sudo snap connect npyquick:removable-media" ) - return "Can't open this file — the Snap only reaches your home folder and removable media." + return ( + "Can't open this file — the Snap package can only access files in " + "your home folder and connected removable-media locations." + ) def load_file(self, path: str) -> bool: try: diff --git a/tests/test_app_workflow.py b/tests/test_app_workflow.py index c62c547..6f6b94d 100644 --- a/tests/test_app_workflow.py +++ b/tests/test_app_workflow.py @@ -51,7 +51,7 @@ def test_corrupt_npy_load_preserves_previously_loaded_array( def test_sandbox_hint_absent_outside_snap(monkeypatch): monkeypatch.delenv("SNAP", raising=False) - assert MainWindow._snap_sandbox_hint("/data/x.npy") is None + assert MainWindow._snap_sandbox_hint("/nonexistent_drive/x.npy") is None def test_sandbox_hint_absent_for_paths_under_home(monkeypatch): @@ -63,23 +63,42 @@ def test_sandbox_hint_absent_for_paths_under_home(monkeypatch): def test_sandbox_hint_for_other_drive(monkeypatch): monkeypatch.setenv("SNAP", "/snap/npyquick/x1") monkeypatch.setenv("SNAP_REAL_HOME", "/home/alice") - msg = MainWindow._snap_sandbox_hint("/data/experiments/x.npy") - assert msg is not None and "removable media" in msg and "snap connect" not in msg + msg = MainWindow._snap_sandbox_hint("/nonexistent_drive/experiments/x.npy") + assert msg is not None and "home folder" in msg and "snap connect" not in msg def test_sandbox_hint_for_removable_media(monkeypatch): monkeypatch.setenv("SNAP", "/snap/npyquick/x1") monkeypatch.setenv("SNAP_REAL_HOME", "/home/alice") - msg = MainWindow._snap_sandbox_hint("/media/alice/usb/x.npy") - assert msg is not None and "snap connect npyquick:removable-media" in msg + msg = MainWindow._snap_sandbox_hint("/media/nonexistent/usb/x.npy") + assert msg is not None and "sudo snap connect npyquick:removable-media" in msg + + +def test_sandbox_hint_follows_symlink_to_other_drive(tmp_path, monkeypatch): + """A symlink under home pointing to another drive resolves out of reach.""" + monkeypatch.setenv("SNAP", "/snap/npyquick/x1") + monkeypatch.setenv("SNAP_REAL_HOME", str(tmp_path)) # treat tmp_path as home + link = tmp_path / "link.npy" + link.symlink_to("/nonexistent_drive/experiments/x.npy") + msg = MainWindow._snap_sandbox_hint(str(link)) + assert msg is not None and "home folder" in msg + + +def test_sandbox_hint_absent_when_path_is_visible(tmp_path, monkeypatch): + """A reachable file that fails for another reason keeps its real error.""" + monkeypatch.setenv("SNAP", "/snap/npyquick/x1") + monkeypatch.setenv("SNAP_REAL_HOME", str(tmp_path / "home")) # home elsewhere + f = tmp_path / "x.npy" + f.write_bytes(b"x") # exists / visible, but outside the (fake) home + assert MainWindow._snap_sandbox_hint(str(f)) is None def test_load_file_uses_sandbox_hint_when_confined(main_window, monkeypatch): """A failed load of an out-of-sandbox path reports the hint, not the raw error.""" monkeypatch.setenv("SNAP", "/snap/npyquick/x1") monkeypatch.setenv("SNAP_REAL_HOME", "/home/alice") - main_window.load_file("/data/experiments/missing.npy") - assert "only reaches your home folder" in main_window._sb.currentMessage() + main_window.load_file("/nonexistent_drive/experiments/missing.npy") + assert "home folder" in main_window._sb.currentMessage() # --------------------------------------------------------------------------- From f453b2600cbd701233f7c6414b5be090f715491c Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Fri, 26 Jun 2026 00:19:15 +0900 Subject: [PATCH 09/12] Resolve home via realpath and shorten the sandbox hint Resolve SNAP_REAL_HOME with realpath before the prefix check so a home directory that is itself a symlink (or on another mount) still matches. Shorten the out-of-reach message. Parametrize the removable-media test over the /media, /run/media and /mnt roots. Verified on a real Snap install: a file on another drive shows the hint, a corrupt file under home keeps its real error, home files open normally. Co-Authored-By: Claude Opus 4.8 --- src/npyquick/app.py | 6 +++--- tests/test_app_workflow.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/npyquick/app.py b/src/npyquick/app.py index 7a49e2e..d40bfd2 100644 --- a/src/npyquick/app.py +++ b/src/npyquick/app.py @@ -419,7 +419,7 @@ def _snap_sandbox_hint(path: str) -> str | None: if os.path.exists(path): return None real = os.path.realpath(path) - home = os.environ.get("SNAP_REAL_HOME") or os.path.expanduser("~") + home = os.path.realpath(os.environ.get("SNAP_REAL_HOME") or os.path.expanduser("~")) if real == home or real.startswith(home + os.sep): return None # a missing file under home is just a normal "not found" if real.startswith(("/media/", "/run/media/", "/mnt/")): @@ -428,8 +428,8 @@ def _snap_sandbox_hint(path: str) -> str | None: "sudo snap connect npyquick:removable-media" ) return ( - "Can't open this file — the Snap package can only access files in " - "your home folder and connected removable-media locations." + "Can't open this file — Snap can only access your home folder " + "and connected removable-media locations." ) def load_file(self, path: str) -> bool: diff --git a/tests/test_app_workflow.py b/tests/test_app_workflow.py index 6f6b94d..30684ea 100644 --- a/tests/test_app_workflow.py +++ b/tests/test_app_workflow.py @@ -7,6 +7,7 @@ from __future__ import annotations import numpy as np +import pytest from npyquick.app import MainWindow @@ -67,10 +68,15 @@ def test_sandbox_hint_for_other_drive(monkeypatch): assert msg is not None and "home folder" in msg and "snap connect" not in msg -def test_sandbox_hint_for_removable_media(monkeypatch): +@pytest.mark.parametrize("path", [ + "/media/nonexistent/usb/x.npy", + "/run/media/alice/USB/x.npy", + "/mnt/nonexistent/x.npy", +]) +def test_sandbox_hint_for_removable_media(path, monkeypatch): monkeypatch.setenv("SNAP", "/snap/npyquick/x1") monkeypatch.setenv("SNAP_REAL_HOME", "/home/alice") - msg = MainWindow._snap_sandbox_hint("/media/nonexistent/usb/x.npy") + msg = MainWindow._snap_sandbox_hint(path) assert msg is not None and "sudo snap connect npyquick:removable-media" in msg From 03738121d7e2c51b14fc45fbcb3f48fa966add78 Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Fri, 26 Jun 2026 01:03:19 +0900 Subject: [PATCH 10/12] Document the Snap install in the README Add a Linux (Snap) install section after the AppImage one, a Snap Store version badge, and note the two ways the Snap differs from the AppImage/pip builds: sandboxed file access (home + connected removable media) and no double-click file association. Also picks up the AppImage filename fix (npyquick--x86_64.AppImage). Co-Authored-By: Claude Opus 4.8 --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f8217cf..d9324d1 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Python versions Windows EXE Linux AppImage + Snap Store License

@@ -31,7 +32,7 @@ npyquick is designed as a small, practical viewer for this job: **Linux (AppImage):** -On x86-64 Linux, AppImage is available for one-click installation. Download `npyquick-x86_64.AppImage` from the [latest release](https://github.com/LiukDiihMieu/npyquick/releases/latest), then: +On x86-64 Linux, AppImage is available for one-click installation. Download `npyquick--x86_64.AppImage` from the [latest release](https://github.com/LiukDiihMieu/npyquick/releases/latest), then: ```bash chmod +x npyquick-x86_64.AppImage @@ -40,6 +41,19 @@ chmod +x npyquick-x86_64.AppImage To open `.npy` / `.npz` by double-clicking in your file manager, register the default handler once with `./npyquick-x86_64.AppImage --install-desktop` (details in [Linux desktop integration](#linux-desktop-integration)). If you integrate the AppImage with a tool like [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher), this association is set up for you automatically. +**Linux (Snap):** + +On Ubuntu and other distributions with snap support, install from the Snap Store: + +```bash +sudo snap install npyquick +``` + +The Snap has two limitations the AppImage and pip builds don't: + +- **File access:** it's sandboxed to your home folder, plus removable media after you run `sudo snap connect npyquick:removable-media`. Files on other drives won't open — including home folders that are symlinks to another disk — so use the AppImage or pip install for those. +- **No double-click:** the Snap can't register `.npy` / `.npz` file associations, so double-clicking won't launch it. Open npyquick first, then drag a file onto the window, or use File › Open. + **Windows:** Download the `npyquick--setup.exe` installer from the [latest release](https://github.com/LiukDiihMieu/npyquick/releases/latest) and run it. It installs per-user (no administrator rights needed) and adds a Start Menu shortcut. To open `.npy` / `.npz` by double-clicking, tick *Associate .npy and .npz files with npyquick* during setup. From 16df1e463359091af192657c5715d138ea0d863b Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Fri, 26 Jun 2026 11:08:23 +0900 Subject: [PATCH 11/12] Add the Snap build/publish steps to the release checklist A manual Linux Snap section (built on an Ubuntu 22.04 host with LXD, not the tag CI): build, dangerous-install smoke test across views, menu icon, X11 and Wayland sessions, sandbox-hint check, edge upload, promotion to stable, and store metadata. Co-Authored-By: Claude Opus 4.8 --- docs/RELEASE_CHECKLIST.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index fa0230a..748a97f 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -30,6 +30,20 @@ verification of what CI produced (and the recipe if you ever build locally). - [ ] `--install-desktop` writes a correct desktop entry. - [ ] AppImageLauncher integration still works. +## Linux Snap + +Built and published manually (not by the tag CI), on an Ubuntu 22.04 host with LXD. + +- [ ] `snapcraft pack --use-lxd` builds the `.snap` (version is adopted from the AppStream ``). +- [ ] Installs with `sudo snap install --dangerous`. +- [ ] `.npy` and `.npz` sample files open across the image / table / cross-section / histogram views. +- [ ] Launches from the application menu with the correct icon. +- [ ] Works on both X11 and Wayland sessions (the Qt xcb path needs the staged xcb/xkb libs). +- [ ] A file outside the sandbox (e.g. on another drive) shows the sandbox hint, not a raw error. +- [ ] Uploaded to the edge channel: `snapcraft upload --release=edge ...`. +- [ ] After edge verification, promoted to stable: `snapcraft release npyquick stable`. +- [ ] Store text/icon updated with `snapcraft upload-metadata --force`; links and screenshots set in the web dashboard. + ## Windows installer - [ ] PyInstaller onedir build succeeds on Windows. From 5a9d5637d4560a03bbaabe5fa4a7f16022af1054 Mon Sep 17 00:00:00 2001 From: liukdiihmieu Date: Fri, 26 Jun 2026 15:16:37 +0900 Subject: [PATCH 12/12] Add a gated Snap CI workflow publishing to edge A separate snap.yml (workflow_dispatch + v* tags) builds the snap, smoke-tests it, and uploads to the Snap Store edge channel behind a manually-approved snap-store environment. Promotion edge -> stable stays manual. Also switches the snap base to core24. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/snap.yml | 66 ++++++++++++++++++++++++++++++++++++++ snap/snapcraft.yaml | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/snap.yml diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml new file mode 100644 index 0000000..dba7e10 --- /dev/null +++ b/.github/workflows/snap.yml @@ -0,0 +1,66 @@ +name: snap + +# Snap is its own pipeline, separate from the wheel-derived artifacts: it doesn't +# consume the shared wheel, it ships its own runtime (base: core24), and it +# publishes to the Snap Store rather than PyPI / the GitHub Release. It's also a +# heavy build, so it runs only on a tag or on demand — not on every push/PR. +on: + workflow_dispatch: + push: + tags: + - "v*" + +jobs: + # Build and smoke-test the snap, then hand it off as an artifact. This runs + # freely; only the store upload below is gated. + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # action-build installs LXD + snapcraft and builds inside a core24 + # container, so the runner OS is irrelevant. The version comes from the + # AppStream via adopt-info in snapcraft.yaml. + - name: Build the snap + uses: canonical/action-build@v1.3.0 + id: snapcraft + + # Catch "builds but won't run" before it can reach the store. + - name: Smoke-test the snap + run: | + sudo snap install --dangerous ${{ steps.snapcraft.outputs.snap }} + npyquick --version + + - name: Upload the snap + uses: actions/upload-artifact@v4 + with: + name: snap + path: ${{ steps.snapcraft.outputs.snap }} + + # Upload to the Snap Store's edge channel. Gated on the `snap-store` + # environment (like publish-pypi is on `pypi`): the job waits for a manual + # approval before anything is published. Promotion edge -> stable stays a + # separate manual step. Needs the SNAPCRAFT_STORE_CREDENTIALS secret (from + # `snapcraft export-login`); scope it to this environment. + publish: + needs: build + runs-on: ubuntu-latest + environment: snap-store + steps: + - name: Download the snap + uses: actions/download-artifact@v4 + with: + name: snap + path: snap-artifact + + - name: Locate the snap + id: loc + run: echo "path=$(ls snap-artifact/*.snap)" >> "$GITHUB_OUTPUT" + + - name: Publish to the edge channel + uses: canonical/action-publish@v1.2.0 + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + with: + snap: ${{ steps.loc.outputs.path }} + release: edge diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index bcf8bfe..8e2a6fe 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: npyquick -base: core22 +base: core24 # title, version, summary and description are adopted from the AppStream # metainfo file (see the `metadata` part), keeping them in one place shared # with the AppImage build instead of hand-maintaining them here.