Files
Unlucky-Launcher/crates/app/src/main.rs
T
AskaEth f159529936 feat(presenter): 新建 unlucky-presenter,把 host/状态/DTO/设置桥从 app 迁出(M85 第 1 块)
D26(iced 重写)的关键一次性动作:UI 框架无关的宿主层独立成 crate,
Slint 版(crates/app)与 iced 版(crates/app-iced)共用,Slint 退役后不再重写一遍。

- 新建 crates/presenter(package `unlucky-presenter` / lib `unlucky_presenter`),
  workspace 登记 + crate 文档头写清「这层为什么存在」、对外表面、UI 侧允许/禁止、
  iced 接线骨架、以及哪些东西留在了 crates/app 及为什么。
- `git mv`(保留文件历史)迁入:host/(25 文件)、dto/(11 文件)、state.rs(+tests)、
  prefs.rs、settings_bridge.rs、toast.rs、router.rs、dialogs.rs、wizard.rs、
  mock.rs、mock_data.rs、backend.rs。模块路径与公开 API 名称不变(`host::install` 等),
  内部实现零改动。
- 新增 `theme`:把原本混在 ui_bind/theme_runtime 里的**纯判据**(CONFIG_POLL_TICKS /
  SYSTEM_THEME_POLL_TICKS / TILE_REGENERATE_STEP / BackgroundKey / tile_viewport /
  system_theme_value / 两个 poll 判据)抽出来,附 5 个单测;app 侧改成调用它们。
- crates/app 改为消费者:删掉迁走的文件,`crate::host|state|dto|prefs|settings_bridge|
  toast|router|dialogs|wizard|mock|mock_data|backend` 全部改写为 `unlucky_presenter::*`
  (64 处),backend.rs 的 config_path/build_backend 提为 pub。
- crates/app-iced 只登记 unlucky-presenter 依赖,本 mission 不接线。
- prefs.rs 的 2 处 slint 文档引用改写;`grep -rn slint crates/presenter` 现在无输出。
2026-11-02 09:29:31 +08:00

