feat: 实现 Phase 1 — Bootstrap(Slint hello window + 编译验证通过)
完成 AGENTS.md §57 Phase 1 的全部交付:可编译的 Cargo 工程、能打开的
Slint hello window、release profile、Windows subsystem、tracing 日志、
thiserror 错误框架。`cargo build` 与 `cargo build --release` 均通过,
0 warning。
实现概览
--------
新增 / 修改的文件:
src/lib.rs lib crate 入口;`slint!{}` 内嵌一个最小 AppWindow
(标题 "Gotify",480×320,深灰背景 + 两行文本);
`pub fn run() -> AppResult<()>` 调 AppWindow::new
+ run()。
src/main.rs thin binary entry;含
`#![cfg_attr(not(debug_assertions),
windows_subsystem = "windows")]`
让 release build 不弹 console(AGENTS.md §43);
debug build 保留 console 让 tracing 日志可见。
src/error.rs `AppError` + `AppResult`,thiserror 2.0.20;Phase 1
只有 `Logging(String)` 与 `Ui(String)` 两个变体,
§24 的 ConfigError / NetworkError / WebSocketError
/ StorageError / WindowsError / ParseError 等后续
Phase 落地时再加。
src/logging.rs `init_logging()`:tracing-subscriber 0.3.23,启
env-filter feature;debug 默认 debug 级,release
默认 info 级,`RUST_LOG` 可覆盖;关闭 ANSI(release
通常没有真实终端)。符合 §25 的"默认不记录消息正文"
基线——具体红线靠代码 review 强制。
Cargo.toml 实际可编译的 manifest:[[bin]] 指向 src/main.rs;
仅引入 Phase 1 真正用到的依赖(slint / tracing /
tracing-subscriber / thiserror);reqwest /
tungstenite / serde / url / anyhow 等推迟到对应
Phase 的模块创建后再引入——避免提前下载无用 crate,
也让 AGENTS.md §44 的 "Cargo 依赖最小化原则" 落到
每一阶段的 commit 上。
Cargo.lock 已生成(564 个 transitive deps,含 slint 1.18.0、
skia-bindings、accesskit、parley、fontique 等)。
二进制工程,lock 文件进 git——保证可重现构建。
技术选型说明
------------
- Slint 版本选择:AGENTS.md §1 写的是 "Slint 1.17.x 或当前稳定兼容版
本"。首次尝试钉死 `slint = { version = "1", features =
["internal-interpreter"] }`,cargo 报 `internal-interpreter` feature 不
存在(slint 1.18.0 已移除该 feature,slint!{} macro 走 default feature
set 即可)。改为 `slint = "1"`,cargo 自动选到 1.18.0,与 Rust 1.98.1 +
MSVC 工具链编译通过。AGENTS.md §1 的措辞明确允许"当前稳定兼容版本",
本次选 1.18.0 不偏离约束。
- release profile:opt-level=z / lto / codegen-units=1 / panic=abort /
strip=true(AGENTS.md §42 模板)。Phase 1 未做 benchmark,所以 release
vs release-balanced 的实际选择留到 Phase 8 Performance。
- 没引 slint-build / 没建 build.rs:Phase 1 hello window 内嵌在
`slint!{}` macro 里,足以通过验收。Phase 2 切到独立 .slint 文件 +
`include_slint!()` 时再决定是否需要 build.rs。
验证结果
--------
cargo build exit 0, 5.21s(增量;首次冷编译约 1m40s,要编
i-slint-core / parley / skia 等)
cargo build --release exit 0, 1m38s(profile 内 LTO 阶段耗时长)
warnings 0
debug .exe 25.58 MB, PE subsystem = 3 (WINDOWS_CUI / console)
release .exe 7.82 MB, PE subsystem = 2 (WINDOWS_GUI)
AGENTS.md §57 Phase 1 验收标准(`cargo run` + `cargo build --release`
能打开无 console 的 Windows GUI)全部满足。PE subsystem 通过直接读
Optional Header 的 Subsystem 字段验证——而不是盲目信任 cfg_attr。
下一步
------
进 Phase 2 — UI Shell(MainWindow / Sidebar / Message list / Empty
state / Settings / Dark-Light-System 主题)。本阶段仍然只用 mock 消息,
不接网络。
遗留
----
- commit message 之前那条 root commit (`a3a6a7c`) subject 前有个 UTF-8
BOM (`\ufeff`),是 PowerShell 5.1 `Set-Content -Encoding UTF8` 默认带
BOM 造成的。本 commit 用 `[System.IO.File]::WriteAllText` 直接写
UTF-8 without BOM,新 commit 不再带 BOM;老的那个根 commit 的 BOM 只
是显示瑕疵,不影响任何 git 操作,留着不重写。
- 首次 cargo build 拉了 564 个 crate、走代理拉取成功(crates.io 直连也
通,HTTPS_PROXY 走 127.0.0.1:7890 是兜底)。
- target/ 1.4 GB 在 .gitignore 内,不会进 git。
- 用户的 .cargo/bin 不在 PowerShell 当前 session 的 PATH 上(用 rustup
默认安装路径 `C:\Users\Administrator\.cargo\bin`),后续 cargo 命令
脚本里临时加 PATH 即可,未修改用户全局环境。
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
//! Top-level application error type.
|
||||
//!
|
||||
//! The full taxonomy in AGENTS.md §24 is
|
||||
//! `ConfigError / NetworkError / WebSocketError / StorageError /
|
||||
//! WindowsError / ParseError`, plus user-facing variants derived from them.
|
||||
//! Only the categories reachable in the current phase are wired up here; new
|
||||
//! variants land in later phases.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub type AppResult<T> = std::result::Result<T, AppError>;
|
||||
|
||||
/// Application-wide error.
|
||||
///
|
||||
/// All variants carry a `String` payload in Phase 1 because the upstream
|
||||
/// errors we propagate are themselves heterogeneous (tracing init, Slint,
|
||||
/// later reqwest/tungstenite). When the corresponding Phase adds a dedicated
|
||||
/// module, the String is replaced with a structured error from that module
|
||||
/// and the `From` impl is added.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppError {
|
||||
#[error("logging initialization failed: {0}")]
|
||||
Logging(String),
|
||||
|
||||
#[error("ui error: {0}")]
|
||||
Ui(String),
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
//! Windows Gotify Client (Rust) — core library.
|
||||
//!
|
||||
//! Phase 1 only exposes the bare-minimum surface needed to spin up the
|
||||
//! Slint hello window:
|
||||
//! - [`error`]: AppError / AppResult
|
||||
//! - [`logging`]: tracing-subscriber init
|
||||
//! - [`run`]: entry that builds the Slint UI and runs its event loop
|
||||
//!
|
||||
//! Larger subsystems (gotify, storage, windows integration, ...) are added in
|
||||
//! later phases per AGENTS.md §57.
|
||||
|
||||
pub mod error;
|
||||
pub mod logging;
|
||||
|
||||
use slint::ComponentHandle;
|
||||
|
||||
slint::slint! {
|
||||
export component AppWindow inherits Window {
|
||||
title: "Gotify";
|
||||
width: 480px;
|
||||
height: 320px;
|
||||
background: #1f2024;
|
||||
|
||||
VerticalLayout {
|
||||
padding: 24px;
|
||||
spacing: 12px;
|
||||
|
||||
Text {
|
||||
text: "Windows Gotify Client (Rust)";
|
||||
color: #e6e6e6;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Phase 1 — Bootstrap OK";
|
||||
color: #8a8a8a;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the Slint UI and run its event loop until the window is closed.
|
||||
///
|
||||
/// In release builds the Windows subsystem is set to `windows` via the
|
||||
/// `#[cfg_attr]` on the binary entry point, so no console window is shown.
|
||||
pub fn run() -> AppResult<()> {
|
||||
tracing::info!(
|
||||
"starting windows-gotify-client-rust v{}",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
let ui = AppWindow::new().map_err(|e| AppError::Ui(e.to_string()))?;
|
||||
ui.run().map_err(|e| AppError::Ui(e.to_string()))
|
||||
}
|
||||
|
||||
pub use error::{AppError, AppResult};
|
||||
@@ -0,0 +1,29 @@
|
||||
//! Logging bootstrap — `tracing` + `tracing-subscriber`.
|
||||
//!
|
||||
//! Policy (AGENTS.md §25):
|
||||
//! - Debug builds default to `debug` level.
|
||||
//! - Release builds default to `info` level.
|
||||
//! - Message bodies and tokens MUST NOT be logged (enforced by code review,
|
||||
//! not by this module).
|
||||
//! - Override at runtime with the `RUST_LOG` env var.
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
/// Initialize the global tracing subscriber.
|
||||
///
|
||||
/// Idempotent at the type level — repeated calls fail with `AppError::Logging`
|
||||
/// (tracing only allows one global subscriber per process). That is intentional:
|
||||
/// the caller should call exactly once during startup.
|
||||
pub fn init_logging() -> AppResult<()> {
|
||||
let default_level = if cfg!(debug_assertions) { "debug" } else { "info" };
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new(default_level));
|
||||
|
||||
fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(false)
|
||||
.with_ansi(false)
|
||||
.try_init()
|
||||
.map_err(|e| AppError::Logging(e.to_string()))
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Windows GUI application: release builds hide the console window entirely.
|
||||
// Debug builds keep the console so logs from `tracing` are visible.
|
||||
//
|
||||
// See AGENTS.md §43.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use windows_gotify_client_rust::{logging, AppResult};
|
||||
|
||||
fn main() -> AppResult<()> {
|
||||
logging::init_logging()?;
|
||||
windows_gotify_client_rust::run()
|
||||
}
|
||||
Reference in New Issue
Block a user