推荐我开发的 Linux AppImage 管理器:AppimageMannger
本文摘要
AppimageMannger 是我开发的 Linux AppImage 管理器,用来集中管理应用文件、图标、版本、菜单入口和 GitHub 更新源,默认应用库为 ~/Applications。它提供 Ely/GPUI、Qt 6 Widgets 和 GTK 4/libadwaita 三种原生界面,共用一套 Rust 引擎和 26 种语言目录。除了展示我偏爱的 GNOME 界面,文章还结合带中文注释的代码与 Mermaid 图,介绍前端进程通信、配置保存、扫描快照、图标缓存以及更新校验、备份和回滚的实现。1.0.1 与 1.0.3 发布程序的扫描对比展示了大应用库中减少重复 I/O 的收益,单个真实 AppImage 和翻译微基准则说明了不同负载下的表现。文末介绍项目参考的 Google、微软 Rust 规范,提供开发文档、适用范围和 AGPL-3.0-only 许可说明,方便试用、阅读源码或参与开发。
AI 辅助声明|AI agent coding 辅助创作
本项目的部分代码、测试与本文写作由 Codex 辅助完成。
AppImage 是 Linux 上一种将应用打包成单个文件的分发格式。
下载下来,通常给个执行权限就能试用,但 Linux 桌面仍然需要统一的应用管理。
一些现有的 AppImage 管理器比较旧,我在 Ubuntu 26.04 上使用时还遇到了 Bug,我不喜欢。
所以我做了 AppimageMannger。
程序在桌面显示为「AppImage 管理器」,命令名是 appimage-mannger。
它默认把 ~/Applications 当作应用库,把文件、图标、版本、菜单入口和更新源放在同一处处理。
更好玩的是:同一个 Rust 引擎,能打开三种真正的原生界面。
我喜欢 GNOME,所以功能介绍主要使用 GTK 4 / libadwaita 界面;另外两种原生界面也各有自己的布局。
| 入口 | 链接 |
|---|---|
| 项目源码 | xingwangzhe/AppimageMannger |
| 安装与下载 | GitHub Releases |
| 代码许可 | AGPL-3.0-only · LICENSE |
这篇可以按自己的兴趣读:
| 阅读目的 | 建议内容 |
|---|---|
| 想先试用 | 看三种界面、GNOME 设置和 About。 |
| 想了解实现 | 看共享 Rust 引擎、RPC、扫描与更新流程。 |
| 想判断性能或参与开发 | 看 DEB 实测、规范、检查命令和适用范围。 |
三种原生界面:先看看这个应用
以下界面运行在 Ubuntu 26.04.1 LTS 的 GNOME 50.1 / Wayland 桌面,管理器版本为 1.0.3。
图里的 GpgFrontend 2.1.10 是被管理的示例应用。
Ely / GPUI:Rust 这边的主界面

想自己做 Rust 桌面应用,这几个入口可以直接收藏:
| 开发入口 | 说明与链接 |
|---|---|
| 看组件效果 | Ely / GPUI 组件官网,可在线体验组件。 |
| 看接入与源码 | Ely GPUI Components。 |
| 学底层框架 | GPUI 官网与开发文档。 |
开发提示:本项目的
ely-core和 GPUI 版本以 Cargo.toml与锁文件为准。接入时先确认版本配套,再复制示例。
默认前端用 Ely 组件和 GPUI 绘制。
标题栏、菜单、卡片、输入框和分栏组合成一个主从布局:左边找应用,右边看详情。
长内容可以滚动,应用多时也不用把每个窗口都打开。
Qt:熟悉的菜单栏和工具栏

Qt 版由 native/qt.cpp 实现,使用真正的 Qt 6 Widgets。
它有自己的原生控件、事件循环和 QSplitter,数据处理仍然交给同一个 Rust 引擎。
GNOME:接下来轮到我偏爱的这一位

GNOME 版在 native/gnome.c,使用 GTK 4 / libadwaita。
HeaderBar、圆角卡片和留白让我觉得很顺眼;常用动作就在对应内容旁边,找起来省事。
后面的设置和 About 都继续用它来介绍。
三个选项对应三套实际工具包。
它们共同复用配置、翻译目录和管理逻辑,因此切换界面时不必重新维护一份应用清单。
GNOME 页面的日常:设置能记住,About 能找到人
设置:换界面之前,先把状态收好

