持久化、主题与资源
一个桌面应用不能只在内存里活着。用户关闭应用后,请求历史、主题、窗口偏好和最近打开的文件都应该被保存。GPUI 不限制你如何持久化数据,HiPoster 采用了简单可靠的 JSON 文件方案。
配置目录
不要把配置写到项目源码目录。跨平台应用应该写到系统认可的应用配置目录:
- macOS:
~/Library/Application Support/... - Windows:
%AppData%\Roaming\... - Linux:
~/.config/...
Rust 中可以用 directories:
use directories::ProjectDirs;
use std::path::PathBuf;
fn config_dir() -> Option<PathBuf> {
ProjectDirs::from("com", "obity", "hiposter")
.map(|dirs| dirs.config_dir().to_path_buf())
}
fn ensure_config_dir() -> Option<PathBuf> {
let dir = config_dir()?;
if !dir.exists() {
std::fs::create_dir_all(&dir).ok()?;
}
Some(dir)
}
三个参数通常是 qualifier、organization、application。项目定下来后不要轻易改,否则用户旧数据会留在旧目录。
JSON 持久化
历史记录模型:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryItem {
pub request: HttpRequest,
pub timestamp: u64,
}
读取:
pub fn load_history(&mut self) {
if let Some(mut path) = Self::ensure_config_dir() {
path.push("history.json");
if let Ok(data) = std::fs::read_to_string(path) {
if let Ok(history) = serde_json::from_str(&data) {
self.history = history;
}
}
}
}
保存:
pub fn save_history(&self) {
if let Some(mut path) = Self::ensure_config_dir() {
path.push("history.json");
if let Ok(data) = serde_json::to_string(&self.history) {
let _ = std::fs::write(path, data);
}
}
}
教学项目可以这样写;生产项目建议进一步处理错误、日志和原子写入。
原子写入
直接 fs::write 有一个风险:写到一半断电或崩溃,文件可能损坏。更稳妥的策略是:
- 写到同目录临时文件。
- flush。
- rename 到正式文件。
fn write_json_atomic(path: &std::path::Path, content: &str) -> std::io::Result<()> {
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, content)?;
std::fs::rename(tmp, path)?;
Ok(())
}
Windows 上如果目标文件已存在,rename 行为需要格外检查;可以先写入临时文件,再使用更完整的文件替换 crate,或退一步先删除旧文件再 rename。
数据迁移
HiPoster 做了一个简单迁移:如果旧版本把 history.json 写在当前目录,新版本会把它移动到标准配置目录。
for file in ["history.json", "theme_config.json"] {
if let Ok(old_path) = std::env::current_dir().map(|dir| dir.join(file)) {
if old_path.exists() {
let mut new_path = dir.clone();
new_path.push(file);
let _ = std::fs::rename(old_path, new_path);
}
}
}
更复杂的应用应给配置文件加版本号:
#[derive(Serialize, Deserialize)]
struct AppConfig {
version: u32,
theme: AppTheme,
}
启动时根据版本执行迁移。
主题结构
HiPoster 的主题是一个枚举:
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum AppTheme {
GitHubLight,
SolarizedLight,
OneLight,
VitesseLight,
CatppuccinLatte,
OceanicNext,
Monokai,
}
枚举转颜色表:
pub struct ThemeColors {
pub bg: Hsla,
pub sidebar: Hsla,
pub surface: Hsla,
pub border: Hsla,
pub text: Hsla,
pub subtext: Hsla,
pub blue: Hsla,
pub green: Hsla,
pub yellow: Hsla,
pub red: Hsla,
}
视图渲染时只关心颜色表:
let colors = self.theme.colors();
v_flex()
.size_full()
.bg(colors.bg)
.child(render_sidebar(&colors))
这样比到处写 match self.theme 干净。
应用 GPUI Component 主题
如果使用 GPUI Component,可以更新全局 Theme:
cx.update_global::<gpui_component::theme::Theme, _>(|global_theme, cx| {
let config = serde_json::json!({
"name": theme.name(),
"mode": if theme.is_dark() { "dark" } else { "light" },
"colors": {
"background": hsla_to_hex(colors.bg),
"foreground": hsla_to_hex(colors.text),
"primary": hsla_to_hex(colors.blue),
"border": hsla_to_hex(colors.border)
}
});
if let Ok(config) = serde_json::from_value::<gpui_component::theme::ThemeConfig>(config) {
global_theme.apply_config(&std::rc::Rc::new(config));
gpui_component::theme::Theme::change(
if theme.is_dark() {
gpui_component::theme::ThemeMode::Dark
} else {
gpui_component::theme::ThemeMode::Light
},
None,
cx,
);
}
});
切换主题后,已经创建的输入框和代码高亮器可能也要刷新。HiPoster 会遍历所有 Tab,重新设置 body 和 response 的 highlighter。
资源与图标
GPUI 应用常见资源包括:
- 应用图标:
.ico、.icns、PNG。 - SVG 图标。
- 字体。
- 图片和示例截图。
- macOS
Info.plist。
HiPoster 的资源目录:
resources/
├── Info.plist
└── icons/
├── icon.ico
├── icon.icns
├── logo.png
└── logo_v2.svg
GPUI Component 的 Icon 元素不内置所有 SVG 文件。组件库文档建议项目自己提供图标资源,名称与 IconName 对应。应用初始化时通过 with_assets(AppAssets) 把资源交给 GPUI:
let app = gpui_platform::application().with_assets(AppAssets);
资源模块通常由宏或构建脚本生成,具体写法取决于项目使用的 asset 方案。
持久化清单
- 配置写入系统目录,不写项目目录。
- 模型实现
Serialize和Deserialize。 - 文件格式带版本号,给迁移留空间。
- 大文件或关键配置使用原子写入。
- 历史记录设置上限,避免无限增长。
- 主题只保存主题 id,不保存所有派生颜色。
- 资源放在稳定目录,打包脚本逐平台复制。
做好这些,应用才像一个真正的桌面软件,而不只是一个能跑的窗口。