21 Commits
Author SHA1 Message Date
gronod 193e492c10 Update .gitea/workflows/build-macos.yml
Build Windows Packages / Build Windows (push) Successful in 7m35s
Build macOS Packages / Build macOS (Apple Silicon) (push) Successful in 9m25s
Build Linux Packages / Build Linux (push) Successful in 10m52s
Build macOS Packages / Build macOS (Intel) (push) Successful in 7m14s
Re-enabled arm64
2026-09-06 20:16:52 +01:00
gronod 899718d896 Merge pull request 'Merge development into main' (#220) from development into main
Build Windows Packages / Build Windows (push) Successful in 7m44s
Build macOS Packages / Build macOS (Intel) (push) Successful in 8m18s
Build Linux Packages / Build Linux (push) Successful in 10m18s
2026-09-06 19:26:32 +01:00
gronod 7a9637b5d0 Merge pull request 'ci(macos): allow headless DMG bundling on Gitea runners' (#219) from ci/fix-macos-dmg-bundling into development 2026-09-06 19:26:24 +01:00
gronod 1af844c7e2 ci(macos): allow headless DMG bundling on Gitea runners 2026-09-06 19:24:30 +01:00
gronod 56f275a023 Merge branch 'ci/215-frontend-tests' into development (#215) 2026-09-06 16:40:43 +01:00
gronod 54792a8690 Merge branch 'fix/213-atomic-history-writes' into development (#213) 2026-09-06 16:40:41 +01:00
gronod 906b4f9104 ci: add npm test script and integrate frontend tests into CI workflows (#215) 2026-09-06 16:40:37 +01:00
gronod 5f5bf76594 fix(quality_store): atomic file writes for verification_history.json (#213) 2026-09-06 16:39:53 +01:00
gronod 9c7d563dbf chore: bump version to v0.8.4 2026-09-06 16:37:59 +01:00
gronod 9102c95468 ci(macos): remove redundant cargo test step from release workflow
Build Windows Packages / Build Windows (push) Successful in 7m13s
Build Linux Packages / Build Linux (push) Successful in 10m27s
Build macOS Packages / Build macOS (Intel) (push) Failing after 10m35s
Merge pull request 'ci(macos): remove redundant cargo test step from release workflow' (#218) from development into main
2026-09-06 16:23:09 +01:00
gronod 743ad86f58 ci(macos): remove redundant cargo test step from release workflow 2026-09-06 16:22:57 +01:00
gronod 11e796fd17 Development
Build Windows Packages / Build Windows (push) Successful in 7m39s
Build Linux Packages / Build Linux (push) Successful in 9m54s
Build macOS Packages / Build macOS (Intel) (push) Failing after 14m45s
Merge pull request 'Development' (#217) from development into main
2026-09-06 16:08:49 +01:00
gronod ebbdbd4f55 chore: disable Apple Silicon and Universal build targets in macOS workflow 2026-09-06 16:06:24 +01:00
gronod ef0d65453e chore: configure git permissions in CI workflows and restrict crate source inclusion and build re-run triggers 2026-09-06 16:04:34 +01:00
gronod 9f2ff015d7 fix(cgats): support dataset file selection, sync wizard target state, and bump to v0.8.3 (#211) 2026-09-06 14:57:11 +01:00
gronod a8c4025c01 Merge pull request 'Development' (#216) from development into main
Build Windows Packages / Build Windows (push) Successful in 7m38s
Build Linux Packages / Build Linux (push) Successful in 8m59s
Build macOS Packages / Build macOS (Apple Silicon) (push) Failing after 1m13s
Build macOS Packages / Build macOS (Universal (Intel + Apple Silicon)) (push) Failing after 1m28s
Build macOS Packages / Build macOS (Intel) (push) Failing after 51s
Reviewed-on: #216
2026-09-06 13:56:17 +01:00
gronod 5a1ad654b8 fix(colprof): add select_spectrum_file command for custom sp browse (#210) 2026-09-06 12:51:52 +01:00
gronod ec7eb2fcb4 test(gamut): fix gamut_viewer.test.js node execution and guard window globals (#212) 2026-09-06 12:48:43 +01:00
gronod c5ff3d1a49 docs: resolve documentation and license inaccuracies (#214) 2026-09-06 12:41:32 +01:00
gronod 8008d9bda4 ci(macos): drop redundant .zip archive from macOS release assets (#209) 2026-09-06 12:23:14 +01:00
gronod ebcb3cdc2a ci(macos): drop redundant .zip archive from macOS release assets 2026-09-06 12:22:37 +01:00
21 changed files with 310 additions and 103 deletions
+3
View File
@@ -64,6 +64,9 @@ jobs:
- name: Install Node dependencies
run: npm ci
- name: Run frontend tests
run: npm test
- name: Set Release Environment
id: set_env
shell: bash
+21 -42
View File
@@ -9,6 +9,8 @@ jobs:
build-macos:
name: Build macOS (${{ matrix.platform.name }})
runs-on: ${{ matrix.platform.os }}
env:
XDG_CONFIG_HOME: ${{ runner.temp }}/.config
strategy:
matrix:
platform:
@@ -22,16 +24,26 @@ jobs:
target: aarch64-apple-darwin
binary_dir: macos-aarch64
arch: arm64
- name: Universal (Intel + Apple Silicon)
os: macos
target: universal-apple-darwin
binary_dir: macos-universal
arch: universal
# - name: Universal (Intel + Apple Silicon)
# os: macos
# target: universal-apple-darwin
# binary_dir: macos-universal
# arch: universal
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Ensure workspace and git permissions
run: |
mkdir -p "${{ runner.temp }}/.config/git"
touch "${{ runner.temp }}/.config/git/ignore"
mkdir -p .git/info
touch .git/info/exclude
chmod -R u+rwX .git || true
chmod 644 .git/info/exclude || true
git config --local core.excludesFile "${{ runner.temp }}/.config/git/ignore" || true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -47,6 +59,9 @@ jobs:
- name: Install Node dependencies
run: npm ci
- name: Run frontend tests
run: npm test
- name: Fetch ArgyllCMS binaries
env:
ARGYLL_SERVER_URL: ${{ github.server_url }}
@@ -59,21 +74,6 @@ jobs:
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/instlist"
test -x "src-tauri/argyll/${{ matrix.platform.binary_dir }}/targen"
- name: Run Rust tests
shell: bash
env:
CARGO_INCREMENTAL: "0"
CARGO_TARGET_DIR: "${{ runner.temp }}/cargo-target"
run: |
cd src-tauri
if [ "${{ matrix.platform.target }}" = "universal-apple-darwin" ]; then
cargo test
elif [ "${{ matrix.platform.target }}" = "aarch64-apple-darwin" ] && [ "$(uname -m)" != "arm64" ]; then
cargo test --no-run --target ${{ matrix.platform.target }}
else
cargo test --target ${{ matrix.platform.target }}
fi
- name: Set Release Environment
id: set_env
shell: bash
@@ -84,23 +84,9 @@ jobs:
echo "SHORT_SHA=${SHORT_SHA}" >> $GITHUB_ENV
echo "PREFIX=ICCery_${TAG}-${SHORT_SHA}-macos-${{ matrix.platform.arch }}" >> $GITHUB_ENV
- name: Warm up macOS GUI for DMG layout
shell: bash
run: |
# Tauri's DMG bundler runs Finder AppleScript to set the background
# picture and icon positions. On CI Macs, Finder/Aqua may be idle,
# which causes AppleEvent timeouts (-1712). Wake them before build.
echo "WindowServer: $(pgrep -l WindowServer || echo 'NOT RUNNING')"
echo "console user: $(stat -f '%Su' /dev/console 2>/dev/null || echo unknown)"
echo "whoami: $(whoami)"
open -g -a Finder || true
open -g -a "System Events" || true
osascript -e 'tell application "Finder" to get name' || true
sleep 10
- name: Build Tauri App
env:
TAURI_BUNDLER_DMG_IGNORE_CI: "true"
CI: "true"
run: npm run tauri build -- --target ${{ matrix.platform.target }}
- name: Prepare Release Assets
@@ -114,13 +100,6 @@ jobs:
cp "$DMG_FILE" "release-assets/${PREFIX}.dmg"
fi
# Stage ZIP archive of .app bundle
APP_DIR=$(find src-tauri/target -type d -path "*/release/bundle/macos/*.app" | head -n 1)
if [ -n "$APP_DIR" ] && [ -d "$APP_DIR" ]; then
APP_PARENT=$(dirname "$APP_DIR")
APP_NAME=$(basename "$APP_DIR")
(cd "$APP_PARENT" && zip -r "${GITHUB_WORKSPACE}/release-assets/${PREFIX}.zip" "$APP_NAME")
fi
- name: Upload Artifacts
uses: actions/upload-artifact@v3
+3
View File
@@ -41,6 +41,9 @@ jobs:
- name: Install Node dependencies
run: npm ci
- name: Run frontend tests
run: npm test
- name: Fetch ArgyllCMS binaries
shell: powershell
env:
+10 -7
View File
@@ -35,6 +35,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Ensure workspace and git permissions
run: |
mkdir -p "${{ runner.temp }}/.config/git"
touch "${{ runner.temp }}/.config/git/ignore"
mkdir -p .git/info
touch .git/info/exclude
chmod -R u+rwX .git || true
chmod 644 .git/info/exclude || true
git config --local core.excludesFile "${{ runner.temp }}/.config/git/ignore" || true
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -103,13 +113,6 @@ jobs:
cp "$DMG_FILE" "release-assets/${PREFIX}.dmg"
fi
# Stage ZIP archive of .app bundle
APP_DIR=$(find src-tauri/target -type d -path "*/release/bundle/macos/*.app" | head -n 1)
if [ -n "$APP_DIR" ] && [ -d "$APP_DIR" ]; then
APP_PARENT=$(dirname "$APP_DIR")
APP_NAME=$(basename "$APP_DIR")
(cd "$APP_PARENT" && zip -r "${GITHUB_WORKSPACE}/release-assets/${PREFIX}.zip" "$APP_NAME")
fi
- name: Upload Artifacts
uses: actions/upload-artifact@v4
+6 -6
View File
@@ -115,7 +115,7 @@ The "Preferences" button opens the native macOS `NSPrintPanel` (not CUPS web UI
- Record schema (`VerificationRecord` in `src-tauri/src/quality_store.rs`):
- `id`: unique record identifier in the format `vr-<epoch_millis>-<seq>`.
- `profile_name`: target profile filename.
- `printer`: device name captured at print spooling (`wizardState.printerName`), or "Unknown".
- `printer_name`: device name captured at print spooling (`wizardState.printerName`), or "Unknown".
- `avg_de`, `max_de`, `rms_de`: CIEDE2000 metrics from `profcheck` (using `-u` JSON summary).
- `patch_count`: number of test patches evaluated.
- `status`: classified status using **ICCery verification bands (issue #95)**:
@@ -124,7 +124,7 @@ The "Preferences" button opens the native macOS `NSPrintPanel` (not CUPS web UI
- `< 3.5`: "Acceptable" (`badge-acceptable`)
- `>= 3.5`: "Warning" (`badge-poor`)
- `timestamp`: ISO-8601 UTC string.
- Max capacity is 500 records; oldest records evicted on overflow.
- Max capacity is 1,000 records; oldest records evicted on overflow.
- Atomic file writes (`.tmp` write followed by `rename`) prevent data corruption.
- Tauri IPC command casing:
- Nested struct fields (`VerificationRecord`) serialize with `snake_case`.
@@ -163,9 +163,9 @@ The "Preferences" button opens the native macOS `NSPrintPanel` (not CUPS web UI
- Defaults to `false` ensuring 100% out-of-the-box compatibility with stock upstream ArgyllCMS binaries.
- Subprocess error diagnostics in `chartread.js` capture `lastStderrLine` from `process:stderr`, auto-expanding the Process Output `<details>` panel with the stderr explanation if an unpatched binary rejects `-Y l`.
## CI & Cross-Compilation Testing
## CI & Cross-Compilation
- macOS CI workflow lives in `.gitea/workflows/build-macos.yml`.
- On Intel runner hosts, cross-compiling for Apple Silicon (`aarch64-apple-darwin`) must use `cargo test --no-run --target aarch64-apple-darwin`. This validates sidecar packaging, compilation, and link correctness without attempting to execute ARM64 binaries on an Intel CPU (`Bad CPU type in executable (os error 86)`).
- Universal and native Intel matrix jobs run and execute full tests natively.
- Release packaging workflows live under `.gitea/workflows/` (`build-macos.yml`, `build-linux.yml`, `build-windows.yml`).
- Tag release builds focus exclusively on packaging via `npm run tauri build` without redundant debug-profile test compilations.
- Local/CI cross-compilation test execution for Apple Silicon (`aarch64-apple-darwin`) on Intel hosts must use `cargo test --no-run --target aarch64-apple-darwin` to avoid executing ARM64 binaries on an x86_64 CPU (`Bad CPU type in executable (os error 86)`).
+2 -2
View File
@@ -2,7 +2,7 @@
> Modern, cross-platform native desktop application for printer profiling, powered by ArgyllCMS.
[![Release](https://img.shields.io/badge/version-v0.8.2-blue.svg)](https://git.i3omb.com/gronod/ICCery)
[![Release](https://img.shields.io/badge/version-v0.8.4-blue.svg)](https://git.i3omb.com/gronod/ICCery)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://git.i3omb.com/gronod/ICCery)
[![Framework](https://img.shields.io/badge/framework-Tauri%20v2%20%2B%20Rust-orange.svg)](https://tauri.app)
[![License](https://img.shields.io/badge/license-Proprietary%20%2F%20EULA-blue.svg)](LICENCE.md)
@@ -27,7 +27,7 @@
- **Noise Reduction**: Multi-pass sheet averaging (`average`) to eliminate spectrophotometer noise.
4. **Stage 4 — Profile Calculation (`colprof`)**: Generate high-precision cLUT mathematical ICC/ICM profiles with configurable algorithm quality, OBA/FWA compensation (`-f`), illuminant (`-i`) and observer (`-o`) overrides, viewing-condition transforms (`-c`, `-d`), custom ambient spectrum support, descriptions, and copyright tagging.
5. **Stage 5 — Verification, Drift Analytics & 3D Gamut (`profcheck` + `iccgamut`)**:
- **Longitudinal Printer Drift Analytics (#95)**: Historical verification logging persisted to `verification_history.json` (up to 500 records), an interactive dual-series SVG trend chart with shaded ICCery verification reference bands, consecutive-breach alert recommendation card (detecting drift across distinct dates or $\ge 1$ hour apart), and RFC-4180 compliant CSV export.
- **Longitudinal Printer Drift Analytics (#95)**: Historical verification logging persisted to `verification_history.json` (up to 1,000 records), an interactive dual-series SVG trend chart with shaded ICCery verification reference bands, consecutive-breach alert recommendation card (detecting drift across distinct dates or $\ge 1$ hour apart), and RFC-4180 compliant CSV export.
- **Mathematical Accuracy Report**: Peak, Average, and RMS CIEDE2000 metrics with robust parsing of both Argyll JSON summaries (`-u`) and legacy plain-text reports.
- **Interactive 3D Gamut Viewer**: CIELAB coordinate scaffold with crisp CSS2D labels, per-vertex true-colour profile gamut shading, layer visibility toggles and opacity sliders, camera reset (press **R**), touch controls, and bundled sRGB reference wireframe comparison.
- 📊 **CGATS Dataset Interoperability (#94)**: Native parser for external CGATS and Argyll `.ti3` datasets with canonical normalization (0–255 scaling, field aliasing, metadata synthesis) and direct-jump workflows to Stage 4 (Profile Calculation) and Stage 5 (Verification).
+11 -2
View File
@@ -14,7 +14,7 @@ ICCery is a native, cross-platform desktop application built with:
- **Frontend**: Vanilla JS (ES Modules) + HTML5/CSS3 with a modern dark theme and responsive layout.
- **Visualization**: Three.js WebGL engine for 3D CIELAB color gamut volumes and sRGB reference comparisons.
- **Engine**: ArgyllCMS command-line utilities orchestrated over isolated standard stream IPC (`stdin`, `stdout`, `stderr`).
- **Current Version**: `v0.8.2` (Production release).
- **Current Version**: `v0.8.4` (Production release).
---
@@ -104,10 +104,19 @@ ICCery is a native, cross-platform desktop application built with:
- [x] **macOS CI Cross-Compilation Testing**: Hardened `.gitea/workflows/build-macos.yml` by compiling Apple Silicon tests with `--no-run` on Intel runner hosts to avoid architecture execution mismatch.
### Milestone 12 — Future Workflow & Advanced Analytics (`v0.8.2`)
- [x] **Printer Drift Tracking & Verification Analytics (#95)**: Track longitudinal printer drift in Stage 5 over time with historical run logging in `verification_history.json` (500 records), interactive dual-series SVG trend chart with ICCery verification reference bands, consecutive-breach alert recommendation card, and RFC-4180 CSV export.
- [x] **Printer Drift Tracking & Verification Analytics (#95)**: Track longitudinal printer drift in Stage 5 over time with historical run logging in `verification_history.json` (1,000 records), interactive dual-series SVG trend chart with ICCery verification reference bands, consecutive-breach alert recommendation card, and RFC-4180 CSV export.
- [x] **XY Automated Scanning Tables (#93)**: Full Stage 3 support for automated XY scanning tables (GretagMacbeth SpectroScan, X-Rite i1iO) with pure multi-line prompt classification, fiducial alignment prompts, 4-step sequence checklist, and graceful head parking on cancel.
- [ ] ~~**Multi-Language Localization (#96)**~~: *Closed — Won't Fix* (English UI retained as standard color-management terminology).
### Maintenance & Reliability Release (`v0.8.3`)
- [x] **Gamut Viewer Node Test Runner Support (#212)**: Guarded `window` and `window.__TAURI__` globals in `gamut_viewer.js` and added polyfill mock harness to `gamut_viewer.test.js` to enable automated headless test execution via `node src/js/gamut_viewer.test.js`.
- [x] **Custom Spectrum File Picker Dialog (#210)**: Implemented native `select_spectrum_file` command wrapping Tauri file dialog with `.sp` filter for custom FWA/OBA spectrum selection in Stage 4 profile generation.
- [x] **CGATS Dataset Import File Picker & State Synchronization (#211)**: Implemented native `select_dataset_file` command with `.ti3`, `.txt`, `.cgats`, and `.csv` filter, synchronized wizard target directory and basename upon import, and guarded against empty target states.
### Maintenance & Reliability Release (`v0.8.4`)
- [x] **Atomic Verification History Persistence (#213)**: Hardened `quality_store.rs` with atomic temporary file writes (`.tmp`), explicit flush/sync, and atomic rename to prevent historical drift data loss or corruption upon unexpected system crashes.
- [x] **Frontend Unit Testing & CI Integration (#215)**: Added standard `npm test` script executing the 3 frontend test suites (`profcheck`, `chartread`, and `gamut_viewer`) and integrated automated frontend test validation into macOS, Linux, and Windows CI workflows.
---
## 3. Future Roadmap
+18 -12
View File
@@ -23,20 +23,26 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
## Delaunator
Copyright (c) 2017, Mapbox
## quickhull3d
MIT License
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
## Tauri Framework & Rust Dependencies
Copyright (c) 2019-present, Tauri Programme within The Commons Conservancy.
+3 -2
View File
@@ -1,11 +1,12 @@
{
"name": "iccery",
"private": true,
"version": "0.8.2",
"version": "0.8.4",
"type": "module",
"scripts": {
"fetch-argyll": "node scripts/fetch-argyll.mjs",
"tauri": "tauri"
"tauri": "tauri",
"test": "node src/js/profcheck.test.js && node src/js/chartread.test.js && node src/js/gamut_viewer.test.js"
},
"devDependencies": {
"@tauri-apps/cli": "^2"
+1 -1
View File
@@ -1423,7 +1423,7 @@ dependencies = [
[[package]]
name = "iccery"
version = "0.8.2"
version = "0.8.4"
dependencies = [
"base64 0.22.1",
"image",
+12 -1
View File
@@ -1,9 +1,20 @@
[package]
name = "iccery"
version = "0.8.2"
version = "0.8.4"
description = "Modern Printer Profiling UI frontend for ArgyllCMS"
authors = ["Gordon"]
edition = "2021"
include = [
"src/**/*",
"build.rs",
"Cargo.toml",
"Cargo.lock",
"tauri.conf.json",
"capabilities/**/*",
"gen/**/*",
"icons/**/*",
"windows/**/*",
]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+6
View File
@@ -10,6 +10,12 @@ fn main() {
println!("cargo:rustc-env=BUILD_DATE={}", build_date);
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src");
println!("cargo:rerun-if-changed=tauri.conf.json");
println!("cargo:rerun-if-changed=Cargo.toml");
println!("cargo:rerun-if-changed=argyll");
// Validate that ArgyllCMS sidecar binaries are staged before building
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
+55
View File
@@ -391,6 +391,61 @@ pub async fn select_profile_file(
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
}
#[tauri::command]
pub async fn select_spectrum_file(
app: AppHandle,
default_dir: Option<String>,
) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let mut builder = app
.dialog()
.file()
.add_filter("Argyll Spectrum File (*.sp)", &["sp"]);
if let Some(ref dir) = default_dir {
if !dir.trim().is_empty() {
builder = builder.set_directory(std::path::PathBuf::from(dir));
}
}
let (tx, rx) = tokio::sync::oneshot::channel();
builder.pick_file(move |file_path| {
let res = file_path.map(|p| p.to_string());
let _ = tx.send(res);
});
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
}
#[tauri::command]
pub async fn select_dataset_file(
app: AppHandle,
default_dir: Option<String>,
) -> Result<Option<String>, String> {
use tauri_plugin_dialog::DialogExt;
let mut builder = app
.dialog()
.file()
.add_filter(
"Measurement Dataset (*.ti3, *.txt, *.cgats, *.csv)",
&["ti3", "txt", "cgats", "csv"],
);
if let Some(ref dir) = default_dir {
if !dir.trim().is_empty() {
builder = builder.set_directory(std::path::PathBuf::from(dir));
}
}
let (tx, rx) = tokio::sync::oneshot::channel();
builder.pick_file(move |file_path| {
let res = file_path.map(|p| p.to_string());
let _ = tx.send(res);
});
rx.await.map_err(|e| format!("Dialog channel error: {}", e))
}
#[tauri::command]
pub async fn select_target_file(
app: AppHandle,
+2
View File
@@ -51,6 +51,8 @@ pub fn run() {
commands::inspect_dataset_preview,
commands::select_existing_target,
commands::select_profile_file,
commands::select_spectrum_file,
commands::select_dataset_file,
commands::select_target_file,
commands::select_directory,
commands::send_stdin,
+70 -4
View File
@@ -100,15 +100,21 @@ pub fn load_history_file(path: &Path) -> Vec<VerificationRecord> {
}
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return Vec::new(),
Err(e) => {
log::warn!("Failed to read verification history at {}: {}", path.display(), e);
return Vec::new();
}
};
match serde_json::from_str::<VerificationHistoryStore>(&content) {
Ok(store) => store.records,
Err(_) => Vec::new(),
Err(e) => {
log::error!("Failed to parse verification history at {}: {}", path.display(), e);
Vec::new()
}
}
}
/// Writes verification records to the specified JSON path.
/// Writes verification records to the specified JSON path atomically.
/// Automatically creates parent directories if needed.
/// Evicts oldest records by timestamp if count exceeds HISTORY_CAP (1,000).
pub fn write_history_file(path: &Path, records: &[VerificationRecord]) -> Result<(), String> {
@@ -132,7 +138,33 @@ pub fn write_history_file(path: &Path, records: &[VerificationRecord]) -> Result
let json = serde_json::to_string_pretty(&store)
.map_err(|e| format!("Failed to serialize verification history: {}", e))?;
fs::write(path, json).map_err(|e| format!("Failed to write verification history: {}", e))?;
let mut tmp_path = path.as_os_str().to_os_string();
tmp_path.push(".tmp");
let tmp_path = PathBuf::from(tmp_path);
// Write to temporary file with explicit flush and sync
{
use std::io::Write;
let mut file = fs::File::create(&tmp_path)
.map_err(|e| format!("Failed to create temp history file: {}", e))?;
file.write_all(json.as_bytes())
.map_err(|e| {
let _ = fs::remove_file(&tmp_path);
format!("Failed to write temp history file: {}", e)
})?;
file.sync_all()
.map_err(|e| {
let _ = fs::remove_file(&tmp_path);
format!("Failed to sync temp history file: {}", e)
})?;
}
// Atomically replace destination file
if let Err(e) = fs::rename(&tmp_path, path) {
let _ = fs::remove_file(&tmp_path);
return Err(format!("Failed to atomically replace verification history file: {}", e));
}
Ok(())
}
@@ -418,4 +450,38 @@ mod tests {
assert_eq!(p2.extension().unwrap(), "csv");
assert_eq!(p2.to_string_lossy(), "/tmp/history.csv");
}
#[test]
fn test_atomic_write_preserves_data_and_cleans_tmp() {
let temp_dir = std::env::temp_dir().join("iccery_test_atomic_write");
let _ = fs::remove_dir_all(&temp_dir);
fs::create_dir_all(&temp_dir).unwrap();
let file_path = temp_dir.join("verification_history.json");
let mut tmp_file_path = file_path.as_os_str().to_os_string();
tmp_file_path.push(".tmp");
let tmp_file_path = std::path::PathBuf::from(tmp_file_path);
let rec = sample_record("vr-atom-1", "2026-09-06T12:00:00Z", 0.5);
write_history_file(&file_path, &[rec.clone()]).unwrap();
assert!(file_path.exists(), "Target file must exist");
assert!(!tmp_file_path.exists(), "Temporary file must not remain after successful atomic write");
let loaded = load_history_file(&file_path);
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0], rec);
// Overwrite with updated records to verify atomic replacement
let rec2 = sample_record("vr-atom-2", "2026-09-06T12:05:00Z", 1.2);
write_history_file(&file_path, &[rec.clone(), rec2.clone()]).unwrap();
assert!(!tmp_file_path.exists(), "Temporary file must not remain after overwrite");
let loaded2 = load_history_file(&file_path);
assert_eq!(loaded2.len(), 2);
assert_eq!(loaded2[0], rec);
assert_eq!(loaded2[1], rec2);
let _ = fs::remove_dir_all(&temp_dir);
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ICCery",
"version": "0.8.2",
"version": "0.8.4",
"identifier": "com.gronod.iccery",
"build": {
"frontendDist": "../src"
+3 -3
View File
@@ -1059,9 +1059,9 @@
<p>Copyright (c) 2010-2023 three.js authors</p>
<p class="license-legal-block">Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:<br/><br/>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.<br/><br/>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
<h5>Delaunator</h5>
<p>Copyright (c) 2017, Mapbox</p>
<p class="license-legal-block">Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.<br/><br/>THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.</p>
<h5>quickhull3d</h5>
<p>MIT License</p>
<p class="license-legal-block">Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:<br/><br/>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.<br/><br/>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
<h5>Tauri Framework &amp; Rust Dependencies</h5>
<p>Copyright (c) 2019-present, Tauri Programme within The Commons Conservancy.</p>
+40 -11
View File
@@ -1,4 +1,6 @@
const { invoke } = window.__TAURI__.core;
const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke
? window.__TAURI__.core.invoke
: async () => {};
export class CgatsInterop {
constructor(appState) {
@@ -22,26 +24,53 @@ export class CgatsInterop {
async handleImport() {
try {
const filePath = await invoke('select_target_file');
const filePath = await invoke('select_dataset_file', {
defaultDir: this.appState?.cwd || null,
});
if (!filePath) return; // User cancelled
// Inspect first to show modal (optional, skipping for now, directly import)
// Since we are mocking the UI a bit for this branch, we will just import directly
const isWindows = filePath.includes('\\');
const sep = isWindows ? '\\' : '/';
const parts = filePath.split(sep);
const fileName = parts.pop();
const fileDir = parts.join(sep);
const fileStem = fileName.replace(/\.[^/.]+$/, '');
const targetCwd = this.appState?.cwd || fileDir;
const targetBasename = this.appState?.basename || fileStem;
const targetBasenameInput = document.getElementById('targetBasename');
if (targetBasenameInput && !targetBasenameInput.value.trim()) {
targetBasenameInput.value = targetBasename;
}
const selectedPathDisplay = document.getElementById('selectedPathDisplay');
if (selectedPathDisplay && (!selectedPathDisplay.textContent || selectedPathDisplay.textContent.includes('No directory') || !this.appState?.cwd)) {
selectedPathDisplay.textContent = `Directory: ${targetCwd}`;
}
if (this.appState?.setTarget) {
await this.appState.setTarget(targetBasename, targetCwd);
}
const summary = await invoke('import_measurement_dataset', {
filePath,
targetCwd: this.appState.cwd,
targetBasename: this.appState.basename,
targetCwd,
targetBasename,
});
this.appState.showNotice(`Successfully imported dataset (${summary.patch_count} patches)`, 'success');
this.appState?.showNotice?.(`Successfully imported dataset (${summary.patch_count} patches)`, 'success');
// Update state to jump to stage 4
await this.appState.updateGating();
this.appState.currentStage = 4;
this.appState.applyStageDOM(4);
if (this.appState?.updateGating) {
await this.appState.updateGating();
}
if (this.appState) {
this.appState.currentStage = 4;
this.appState.applyStageDOM?.(4);
}
} catch (e) {
this.appState.showNotice(`Failed to import dataset: ${e}`, 'error');
this.appState?.showNotice?.(`Failed to import dataset: ${e}`, 'error');
}
}
+2 -2
View File
@@ -54,8 +54,8 @@ export function initColprof() {
if (btnBrowseCustomSp && colprofCustomSpPath) {
btnBrowseCustomSp.addEventListener("click", async () => {
try {
const selected = await window.__TAURI__.dialog.open({
filters: [{ name: 'Spectrum', extensions: ['sp'] }]
const selected = await invoke("select_spectrum_file", {
defaultDir: chartreadCwd || wizardState.cwd || null,
});
if (selected) {
colprofCustomSpPath.value = selected;
+2 -3
View File
@@ -1,8 +1,7 @@
import { computeQuickHull } from "./vendor/quickhull.js";
import { computeQuickHull } from "./vendor/quickhull.js";
import { labToSrgb } from "./color_convert.js";
const { invoke } = window.__TAURI__.core;
const { listen } = window.__TAURI__.event;
const invoke = typeof window !== 'undefined' && window.__TAURI__?.core?.invoke ? window.__TAURI__.core.invoke : null;
let scene, camera, renderer, labelRenderer, controls;
let currentProfileMesh = null;
+39 -4
View File
@@ -1,20 +1,50 @@
// Manual / browser-console tests for gamut_viewer.js and profcheck.js parsing.
// Run in a browser/devtools console after the app has loaded:
// Unit & console tests for gamut_viewer.js parsing.
// Can be run in browser devtools console:
// import('./gamut_viewer.test.js').then(m => m.runAll())
// Or in Node:
// node src/js/gamut_viewer.test.js
import { parseGamutFile } from './gamut_viewer.js';
// Node environment polyfill for browser globals
if (typeof window === 'undefined') {
globalThis.window = {
__TAURI__: {
core: { invoke: () => Promise.resolve() },
event: { listen: () => Promise.resolve(() => {}) }
},
addEventListener: () => {},
dispatchEvent: () => {}
};
globalThis.document = {
getElementById: () => null,
querySelectorAll: () => []
};
}
const { parseGamutFile } = await import('./gamut_viewer.js');
let passed = 0;
let total = 0;
export function runAll() {
console.group('gamut/profcheck parser tests');
console.group('Gamut Viewer Parser Tests');
passed = 0;
total = 0;
testParseGamutBasic();
testParseGamutDualTable();
testParseGamutWithComments();
console.log(`\nResults: ${passed} / ${total} tests passed.`);
console.groupEnd();
if (passed !== total) {
throw new Error(`Gamut viewer parser tests failed: ${total - passed} failure(s)`);
}
}
function assertEqual(actual, expected, message) {
total++;
const ok = JSON.stringify(actual) === JSON.stringify(expected);
if (ok) {
passed++;
console.log('PASS:', message);
} else {
console.error('FAIL:', message, 'expected', expected, 'got', actual);
@@ -78,3 +108,8 @@ END_DATA`;
assertEqual(vertices.length, 4, 'commented gamut vertex count');
assertEqual(faces.length, 2, 'commented gamut face count');
}
// Auto-run if executed in Node.js
if (typeof process !== 'undefined' && process.argv && process.argv[1]?.endsWith('gamut_viewer.test.js')) {
runAll();
}