复审 r1(reviewer-accscope-code,p2-2items / fix-then-merge)的两条阻塞 + 四条顺手的 p3。 ## p2-1 `[window]` 的三个窗口属性曾是格式串常量(U1:常量冒充观测结果) 第一版把 `resizable=true decorations=false transparent=false` 写在格式化串里,与交给 iced 的 `window::Settings` 没有任何绑定;定向变异把 `settings.decorations` 改成 `true`,**371 个测试全绿**, 而验收包里 `[caps]` 与 `[window]` 会互相矛盾。 - 新增 `acceptance::window::WindowFacts` + `from_settings()`:字段只能从**真实 settings** 抽出来, 没有手抄入口;`window_text()` 逐字段打印。 - 窗口设置收成单一产地 `app/boot/diagnostics::window_settings()`,`launch()` 与 `[window]`/`[caps]` 两行日志共用同一个 `&window::Settings`;`pages/harness.rs` 同样从自己那份 settings 取。 - 守门测试 `window_settings_match_what_the_acceptance_logs_claim`,含「from_settings 必须逐字段 搬运」的反向断言。变异实证:`window_settings` 的 `decorations → true` ⇒ 红并点名; `from_settings` 里写死 `decorations: false` ⇒ 红(第一次写测试时这条漏抓,补强后实测红)。 ## p2-2 语言维度空转、且没有任何结论断言 `lang::resolve` 是 setting → 平台源 → env,macOS 的 AppleLanguages 几乎恒有值 ⇒ `LANG=zh_CN` / `LANG=en_US` 两档同值,而原脚本只断言「`[i18n]` 这行存在」——整条链退化成 `source=default lang=en` 时核心集一条都不红。 - 脚本加两条结论断言:`^\[i18n\].*platform=applelanguages:[A-Za-z-]+` 与 ` source=applelanguages` (退化日志上实测红、健康日志上实测绿)。 - `REPORT.md` 语言行下注明:env 优先级低于平台源,正常 Mac 上两档同值,这一维验的是「平台源 真读到了 + 中文字体回退」,不是中英对照。 ## p3(复审 + 第二路自跑复核同源的三条) - `[perf] boot probe=0ms` → `probe=skipped`(没计时就不报数;降级重开那一轮改为**真计时**)。 变异实证:把 skipped 写成 0ms ⇒ `perf_lines_carry_measured_millis` 红。 - `[env] gpu=` 排序改成 `DiscreteGpu > IntegratedGpu > 其它`:本机原来报核显,而 iced 实际选的是 独显(现在报 `AMD Radeon RX 6600 XT [DiscreteGpu]`,与 `Selected:` 一致)。 - `[i18n] setting=<未设置>` → `<未接线>`(配置侧根本没读取点,写「未设置」等于把「我们没读」 说成「用户没设」)。 - `[backend] final=` 失败行:`refuse` / `none` 两条路是 `final=none`(什么都没跑起来),只有 `respawn` 才是兜底名;`pages/harness.rs` 不再写死 `via="refuse"`,改用共用的 `failure_via()`。 实跑负例:`[backend] final=none result=failed primary=wgpu fallback=tiny-skia via=refuse`。 - 脚本 `[tick]` 那处唯一未锚定的 `field()`:加 `^` 锚定、正则改 `clock=[a-z0-9-]+`(真值是 `idle-1hz`,旧 `[a-z]+` 把它截成 `idle`,靠前缀巧合通过);断言也改成 `clock=idle-1hz`。 - 脚本 `[backend] final` 取值改 `field_backend()`:优先 `result=started`(respawn 场景一条日志里 有父 failed + 子 started 两条,旧 `head -1` 会把「这次没跑起来」写进报告)。 - 脚本 `exec 3>&1` 全程留痕(终端提示也进 `logs/00-run.log`)、跑前清空 `$OUT/shots/`、 python3 缺失时在 REPORT 里写明「几何列未采」而不是留空。 - `renderer/mod.rs` 补一段依据:为什么「单项 ICED_BACKEND ⇒ 没有静默回退」成立(`iced_tiny_skia` 的 `with_backend` 只接受 `tiny-skia`),以及它依赖 iced 实现细节的两层缓解。 ## 结构 `app/boot.rs` 400 行零余量 → 拆成 `boot.rs`(248) + `boot/fallback.rs`(117,兜底/重开/退出码) + `boot/diagnostics.rs`(110,窗口设置与四段报告);`run()` 回到「探测 → 字体 → 打印 → 起 iced」。 `acceptance` 再把 `[window]` 拆成 `window.rs`(142)。 门禁:`cargo fmt --check` / `clippy --all-targets -- -D warnings` / `test`(373 lib + 6 integration)全绿。
247 lines
9.1 KiB
Rust
247 lines
9.1 KiB
Rust
//! `[env]` 与 `[i18n]`:**本机是什么**(系统 / arch / 核数 / GPU / 时区)与**为什么是这个语言**。
|
||
//!
|
||
//! ## 语言链为什么在这里而不是 `platform/`
|
||
//!
|
||
//! `platform::locale::SystemLocale` 是 **Windows 注册表**那一路(`reg.exe`,零 FFI)。macOS 的
|
||
//! 真值源是 `defaults read -g AppleLanguages`,而 M98 的 scope 没有 `platform/**`,所以
|
||
//! [`AppleLanguages`] 先落在这里。**它该搬去 `platform/locale.rs`**——两处并存会漂(见交接报告)。
|
||
|
||
use unlucky_i18n::lang::{self, Detection, LocaleSource};
|
||
|
||
use crate::Probe;
|
||
|
||
// ---------------------------------------------------------------- [env]
|
||
|
||
/// `[env]`:系统 / arch / 核数 / 语言 / 时区 / 缩放请求 / GPU。
|
||
///
|
||
/// `gpu` 与 `hardware` **来自真实适配器枚举**([`Probe`]),不是请求值;枚举不到就写
|
||
/// `<无适配器>` 并把 `hardware=false` 一起打出来——「没有适配器」与「没枚举」是两件事。
|
||
///
|
||
/// `display=` 恒为 `needs-window`:显示器尺寸只有拿到窗口句柄才问得到,而这一行在开窗之前就打了。
|
||
/// 真值由验收脚本用 `system_profiler SPDisplaysDataType` 采进 `logs/00-env.txt`——**写个猜的值
|
||
/// 比留白更坏**(那会让「Monitor 4K / 无外接」这种结论变成猜测)。
|
||
#[must_use]
|
||
pub fn env_line(probe: &Probe, language: &str, scale: Option<f32>, tz: Option<&str>) -> String {
|
||
format!(
|
||
"[env] os={} arch={} cores={} language={language} tz={} scale_requested={} display=needs-window \
|
||
adapters={} hardware={} gpu=\"{}\"",
|
||
std::env::consts::OS,
|
||
std::env::consts::ARCH,
|
||
std::thread::available_parallelism().map_or(0, std::num::NonZeroUsize::get),
|
||
tz.unwrap_or("<未设置>"),
|
||
scale.map_or_else(|| "system".to_owned(), |value| value.to_string()),
|
||
probe.adapters.len(),
|
||
probe.hardware,
|
||
gpu_summary(probe),
|
||
)
|
||
}
|
||
|
||
/// 枚举结果里**最该被当成 GPU** 的那个适配器的摘要:`DiscreteGpu > IntegratedGpu > 其它`。
|
||
///
|
||
/// 为什么不是「第一个非 Cpu/Other」(M98 r1 p3-2):本机枚举顺序是核显在前,于是 `[env] gpu=`
|
||
/// 报的是 `AMD Radeon(TM) Graphics [IntegratedGpu]`,而 iced 实际选的是
|
||
/// `AMD Radeon RX 6600 XT [DiscreteGpu]`。一行「机器有什么显卡」的日志报错型号,比不报更坏。
|
||
/// 判据仍然只管**型号名称**:真正的「iced 选了谁」只有 `[gpu] …Selected: AdapterInfo` 那行有。
|
||
#[must_use]
|
||
pub fn gpu_summary(probe: &Probe) -> String {
|
||
probe
|
||
.adapters
|
||
.iter()
|
||
.max_by_key(|adapter| gpu_rank(adapter.device_type))
|
||
.map_or_else(
|
||
|| "<无适配器>".to_owned(),
|
||
|adapter| format!("{} [{}]", adapter.name, adapter.device_type),
|
||
)
|
||
}
|
||
|
||
/// 适配器的「该不该被当成这台机器的显卡」排序权重(越大越该)。
|
||
fn gpu_rank(device_type: &str) -> u8 {
|
||
match device_type {
|
||
"DiscreteGpu" => 3,
|
||
"IntegratedGpu" => 2,
|
||
"VirtualGpu" => 1,
|
||
"Cpu" | "Other" => 0,
|
||
_ => 1,
|
||
}
|
||
}
|
||
|
||
/// 时区(`TZ` 环境变量)。`std` 拿不到系统时区,所以这里**只报环境变量**,没有就写未设置。
|
||
///
|
||
/// macOS 上的真值时区由脚本用 `date +%Z%z` 采(那份是观测值,这个只是进程视角)。
|
||
#[must_use]
|
||
pub fn tz() -> Option<String> {
|
||
std::env::var("TZ")
|
||
.ok()
|
||
.filter(|value| !value.trim().is_empty())
|
||
}
|
||
|
||
// ---------------------------------------------------------------- [i18n]
|
||
|
||
/// 语言决策链的四级(顺序即优先级:显式值 > 平台源 > 环境变量 > 英文)。
|
||
///
|
||
/// 每一级的**原始值**都留着,日志才能解释「为什么是英文」——只打结论的话,macOS 上「英文」
|
||
/// 到底是「用户就是英文」还是「一个源都没取到」根本分不出来。
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct Chain {
|
||
/// `ui.language` 的显式值(当前没有读取点,恒 `None`)。
|
||
pub setting: Option<String>,
|
||
/// (源自称的名字, 原始 locale)。
|
||
pub platform: (&'static str, Option<String>),
|
||
/// (源自称的名字, 原始 locale)。
|
||
pub env: (&'static str, Option<String>),
|
||
/// 结论(由 [`lang::resolve`] 算,不在本模块重写一遍优先级)。
|
||
pub detection: Detection,
|
||
}
|
||
|
||
impl Chain {
|
||
/// 用三个源探测一次。**每个源只读一次**(`defaults` / `reg.exe` 都是子进程)。
|
||
#[must_use]
|
||
pub fn read(
|
||
setting: Option<&str>,
|
||
platform: &dyn LocaleSource,
|
||
env: &dyn LocaleSource,
|
||
) -> Self {
|
||
let platform = Captured {
|
||
label: platform.label(),
|
||
raw: platform.locale(),
|
||
};
|
||
let env = Captured {
|
||
label: env.label(),
|
||
raw: env.locale(),
|
||
};
|
||
// 优先级仍由 `resolve` 单点决定:这里把**已读到的值**包成同标签的源喂回去,
|
||
// 不复制一遍 if/else(复制就会漂)。
|
||
let detection = lang::resolve(setting, &platform, &env);
|
||
Self {
|
||
setting: setting.map(str::to_owned),
|
||
platform: (platform.label, platform.raw),
|
||
env: (env.label, env.raw),
|
||
detection,
|
||
}
|
||
}
|
||
|
||
/// 本机(平台源 + 环境变量;配置未接线)。
|
||
#[must_use]
|
||
pub fn host() -> Self {
|
||
Self::read(None, platform_locale_source(), &lang::EnvLocale)
|
||
}
|
||
|
||
/// 结论语言标签(`zh-CN` / `en`)。
|
||
#[must_use]
|
||
pub fn language(&self) -> &'static str {
|
||
self.detection.lang.tag()
|
||
}
|
||
|
||
/// `[i18n]` 一行:四级链 + 结论。
|
||
///
|
||
/// `ui_text=pinned-en` 是**今天的事实**:`pages/mod.rs::render` 里下载页固定传 `Lang::En`、
|
||
/// 日志页传 `model.lang`(默认也是 `En`),也就是**文案还没接这条结论**。不写这一格的话,
|
||
/// 日志会让人以为「语言已经是中文了」——那是新的假绿。
|
||
#[must_use]
|
||
pub fn line(&self) -> String {
|
||
format!(
|
||
"[i18n] setting={} platform={}:{} env={}:{} -> {} ui_text=pinned-en",
|
||
// `<未接线>` 而不是 `<未设置>`:配置侧的读取点**还不存在**(M98 r1 p3-3)。
|
||
// 写「未设置」会让读的人以为用户没配过;真实情况是我们没读。
|
||
self.setting.as_deref().unwrap_or("<未接线>"),
|
||
self.platform.0,
|
||
show(self.platform.1.as_deref()),
|
||
self.env.0,
|
||
show(self.env.1.as_deref()),
|
||
self.detection.describe()
|
||
)
|
||
}
|
||
}
|
||
|
||
fn show(value: Option<&str>) -> String {
|
||
value.map_or_else(|| "<未设置>".to_owned(), |raw| raw.to_owned())
|
||
}
|
||
|
||
/// 把值钉住的源(为了「读一次、按真优先级算」)。
|
||
struct Captured {
|
||
label: &'static str,
|
||
raw: Option<String>,
|
||
}
|
||
|
||
impl LocaleSource for Captured {
|
||
fn locale(&self) -> Option<String> {
|
||
self.raw.clone()
|
||
}
|
||
|
||
fn label(&self) -> &'static str {
|
||
self.label
|
||
}
|
||
}
|
||
|
||
/// 本平台的「系统语言」真值源。
|
||
///
|
||
/// - Windows:`reg.exe` 读注册表([`crate::platform::locale::SystemLocale`]);
|
||
/// - macOS:[`AppleLanguages`](`defaults read -g AppleLanguages`);
|
||
/// - 其它:没有平台源(`resolve` 会继续往环境变量走)。
|
||
///
|
||
/// 为什么 macOS 不复用 `SystemLocale`:它 `cfg!(windows)` 之外恒返回 `None`,在 Mac 上等于
|
||
/// 「没有平台源」——于是语言只剩环境变量一级,而 GUI 程序从 Finder 启动时 `LANG` 通常是空的。
|
||
#[must_use]
|
||
pub fn platform_locale_source() -> &'static dyn LocaleSource {
|
||
#[cfg(target_os = "macos")]
|
||
{
|
||
&AppleLanguages
|
||
}
|
||
#[cfg(not(target_os = "macos"))]
|
||
{
|
||
&crate::platform::locale::SystemLocale
|
||
}
|
||
}
|
||
|
||
/// macOS 的系统语言源:`defaults read -g AppleLanguages` 的第一项。
|
||
#[derive(Debug, Clone, Copy, Default)]
|
||
pub struct AppleLanguages;
|
||
|
||
impl LocaleSource for AppleLanguages {
|
||
fn locale(&self) -> Option<String> {
|
||
#[cfg(target_os = "macos")]
|
||
{
|
||
let output = std::process::Command::new("defaults")
|
||
.args(["read", "-g", "AppleLanguages"])
|
||
.output()
|
||
.ok()?;
|
||
if !output.status.success() {
|
||
return None;
|
||
}
|
||
parse_apple_languages(&String::from_utf8_lossy(&output.stdout))
|
||
}
|
||
#[cfg(not(target_os = "macos"))]
|
||
{
|
||
None
|
||
}
|
||
}
|
||
|
||
fn label(&self) -> &'static str {
|
||
"applelanguages"
|
||
}
|
||
}
|
||
|
||
/// 解析 `defaults` 的输出,取**第一条**语言(真机格式见测试夹具)。
|
||
///
|
||
/// ```text
|
||
/// (
|
||
/// "zh-Hans-CN",
|
||
/// "en-US"
|
||
/// )
|
||
/// ```
|
||
///
|
||
/// 首项是用户的**首选语言**;后面几项是「语言与地区」里的备选,与本 crate「只看主语言子标签」
|
||
/// 的口径无关。
|
||
#[must_use]
|
||
pub fn parse_apple_languages(text: &str) -> Option<String> {
|
||
let start = text.find('"')?;
|
||
let rest = &text[start + 1..];
|
||
let end = rest.find('"')?;
|
||
let value = rest[..end].trim();
|
||
(!value.is_empty()).then(|| value.to_owned())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "locale_tests.rs"]
|
||
mod tests;
|