Skip to main content

持久化、主题与资源

一个桌面应用不能只在内存里活着。用户关闭应用后,请求历史、主题、窗口偏好和最近打开的文件都应该被保存。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 有一个风险:写到一半断电或崩溃,文件可能损坏。更稳妥的策略是:

  1. 写到同目录临时文件。
  2. flush。
  3. 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 方案。

持久化清单

  • 配置写入系统目录,不写项目目录。
  • 模型实现 SerializeDeserialize
  • 文件格式带版本号,给迁移留空间。
  • 大文件或关键配置使用原子写入。
  • 历史记录设置上限,避免无限增长。
  • 主题只保存主题 id,不保存所有派生颜色。
  • 资源放在稳定目录,打包脚本逐平台复制。

做好这些,应用才像一个真正的桌面软件,而不只是一个能跑的窗口。