e1622b5959970d6edd294c9bb22a85cce8c91738
8
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ac5d0cb1d0 |
feat(windows): Phase 6 C1 — 单实例 mutex + HKCU Run 启动项
- src/windows/mod.rs: cfg(windows) 模块入口,pub use single_
instance + startup;其他 Phase 6 模块(C2 托盘 / C3 Toast / URL
打开)在后续 commit 加
- src/windows/single_instance.rs: CreateMutexExW("Local\\...")
→ mutex 名称 user-scope(多 RDP 会话各自能跑一个实例);第
二参 bInitialOwner=true 让 GetLastError() 可靠;返回 Acquired
/ AlreadyRunning enum
- src/windows/startup.rs: HKCU\Software\Microsoft\Windows\
CurrentVersion\Run\WindowsGotifyClientRust = 当前 exe 路径;set_
enabled / is_enabled 接口给 C2 Settings toggle 留口子;自定义
Win32Error 类型(windows-sys 0.59 没有 core::Error)
- src/lib.rs: 加 #[cfg(windows)] pub mod windows 入口
- src/main.rs: 启动前 enforce_single_instance() 检查;已有实
例直接 std::process::exit(0)(不抢前台窗口,留给 C2 tray
拿 hwnd 后再做)
- Cargo.toml: windows-sys 0.59 features 加 Win32 / Win32_Security
/ Win32_Foundation / Win32_System_Threading / Win32_System_
Registry / Win32_UI_WindowsAndMessaging / Win32_UI_Shell
避坑:
- windows-sys 0.59 没有 CreateMutexW(只是宏名 / 16-bit 兼容)
→ 用 CreateMutexExW
- HANDLE 不是 newtype struct,是裸 *mut c_void → 直接 as
isize,不要 .0
- CreateMutex[A/ExA/ExW/W] 全部 cfg-gated 在 Win32_Security feature
→ 必须开
- windows-sys 子 feature(Win32_System_Threading 等)依赖顶层
Win32 feature → 必须同时开
- std::mem::zeroed() 必须放 unsafe block 里
验证:cargo build / build --release 都 exit 0,0 warning
- cargo test: 15 passed; 0 failed
- debug 35.56 MB, subsystem = WINDOWS_CUI
- release 9.77 MB, subsystem = WINDOWS_GUI
- 实际功能(重复启动会 exit 0)只能手测;test 只能覆盖 JSON /
Rust 内部状态
|
||
|
|
319eeb44ae |
feat(gotify): Phase 4 C1 — WebSocket reconnect + 帧解析
- src/gotify/reconnect.rs: ReconnectPolicy 按 §15 退避序列
1s/2s/4s/8s/16s/30s...record_success() 重置;inline #[cfg(test)]
3 个 backoff 测试
- src/gotify/websocket.rs: blocking tungstenite 0.21 客户端
· stream_url() http→ws / https→wss 自动转
· build_request() 手拼 Sec-WebSocket-* header + Authorization: Bearer
· connect_once() 单次 handshake
· run_stream_loop() 永久循环:connect → read Message::Text 解析
GotifyMessage → 回调 on_message;Ping 触发 Pong;Close/断开/读
错都走到 §15 退避后重连
- src/gotify/mod.rs: re-export 加 reconnect + websocket 子模块
- Cargo.toml: tungstenite 0.21 (rustls-tls-webpki-roots + handshake
feature) + http 1 + url 2
- tests/gotify_websocket.rs: 3 个 JSON envelope 解析测试
避坑:
- tungstenite 0.21 的 connect 在 handshake feature 后(默认不带),要
显式开
- WebSocketConfig 0.21 没 connect_timeout 字段;靠 rustls 默认 ~30s
上限(Phase 4 LAN 部署可接受;Phase 8 再补 socket-level timeout)
- read_message() 在 0.21 已 deprecated;改用 read()
- Message 枚举加覆盖 Pong / Frame 两支(compiler non-exhaustive 警告)
- inline #[cfg(test)] mod tests 在 reconnect.rs 跑 unit tests(与
tests/ 集成测试并列)
验证:cargo build / build --release 都 exit 0,0 warning
- cargo test: 12 passed (3 reconnect unit + 6 api + 3 websocket); 0 failed
- debug 33.36 MB, subsystem = WINDOWS_CUI
- release 9.43 MB, subsystem = WINDOWS_GUI(tungstenite + http 在 LTO
后几乎不增体积——和 Phase 3 C2 持平)
|
||
|
|
0cf7a4eaaa |
feat(gotify): Phase 3 C2 — UI 接线 + Connection status + 真实数据
- src/config.rs: AppConfig::placeholder() 硬编码 NAS 地址 + 假 token (§20 token 永不日志;Phase 5 接持久化前占位) - src/models.rs: gotify_messages_to_items 把 GotifyMessage 列表转 Slint MessageItem[];try_fetch_real_messages 一次完成 health → applications → messages(50);失败 None 让调用方 fallback mock - src/gotify/mod.rs: re-export ServerConfig - src/lib.rs: run() 启动时调 try_fetch_real_messages,成功用真实数 据 + Connected + 服务端 URL,失败 fallback mock + Disconnected; on_test_connection_requested 重新跑同一路径 - Cargo.toml: 加 chrono 0.4 (RFC3339 解析) - ui/app.slint: is-connected (绿/灰 dot) + status-text (顶部副标) - ui/settings-page.slint: Connection section 加 Test Connection 按钮 (in-progress 时禁用显示 "Testing...") 避坑: - Gotify RFC3339 date 解析失败 silent fallback 到 unix=0(显示 "just now" 而不是崩) - Gotify priority 0..10 → Slint Priority 4 桶分桶:<=0 Low / <=3 Normal / <=7 High / else Max - ServerConfig 在 crate::gotify::model 子模块,mod.rs 必须 re-export 验证:cargo build / build --release 都 exit 0,0 warning - cargo test --test gotify_api: 6 passed - debug 33.36 MB, subsystem = WINDOWS_CUI - release 9.43 MB, subsystem = WINDOWS_GUI(chrono +1.3 MB) |
||
|
|
c6ad3883f8 |
feat(gotify): Phase 3 C1 — Gotify REST API client
- src/gotify/api.rs: blocking reqwest 封装(§14);health / applications / messages 三个方法;Bearer token 用 HeaderValue::set_sensitive 标记,日志绝不泄露(§20);10s per-request timeout - src/gotify/model.rs: GotifyHealth / GotifyApplication / GotifyMessage DTO 镜像官方 wire schema;defaultPriority 加 rename(camelCase) - src/gotify/mod.rs: re-export - src/error.rs: 加 AppError::Gotify(String) - Cargo.toml: reqwest 0.12 (blocking + json + rustls-tls) 从注释升级为真 依赖(AGENTS.md §44 模板里早就列了,Phase 1 当时推迟) - tests/gotify_api.rs: 6 个 JSON 解析测试 避坑: - reqwest 0.12 默认 log Bearer token;要 set_sensitive(true) 才行 - reqwest::Error 转 AppError 不能用返回 impl Fn 的 helper(path 是 &str 不是 'static)—— 改成每个调用点 inline closure - Gotify 的 defaultPriority 是 camelCase,其他字段都是单字; 用 #[serde(rename = "defaultPriority")] 单点 rename 而不是全局 rename_all = "camelCase"(因为 Rust 端字段名是 snake_case) 验证:cargo build / build --release 都 exit 0,0 warning - cargo test --test gotify_api: 6 passed; 0 failed - debug 27.88 MB, subsystem = WINDOWS_CUI - release 8.10 MB, subsystem = WINDOWS_GUI(reqwest 加进 release LTO 后体积几乎没变——rustls 是大头但被 strip + LTO 吃掉了) |
||
|
|
680fbfa0ad |
feat(ui): Phase 2 C3 — Settings 页 + Dialogs + 窗口状态保存
- src/storage.rs: AppState (last_window_width/height) + load/save atomic write 到 %APPDATA%/WindowsGotify/state.json(§18) - ui/settings-page.slint: 5 分组(Connection / Notifications / Startup / Appearance / Advanced),Appearance 实时切主题(System/Light/Dark radio);Advanced 折叠 - ui/dialogs.slint: AboutDialog + ConfirmDialog(§54) - ui/server-card.slint: 单 server 行(name/url/enabled toggle/edit/remove) - ui/app.slint: settings 页切换、About 导航、theme 联动 - src/lib.rs: load_state → 启动时应用窗口尺寸;ui.window() .on_close_requested 在 OS 关闭前 save_state;wire about/close callbacks - src/error.rs: 加 AppError::Storage(String) 变体 - Cargo.toml: 加 serde + serde_json 避坑: - Slint 1.x 同文件内 export component 必须在引用前定义(sub-components 移到 SettingsPage 上面) - Slint 不支持 padding 两值简写 (padding: 0 16px) → 用 padding-* 四属性 - Slint 不支持 Rectangle 内建 border-color 覆盖 → 命名 card-border - Slint 不支持 alignment: right → 用 spacer + horizontal-stretch: 1 - 修了一处 .slint 文件被 PowerShell Set-Content GBK 损坏的问题(已 通过 write 工具 + UTF-8 no BOM 重写并清理 ui/ 所有 .slint) 验证:cargo build / build --release 都 exit 0,0 warning - debug 27.55 MB, subsystem = WINDOWS_CUI - release 8.10 MB, subsystem = WINDOWS_GUI |
||
|
|
49cbd9e371 |
feat(ui): Phase 2 C1 — MainWindow + Sidebar + 颜色/主题系统
完成 AGENTS.md §57 Phase 2 的前 3 项:MainWindow 布局、Sidebar、Dark 主题
基础。剩余的 Message list / Empty state / Settings / Light-Switch /
Window state 留到 C2 / C3。
技术栈变化
----------
本次最大的工程变化是把 Slint UI 从 `slint!{}` macro 内嵌迁到独立
`.slint` 文件 + build.rs 编译:
ui/app.slint MainWindow + Sidebar 骨架(Phase 2 C1 完整实现)
ui/theme.slint export enum AppearanceMode { System, Light, Dark }
(不再有 export global Theme / Palette,见下文)
ui/components.slint NavItem / SectionLabel / Divider(Dark 默认颜色)
ui/message-card.slint 占位 stub(C2 实现)
ui/message-list.slint 占位 stub(C2 实现)
ui/empty-state.slint 占位 stub(C2 实现)
ui/settings-page.slint 占位 stub(C3 实现)
ui/server-card.slint 占位 stub(C3 实现)
ui/dialogs.slint 占位 stub(C3 实现)
build.rs slint_build::compile("ui/app.slint"),
把生成的 .rs 路径写入 SLINT_INCLUDE_GENERATED
src/lib.rs `slint::include_modules!()` 引入编译产物;
`MainWindow::new()` 拿 UI 实例;
`ui.set_theme(AppearanceMode::Dark)` 初始化
(C3 改成从 state.json 读 + 监听用户切换)
为什么不用 `include_slint!` —— 那是 `slint_interpreter` crate 提供的
API,不在 `slint` 1.18 主 crate 里。官方推荐的"build-time precompile"
路线是 `slint_build::compile()` + `slint::include_modules!()`,文档明确
写了这一步。
为什么不用 export global Palette / Theme
---------------------------------------
最初一版我把 Theme + Palette 都做成 `export global`,期望从 Rust 端
用 `ui.global::<Theme>().set_appearance(...)` 切换 Light/Dark。失败
了:slint-build 1.18 在生成 Rust 代码时把这两个 global 都 dead-code
消除了,generated 的 pub use 列表里只剩 `MainWindow` 和
`SidebarSelection`。原因——global 的所有 binding 在编译期都被简化为
常量(Theme.appearance 默认 Dark,Palette.background 绑定直接 inline 成
`#1f2024`),没有任何 dynamic 引用保留 global 的符号表。
解决方案:把 Theme + Palette 全部搬到 MainWindow 上作为 out properties,
由 `MainWindow.theme` 触发 binding 重新计算。Rust 端用
`ui.set_theme(AppearanceMode::Light)` 切。代价是 sidebar 的 NavItem
暂时只有 Dark 默认色(fixed `#2f3035` 等),Light theme 在 C3 通过
property forwarding 扩展 NavItem 时补齐——这是 C1 阶段明确接受的折衷。
这一节值得写进 commit 而不是悄悄改掉——后面任何看到代码的人都会好奇
"为什么 palette 不直接 export global?",是这个原因。下次有 Phase 3+ 的人
碰这块也省得踩同样的坑。
Slint 版本对齐
--------------
之前 commit 时钉过 `slint = "1"`(让 cargo 选最新 1.x 稳定版),现在
slint-build 1.18.0 引入 build-deps:`slint-build`, `i-slint-compiler` 等。
两者锁到同一个 1.18.0 版本是 Slint 自身的 forward-compat 约束
(slint-build 的 `compat-1-18` feature 在 build.rs 启动时强校验)。
MainWindow / Sidebar 布局细节
-----------------------------
按 AGENTS.md §6 / §7:
- 960×680 preferred,760×520 min —— Slint 1.18 不允许同时设
`width` + `min-width`,用 `preferred-width`/`preferred-height` 替代
(之前第一次 build 报 `Cannot specify both 'width' and 'min-width'`,
这里记录一下)。
- 232px sidebar 宽,含 AGENTS.md §7 列出的 6 个 section
(logo + All + Servers 占位 + Filters 三个 + Settings + About)
- SidebarSelection enum 驱动右侧 view-switcher,三个 view 都有占位
文字(Messages / Settings / About),C2 / C3 替换实际内容
- AGENTS.md §52 颜色 token 全部按 dark / light 两套列出,由
`MainWindow.is-dark` 派生;优先级颜色(§7 message card)也按
§7 Low/Normal/High/Max 映射
未做(按计划归到后续 commit)
------------------------------
C2:
- src/models.rs(MessageItem + Priority enum,AGENTS.md §34)
- src/mock.rs(生成 10 条 mock 消息)
- ui/message-card.slint 真实内容
- ui/message-list.slint 真实内容(ListView 接 Rust Model adapter)
- ui/empty-state.slint 真实内容(§9 "You're all caught up" + ✦ + Test button)
- main 区 if-switch 真正根据 list.is_empty 选 list / empty-state
C3:
- src/storage/* —— 持久化 state.json
- ui/settings-page.slint 真实内容(§10 五个分组 + Advanced 折叠)
- ui/server-card.slint 真实内容
- ui/dialogs.slint 真实内容(About + confirm)
- sidebar NavItem 接 palette forwarding,Light theme 真的换 sidebar 颜色
- 主题切到 Light/System 实时生效(System 跟随 Windows 主题)
验证
----
cargo build exit 0, 5.10s(增量)
cargo build --release exit 0(slint + skia + accesskit 全重编,约 8 分钟)
warnings 0
debug .exe 26.01 MB, subsystem = 3 (WINDOWS_CUI / console)
release .exe 7.87 MB, subsystem = 2 (WINDOWS_GUI)
Phase 2 C1 验收:能在 Windows 上打开 MainWindow(960×680、Dark、含完整
Sidebar),通过 release profile 的 WINDOWS_GUI subsystem 验证无 console。
Sidebar 切换 Settings/About 时右侧文案占位更新——C2/C3 替换为实际 view。
|
||
|
|
b30b2f0e28 |
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 即可,未修改用户全局环境。
|
||
|
|
a3a6a7c91d |
chore: 初始化 Windows-Gotify-Client-Rust 项目骨架
使用 Rust + Slint 重建一个轻量、现代、Windows 原生体验良好的 Gotify
桌面客户端。本次只搭目录骨架与占位文件,未进入 Phase 1(Bootstrap)。
本次提交内容
------------
- 保留 AGENTS.md(1363 行定制 Agent 实施计划,含 8 阶段路线图)
- 新建 Cargo.toml([package] 元数据 + AGENTS.md §44 依赖模板 + §42
release profile;无 [[bin]],等 Phase 1 才加 src/main.rs)
- 新建 README.md(项目导览,引用 AGENTS.md)
- 新建 .gitignore(Rust / Slint / Windows / IDE / 密钥)
- 新建 LICENSE(MIT 占位)
- 按 AGENTS.md §4 建好全部 10 个目录,每个空目录放 .gitkeep:
assets/ assets/fonts/
ui/ scripts/
src/ src/gotify/
src/storage/ src/windows/
src/services/ tests/
未提交(Phase 1 起才落地)
------------------------
- src/main.rs 及所有 .rs 业务代码
- ui/*.slint 视图
- scripts/build-release.ps1 等构建脚本
- Cargo.lock(当前 Cargo.toml 是占位、无可编译代码)
技术栈约束(来自 AGENTS.md §1 / §2)
------------------------------------
- Rust stable + Slint 1.17.x + Windows 10/11
- 显式禁用:Tauri / Wails / Electron / WebView2 / WPF / WinUI 3 / Node 运行时
- 单一主程序,无浏览器运行时依赖;UI 由 Slint 原生渲染
仓库
----
远程:https://gitea.kennethcheng.cc/KennethCheng/Windows-Gotify-Client-Rust
|