117 lines
5.7 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.
//! Unlucky-Launcher 桌面壳(`unlucky_app`):Slint UI + Metro 视觉。
//!
//! 边界(决策 D3 / D16):本 crate 是**唯一**允许依赖 UI 框架的 crate;
//! 它只经「快照 + 事件流 + 注入的显示服务」消费 core/agent,不直接触碰其内部类型
//! (P0 的 host 是 `mock` 假后端,`unlucky_presenter::dto` 是唯一跨界类型集合)。
//!
//! 渲染策略(`DevDocs/PlanDoc/01c-native-gui-options.md` §6):
//! GL 优先(femtovg),启动参数 `--software-renderer` 或 Win7 老机探测失败时切 CPU 软渲染。
//!
//! 主题与外观(D17 / 09 §101-113):
//! - 主题三态 `appearance.theme_mode ∈ {auto, light, dark}`(出厂 auto);
//! - 「外观(高级)」四件(背景图 / 面板半透明度 / 玻璃拟态 / 强调色)出厂全关;
//! - 全部走 `core::setting` 的 schema(`unlucky_presenter::settings_bridge`),AI 可写并且**热切**
//! (tick 里 1s 一次 mtime 比对);系统主题由 `core::platform::theme_probe` 对齐 + 兜底轮询。
//!
//! 一个二进制两种用法(M82):`unlucky-launcher` 不带子命令时起 GUI;带子命令
//! (`instances` / `install` / `launch` / `accounts` / `sessions` / `logs` / `diag` /
//! `config` / `java` / `agent`)时直接转给 `unlucky_cli::run`,**不初始化任何 UI**。
//! 子命令表不在这里抄一份——唯一真相源是 `crates/cli` 的解析器(见
//! `unlucky_cli::cli::looks_like_cli`)。
//! 零 UI 依赖的 `ull` 仍然保留:它连 fontconfig 都不需要,给服务器 / CI / Termux 用。
//!
//! 架构分层(本文件只有一个职责:分发 + 把窗口拉起来):
//! - `state.rs` / `prefs.rs` / `router.rs` / `dialogs.rs` / `toast.rs`:纯 Rust 状态(可单测,无 Slint)
//! - `dto/`:跨边界 DTO 与事件/指令
//! - `conv.rs`:状态 → Slint 视图模型
//! - `ui_bind.rs` + `callbacks.rs`:窗口/状态/后端缝合 + 回调接线
//! - `settings_bridge.rs`:`config/launcher.json` 的读写与热切(core::setting 的 schema API)
//! - `bg_image.rs` + `png.rs`:背景图/模糊层加工与截图编码(09 §101/§103 的预生成手法)
//! - `mock.rs` + `mock_data.rs`:P0 假后端(脚本化事件)
//!
//! 关于 unsafe:本 crate 不写 unsafe,workspace lint 已设 `unsafe_code = "deny"`。
//! 这里不能升级成 `#![forbid(unsafe_code)]`——Slint 生成的代码自带 `#[allow(unsafe_code)]`,
//! 而 forbid 无法被内层 allow 覆盖(会直接编译失败)。同理 Slint 生成类型不带文档注释,
//! 只能在 crate 级放开 `missing_docs`(库 crate 仍由 workspace lint 保持 warn)。
// 关于 dead_code:P0 骨架里有一批「已定义、等接线」的公共 API——事件通道 / 偏好键登记 /
// 诊断文案 / mock 数据构造器等,它们被单测覆盖但业务侧要 P1 才用到。逐条 `#[allow]`
// 会把噪音藏进实现细节,这里统一放开并在 P1 接线时逐个转正(届时删掉本行即可暴露真死码)。
#![allow(missing_docs, dead_code)]
use std::process::ExitCode;
mod appearance_bind;
mod bg_image;
mod callbacks;
mod callbacks_pages;
mod conv;
mod page_sync;
mod png;
mod screenshot;
mod theme_runtime;
mod ui_bind;
slint::include_modules!();
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
if let Some(code) = run_cli_if_requested(&args) {
return code;
}
match run_gui() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("[run] {error}");
ExitCode::FAILURE
}
}
}
/// 把窗口拉起来(分发之后的唯一路径)。
fn run_gui() -> Result<(), slint::PlatformError> {
apply_renderer_flag();
let app = ui_bind::AppUi::with_options(MainWindow::new()?, screenshot::app_options());
// 截图验收通道(M41 视觉验收;无交互桌面/无 GPU 环境下的唯一可靠取图方式:
// 不截屏幕,而是让 Slint 把当前场景渲染进内存像素缓冲再自己编码 PNG)。
// P3 追加页面选择与中间态(安装进度 / 启动事件流),见 `crate::screenshot`。
if let Some(options) = screenshot::shot_options() {
screenshot::capture(&app, &options)?;
return Ok(());
}
app.start_timer();
app.run()
}
/// 子命令命中 CLI 就跑 CLI 并以它的退出码收尾;返回 `None` 表示「这不是 CLI 调用,去开窗」。
///
/// 这条路径上**不构造窗口、不探后端、不扫字体、不依赖任何图形库**(这些库缺失时同一条命令照样
/// 跑完,实测见 packaging/README.md §7.4)。`unlucky-launcher launch …` 的耗时因此与首帧无关——
/// 这句是结构性判断,**没有重量**(原因见 §10-8)。
///
/// 「这条命令行算不算 CLI 调用」的判据不在这里重复:`unlucky_cli::cli::looks_like_cli` 是唯一
/// 一处(M82 复审去重),两个 GUI 二进制都只调它,改语义不会漂。
fn run_cli_if_requested(args: &[String]) -> Option<ExitCode> {
if !unlucky_cli::cli::looks_like_cli(args) {
return None;
}
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
let code = unlucky_cli::run(args, &mut stdout, &mut stderr);
let _ = std::io::Write::flush(&mut stdout);
let _ = std::io::Write::flush(&mut stderr);
Some(ExitCode::from(u8::try_from(code).unwrap_or(2)))
}
/// Win7 老机 / 无可用 OpenGL 驱动时的软渲染兜底开关(01c §6 的 Slint 核心卖点)。
fn apply_renderer_flag() {
let software = std::env::args()
.skip(1)
.any(|arg| arg == "--software-renderer");
if software {
std::env::set_var("SLINT_BACKEND", "winit-software");
}
}