Part of #260: Help ▸ Open Log Folder shows the log file in the file manager

The desktop app keeps logs/lightcraft.log in its settings folder (#286)
and a failed start names it (#370), but a user still had to find the
folder by hand. The host now hands the UI its log file (Services
.log_file) and Help ▸ Open Log Folder (app.openLogFolder) reveals it
through the existing reveal service: Finder and Explorer select the
file, Linux opens the folder. Off where no log is kept (the web,
--memory, LIGHTCRAFT_NO_PREFS). Translated in de, es, ja, pt-br, ru,
zh-hans and zh-hant; parity row MENU-HELP-LOGFOLDER; What's New and
README note it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ragtux 2026-10-08 18:06:25 -05:00
parent bba4172f83
commit 0cf3ceb438
15 changed files with 75 additions and 12 deletions

View File

@ -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

View File

@ -219,6 +219,7 @@ fn services(originals: Originals, backend: Option<Backend>, 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")?;

View File

@ -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;
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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": "ログフォルダーを開く"
}

View File

@ -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",

View File

@ -776,6 +776,7 @@
"Opacity (%)": "Непрозрачность (%)",
"Open Library": "Открыть библиотеку",
"Open Library…": "Открыть библиотеку…",
"Open Log Folder": "Открыть папку журналов",
"Open in": "Открыть в",
"Open in Detail": "Открыть в режиме «Детализация»",
"Open the place in OpenStreetMap": "Открыть место в OpenStreetMap",

View File

@ -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": "打开日志文件夹"
}

View File

@ -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": "開啟記錄資料夾"
}

View File

@ -118,6 +118,9 @@ pub struct Services {
pub png: Option<PngEncode>,
/// Show a file in the system file manager (desktop only).
pub reveal: Option<RevealFn>,
/// The host's current log file (`<settings>/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<String>,
/// Choose a folder (Settings → General → Open Library…; desktop only).
pub pick_folder: Option<PickFolder>,
/// Open a Lightroom Classic `.lrcat` catalog for read-only import.

View File

@ -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::<String>::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()));

View File

@ -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<Re
}
"app.export" => 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<Value, String> {
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<Value, String> {
let id = app.session.active().ok_or("no photo selected")?;

View File

@ -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.
<!-- /parity:summary -->
## 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 |

View File

@ -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