KennethCheng
|
301052698d
|
feat(gotify): Phase 7 C2 — Win32 Toast 通知 + URL 打开接线 + 单 VecModel
- tray.rs: 增加 show_toast 方法(PostMessageW + 堆分配 payload + WndProc 内 Shell_NotifyIconW NIM_MODIFY),遵守 NIM_MODIFY 必须 NIM_ADD 同线程的硬性要求
- WS drain 每条新消息根据 priority + cached desktop_notifications/notify_high/notify_max/paused atomics 决定是否弹 toast,icon_flags 按 High/Max 分别用 NIIF_WARNING/NIIF_ERROR
- on_message_card_clicked:点击含 has_click_url 的卡片时调 windows::url::open(已有 http/https scheme 严格校验)
- TrayState 增加 desktop_notifications/notify_high/notify_max/restart_requested 原子,Settings 切换实时同步,tray on_restart 只翻 restart_requested flag
- 修复 Phase 6 C3 隐藏 bug:Test Connection / tray Restart 用 set_messages(new_model) 会让 WS drain 推入旧 model。新增单例 Rc<VecModel>,统一改用 set_vec
- on_restart 改用 Arc<OnceLock<Arc<TrayState>>> 占位 + 设置,避免 tray 线程闭包捕获 Rc<VecModel>(!Send)
- ActiveConfig 派生 Clone(托盘初始化快照用)
|
2026-09-19 08:20:03 +08:00 |
|
KennethCheng
|
2614909c4c
|
feat(windows): Phase 7 C1 — 托盘菜单增强 + 单实例激活现有窗口
- 托盘菜单加 4 项:New messages: N(灰)/ Pause notifications(勾选态)/ Settings.../ Restart connection
- 单实例:第二次启动通过 RegisterWindowMessageW 发激活请求,托盘 WndProc 收后调用 on_open(show+reset counter);main.rs AlreadyRunning 路径调用 activate_existing_instance
- AppConfig 加 notifications_paused 字段(与 desktop_notifications 区分,tray 暂停持久化)
- WS drain 通过 Rc<RefCell<Option<Arc<TrayState>>>> 与托盘共享 new_messages 计数器
- WndProc 在调用 on_open 前自己清零计数器,免去回调侧的 RefCell 捕获(保持 Send+Sync)
|
2026-09-19 08:13:42 +08:00 |
|
KennethCheng
|
1df072356f
|
feat(windows): Phase 6 C3 — 系统托盘 + URL 打开
- src/windows/url.rs: open() 用 url::Url parse 校验 + scheme 严格
限制(只 http/https,拒绝 file:// / javascript: / data: / ftp /
自定义 scheme);通过 ShellExecuteW 调默认浏览器打开;§13
/ §38 安全约束
- src/windows/tray.rs:
· RegisterClassExW 注册隐藏 helper window class
· CreateWindowExW(WS_EX_TOOLWINDOW, 0×0) 创建不显示的
message-only helper window
· Shell_NotifyIconW NIM_ADD 注册托盘图标(默认 NULL icon,
Phase 7 polish 再换 .ico)
· dedicated thread 跑 GetMessageW / DispatchMessageW 标准
message loop
· WM_USER+1 uCallbackMessage 处理 tray 鼠标事件
· WM_LBUTTONDBLCLK = Open callback
· WM_RBUTTONUP = CreatePopupMenu + TrackPopupMenuEx 显示
Open / Quit 菜单
· WM_COMMAND 处理菜单 item IDM_OPEN / IDM_QUIT
· Drop 时 NIM_DELETE + DestroyWindow + thread.join() 干净退出
- src/lib.rs:
· #[cfg(windows)] 启动时建 TrayIcon,绑 on_open(show window)
/ on_quit(std::process::exit(0));失败时只 warn 不 abort
(托盘是便利功能,不致命)
· 引入 url::open;Phase 7 polish 时接到 message-card 点击
- src/windows/mod.rs: pub use tray + url
- Cargo.toml: windows-sys features 加 Win32_Graphics_Gdi
(WNDCLASSEXW 依赖)
避坑:
- LRESULT / WPARAM / LPARAM 在 windows-sys 0.59 是 type
alias,不是 struct — 不能 LRESULT(0) 构造;直接 0 返回
隐式 cast
- HMENU 是 *mut c_void;用 std::ptr::null_mut()(不是 null())
- NOTIFYICONDATAW_0 在 0.59 没 derive Default;用
unsafe { std::mem::zeroed() }
- std::mem::GUID::zeroed() 不存在;用 windows_sys::core::GUID
显式构造
- NIM_VERSION 常量 windows-sys 0.59 没 export;用字面 4u32
(NIM_VERSION = 4 in Win32 header)
- HWND (raw pointer) 不 Send;跨线程传递时转 usize
- Box<dyn Fn> 不能 derive Clone;用 Box + 唯一 move 到
TrayShared 不需要 Clone
- on_quit closure 不能捕获 Rc<RefCell<AppState>>(不是 Sync)
→ 简化:on_quit 直接 std::process::exit(0);之前 close
已经 save_state,丢失窗口只是 size 增量
- WNDCLASSEXW + RegisterClassExW 在 Win32_Graphics_Gdi
feature 后(hbrBackground 用 HBRUSH = GDI 类型)
验证:cargo build / build --release 都 exit 0,0 warning
- cargo test: 19 passed (新增 4 个 url rejection 测试 +
之前的 15 个); 0 failed
- debug 35.66 MB, subsystem = WINDOWS_CUI
- release 9.79 MB, subsystem = WINDOWS_GUI
- 托盘只能手测(CI 不能断言 WM_USER 消息);自动测试覆盖
URL 校验逻辑的拒绝路径
|
2026-09-19 07:28:49 +08:00 |
|
KennethCheng
|
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 内部状态
|
2026-09-19 07:03:29 +08:00 |
|
KennethCheng
|
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
|
2026-09-19 04:02:51 +08:00 |
|