什么是 Desktop Entry
在 GNOME 桌面环境里,应用菜单、搜索结果、Dock 固定项、文件关联、右键动作菜单,很多都依赖 .desktop 文件。
一个关键结论
严格来说,并不存在一套单独的“GNOME Entry 规范”。GNOME 主要遵循的是 freedesktop.org 的 Desktop Entry Specification。因此,平时说的 “GNOME
.desktop启动器” 本质上是 Desktop Entry 规范在 GNOME 中的实现。
也就是说:
- 文件扩展名通常是
.desktop - 语法是 INI 风格的键值对
- 核心规范来自 freedesktop.org
- GNOME 会在此基础上支持少量 GNOME 扩展字段或行为
阅读路线
这篇笔记现在可以按三个角度来读:
- #入门篇:先快速写出一个能出现在 GNOME 菜单里的用户级启动器
- #规范介绍篇:系统理解字段、Desktop Entry ID、显示控制与本地化
- #实战篇:直接解决代理、自启动、Dock 合并、重复菜单项等问题
1. Desktop Entry 能做什么
.desktop 文件最常见的用途包括:
- 出现在应用菜单和搜索中
- 定义应用如何启动(
Exec=) - 指定图标(
Icon=) - 声明 MIME 关联(
MimeType=) - 控制是否只在 GNOME 中显示(
OnlyShowIn=GNOME;) - 定义右键动作菜单(
Actions=) - 用于开机自启(放到
~/.config/autostart/时)
2. 常见存放路径与优先级
在 GNOME / Linux 环境中,常见位置如下:
应用启动器目录
- 系统级:
/usr/share/applications/ - 用户级:
~/.local/share/applications/ - Flatpak 导出:
~/.local/share/flatpak/exports/share/applications//var/lib/flatpak/exports/share/applications/
- Snap:
/var/lib/snapd/desktop/applications/
自启动目录
- 用户级 autostart:
~/.config/autostart/ - 系统级 autostart:
/etc/xdg/autostart/
> 日常自定义时,优先使用
~/.local/share/applications/。这样不会直接改系统包自带文件,升级后也不容易被覆盖。
覆盖关系
通常情况下:
- 用户级
.desktop文件会覆盖系统级同名文件 - 如果系统目录和用户目录都存在同一个 Desktop Entry ID,GNOME 一般优先采用用户目录版本
这也是为什么很多“修改菜单启动参数”的做法都是:
- 先把系统文件复制到
~/.local/share/applications/ - 再修改复制后的文件
3. 一个最小可用示例
[Desktop Entry]
Type=Application
Name=Obsidian
Exec=/opt/Obsidian/obsidian %U
Icon=obsidian
Terminal=false
Categories=Office;Utility;
这个示例表示:
- 这是一个应用(
Type=Application) - 菜单中显示名称是
Obsidian - 启动命令是
/opt/Obsidian/obsidian %U - 图标名称是
obsidian - 启动时不打开终端
- 归类到
Office和Utility
入门篇
第一次接触 .desktop 文件时,先记住什么
如果你现在只想先“做出一个能在 GNOME 菜单里出现、能点击启动的条目”,建议只先记住下面几件事:
- 文件通常放在
~/.local/share/applications/ - 内容从
[Desktop Entry]开始 - 最重要字段是:
Type、Name、Exec、Icon、Terminal - 先让它能启动,再去考虑代理、动作菜单、自启动、MIME 关联等进阶需求
> 新手第一版建议尽量保持简单:不要一上来就在
Exec=中混入复杂 shell 逻辑。先跑通最小版本,再逐步加功能。
5 分钟写出第一个用户级启动器
步骤 1:准备目录
mkdir -p ~/.local/share/applications
步骤 2:创建一个最小示例
例如创建 ~/.local/share/applications/hello-obsidian.desktop:
[Desktop Entry]
Type=Application
Name=Hello Obsidian
Exec=/opt/Obsidian/obsidian %U
Icon=obsidian
Terminal=false
Categories=Office;Utility;
步骤 3:校验语法
desktop-file-validate ~/.local/share/applications/hello-obsidian.desktop
步骤 4:用接近 GNOME 菜单的方式测试
gtk-launch hello-obsidian
如果这一步能正常启动,说明你已经完成了一个最基础的 Desktop Entry。
步骤 5:再考虑要不要继续增强
当最小版本能工作后,再逐步决定是否补充:
Comment=:加应用说明MimeType=:做文件类型/协议关联StartupWMClass=:处理 Dock 图标合并Actions=:添加右键动作菜单OnlyShowIn=:限制桌面环境
新手版最值得先理解的 6 个字段
| 字段 | 为什么先学它 |
|---|---|
Type | 告诉桌面环境这是什么类型的条目 |
Name | 决定菜单/搜索中看到的名字 |
Exec | 决定应用到底怎么启动 |
Icon | 决定图标是否正常显示 |
Terminal | 决定是否需要终端窗口 |
Categories | 影响菜单分类与归档 |
入门阶段的完成标准
如果你已经做到下面 4 点,就算正式入门:
- 能在应用搜索里找到这个条目
- 点击后能正确启动程序
- 图标能正常显示
- 知道优先改
~/.local/share/applications/,而不是直接改系统文件
规范介绍篇
如果你已经知道 .desktop 文件大概怎么写,接下来最值得系统理解的是:
- 哪些字段属于“高频核心字段”
- Desktop Entry ID 是如何影响覆盖关系和菜单显示的
- 哪些字段控制“在哪些桌面显示 / 不显示”
- 哪些字段会影响 GNOME 搜索、Dock 合并、MIME 关联
推荐的理解顺序
- 先理解
Type、Name、Exec、Icon - 再理解
Categories、MimeType、Keywords - 然后理解
OnlyShowIn、NotShowIn、NoDisplay、Hidden - 最后理解
Actions、StartupWMClass、autostart 扩展字段
Desktop Entry ID 是什么
虽然 .desktop 文件本身是普通文件,但桌面环境在内部通常会把它识别为一个 Desktop Entry ID。实践上可以先粗略理解为:
- 一个
.desktop文件对应一个条目身份 - 它会影响菜单识别、覆盖关系、默认应用关联、Dock 匹配等行为
- 同一个应用如果出现多个不同
.desktop文件,往往就会产生“重复菜单项”或“图标不合并”问题
对日常维护来说,可以先记住这个经验法则:
> 如果你想“覆盖系统自带启动器”,优先复制原文件到用户目录后在原文件基础上改,而不是另起一个完全不同的文件名。
显示控制相关字段
这一组字段容易混淆,但其实作用边界很清楚:
| 字段 | 常见用途 |
|---|---|
OnlyShowIn | 只在指定桌面环境显示 |
NotShowIn | 在指定桌面环境隐藏 |
NoDisplay | 不出现在常规菜单中,但保留可调用能力 |
Hidden | 通常表示该条目被禁用/隐藏 |
例如:
OnlyShowIn=GNOME;
表示这个条目主要面向 GNOME。
NoDisplay=true
表示它通常不主动展示在应用菜单中,但仍可能被系统、协议关联或其他条目调用。
本地化字段
Desktop Entry 规范还支持本地化写法,例如:
Name=Files
Name[zh_CN]=文件
Comment=Browse files
Comment[zh_CN]=浏览文件
这类写法适用于:
- 需要多语言展示名称
- 面向不同语言环境发布应用
- 想让自定义工具在中文系统里显示得更自然
规范视角下的字段分层
从维护成本角度,可以把字段分成三层:
第一层:启动必需层
TypeNameExec
第二层:体验增强层
IconCommentCategoriesKeywordsStartupNotify
第三层:行为控制层
MimeTypeOnlyShowInNotShowInNoDisplayHiddenActionsStartupWMClass
理解这个分层后,你在写 .desktop 文件时就不会“每次都追求一次写全”,而是可以按需求逐步增强。
4. 核心字段说明
下面是最常用的一批字段:
| 字段 | 作用 | 常见说明 |
|---|---|---|
Type | 条目类型 | 常见值:Application |
Name | 应用名称 | 菜单中主要显示名称 |
GenericName | 通用名称 | 如“Web Browser” |
Comment | 简短描述 | 鼠标悬停或详情中可见 |
Exec | 启动命令 | 最重要字段 |
TryExec | 启动前检查命令是否存在 | 不存在时该条目可被隐藏 |
Icon | 图标名或图标路径 | 可写主题图标名或绝对路径 |
Terminal | 是否在终端中运行 | true / false |
Categories | 菜单分类 | 每项通常以 ; 结尾 |
MimeType | 文件/协议关联 | 如 x-scheme-handler/obsidian; |
Keywords | 搜索关键字 | 有助于 GNOME 搜索 |
StartupNotify | 是否启用启动通知 | 常见为 true |
StartupWMClass | 窗口类名匹配 | 影响 Dock 合并与图标归属 |
OnlyShowIn | 只在某些桌面显示 | 如 GNOME; |
NotShowIn | 在某些桌面隐藏 | 如 KDE; |
NoDisplay | 不在菜单中显示 | 常用于辅助启动器 |
Hidden | 隐藏/禁用此条目 | 常见于 autostart 禁用 |
Actions | 定义附加动作 | 用于右键动作菜单 |
5. Exec= 字段是最容易踩坑的部分
5.1 Exec 不是 shell 脚本
很多人第一次写 .desktop 文件时,会误以为 Exec= 是“像 shell 一样执行一整行命令”。其实并不完全是。
>
Exec=并不是默认通过/bin/sh -c执行。
所以像管道、重定向、
&&、变量展开等 shell 语法,不应该直接写进去,除非你显式调用 shell 或包装脚本。
例如下面这种写法通常不可靠:
Exec=obsidian --proxy-server="http://127.0.0.1:7890" && echo done
更稳妥的做法有两种:
方式 A:直接写程序和参数
Exec=/opt/Obsidian/obsidian --proxy-server="http://127.0.0.1:7890" %U
方式 B:把复杂逻辑写到脚本里
Exec=/home/dev/.local/bin/start-obsidian-with-proxy.sh %U
6. Exec 中常见占位符
Desktop Entry 规范对 Exec= 里的一些占位符有明确定义。
| 占位符 | 含义 |
|---|---|
%u | 单个 URL |
%U | 多个 URL |
%f | 单个文件 |
%F | 多个文件 |
%i | 图标参数 |
%c | 条目显示名称 |
%k | 当前 .desktop 文件路径 |
%% | 字面量 % |
例如:
Exec=/opt/Obsidian/obsidian %U
表示这个程序可以接收多个 URL,例如协议唤起:
obsidian://open?...- 文件管理器/浏览器传入的多个链接参数
关于 %U 与 %u
%u:单个 URL%U:多个 URL
平时如果应用可能被“协议唤起”或“从文件管理器传参”,保留 %U 往往更稳妥。
7. GNOME 中常见的实际字段组合
7.1 普通 GUI 应用
[Desktop Entry]
Type=Application
Name=Telegram Desktop
Exec=/opt/telegram/Telegram -- %u
Icon=/opt/telegram/telegram.png
Terminal=false
StartupNotify=true
StartupWMClass=TelegramDesktop
Categories=Network;InstantMessaging;Chat;
MimeType=x-scheme-handler/tg;
7.2 通过 env 注入环境变量
这在需要给单个应用单独加代理时很常见:
[Desktop Entry]
Type=Application
Name=Obsidian
Exec=env http_proxy=http://127.0.0.1:7890 https_proxy=http://127.0.0.1:7890 /opt/Obsidian/obsidian %U
Icon=obsidian
Terminal=false
Categories=Office;
这类写法适合“只给某个应用加环境变量,不影响全局环境”的场景。
7.3 右键动作菜单(Actions)
[Desktop Entry]
Type=Application
Name=FreeRDP
Exec=/home/dev/.local/bin/wlfreerdp-launcher.sh --host=192.168.114.2 --user=alice
Icon=preferences-desktop-remote-desktop
Terminal=false
Actions=WORK;HOME;
[Desktop Action WORK]
Name=Connect WORK
Exec=/home/dev/.local/bin/wlfreerdp-launcher.sh --host=192.168.114.2 --user=alice --domain=WORK
[Desktop Action HOME]
Name=Connect HOME
Exec=/home/dev/.local/bin/wlfreerdp-launcher.sh --host=192.168.3.16 --user=Administrator
这种结构很适合:
- 一个主启动器
- 多个目标动作
- GNOME Dock / 应用菜单右键操作
8. GNOME 常见扩展字段
虽然核心规范来自 freedesktop.org,但 GNOME 生态里也经常看到一些扩展字段,例如:
OnlyShowIn=GNOME;X-GNOME-UsesNotifications=trueX-GNOME-Autostart-enabled=true
这些字段通常不是 Desktop Entry 核心规范的最小集合,但在 GNOME 中可能有额外意义。
例子:仅在 GNOME 下显示
OnlyShowIn=GNOME;
例子:GNOME 自启动
放在 ~/.config/autostart/ 时,常见写法如下:
[Desktop Entry]
Type=Application
Name=My Startup Task
Exec=/home/dev/.local/bin/startup-task.sh
Terminal=false
X-GNOME-Autostart-enabled=true
OnlyShowIn=GNOME;
实战篇
如果说前面两部分解决的是“知道是什么、知道怎么写”,那么这一部分重点解决的是:写完之后为什么没生效、为什么菜单重复、为什么 Dock 不合并、为什么代理参数无效。
场景 1:给单个应用单独加代理
典型需求:
- Obsidian 走代理,但系统其他软件不走
- Discord / Telegram 只在菜单启动时附加参数
- 不想改全局
http_proxy
优先方案通常有两种:
- 在
Exec=中直接使用env - 把逻辑写入脚本,再让
.desktop调用脚本
例如:
Exec=env http_proxy=http://127.0.0.1:7890 https_proxy=http://127.0.0.1:7890 /opt/Obsidian/obsidian %U
如果参数很多、还涉及 no_proxy、日志、预检查,则更适合写脚本。
场景 2:菜单项改了但没生效
优先检查这几件事:
- 是否改错文件(系统级 vs 用户级)
- 当前实际生效的是不是另一个
.desktop Exec=是否写得过于复杂- 是否需要重新启动目标应用
- 是否只是 MIME 数据没更新
最常见的实际原因不是“GNOME 不刷新”,而是:
- 你改的是
/usr/share/applications/foo.desktop - 但真正被 GNOME 读取的是
~/.local/share/applications/foo.desktop
场景 3:Dock 图标不合并 / 固定后分裂成两个图标
这是 GNOME 实战里最常见的问题之一。通常要同时看三件事:
.desktop文件名 / Desktop Entry IDStartupWMClass=- 实际窗口类名(或 Wayland app-id)
经验上:
- Electron、AppImage、自定义启动脚本、远程桌面客户端都容易出这个问题
- 如果主程序窗口类和
.desktop描述不一致,GNOME 很容易把它当成“另一个应用”
场景 4:重复菜单项
常见来源:
- 应用自动生成一个用户级条目
- 你手动又新建了一个系统级条目
- 软件包升级后恢复了系统条目
- AppImage / Electron 首次运行时又写了一个新的
.desktop
排查时建议直接看这些目录:
~/.local/share/applications/
/usr/share/applications/
/var/lib/flatpak/exports/share/applications/
~/.local/share/flatpak/exports/share/applications/
场景 5:把应用做成开机自启
如果你已经写好了一个应用启动器,那么迁移到 autostart 往往只差两步:
- 放到
~/.config/autostart/ - 视情况补上
X-GNOME-Autostart-enabled=true
例如:
[Desktop Entry]
Type=Application
Name=My Service Tray
Exec=/home/dev/.local/bin/my-service.sh
Terminal=false
X-GNOME-Autostart-enabled=true
OnlyShowIn=GNOME;
场景 6:何时该写脚本,何时只写 Exec=
可以用下面这个判断法:
直接写在 Exec= 里即可
适合:
- 程序路径固定
- 参数少
- 只是加 1~3 个简单参数
- 不需要流程控制
应该改为脚本
适合:
- 需要判断环境变量
- 需要密码输入、缓存、日志
- 需要
if/else、循环、预检查 - 需要多步启动逻辑
- 需要避免把复杂逻辑塞进一行
> 一个很实用的经验是:当你开始想在
Exec=里放bash -c '...'且内容越来越长时,通常已经到了“该拆脚本”的时候。
9. Desktop Entry ID 与“重复菜单项”问题
很多“开始菜单出现重复应用”的问题,本质都和 Desktop Entry ID、文件位置、自动生成条目有关。
常见场景:
- 手动创建了系统级
.desktop - 应用自己又在
~/.local/share/applications/自动生成了一个用户级.desktop - 两者名称不同或路径不同,于是 GNOME 菜单里出现多个相似项
这也是为什么像 Telegram、AppImage、Electron 应用经常出现“重复菜单项”“图标不合并”“固定到 Dock 后分裂成两个图标”等问题。
> 如果想让同一个应用在 GNOME Dock 中稳定合并,通常要同时关注:
.desktop文件名 / 条目 IDStartupWMClass- 实际窗口的 WM_CLASS / app-id
10. GNOME 下调试 .desktop 文件的常用命令
10.1 语法校验
desktop-file-validate ~/.local/share/applications/example.desktop
10.2 更新 desktop 数据库
update-desktop-database ~/.local/share/applications
>
update-desktop-database主要与 MIME 信息缓存相关,不等于“强制刷新 GNOME 菜单”。
很多情况下 GNOME 会自动发现变更;只有在文件关联、缓存或菜单行为异常时,才需要手动执行它。
10.3 按 Desktop Entry 启动
gtk-launch obsidian
这比“直接执行二进制”更接近菜单点击行为,因此很适合排查:
Exec=是否写对- 参数是否正确传入
- 协议/URL 占位符是否生效
10.4 检查 MIME 关联
gio mime x-scheme-handler/obsidian
11. GNOME / Linux 环境下的常见坑
11.1 改错文件
你修改了 /usr/share/applications/foo.desktop,但真正生效的是:
~/.local/share/applications/foo.desktop
结果看起来就像“改了没生效”。
11.2 Exec= 里直接写 shell 逻辑
错误思路:
- 直接写
&& - 直接写
>、| - 期待 shell 自动展开变量和通配符
更可靠的做法:
- 简单参数直接写在
Exec=里 - 复杂逻辑单独写到脚本里
11.3 StartupWMClass 不匹配
表现通常是:
- GNOME Dock 中图标不合并
- 固定的图标和实际运行窗口不是同一个
- 打开后又多出一个新图标
11.4 直接修改系统文件导致升级后丢失
如果你直接编辑:
/usr/share/applications/obsidian.desktop
软件包升级时很可能被覆盖。
11.5 重复菜单项
原因常见于:
- AppImage 自动生成用户级
.desktop - 手动创建系统级
.desktop - 同一应用存在多个不同文件名的启动器
12. 一个推荐的维护流程
如果你要在 GNOME 中长期维护某个应用的启动器,比较推荐下面这套流程:
- 找到系统原始
.desktop文件 - 复制到
~/.local/share/applications/ - 在用户目录版本上修改
Exec=/Icon=/StartupWMClass= - 使用
desktop-file-validate校验 - 使用
gtk-launch测试行为 - 如涉及 MIME 关联,再执行
update-desktop-database - 必要时重新登录 GNOME 会话做最终验证
13. 和 autostart 的关系
很多人会把“应用菜单启动器”和“开机自启文件”看成两套东西,但它们实际上共用同一套 Desktop Entry 语法体系。
区别主要在于放置目录不同:
- 菜单启动器:
~/.local/share/applications/ - 自启动:
~/.config/autostart/
也就是说,如果你已经理解 .desktop 的核心字段,那么理解 autostart 也会容易很多。
14. 延伸阅读建议
如果你准备把这篇内容继续拆成系列笔记,可以按下面的结构扩展:
A. 入门篇(适合第一次接触 .desktop)
可以继续单独拆成:
- 如何手写第一个
.desktop文件 - 如何把应用固定到 GNOME Dock
- 如何给 AppImage 创建菜单项
- 如何判断应该改系统级还是用户级条目
B. 规范介绍篇(适合系统整理知识)
可以继续单独拆成:
Exec字段与参数占位符详解OnlyShowIn/NotShowIn/NoDisplay/Hidden的区别- Desktop Entry ID 与覆盖关系
- MIME、协议唤起与默认应用绑定机制
C. 实战篇(适合解决真实问题)
可以继续单独拆成:
- Electron / Obsidian / Discord 的代理启动方案
- Telegram / AppImage 的重复菜单项治理
StartupWMClass与 GNOME Dock 图标合并排查autostart与托盘程序开机启动实践