Files
Unlucky-Launcher/crates/app/src/screenshot.rs
T
AskaEth 8a8e5ca0c0
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Unlucky Launcher v0.2.0
首个 iced + Material 3 Expressive 版本的源码快照。
完整历史与日常提交在 dev 分支;main 只放阶段性快照。
2026-11-05 14:22:11 +08:00

178 lines
6.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 截图验收通道的参数解析与流程(`--screenshot*` / `--data-dir` / `--mock`)。
//!
//! 为什么用进程内渲染而不是截屏:验收机没有交互桌面(`CopyFromScreen` 直接失败),
//! 而 Slint 的软件渲染器能把场景渲进内存像素缓冲(`Window::take_snapshot`),
//! 结果与屏幕上逐像素一致且不需要 GPU(M41 落地的通道,这里只加页面/中间态参数)。
//!
//! 中间态(安装进度 / 启动事件流)走真实后端:`--screenshot-install <版本>` 会真的发起
//! 一次安装并停若干毫秒后取图,`--screenshot-launch <实例>` 同理。
use std::path::PathBuf;
use crate::ui_bind::AppUi;
use unlucky_presenter::backend::AppOptions;
use unlucky_presenter::dto::AppCommand;
/// 一次截图请求。
#[derive(Debug, Clone, Default)]
pub struct ShotOptions {
/// 输出 PNG 路径
pub path: String,
/// 页面名(instances / download / java / logs / settings / account / appearance / agent)
pub page: Option<String>,
/// 打开新建实例向导
pub wizard: bool,
/// 发起一次真实安装并停 `dwell_ms` 后取图(中间态)
pub install: Option<String>,
/// 安装走哪个加载器(`--screenshot-loader forge`;缺省 = 原版)
pub loader: Option<String>,
/// 发起一次真实启动并停 `dwell_ms` 后取图(中间态)
pub launch: Option<String>,
/// 打开一个日志会话(日志页有数据态:日志行 + 诊断卡片)
pub session: Option<String>,
/// 中间态停留毫秒(默认 2500)
pub dwell_ms: u64,
}
impl ShotOptions {
/// 中间态默认停留时长。
pub const DEFAULT_DWELL_MS: u64 = 2_500;
}
/// 从命令行读启动选项(数据根 / mock)。
pub fn app_options() -> AppOptions {
AppOptions {
data_root: value_of("--data-dir").map(PathBuf::from),
mock: flag("--mock"),
}
}
/// 从命令行读截图请求;没有 `--screenshot` 时返回 `None`。
pub fn shot_options() -> Option<ShotOptions> {
let path = value_of("--screenshot")?;
let page = value_of("--screenshot-page");
let mut options = ShotOptions {
path,
page: if flag("--screenshot-settings") {
Some("appearance".to_string())
} else {
page
},
wizard: flag("--screenshot-wizard"),
install: value_of("--screenshot-install"),
loader: value_of("--screenshot-loader"),
launch: value_of("--screenshot-launch"),
session: value_of("--screenshot-session"),
dwell_ms: value_of("--screenshot-dwell")
.and_then(|value| value.parse().ok())
.unwrap_or(ShotOptions::DEFAULT_DWELL_MS),
};
if options.dwell_ms < 500 {
options.dwell_ms = 500;
}
options.page = options.page.filter(|page| !page.trim().is_empty());
Some(options)
}
/// 执行截图流程:切页 → (可选)中间态 → 渲染 PNG。
///
/// # Errors
/// 窗口显示/快照/写文件失败时返回错误。
pub fn capture(app: &AppUi, options: &ShotOptions) -> Result<(), slint::PlatformError> {
if let Some(page) = &options.page {
app.open_page(page);
// 先给后端一次全量刷新:版本清单要在后台线程里拉(12h 缓存),
// 下载页/Java 页/向导需要它才有「有数据态」。
let warmup = if matches!(page.as_str(), "download" | "java") {
4_000
} else {
1_200
};
app.dispatch_and_run(AppCommand::RefreshAll, warmup);
}
if options.wizard {
app.open_wizard();
}
if let Some(version) = &options.install {
app.dispatch_and_run(
install_command(version, options.loader.as_deref()),
options.dwell_ms,
);
}
if let Some(session) = &options.session {
app.open_session(session);
app.dispatch_and_run(AppCommand::RefreshLogSessions, options.dwell_ms.min(1_000));
}
if let Some(instance) = &options.launch {
app.dispatch_and_run(
AppCommand::LaunchInstance(instance.clone()),
options.dwell_ms,
);
}
app.render_screenshot(&options.path)?;
// 启动中间态截完就把进程收掉:截图进程退出后子进程会变孤儿,
// 所以在取图之后显式 stop(kill_tree),不留给系统。
if let Some(instance) = &options.launch {
app.dispatch_and_run(AppCommand::StopInstance(instance.clone()), 1_500);
}
Ok(())
}
/// 截图通道的安装指令:缺省原版,给了 `--screenshot-loader` 就走向导同一条路
/// (`CreateInstance`),因此 Forge/NeoForge 的中间态也能取图。
fn install_command(version: &str, loader: Option<&str>) -> AppCommand {
match loader.map(unlucky_presenter::dto::LoaderKind::parse) {
Some(Some(kind)) => AppCommand::CreateInstance(unlucky_presenter::dto::CreateRequest {
version: version.to_string(),
loader: kind,
loader_version: String::new(),
name: version.to_string(),
}),
_ => AppCommand::InstallVersion(version.to_string()),
}
}
/// `--flag` 是否存在。
pub fn flag(name: &str) -> bool {
std::env::args().skip(1).any(|arg| arg == name)
}
/// `--key value` / `--key=value` 的取值。
pub fn value_of(name: &str) -> Option<String> {
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
if let Some(value) = arg.strip_prefix(&format!("{name}=")) {
return Some(value.to_string());
}
if arg == name {
return args.next();
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_dwell_is_a_visible_mid_state() {
const DEFAULT: u64 = ShotOptions::DEFAULT_DWELL_MS;
let dwell = DEFAULT;
assert!(dwell >= 1_000, "中间态至少要停 1 秒才看得见进度");
}
#[test]
fn options_without_screenshot_are_none() {
// 测试进程没有 --screenshot 参数
assert!(
shot_options().is_none() || std::env::args().any(|arg| arg.contains("--screenshot"))
);
}
#[test]
fn value_of_ignores_absent_flags() {
assert!(value_of("--definitely-not-a-real-flag").is_none());
}
}