diff --git a/PRIVACY.md b/PRIVACY.md
index 54fa2f86..a1afbae6 100644
--- a/PRIVACY.md
+++ b/PRIVACY.md
@@ -66,7 +66,7 @@ To be absolutely clear:
- ❌ **Does not use advertising or ad networks**
- ❌ **Does not send your data to any cloud AI** — The text in your pictures is read on your machine, by the operating system itself (Windows OCR on Windows, Apple Vision on macOS)
- ❌ **Does not sync across devices**
-- ❌ **Does not upload crash reports** — A crash is written to the local log beside the history, and sharing it is a file you attach yourself
+- ❌ **Does not upload crash reports** — A crash is written to the local log beside the history, and sharing it is a file you attach yourself. **Save a report…** in About puts that log, the version and the system into one file, with your user name and home folder blanked out, wherever you choose
- ❌ **Does not phone home** — No background network calls except the update checker described below (all platforms)
---
diff --git a/app/package-lock.json b/app/package-lock.json
index 9b28cea2..99f9163c 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -4690,13 +4690,14 @@
}
},
"node_modules/qs": {
- "version": "6.15.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
- "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "version": "6.16.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
+ "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
- "side-channel": "^1.1.0"
+ "es-define-property": "^1.0.1",
+ "side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
@@ -4971,9 +4972,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -5161,15 +5162,15 @@
}
},
"node_modules/typed-rest-client": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.1.tgz",
- "integrity": "sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.0.tgz",
+ "integrity": "sha512-FfBj5tjviexjIus3La4n4s9i+f81Zj7HU+lUlQWK219HMRfmzLsbIf4PZF2+X6EouJKyuANpvvef5VrUWM4AFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"des.js": "^1.1.0",
"js-md4": "^0.3.2",
- "qs": "6.15.1",
+ "qs": "^6.14.1",
"tunnel": "0.0.6",
"underscore": "^1.13.8"
},
diff --git a/app/package.json b/app/package.json
index 4f398ad6..4827820a 100644
--- a/app/package.json
+++ b/app/package.json
@@ -44,6 +44,6 @@
"vitest": "4.1.11"
},
"overrides": {
- "qs": "6.15.2"
+ "qs": "6.16.0"
}
}
diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs
index b98f6b51..bf78823f 100644
--- a/app/src-tauri/src/lib.rs
+++ b/app/src-tauri/src/lib.rs
@@ -4,6 +4,7 @@ mod links;
mod looks;
mod note;
mod panel;
+mod report;
mod reviving;
mod settings;
mod tray;
@@ -45,6 +46,8 @@ pub fn run() {
backup::load_backup,
backup::bring_former,
backup::drop_former,
+ report::save_report,
+ report::open_log,
update::update_ready,
update::install_route,
update::update_install,
diff --git a/app/src-tauri/src/report.rs b/app/src-tauri/src/report.rs
new file mode 100644
index 00000000..b6d6348d
--- /dev/null
+++ b/app/src-tauri/src/report.rs
@@ -0,0 +1,243 @@
+use std::io::{Read, Seek, SeekFrom};
+use std::path::{Path, PathBuf};
+
+use tauri_plugin_dialog::DialogExt;
+use tauri_plugin_opener::OpenerExt;
+
+const TAIL: u64 = 2 * 1024 * 1024;
+const LINES: usize = 2_000;
+const LOGS: [&str; 4] = [
+ "cp-gui.log.1",
+ "cp-gui.log",
+ "cp-panel.log.1",
+ "cp-panel.log",
+];
+
+fn logs() -> PathBuf {
+ let log = crate::note::where_to();
+ log.parent()
+ .map_or_else(std::env::temp_dir, Path::to_path_buf)
+}
+
+#[tauri::command]
+pub fn open_log(app: tauri::AppHandle) -> Result<(), String> {
+ let dir = logs();
+ std::fs::create_dir_all(&dir).map_err(|why| why.to_string())?;
+ let dir = seen_outside(dir);
+ app.opener()
+ .open_path(dir.to_string_lossy(), None::<&str>)
+ .map_err(|why| why.to_string())
+}
+
+#[cfg(windows)]
+fn seen_outside(dir: PathBuf) -> PathBuf {
+ if crate::update::route() != crate::update::Route::Store {
+ return dir;
+ }
+ cp_win_sys::paths::in_package(&dir)
+ .filter(|it| it.exists())
+ .unwrap_or(dir)
+}
+
+#[cfg(not(windows))]
+fn seen_outside(dir: PathBuf) -> PathBuf {
+ dir
+}
+
+#[tauri::command(async)]
+pub fn save_report(
+ app: tauri::AppHandle,
+ window: tauri::WebviewWindow,
+) -> Result
-
+ {report && (
+
+ {t("reportSaved")}{" "}
+ {
+ revealItemInDir(report).catch(() => setTrouble(t("reportShowRefused")));
+ }}
+ >
+ {t("reportShow")}
+
+
+ )}
diff --git a/crates/cp-win-sys/src/paths.rs b/crates/cp-win-sys/src/paths.rs
index f7764c16..5df2469e 100644
--- a/crates/cp-win-sys/src/paths.rs
+++ b/crates/cp-win-sys/src/paths.rs
@@ -21,18 +21,30 @@ pub fn legacy_in(local: &std::path::Path) -> PathBuf {
if plain.join("clipboard.db").exists() {
return plain;
}
- let packaged = local
- .join("Packages")
- .join(STORE_FAMILY)
- .join("LocalCache")
- .join("Local")
- .join("CopyPaste");
+ let packaged = in_package_of(local, &plain).unwrap_or_else(|| plain.clone());
if packaged.join("clipboard.db").exists() {
return packaged;
}
plain
}
+pub fn in_package(path: &std::path::Path) -> Option
{
+ let local = std::env::var_os("LOCALAPPDATA")?;
+ in_package_of(std::path::Path::new(&local), path)
+}
+
+pub fn in_package_of(local: &std::path::Path, path: &std::path::Path) -> Option {
+ let rest = path.strip_prefix(local).ok()?;
+ Some(
+ local
+ .join("Packages")
+ .join(STORE_FAMILY)
+ .join("LocalCache")
+ .join("Local")
+ .join(rest),
+ )
+}
+
pub fn legacy_database() -> Option {
Some(legacy_dir()?.join("clipboard.db"))
}
diff --git a/crates/cp-win-sys/src/paths_test.rs b/crates/cp-win-sys/src/paths_test.rs
index 4c86d338..0196d1b3 100644
--- a/crates/cp-win-sys/src/paths_test.rs
+++ b/crates/cp-win-sys/src/paths_test.rs
@@ -97,3 +97,26 @@ fn with_no_2x_anywhere_the_plain_folder_is_the_answer() {
assert_eq!(legacy_in(&local), local.join("CopyPaste"));
let _ = std::fs::remove_dir_all(&local);
}
+
+#[test]
+fn a_folder_under_local_app_data_has_its_twin_inside_the_package() {
+ let local = std::path::Path::new(r"C:\Users\u\AppData\Local");
+ assert_eq!(
+ in_package_of(local, &local.join("CopyPaste").join("logs")),
+ Some(packaged_in(local).join("logs"))
+ );
+ assert_eq!(
+ in_package_of(local, std::path::Path::new(r"D:\elsewhere")),
+ None
+ );
+}
+
+#[test]
+fn the_package_twin_is_looked_for_under_this_machine_s_local_app_data() {
+ let Some(dir) = data_dir() else {
+ return;
+ };
+ let local = std::path::PathBuf::from(std::env::var_os("LOCALAPPDATA").expect("local"));
+ assert_eq!(in_package(&dir), in_package_of(&local, &dir));
+ assert!(in_package(&dir).is_some_and(|it| it.ends_with("CopyPaste")));
+}
diff --git a/package-lock.json b/package-lock.json
index 2f31fa15..50b652c5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -158,13 +158,13 @@
}
},
"node_modules/commander": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
- "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
+ "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">= 12"
+ "node": ">=22.12.0"
}
},
"node_modules/debug": {
@@ -469,9 +469,9 @@
}
},
"node_modules/katex": {
- "version": "0.16.47",
- "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
- "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
+ "version": "0.18.10",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.18.10.tgz",
+ "integrity": "sha512-/B6p9eY9DX7aHBfpkHdpirDTZ5QH9xTwL9y837PWuzuv41O5jFdqNQwVQEQsimY0/iVNkwBY4+4hMfhQ7d4Dbw==",
"dev": true,
"funding": [
"https://opencollective.com/katex",
@@ -479,7 +479,7 @@
],
"license": "MIT",
"dependencies": {
- "commander": "^8.3.0"
+ "commander": "^15.0.0"
},
"bin": {
"katex": "cli.js"
@@ -1301,9 +1301,9 @@
}
},
"node_modules/smol-toml": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.8.0.tgz",
- "integrity": "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==",
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz",
+ "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
diff --git a/package.json b/package.json
index 4d01dd50..2657e44a 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,10 @@
"devDependencies": {
"markdownlint-cli2": "^0.23.2"
},
+ "overrides": {
+ "katex": "^0.18.2",
+ "smol-toml": "^1.9.0"
+ },
"scripts": {
"prepare": "git config core.hooksPath hooks || echo the hooks are not set: this is not a git clone",
"lint:md": "markdownlint-cli2",