什么是 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 文件最常见的用途包括:

  1. 出现在应用菜单和搜索中
  2. 定义应用如何启动(Exec=)
  3. 指定图标(Icon=)
  4. 声明 MIME 关联(MimeType=)
  5. 控制是否只在 GNOME 中显示(OnlyShowIn=GNOME;)
  6. 定义右键动作菜单(Actions=)
  7. 用于开机自启(放到 ~/.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 一般优先采用用户目录版本

这也是为什么很多“修改菜单启动参数”的做法都是:

  1. 先把系统文件复制到 ~/.local/share/applications/
  2. 再修改复制后的文件

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 菜单里出现、能点击启动的条目”,建议只先记住下面几件事:

  1. 文件通常放在 ~/.local/share/applications/
  2. 内容从 [Desktop Entry] 开始
  3. 最重要字段是:Type、Name、Exec、Icon、Terminal
  4. 先让它能启动,再去考虑代理、动作菜单、自启动、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 关联

推荐的理解顺序

  1. 先理解 Type、Name、Exec、Icon
  2. 再理解 Categories、MimeType、Keywords
  3. 然后理解 OnlyShowIn、NotShowIn、NoDisplay、Hidden
  4. 最后理解 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]=浏览文件

这类写法适用于:

  • 需要多语言展示名称
  • 面向不同语言环境发布应用
  • 想让自定义工具在中文系统里显示得更自然

规范视角下的字段分层

从维护成本角度,可以把字段分成三层:

第一层:启动必需层

  • Type
  • Name
  • Exec

第二层:体验增强层

  • Icon
  • Comment
  • Categories
  • Keywords
  • StartupNotify

第三层:行为控制层

  • MimeType
  • OnlyShowIn
  • NotShowIn
  • NoDisplay
  • Hidden
  • Actions
  • StartupWMClass

理解这个分层后,你在写 .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=true
  • X-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

优先方案通常有两种:

  1. 在 Exec= 中直接使用 env
  2. 把逻辑写入脚本,再让 .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:菜单项改了但没生效

优先检查这几件事:

  1. 是否改错文件(系统级 vs 用户级)
  2. 当前实际生效的是不是另一个 .desktop
  3. Exec= 是否写得过于复杂
  4. 是否需要重新启动目标应用
  5. 是否只是 MIME 数据没更新

最常见的实际原因不是“GNOME 不刷新”,而是:

  • 你改的是 /usr/share/applications/foo.desktop
  • 但真正被 GNOME 读取的是 ~/.local/share/applications/foo.desktop

场景 3:Dock 图标不合并 / 固定后分裂成两个图标

这是 GNOME 实战里最常见的问题之一。通常要同时看三件事:

  1. .desktop 文件名 / Desktop Entry ID
  2. StartupWMClass=
  3. 实际窗口类名(或 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 往往只差两步:

  1. 放到 ~/.config/autostart/
  2. 视情况补上 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、文件位置、自动生成条目有关。

常见场景:

  1. 手动创建了系统级 .desktop
  2. 应用自己又在 ~/.local/share/applications/ 自动生成了一个用户级 .desktop
  3. 两者名称不同或路径不同,于是 GNOME 菜单里出现多个相似项

这也是为什么像 Telegram、AppImage、Electron 应用经常出现“重复菜单项”“图标不合并”“固定到 Dock 后分裂成两个图标”等问题。

> 如果想让同一个应用在 GNOME Dock 中稳定合并,通常要同时关注:

  1. .desktop 文件名 / 条目 ID
  2. StartupWMClass
  3. 实际窗口的 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 中长期维护某个应用的启动器,比较推荐下面这套流程:

  1. 找到系统原始 .desktop 文件
  2. 复制到 ~/.local/share/applications/
  3. 在用户目录版本上修改 Exec= / Icon= / StartupWMClass=
  4. 使用 desktop-file-validate 校验
  5. 使用 gtk-launch 测试行为
  6. 如涉及 MIME 关联,再执行 update-desktop-database
  7. 必要时重新登录 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 与托盘程序开机启动实践

15. 参考链接