bos-settings: capture every sidebar section, not just the default

The first pass only proved the Tauri capture pipeline against the
default landing view ("About"). All 24 real sidebar sections are wired
up now (every id in lib/sidebar.ts has a real component in
lib/views/registry.ts — no Placeholder fallbacks to skip).

Since there's no URL/route per section (activePage is plain Svelte
$state, switched by the sidebar's own click handler), the frontend now
also listens for a screenshot-set-view Tauri event in +page.svelte's
onMount and sets activePage from its payload — the same effect a real
sidebar click has, just driven from the Rust side. screenshot.rs emits
that event after the initial page-load settle delay, then waits a
second settle delay for the newly-active view's own Tauri-command data
fetch to land before capturing (each section fetches its own state on
mount, same as the About page's system-info fetch needed 2s in the
first pass).
This commit is contained in:
Breadway 2026-07-29 21:45:09 +08:00
parent ba7a43ad62
commit 93aeb26027
3 changed files with 80 additions and 29 deletions

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from "svelte";
import { listen } from "@tauri-apps/api/event";
import Sidebar from "$lib/components/Sidebar.svelte";
import Placeholder from "$lib/components/Placeholder.svelte";
import { DEFAULT_PAGE } from "$lib/sidebar";
@ -11,6 +12,15 @@
onMount(() => {
initTheme();
// Screenshot mode only (src-tauri's screenshot.rs) — lets the Rust
// core drive which sidebar section is showing for a capture without
// a real user ever clicking the sidebar.
const unlisten = listen<string>("screenshot-set-view", (event) => {
activePage = event.payload;
});
return () => {
unlisten.then((f) => f());
};
});
</script>

View file

@ -12,7 +12,7 @@ pub fn run() {
.setup(move |app| {
commands::theme::watch_and_emit(app.handle());
if let Some(req) = screenshot_req {
screenshot::dispatch(req);
screenshot::dispatch(req, app.handle().clone());
}
Ok(())
})

View file

@ -1,35 +1,69 @@
//! `--screenshot` CLI mode: capture the settings window via
//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s
//! `bread-capture` orchestrator, or run standalone for one-off captures.
//! `--screenshot` CLI mode: switch the Svelte SPA to the named sidebar
//! section, capture it via `bread-screenshots`, then exit — driven by
//! `bread-ecosystem`'s `bread-capture` orchestrator, or run standalone for
//! one-off captures.
//!
//! First pass at a Tauri (webview, not raw GTK4) target: there's no
//! `connect_map`/`glib` signal to hook here the way every other bread-
//! ecosystem app's screenshot mode does, since the window is owned by
//! tao/wry, not gtk4-rs directly. Instead this just waits a fixed
//! [`SETTLE_DELAY`] on Tauri's own async runtime after `setup()` runs —
//! longer than the native apps' settle delays, since a webview's first
//! paint means a full page load (JS bundle parse + Svelte mount), not just
//! GTK widget layout. The window itself is a plain, non-layer-shell
//! toplevel (per `tauri.conf.json`'s fixed 960x640 size), so — same
//! reasoning as breadman/breadhelp — a full known-size canvas capture is
//! enough; no geometry to track.
//! There's no `connect_map`/`glib` signal to hook here the way every other
//! bread-ecosystem app's screenshot mode does, since the window is owned by
//! tao/wry (Tauri's Linux backend), not gtk4-rs directly. Instead this
//! waits a fixed [`INITIAL_SETTLE_DELAY`] on Tauri's own async runtime after
//! `setup()` runs for the page's first paint (JS bundle parse + Svelte
//! mount) — longer than the native apps' settle delays, since a webview's
//! first paint is a full page load, not just GTK widget layout — then emits
//! a `screenshot-set-view` event the frontend listens for
//! (`+page.svelte`'s `onMount`) to switch `activePage` exactly like a real
//! sidebar click would, then waits [`VIEW_SETTLE_DELAY`] more for that
//! view's own data to load (each section fetches its own state over Tauri
//! commands on mount) before capturing. The window itself is a plain,
//! non-layer-shell toplevel (per `tauri.conf.json`'s fixed 960x640 size),
//! so — same reasoning as breadman/breadhelp — a full known-size canvas
//! capture is enough; no geometry to track.
//!
//! Only one view ("default", the settings window's initial landing
//! section) is wired up so far. The frontend is a Svelte SPA with its own
//! sidebar routing for each settings section (Appearance, Network,
//! Bluetooth, ...) — capturing one of those specifically would mean
//! passing a route via a URL/hash on window creation and is real, separate
//! frontend work, deferred past this first pass (which exists to prove the
//! isolated-headless-Sway pipeline works against a Tauri/webview window at
//! all, not to reach full view parity with the native GTK4 apps).
//! View names match `frontend/src/lib/sidebar.ts`'s item ids exactly (see
//! `KNOWN_VIEWS`) — every one of them has a real registered component (see
//! `frontend/src/lib/views/registry.ts`), no Placeholder fallbacks to skip.
use std::path::PathBuf;
use std::time::Duration;
use tauri::Emitter;
const SETTLE_DELAY: Duration = Duration::from_millis(2000);
const KNOWN_VIEWS: &[&str] = &["default"];
const INITIAL_SETTLE_DELAY: Duration = Duration::from_millis(2000);
/// Applied after switching views — shorter than the initial load (no full
/// page/JS reload, just a component swap + that view's own Tauri-command
/// data fetch), but the About page alone needed 2s for its fetch to land
/// (see the initial-pass commit), so this stays generous rather than
/// re-guessing per view.
const VIEW_SETTLE_DELAY: Duration = Duration::from_millis(2000);
const KNOWN_VIEWS: &[&str] = &[
"network",
"breadcrumbs",
"bluetooth",
"firewall",
"sound",
"power",
"datetime",
"hyprland",
"keybinds",
"autostart",
"users",
"appearance",
"breadpaper",
"breadbar",
"breadbox",
"breadclip",
"breadpad",
"breadsearch",
"bread",
"packages",
"aur",
"firmware",
"snapshots",
"about",
];
pub struct ScreenshotRequest {
pub view: String,
pub output: PathBuf,
pub width: u32,
pub height: u32,
@ -73,13 +107,20 @@ pub fn parse(args: &[String]) -> Option<ScreenshotRequest> {
eprintln!("bos-settings: --screenshot requires --output");
std::process::exit(1);
};
Some(ScreenshotRequest { output: output.into(), width, height })
Some(ScreenshotRequest { view, output: output.into(), width, height })
}
/// Schedule the capture-then-exit sequence. Called once from `setup()`.
pub fn dispatch(req: ScreenshotRequest) {
/// Schedule the switch-view-then-capture-then-exit sequence. Called once
/// from `setup()`, which is also where `app` (needed to emit the
/// `screenshot-set-view` event) comes from.
pub fn dispatch(req: ScreenshotRequest, app: tauri::AppHandle) {
tauri::async_runtime::spawn(async move {
tokio::time::sleep(SETTLE_DELAY).await;
tokio::time::sleep(INITIAL_SETTLE_DELAY).await;
if let Err(e) = app.emit("screenshot-set-view", &req.view) {
eprintln!("bos-settings: failed to emit screenshot-set-view: {e}");
std::process::exit(1);
}
tokio::time::sleep(VIEW_SETTLE_DELAY).await;
finish(bread_screenshots::capture_region(
0,
0,