diff --git a/README.md b/README.md index 8245a5c5..5aacbee0 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,8 @@ Language**; the choice applies immediately and persists. See [docs/localization. **Logs:** the desktop app writes its log to standard error and to `logs/lightcraft.log` in its settings folder (Linux `$XDG_CONFIG_HOME/lightcraft/logs/`, by default `~/.config/lightcraft/logs/`; macOS `~/Library/Application Support/LightCraft/logs/`; Windows `%APPDATA%\LightCraft\logs\`), never in the library. A -launch from a desktop menu or the Dock has no terminal, so attach this file to a bug report. Each start moves the +launch from a desktop menu or the Dock has no terminal, so attach this file to a bug report (**Help ▸ Open Log +Folder** shows it in the file manager). Each start moves the previous log to `lightcraft.1.log` and that one to `lightcraft.2.log`, so the log of a run that crashed survives the next start; the file stops growing at 16 MiB, `--version` and `--help` write none, and runs with `LIGHTCRAFT_NO_PREFS` log to standard error only. By default LightCraft's own crates log at `info` and everything else diff --git a/apps/lightcraft-web/src/web.rs b/apps/lightcraft-web/src/web.rs index 16543fd5..9ffbf170 100644 --- a/apps/lightcraft-web/src/web.rs +++ b/apps/lightcraft-web/src/web.rs @@ -219,6 +219,7 @@ fn services(originals: Originals, backend: Option, files: Files, frozen lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(img), &lightcraft_codecs::EncodeMeta::default()).unwrap_or_default() })), reveal: None, + log_file: None, open_with: None, open_url: Some(Box::new(|url: &str| { let w = web_sys::window().ok_or("no window")?; diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index 3f86b776..4c7bb424 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -281,8 +281,9 @@ fn windows_open_url_command(url: &str) -> std::process::Command { c } -/// The platform services; `ctx` repaints when a file dialog closes. -fn services(ctx: egui::Context) -> Services { +/// The platform services; `ctx` repaints when a file dialog closes, and `log_file` is +/// for Help ▸ Open Log Folder. +fn services(ctx: egui::Context, log_file: Option<&std::path::Path>) -> Services { Services { // Commands' file dialogs run on their own thread (#191): a dialog on the UI thread // stopped the window answering the compositor, which then reported the app as hung. @@ -317,6 +318,7 @@ fn services(ctx: egui::Context) -> Services { .map_err(|e| format!("could not start the file dialog: {e}"))?; Ok(rx) })), + log_file: log_file.map(|p| p.display().to_string()), pick_lightroom_catalog: Some(Box::new(|| { rfd::FileDialog::new() .set_title(lightcraft_ui_egui::i18n::tr("Import Lightroom Catalog")) @@ -637,6 +639,8 @@ fn main() -> eframe::Result { wgpu_options: window_wgpu_options(), ..Default::default() }; + // the app's copy: `log_file` is still named in the message of a start that fails + let app_log_file = log_file.clone(); let started = eframe::run_native( "LightCraft", options, @@ -648,7 +652,7 @@ fn main() -> eframe::Result { std::env::var_os("LIGHTCRAFT_SAM3_DIR").map(std::path::PathBuf::from).or_else(|| config_dir().map(|d| d.join("models").join("sam3"))); // the user's own download locations, one base URL per line (LIGHTCRAFT_SAM3_MIRRORS too) session.segmenter.mirrors_file = config_dir().map(|d| d.join("models").join("sam3-mirrors.txt")); - let mut app = LightcraftApp::new(session, services(cc.egui_ctx.clone())); + let mut app = LightcraftApp::new(session, services(cc.egui_ctx.clone(), app_log_file.as_deref())); if let Some(ui) = prefs { app.ui = ui; } diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index ad625ccb..e05e264a 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -1796,5 +1796,6 @@ "Only": "Nur diese", "How to install the model": "So installierst du das Modell", "Install the SAM 3 Model": "SAM-3-Modell installieren", - "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "Diese Version kann das Modell noch nicht herunterladen. Zum Installieren von Hand lege model.safetensors, vocab.json und merges.txt aus Metas facebook/sam3 in den obigen Ordner: Objekt und Beschreiben funktionieren, sobald sie dort liegen." + "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "Diese Version kann das Modell noch nicht herunterladen. Zum Installieren von Hand lege model.safetensors, vocab.json und merges.txt aus Metas facebook/sam3 in den obigen Ordner: Objekt und Beschreiben funktionieren, sobald sie dort liegen.", + "Open Log Folder": "Protokollordner öffnen" } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index 052eeb96..adc8eba7 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -1641,5 +1641,6 @@ "Preview only: editing the camera's embedded JPEG": "Solo vista previa: editando el JPEG incrustado de la cámara", "Captured from": "Capturadas desde", "Captured until": "Capturadas hasta", - "Only": "Solo" + "Only": "Solo", + "Open Log Folder": "Abrir carpeta de registros" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 80c40085..c95a7bf9 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -1641,5 +1641,6 @@ "繁體中文(台灣)": "繁體中文(台灣)", "How to install the model": "モデルのインストール方法", "Install the SAM 3 Model": "SAM 3モデルのインストール", - "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "このビルドではまだモデルをダウンロードできません。手動でインストールするには、Meta の facebook/sam3 の model.safetensors、vocab.json、merges.txt を上のフォルダに置いてください。置いた時点で「オブジェクト」と「説明で選択」が使えるようになります。" + "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "このビルドではまだモデルをダウンロードできません。手動でインストールするには、Meta の facebook/sam3 の model.safetensors、vocab.json、merges.txt を上のフォルダに置いてください。置いた時点で「オブジェクト」と「説明で選択」が使えるようになります。", + "Open Log Folder": "ログフォルダーを開く" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 529dd4bb..d085c572 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -941,6 +941,7 @@ "Opacity (%)": "Opacidade (%)", "Open Library": "Abrir biblioteca", "Open Library…": "Abrir biblioteca…", + "Open Log Folder": "Abrir pasta de logs", "Open in": "Abrir em", "Open in Detail": "Abrir em Detalhe", "Open the place in OpenStreetMap": "Abrir o local no OpenStreetMap", diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index 365a4bf2..16e01fe7 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -776,6 +776,7 @@ "Opacity (%)": "Непрозрачность (%)", "Open Library": "Открыть библиотеку", "Open Library…": "Открыть библиотеку…", + "Open Log Folder": "Открыть папку журналов", "Open in": "Открыть в", "Open in Detail": "Открыть в режиме «Детализация»", "Open the place in OpenStreetMap": "Открыть место в OpenStreetMap", diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index 3ec174f0..337fde80 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -1641,5 +1641,6 @@ "That includes the photos in the folders inside it.": "其中文件夹里的照片也包括在内。", "How to install the model": "如何安装模型", "Install the SAM 3 Model": "安装 SAM 3 模型", - "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚无法下载模型。如需手动安装,请将 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的文件夹:放入后即可使用“对象”和“描述”。" + "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚无法下载模型。如需手动安装,请将 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的文件夹:放入后即可使用“对象”和“描述”。", + "Open Log Folder": "打开日志文件夹" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index 7a60b4dd..5c118858 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -1641,5 +1641,6 @@ "That includes the photos in the folders inside it.": "其中資料夾裡的照片也包含在內。", "How to install the model": "如何安裝模型", "Install the SAM 3 Model": "安裝 SAM 3 模型", - "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚無法下載模型。如需手動安裝,請將 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的資料夾:放入後即可使用「物件」與「描述」。" + "This build can't download the model yet. To install it by hand, put model.safetensors, vocab.json and merges.txt from Meta's facebook/sam3 in the folder above: Object and Describe work as soon as they are there.": "此版本尚無法下載模型。如需手動安裝,請將 Meta 的 facebook/sam3 中的 model.safetensors、vocab.json 和 merges.txt 放入上方的資料夾:放入後即可使用「物件」與「描述」。", + "Open Log Folder": "開啟記錄資料夾" } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 8c723b9b..5df63661 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -118,6 +118,9 @@ pub struct Services { pub png: Option, /// Show a file in the system file manager (desktop only). pub reveal: Option, + /// The host's current log file (`/logs/lightcraft.log`), which Help ▸ Open Log + /// Folder reveals (#260). None where no log is kept: the web, `--memory`, `LIGHTCRAFT_NO_PREFS`. + pub log_file: Option, /// Choose a folder (Settings → General → Open Library…; desktop only). pub pick_folder: Option, /// Open a Lightroom Classic `.lrcat` catalog for read-only import. diff --git a/crates/ui-egui/src/menubar.rs b/crates/ui-egui/src/menubar.rs index 97c2d634..55c205d5 100644 --- a/crates/ui-egui/src/menubar.rs +++ b/crates/ui-egui/src/menubar.rs @@ -231,6 +231,7 @@ const LAYOUT: &[(&str, &[&str])] = &[ "app.whatsNew", "app.shortcuts", "app.systemInfo", + "app.openLogFolder", "---", "app.about", ], @@ -1086,6 +1087,37 @@ mod tests { assert!(run_item(&mut app, "merge.hdrLast", Value::Null).is_err()); } + /// #260: Help ▸ Open Log Folder reveals the log file the host names. Without one (the web, + /// `--memory`) or without a file manager to show it, the item is off and the command says why. + #[test] + fn open_log_folder_reveals_the_hosts_log_file() { + let mut app = app(); + let help = |app: &LightcraftApp| menu_bar(app).into_iter().find(|(t, _)| t == "Help").map(|(_, items)| items).unwrap_or_default(); + assert!(matches!(find(&help(&app), "app.openLogFolder"), Some(MenuNode::Item { enabled: false, .. })), "listed in Help, off without a log"); + assert!(!crate::menus::ui_enabled(&app, "app.openLogFolder")); + assert!(run_item(&mut app, "app.openLogFolder", Value::Null).is_err()); + let shown = std::sync::Arc::new(std::sync::Mutex::new(Vec::::new())); + let s = shown.clone(); + app.services.reveal = Some(Box::new(move |p: &str| { + s.lock().unwrap().push(p.to_string()); + Ok(()) + })); + // a file manager alone is not enough: this session keeps no log + assert!(!crate::menus::ui_enabled(&app, "app.openLogFolder")); + assert!(run_item(&mut app, "app.openLogFolder", Value::Null).is_err()); + assert!(shown.lock().unwrap().is_empty()); + let log = "/home/a/.config/lightcraft/logs/lightcraft.log"; + app.services.log_file = Some(log.into()); + assert!(crate::menus::ui_enabled(&app, "app.openLogFolder")); + assert!(matches!(find(&help(&app), "app.openLogFolder"), Some(MenuNode::Item { enabled: true, .. }))); + let r = run_item(&mut app, "app.openLogFolder", Value::Null).unwrap(); + assert_eq!(r["path"], log); + assert_eq!(shown.lock().unwrap().as_slice(), [log]); + // the file manager's failure is the command's + app.services.reveal = Some(Box::new(|_: &str| Err("no file manager".into()))); + assert_eq!(run_item(&mut app, "app.openLogFolder", Value::Null).unwrap_err(), "no file manager"); + } + #[test] fn find_missing_and_locate() { let dir = std::env::temp_dir().join(format!("lc-ui-missing-{}", std::process::id())); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 85218fb6..891a4201 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -168,6 +168,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[ ("app.openLibrary", "Open Library…", None, "File"), ("app.about", "About LightCraft", None, "Help"), ("app.systemInfo", "System Info…", None, "Help"), + ("app.openLogFolder", "Open Log Folder", None, "Help"), ("app.whatsNew", "What's New", None, "Help"), ("dialog.cull", "Assisted Culling…", None, "Photo"), ("app.help", "LightCraft Help", Some("F1"), "Help"), @@ -1381,6 +1382,7 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option crate::control::export_active(app, p), "app.showInFinder" => show_in_finder(app), + "app.openLogFolder" => open_log_folder(app), "app.discord" | "app.website" | "app.github" | "app.artcraft" | "app.help" | "app.feedback" => { let url = crate::links::url_of(id).unwrap_or(crate::links::WEBSITE); crate::links::open(app, url) @@ -1417,6 +1419,7 @@ pub fn ui_enabled(app: &LightcraftApp, id: &str) -> bool { "view.fullScreenPreview" | "view.infoOverlay" | "view.navigator" => app.session.active().is_some() || app.ui.fullscreen, "app.openLibrary" | "file.addFolder" => app.services.pick_folder.is_some() && !crate::lightroom_import::is_running(app), "file.backupLibrary" => app.services.backup_library.is_some(), + "app.openLogFolder" => app.services.reveal.is_some() && app.services.log_file.is_some(), "file.restoreLibrary" => app.services.restore_library.is_some(), "compare.swap" | "compare.makeSelect" => app.ui.view == ViewMode::Compare, s if s.starts_with("dialog.merge") || (s.starts_with("merge.") && s.ends_with("Last")) => { @@ -1486,6 +1489,15 @@ pub fn reveal_label() -> &'static str { } } +/// Help ▸ Open Log Folder: reveal the host's log file in the system file manager, so it can be +/// attached to a report without hunting for the settings folder (#260). +fn open_log_folder(app: &mut LightcraftApp) -> Result { + let path = app.services.log_file.clone().ok_or("this session keeps no log file")?; + let reveal = app.services.reveal.as_mut().ok_or("not available here")?; + reveal(&path)?; + Ok(json!({"path": path})) +} + /// Reveal the active photo's original in the system file manager. fn show_in_finder(app: &mut LightcraftApp) -> Result { let id = app.session.active().ok_or("no photo selected")?; diff --git a/docs/parity.md b/docs/parity.md index 94302e3e..6ceed281 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -45,12 +45,12 @@ and prints the summary; `cargo xtask parity --write` refreshes the summary table | V. Preferences (PREF) | 6 | 0 | 3 | 2 | 1/1 (100%) | 4/4 (100%) | | W. Cloud & AI infrastructure (CLOUD / AI) | 1 | 0 | 1 | 2 | — | — | | X. Cross-cutting behaviours (BEHAV) | 17 | 5 | 1 | 1 | 8/8 (100%) | 6/8 (75%) | -| Y. Menus | 81 | 0 | 4 | 8 | 47/47 (100%) | 23/23 (100%) | +| Y. Menus | 82 | 0 | 4 | 8 | 47/47 (100%) | 23/23 (100%) | | Z. Keyboard shortcuts (desktop) | 75 | 3 | 3 | 1 | 49/52 (94%) | 23/24 (96%) | | Lightroom Classic extras | 32 | 11 | 39 | 9 | — | 22/24 (92%) | -| **Total** | 392 | 36 | 86 | 36 | 193/200 (97%) | 142/152 (93%) | +| **Total** | 393 | 36 | 86 | 36 | 193/200 (97%) | 142/152 (93%) | -Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **79.8%** of 514 in-scope rows — P0 98.2% of 200 · P1 95.7% of 152 · P2 41.6% of 161. +Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **79.8%** of 515 in-scope rows — P0 98.2% of 200 · P1 95.7% of 152 · P2 42.0% of 162. ## Top gaps @@ -615,6 +615,7 @@ visible menu bar yet: the menu model is only exposed through the control channel | MENU-HELP-WHATSNEW | What's new | P2 | ✅ | `cmd:app.whatsNew` | | | MENU-HELP-SHORTCUTS | Keyboard shortcuts | P1 | ✅ | `cmd:app.shortcuts` | | | MENU-HELP-FEEDBACK | Send feedback | P2 | ✅ | `cmd:app.feedback` | opens a new issue on the project's GitHub | +| MENU-HELP-LOGFOLDER | Open log folder | P2 | ✅ | `cmd:app.openLogFolder` | an extra (the reference app has no such item): Help ▸ Open Log Folder reveals `logs/lightcraft.log` in the system file manager (Finder, Explorer, or the folder on Linux); off on the web and in `--memory` sessions, which keep no log (issue #260) | | MENU-HELP-SYSINFO | System info | P2 | ✅ | `cmd:app.systemInfo`, `cmd:library.info` | Help ▸ System Info…: version, OS, CPU threads, GPU, memory budget, preview size, library, timings; Copy to Clipboard; JSON for agents (`open: false`) | | MENU-CTX-GRID | Photo context menu | P0 | ✅ | `crates/ui-egui/src/panels/grid.rs` (`context_menu`) | rate, flag, label, add to / remove from album, rename, virtual copy, version, stack, copy / paste / paste selected, reset, merge, rotate, show in Finder, export / export with preset, set as album cover (in an album), delete (restore / delete permanently in Recently Deleted) | | MENU-CTX-DETAIL | Loupe context menu | P1 | ✅ | `crates/ui-egui/src/panels/detail.rs` | Zoom submenu (fit, 100%, in, out), then the photo menu | diff --git a/docs/whats-new.md b/docs/whats-new.md index f503d56f..3329742f 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -99,6 +99,8 @@ with the logs of the two previous runs beside it, so warnings and crashes of a run started from a desktop menu or the Dock can be attached to a bug report. `LIGHTCRAFT_LOG` works as before; `RUST_LOG` takes env_logger-style directives. See README → Quick start → Logs. +- Help → Open Log Folder shows that log file in the file manager (Finder, Explorer, or the folder on Linux), so it + can be attached to a report without hunting for the settings folder (issue #260). - `lightcraft-cli` logs warnings on stderr too (issue #168); `LIGHTCRAFT_LOG` or `RUST_LOG` picks another level. - LightCraft no longer crashes at launch on Windows PCs whose Vulkan driver is broken (issue #136, e.g. some Intel UHD 630 drivers): on Windows the window and GPU rendering use DirectX 12 only and never load the Vulkan driver unless