refactor(cli): 判据收敛到 unlucky_cli::cli,banner 按被调用的二进制名自称(M82 复审)

去重:`looks_like_cli(args) -> bool` 落在 `unlucky_cli::cli`,两个 GUI 的 main.rs 只调用它,
判据从两份变一份。cli crate 里**不**出现 GUI 的 flag 表——语义仍是「首个 token 是裸词就接管;
首个 token 是 flag 时只有解析出真命令才接管,解析失败不接管」。原来两个 main.rs 里的 5+5 条
单测合并成 cli crate 的一份(用例集按复审要求原样搬,含 --screenshot/--renderer/--diff/
--mock/--force-state/--scale-factor、--help/-h/--version/-V、10 个已知子命令、--json 前置、
未知子命令),两个 main.rs 的旧测试已删除。

banner:新增 cli::program 模块,从 argv[0] 的 basename 取程序名(剥目录、剥 .exe、空值回落
ull),把帮助/版本/错误提示里独立出现的 `ull` 换成它——`unlucky-launcher --version` 不再自称
`ull`,也不会把人指去敲 `ull --help`。`run()` 签名未改;`ull` 自己跑时输出逐字不变(替换是
恒等),打包脚本的 `ull --version` 冒烟不受影响。
This commit is contained in:
AskaEth committed 2026-11-02 08:08:12 +08:00
1 parent fecff989e7
commit c9e6236a87
9 files changed
+275 -190

No files matched your search

+6 -89
View File
@@ -16,7 +16,8 @@
//! 一个二进制两种用法(M82):`unlucky-launcher` 不带子命令时起 GUI;带子命令
//! (`instances` / `install` / `launch` / `accounts` / `sessions` / `logs` / `diag` /
//! `config` / `java` / `agent`)时直接转给 `unlucky_cli::run`,**不初始化任何 UI**。
//! 子命令表不在这里抄一份——唯一真相源是 `crates/cli` 的解析器(见 [`is_cli_invocation`])。
//! 子命令表不在这里抄一份——唯一真相源是 `crates/cli` 的解析器(见
//! `unlucky_cli::cli::looks_like_cli`)。
//! 零 UI 依赖的 `ull` 仍然保留:它连 fontconfig 都不需要,给服务器 / CI / Termux 用。
//!
//! 架构分层(本文件只有一个职责:分发 + 把窗口拉起来):
@@ -101,8 +102,11 @@ fn run_gui() -> Result<(), slint::PlatformError> {
/// 这条路径上**不构造窗口、不探后端、不扫字体、不依赖任何图形库**(这些库缺失时同一条命令照样
/// 跑完,实测见 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 !is_cli_invocation(args) {
if !unlucky_cli::cli::looks_like_cli(args) {
return None;
}
let mut stdout = std::io::stdout();
@@ -113,30 +117,6 @@ fn run_cli_if_requested(args: &[String]) -> Option<ExitCode> {
Some(ExitCode::from(u8::try_from(code).unwrap_or(2)))
}
/// 这条命令行是 CLI 调用还是 GUI 启动?
///
/// 判据只有一条,**子命令表不在这里重复**:`crates/cli` 的解析器就是唯一真相源,
/// 所以新增子命令不需要动这个文件(iced 版 `crates/app-iced/src/main.rs` 是同一份判据)。
///
/// - 首个 token 是裸词(不以 `-` 开头)→ 一定是子命令位置:GUI 不接受位置参数,
/// 而 `--diff` / `--screenshot` 的取值都在 flag 之后。未知子命令也走这条路,
/// 由 CLI 自己报「未知命令 + `--help`」并以 1 退出(静默落到 GUI 会让脚本用户困惑)。
/// - 首个 token 是 flag → 只有 CLI 自己解析出一条真命令才算 CLI 调用:`--help` / `--version` /
/// `--json instances list` 走 CLI;`--data-dir X` 解析成 `Welcome`(没有子命令)、
/// `--software-renderer` / `--screenshot` / `--diff` 等 GUI 参数干脆解析失败,一律仍走 GUI。
/// 注意「解析失败」在这里是 GUI,不是 CLI:`--screenshot out.png` 也会解析失败,把它判成
/// CLI 就会把截图脚本打死(这是本函数据以正确的那条边界)。
fn is_cli_invocation(args: &[String]) -> bool {
match args.first() {
None => false,
Some(first) if !first.starts_with('-') => true,
Some(_) => match unlucky_cli::cli::parse(args) {
Ok(invocation) => !matches!(invocation.command, unlucky_cli::cli::Command::Welcome),
Err(_) => false,
},
}
}
/// Win7 老机 / 无可用 OpenGL 驱动时的软渲染兜底开关(01c §6 的 Slint 核心卖点)。
fn apply_renderer_flag() {
let software = std::env::args()
@@ -146,66 +126,3 @@ fn apply_renderer_flag() {
std::env::set_var("SLINT_BACKEND", "winit-software");
}
}
#[cfg(test)]
mod tests {
use super::is_cli_invocation;
fn cli(args: &[&str]) -> bool {
is_cli_invocation(&args.iter().map(|arg| (*arg).to_owned()).collect::<Vec<_>>())
}
#[test]
fn no_args_starts_the_gui() {
assert!(!cli(&[]));
}
#[test]
fn gui_flags_keep_working() {
// CI 脚本与人工验收都在用这些参数,一条都不能被当成子命令吃掉。
assert!(!cli(&["--software-renderer"]));
assert!(!cli(&["--screenshot", "out.png", "--data-dir", "/d/tmp/x"]));
assert!(!cli(&["--screenshot-theme", "dark"]));
assert!(!cli(&["--renderer=gpu", "--theme=dark"]));
assert!(!cli(&["--diff", "a.png", "b.png"]));
assert!(!cli(&["--mock"]));
// 只有 --data-dir 时 CLI 解析成 Welcome(没有子命令),仍是 GUI 启动。
assert!(!cli(&["--data-dir", "/d/tmp/x"]));
}
#[test]
fn known_subcommands_go_to_the_cli() {
for args in [
vec!["instances", "list"],
vec!["install", "1.20.1", "--dry-run"],
vec!["launch", "1.20.1", "--dry-run"],
vec!["accounts", "list"],
vec!["sessions", "list"],
vec!["logs", "abc", "--tail", "10"],
vec!["diag", "abc"],
vec!["config", "get"],
vec!["java", "list"],
vec!["agent", "config"],
// 全局选项可以排在子命令前面
vec!["--json", "instances", "list"],
] {
assert!(cli(&args), "{args:?} 应该交给 CLI");
}
}
#[test]
fn help_and_version_are_cli_too() {
// GUI 以前不认 --version(会直接开窗),现在由 CLI 报版本并退出。
assert!(cli(&["--version"]));
assert!(cli(&["--help"]));
assert!(cli(&["-h"]));
}
#[test]
fn unknown_subcommand_is_left_to_the_cli_to_reject() {
// 语义定案(README 两版同款):未知子命令 → CLI 报「未知命令 + ull --help」并以 1 退出,
// 不静默落到 GUI(否则脚本用户会以为参数生效了)。
assert!(cli(&["lst"]));
assert!(cli(&["intsances", "list"]));
}
}