这里能选应用目录、浅色或深色、界面后端和语言。
更改应用目录只切换当前管理的库,不会搬走原来的文件。
切换语言或工具包前会先保存,再重新打开应用。
配置通常在 ~/.config/appimage-mannger/config.json;设置了 XDG_CONFIG_HOME 时,则跟随它。
它会记住:
| 配置类别 | 保存内容 |
|---|---|
| 界面偏好 | 外观、语言和前端工具包。 |
| 窗口状态 | 大小、最大化状态和分栏比例。 |
| 当前上下文 | 选中的应用、应用目录与编辑草稿。 |
下面的配置示例展示了主要字段,使用时将应用目录替换成自己的绝对路径:
{ // 默认应用库是 ~/Applications;序列化时记录绝对路径。 "applications_dir": "/home/your-name/Applications", // 应用语言与工具包内部语言映射分别处理。 "language": "zh-CN", "appearance": "dark", // gpui、qt、gnome 对应实际不同的原生前端。 "window_style": "gnome", "window_width": 1460.0, "window_height": 940.0, "maximized": false, "sidebar_fraction": 0.27, // 草稿独立保存,刷新列表时不应随手覆盖用户输入。 "directory_draft": null, "repository_drafts": {}}保存配置也尽量避免留下半截 JSON。
Config::save_to 的关键部分如下,省略了函数签名与目录准备:
// 临时文件与目标位于同一目录,避免跨文件系统移动。let mut tmp = tempfile::NamedTempFile::new_in(path.parent().unwrap())?;// 先把完整 JSON 写进临时文件,旧配置此时仍然存在。tmp.write_all(&serde_json::to_vec_pretty(self)?)?;// 同步临时文件的内容,然后用 persist 提交到目标路径。tmp.as_file().sync_all()?;tmp.persist(path).map_err(|e| e.error)?;这里同步的是文件内容;这段实现没有额外同步父目录,不能把它描述成覆盖所有断电场景的持久化保证。
对自己用AI写的程序,漂亮话还是少吹一口。
About:图标管认路,文字管说明白


About 集中了项目主页、我的 GitHub、邮箱和个人网站。
我希望尽量用图标减少一长串界面文案,但仍然保留「邮箱」「个人网站」这样的文字说明。
图标负责快速识别,文字负责消除歧义,也照顾辅助技术与不熟悉图标的用户。
文字与图片也能带走。
界面文字和应用内容支持选择、复制,图片有复制入口。
GPUI 的 Linux 图片剪贴板还需要 wl-copy / xclip 配合,包依赖中已考虑这一点;这部分属于需要跨桌面继续维护的集成细节。
共享 Rust 引擎:每个模块负责什么
三种界面共用配置、应用扫描和更新逻辑,主要模块如下。
代码示例以 1.0.3 版本为基础。
src/├── main.rs # CLI 参数入口、RPC 分流与前端启动├── ui.rs # Ely / GPUI 的界面与后台任务衔接├── chrome.rs # GPUI 标题栏和窗口集成├── copy.rs # 可选择文字、复制菜单和图片剪贴板├── frontends.rs # Qt / GTK 启动、JSON RPC 分发├── manager.rs # 配置、扫描、导入、桌面入口和回收站├── metadata.rs # 不执行 AppImage 的元数据与图标读取├── update.rs # GitHub 更新发现、下载校验与替换└── i18n.rs # 共享翻译目录、回退与工具包语言映射native/├── qt.cpp # Qt 6 Widgets 前端└── gnome.c # GTK 4 / libadwaita 前端resources/i18n/ # 26 份共享 JSON 语言目录build.rs # 编译两个 native helper,生成翻译资源scripts/ # Bun 打包、i18n 检查与扫描基准脚本架构文档说明了这些边界。
最关键的分工是:前端负责展示、交互和异步任务衔接,文件修改和更新规则集中在引擎里。
这样修复路径检查时,不用分别想起三套界面各自还有一份逻辑。
原生界面怎样和 Rust 说话
Qt / GTK 把 JSON 请求作为独立参数传给引擎的 --rpc 入口,读取 JSON 响应;它不是一个长期开放的网络服务,也不需要常驻特权 daemon。
GPUI 则直接调用同一套 Rust 模块。
frontends.rs 用同一种响应结构包装成功结果和错误:
pub fn dispatch_json(request: &str) -> Value { match dispatch(request) { // 前端从 data 取业务结果。 Ok(data) => json!({"ok": true, "data": data, "error": null}), // 错误统一放进 error,保留上下文信息。 Err(e) => json!({"ok": false, "data": null, "error": format!("{e:#}")}), }}输入也有限制:请求长度必须小于 1 MiB;保存配置时检查绝对目录、有限的窗口数值和分栏范围。
涉及应用路径的动作继续进入管理边界检查。
JSON 能解析,只代表说的是人话,还没到「可以随便改文件」那一步。
GTK 启动子进程时使用参数列表,路径与 JSON 不拼进 shell 命令。
以下节选自 start_next,省略了前后状态处理:
// s->engine、--rpc、请求 JSON 是三个独立参数。j->process = g_subprocess_new( G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE, &error, s->engine, "--rpc", j->request, NULL);// 等待响应使用异步接口,GTK 主循环继续处理窗口事件。g_subprocess_communicate_utf8_async( j->process, NULL, NULL, rpc_finished, j);异步并不代表任意并发。
Qt 的 EngineClient 和 GTK 的 job queue 都串行执行请求,避免扫描、保存和文件操作互相插队。
下面是 Qt startNext 的入口节选:
// running_ 为真时,当前请求还没收尾;其余请求留在队列中。if (running_ || queue_.empty()) return;running_ = true;// 移出队首请求,使它的生命周期覆盖整个异步响应过程。auto request = std::make_shared<Request>(std::move(queue_.front()));queue_.pop_front();auto *process = new QProcess(this);// 后续代码连接完成信号、解析响应、调用 callback,再 startNext()。这是有意的取舍:操作顺序更容易推理,但一次较长的更新任务也会占住这条队列。
它解决的是业务状态顺序,不能自动消除所有未来的交互问题。
扫描:少做重复 I/O,比给循环加速更实在
manager::scan 的一个主要优化,是复用桌面文件的解析结果。
旧路径会为每个 AppImage 重复查找、读取桌面文件;现在一次扫描先读一份桌面文件快照,再给每个应用匹配。
下面是 1.0.3 scan 的核心循环,省略了前面的目录存在性检查:
let mut apps = Vec::new();// 这一轮扫描共用解析结果,避免为每个应用反复打开同一批文件。let desktops = read_desktops().unwrap_or_default();for entry in fs::read_dir(&config.applications_dir)? { let entry = entry?; let path = entry.path(); // 跳过目录与符号链接等非普通文件项。 if !entry.file_type()?.is_file() { continue; } if path.extension().is_none_or(|e| !e.eq_ignore_ascii_case("AppImage")) { continue; } // 预先构造匹配目标;桌面元数据来自上面的快照。 let target = DesktopTarget::new(&path); let desktop = desktops.iter() .find(|desktop| target.matches(desktop)) .cloned() .unwrap_or_default(); if let Ok(app) = inspect_with_desktop(config, &path, desktop) { apps.push(app); }}// 小写排序键每个应用计算一次。apps.sort_by_cached_key(|a| a.name.to_lowercase());这里还有两个容易被「优化」二字遮住的事实:桌面文件读取失败时可以回退到空元数据;单个应用检查失败会跳过它,整轮目录读取错误则仍可能返回失败。
匹配本身依然是遍历快照,没有把全部 CPU 工作神奇地变成 O(n)。
主要减少的是重复文件打开、解析和字符串准备。
metadata.rs 负责图标和内嵌信息。
它通过受限的 ELF / SquashFS 读取与 unsquashfs 列表、取文件操作获得内容,扫描过程不运行或挂载 AppImage。
图标缓存键包含路径、修改时间和大小,文件变化后会换一个键。
以下是该模块的实际限制常量节选:
// 图标最多 4 MiB;文本列表和 desktop 内容最多 256 KiB。const ICON_LIMIT: usize = 4 * 1024 * 1024;const TEXT_LIMIT: usize = 256 * 1024;// 区分普通子进程操作与完整图标读取的时间预算。const PROCESS_TIMEOUT: Duration = Duration::from_secs(3);const IMAGE_TIMEOUT: Duration = Duration::from_secs(8);// ELF 搜索范围也有上限,不为了找图标无界扫描。const ELF_SEARCH_LIMIT: usize = 1024 * 1024;这些限制与缓存一起存在。
不能为了跑出一个更好看的数字,就把边界检查拿掉。
扫描性能:1.0.1 与 1.0.3 的对比
应用越多,反复读取桌面文件的成本越容易累积。
2026 年 10 月 27 日的测试比较了 1.0.1 和 1.0.3 官方 amd64 DEB 所包含的引擎,测试机器为 Intel Core i7-1260P,系统为 Ubuntu 26.04.1。
两个版本分别预热后交替执行 7 次,取整个扫描命令的墙钟耗时中位数。1
测试使用热缓存,未固定 CPU 频率;两个版本的构建环境也可能影响结果。
合成应用库:观察 desktop 查找的开销
每组测试包含 20、100 或 250 个 128 字节的惰性 ELF/AppImage 文件,以及 500 个无关的 .desktop 文件。
这些文件只用于扫描,不执行,也不包含真实应用的压缩内容和图标,因此这组测试主要反映桌面文件查找的成本。
| 合成应用数 | desktop 总数 | 1.0.1 中位耗时 | 1.0.3 中位耗时 | 耗时比:旧 / 新 |
|---|---|---|---|---|
| 20 | 520 | 29.94 ms | 4.64 ms | 6.45× |
| 100 | 600 | 158.73 ms | 6.37 ms | 24.93× |
| 250 | 750 | 463.16 ms | 11.84 ms | 39.13× |
应用库较大时,减少重复读取能明显缩短扫描耗时。
这些数字不代表窗口启动、图标提取或网络更新也有相同幅度的提升。
单个真实应用:收益更小
使用 GpgFrontend 2.1.10 AppImage 测试单个应用,文件大小为 73,083,384 字节,元数据、图标与文件系统缓存均已预热。
| 真实应用数量 | 1.0.1 中位耗时 | 1.0.3 中位耗时 | 结果 |
|---|---|---|---|
| 1 | 3.45 ms | 3.05 ms | 差约 0.39 ms,采样区间重叠 |
只有一个应用时,进程启动已占据较大比例,扫描快照的收益自然没有大库明显。
这组数据不足以判断整个图形界面的启动或交互速度。
翻译查找:独立的算法微基准
翻译部分使用 1.0.3 源码中的 release 模式测试程序,对比原始查找算法与带索引的实现。
每组执行 100,000 次混合文案查找,语言目录为德语 de,交替采样 7 次。
| 算法 | 中位耗时 |
|---|---|
| 原始逐项查找 | 99.91 ms |
| 带索引的查找 | 22.36 ms |
带索引的实现约为原算法的 4.47 倍速度,计时范围仅包含文案查找,不包含 RPC 和界面绘制。
三个实验的采样值如下:
| 实验数据 | 内容 |
|---|---|
| 合成库扫描 | 各轮耗时、中位数与程序摘要 |
| 真实应用扫描 | 单个 GpgFrontend AppImage 的扫描数据 |
| 翻译查找 | 两种算法的独立微基准数据 |
要比较其他版本,可以先下载对应的 amd64 DEB,解包后在项目源码目录运行基准脚本:
# 将两个 DEB 放在当前目录,分别解包到独立位置。dpkg-deb --extract appimage-mannger_1.0.1_amd64.deb /tmp/appimage-benchmark/v1.0.1dpkg-deb --extract appimage-mannger_1.0.3_amd64.deb /tmp/appimage-benchmark/v1.0.3
# 脚本创建临时测试库,输出样本和中位数。bun run scripts/benchmark-scan.ts \ /tmp/appimage-benchmark/v1.0.1/usr/bin/appimage-mannger \ /tmp/appimage-benchmark/v1.0.3/usr/bin/appimage-mannger \ /tmp/appimage-scan-results.json
# 翻译查找在源码测试程序内单独比较。cargo test --release --locked translation_benchmark -- --ignored --nocapture交替执行新旧实现,能减少固定先后顺序带来的偏差。
扫描基准脚本的采样循环如下:
for (let sample = 0; sample < 7; sample++) { // 偶数轮先旧后新,奇数轮先新后旧。 const order = sample % 2 === 0 ? [0, 1] : [1, 0]; for (const index of order) { // invoke 检查退出状态、应用数和输出 JSON。 samples[index].push(invoke(binaries[index])); }}更新一件应用,至少要把退路留好
主界面的「版本与更新」区域,可以读取支持的内嵌更新信息,或保存 GitHub owner/repository。
引擎优先从公开 Release 页面发现稳定版,API 作为后备;用户不需要为了更新应用先配置 gh 或个人 token。
只发现「有个新文件」还不够。
update.rs 按这些步骤继续处理:
| 步骤 | 处理内容 |
|---|---|
| 1. 选对版本 | 稳定版、匹配资产名称与架构。 |
| 2. 限制传输 | HTTPS、大小上限与下载完整性。 |
| 3. 检查内容 | 有上游 SHA-256 时验证摘要,再核对 ELF 架构。 |
| 4. 准备替换 | 保留备份,提交新文件,必要时尝试恢复。 |
版本不明或协议不支持时应说明情况,不能随便猜一个最新版。
这里的下载循环用同一个 64 KiB 缓冲区分块读取,同时累计大小、更新摘要和写文件。
下面是实际逻辑的节选,省略进度通知:
let mut hasher = Sha256::new();let mut total = 0u64;let mut buf = [0; 64 * 1024]; // 复用缓冲区,避免每块重新分配。loop { let count = response.read(&mut buf)?; if count == 0 { break; } total += count as u64; // 不能超过声明大小,也不能超过独立的 4 GiB 上限。 ensure!( expected_size.is_none_or(|size| total <= size) && total <= 4 * 1024 * 1024 * 1024, "Downloaded asset exceeds the expected size" ); hasher.update(&buf[..count]); download.write_all(&buf[..count])?;}完整实现还包含文件同步、权限、备份、替换和恢复处理。
磁盘故障也可能让恢复步骤报错,因此「有回滚设计」不等于能对任意硬件故障作保证。
上游没有摘要时,也不能把仅有的格式和传输检查描述成独立的发布者真实性验证。
文件边界同样集中检查。
下面是 ensure_managed_path 中的关键部分,省略了最后的普通文件检查:
// 拒绝把符号链接当作可修改的托管应用。ensure!( !fs::symlink_metadata(path)?.file_type().is_symlink(), "Refusing to modify a symbolic link");let root = config.applications_dir.canonicalize()?;let canonical = path.canonicalize()?;// 只接受位于库根目录下的目标,不能沿路径越界修改别处文件。ensure!( canonical.parent() == Some(root.as_path()), "Application is outside the configured library");这些文件操作各有明确边界:
| 操作 | 文件处理边界 |
|---|---|
| 导入 | 保留来源文件;替换时维护已有路径与关联状态。 |
| 卸载 | 托管可执行文件和对应菜单入口进入 freedesktop Trash,并保留恢复元数据。 |
| 个人数据 | 应用自己的配置与数据不在清理范围内。 |
26 种语言:共享文案,少重复一份维护
翻译的源头是 resources/i18n/<locale>.json。
Rust 读取共享目录,build.rs 生成 native 侧的英文回退资源,Qt 和 GTK 从引擎响应获得所选语言文案。
这样修改一个按钮时,不必跑进 C、C++、Rust 三处各找一份翻译表。
热路径里的英文反向查找也改成预构建索引。
OnceLock 初始化一次,索引借用随进程存在的目录字符串;遇到重复英文文案时,保留目录顺序中的第一个 key,维持原有行为。
fn english_keys() -> &'static BTreeMap<&'static str, &'static str> { static KEYS: OnceLock<BTreeMap<&'static str, &'static str>> = OnceLock::new(); KEYS.get_or_init(|| { let mut keys = BTreeMap::new(); if let Some(english) = catalogs().get("en") { for (key, value) in english { // or_insert 保留首次出现的 key,不改变冲突时的取值顺序。 keys.entry(value.as_str()).or_insert(key.as_str()); } } keys })}带占位符的动态文案则先用 RegexSet 筛候选,再按原有顺序匹配并替换,避免每次把全部表达式逐个试一遍。
{name}、{}、%1、%s 等占位符必须保留,诊断等级 Error、Info 等维持英文,第三方应用名称、仓库标识和路径也不会当成普通 UI 文案翻译。
还有一个小坑:应用支持的 26 个 locale,不能原样全部塞给 Ely。
ely_i18n 会映射到它实际接受的少量精确标签,其他内部字符串回退英文。
这个适配层很小,却能避免「共享目录支持一种语言,组件初始化却不接受标签」的尴尬。
Google 和微软的 Rust 规范,怎么用到这个小项目里
项目的 Rust 实现参考了 Google Rust Style Guide 和 Microsoft Pragmatic Rust Guidelines。
Google 的指南强调可读性、维护性以及按应用和库的实际场景选择 API 设计。
对这个桌面程序,我更在意职责明确、借用已有数据和能直接读懂的错误处理;不为了形式统一,把 Chromium / Fuchsia 专属规则整套搬过来。
微软的性能章节中,M-HOTPATH、M-THROUGHPUT 与 M-MEM-REUSE分别提醒我围绕热点测量、批量处理有用工作和复用资源。
扫描快照、一次性翻译索引、下载缓冲区,都能和这些思路对应起来。
静态验证建议则落到已有的 rustfmt、Clippy 和测试流程上。
但不能倒过来说「用了大公司的规范,所以一定更快」。
是否值得优化,以及收益出现在哪种负载上,需要由实际测量判断。
修改代码后,可以用以下检查覆盖格式、静态分析、核心行为与翻译占位符:
# 格式与静态检查;遵守仓库已有约束。cargo fmt --all --checkcargo clippy --all-targets --locked -- -D warnings
# 正确性测试;手动性能测试默认不参与这一轮。cargo test --release --locked
# 共享目录、占位符、应用名称与各语言文档检查。bun run scripts/check-i18n.ts从源码到安装包,还有一段原生工具链
build.rs 既整理语言目录,也通过系统开发包编译 native/qt.cpp 和 native/gnome.c。
安装包会把 Qt 和 GTK 的原生前端辅助程序(helper)放到 /usr/lib/appimage-mannger/;便携场景也有内嵌和导出路径。
只构建一个 Rust 可执行文件,却没检查两个 helper,不能算三种界面都准备好了。
原生库版本会影响安装包能否运行,下载时需要确认项目说明里的系统构建基线。
想先看看程序识别了什么,可以用下面这些只读扫描命令。
它们不会启动库里的 AppImage:
appimage-mannger --version# 确认当前安装的管理器版本。
# 用自己的目录替换路径;--list 输出 JSON。appimage-mannger --list --directory "$HOME/Applications"# smoke-test 扫描与检查,不启动应用。appimage-mannger --smoke-test --directory "$HOME/Applications"它适合谁,许可证又是哪一份
如果你在 Linux 桌面收集了几个到一批 AppImage,想集中查看版本、图标、菜单入口和 GitHub 更新源,这个工具正是为这种日常使用准备的。
喜欢 GNOME 可以选 GTK,习惯 Qt 也有原生 Qt,默认的 Ely / GPUI 则继续保留自己的布局。
它的边界也很具体:目前更新流程围绕支持的 GitHub Release 信息;其他协议不会自动变成已支持。
它不接管 apt / Flatpak 等系统包管理,不提供运行时沙箱,也不会替每个上游应用补齐 FUSE、系统库和硬件依赖。
扫描时不执行文件,是这一步的约束,不能据此把任何下载来的 AppImage 都判断成安全软件。

项目代码许可:
AGPL-3.0-only具体条款以 仓库 LICENSE 为准。
| 范围 | 许可说明 |
|---|---|
| 本项目代码 | 使用上面的 AGPL-3.0-only 标识。 |
| 第三方依赖 | Qt、GTK、Ely 等保留各自的许可证和版权说明。 |
我最喜欢的成果,还是那张 GNOME 主界面:应用、路径、版本和更新源各有位置,About 里也终于能顺手找到作者。
三张脸可以慢慢打磨,共享引擎的边界和失败时的退路则要一直守住。
至于下次优化,先挑一个实际让人等待的地方,再把计时器拿出来。
咱这 AppImage 管理器,导入留原件,更新留备份。
AI 帮着写,源码摊开看,主打一个——不烧心。
Footnotes
-
墙钟耗时从启动 CLI 子进程前开始,到该进程退出后结束,包含进程启动、扫描和 JSON 输出。
它既不是纯函数 CPU 时间,也不是图形窗口的首帧时间。 ↩
推荐我开发的 Linux AppImage 管理器:AppimageMannger
作者:xingwangzhe
本文链接:https://xingwangzhe.fun/posts/appimage-mannger-native-linux/
本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
留言评论
评论资源加载失败,请重试。