基于 niri + Noctalia 的全局明暗主题同步 (NixOS 实践)

5 阅读4分钟

构建跨应用一致的 Wayland 桌面:基于 Noctalia 的全局明暗主题同步 (NixOS 实践)

在日常高强度的终端与代码编辑器使用中,根据外部光线切换主题对保护视力至关重要。例如,在白天使用高对比度的浅色主题(如 Solarized Light),在夜间切换为深色主题。

在现代化的 Wayland 桌面环境中,追求视觉一致性通常面临一个痛点:各种 GUI 应用与终端工具的主题配置标准不一。本文将记录在 NixOS + Niri 架构下,如何以 Noctalia 桌面 Shell 为核心,实现从系统底层到所有上层应用(包括原生支持应用与传统的终端工具如 Neovim/Emacs)的全局明暗主题自动同步。

1. 运行环境与前提条件

本方案基于严格的声明式系统构建,相关环境依赖如下:

  • 操作系统:NixOS (当前配置基于 25.11)
  • 显示服务器:Wayland
  • 窗口管理器 (Compositor) :Niri
  • 桌面 Shell:Noctalia (提供状态栏、OSD 以及统一的系统级控制)
  • 核心底层依赖dconf, xdg-desktop-portal

2. 实现原理:发布-订阅模型与旁路注入

全局主题同步的核心是建立一套可靠的状态流转机制。整个同步过程分为两个流派:

  1. 原生协议流(针对现代 GUI 应用)

    • 发布者:在 Noctalia 面板中切换主题时,Noctalia 会修改底层的 gsettings 键值 (org.gnome.desktop.interface color-scheme)。
    • 网关xdg-desktop-portal 监听到这一底层状态变更,并通过 D-Bus 向当前会话中的所有应用广播这一信号。
    • 订阅者:原生支持该 Wayland 协议的应用(如 Google Chrome, VSCode, Kitty 等)在接收到 D-Bus 信号后,自动重载对应的内部 CSS 或渲染树。
  2. 旁路注入流(针对不支持标准协议的终端程序)

    • 对于 Neovim、Emacs 或部分终端模拟器(如 Alacritty),它们缺乏对外部 D-Bus 主题信号的原生监听能力。
    • 我们需要引入一个自定义的后台守护进程,实时监听 gsettings 的状态转换,并通过进程间通信 (IPC)、Socket 或 RPC 接口,主动将主题变更指令下发到这些运行中的实例。

3. 基础设施配置 (NixOS)

要让原生应用顺利接收主题信号,必须在操作系统的全局配置中启用并正确配置 dconf 与 XDG Portal。

configuration.nix 中添加以下声明:

Nix

# 启用 dconf,作为 gsettings 的底层存储后端
programs.dconf.enable = true;

# 配置 XDG Desktop Portal 进行状态广播
xdg.portal = {
  enable = true;
  # 在 Niri 等非 GNOME/KDE 的独立混成器环境下,
  # 推荐使用 gtk portal 来提供标准的主题配置读取接口
  extraPortals = with pkgs; [ 
    xdg-desktop-portal-gtk 
  ];
  config.common.default = "*";
};

配置完成后,执行 nixos-rebuild switch,此时 Chrome 等现代浏览器的“跟随系统主题”功能即可正常工作。

4. 旁路注入:处理不支持标准协议的应用 (Neovim / Emacs)

为了覆盖日常开发重度使用的编辑器,我们使用 Home Manager 编写一个 Systemd User Service。该服务在后台持续运行,利用 gsettings monitor 监听变量,并通过 IPC 通道分别处理不同的软件。

在 Home Manager 配置文件中引入以下模块:

Nix

systemd.user.services.theme-sync-daemon = {
  Unit = {
    Description = "Global Theme Sync Daemon for Nvim and Emacs";
    # 绑定到图形界面的生命周期
    PartOf = [ "graphical-session.target" ];
    After = [ "graphical-session.target" ];
  };

  Service = {
    # 内联 Shell 脚本,避免维护额外的外部文件
    ExecStart = "${pkgs.writeShellScript "theme-sync-daemon" ''
      # 注入必要的环境变量
      export PATH=${pkgs.glib}/bin:${pkgs.neovim}/bin:${pkgs.emacs}/bin:$PATH

      apply_theme() {
        # 获取当前系统设定的配色方案
        local scheme=$(gsettings get org.gnome.desktop.interface color-scheme)
        
        if [[ "$scheme" == *'prefer-dark'* ]]; then
          echo "Syncing state: Dark Mode"
          
          # 1. 向所有运行中的 Neovim 实例发送暗色指令
          # 依赖于 Nvim 默认在 XDG_RUNTIME_DIR 下生成的 Socket 文件
          for server in $(find ''${XDG_RUNTIME_DIR:-/run/user/$(id -u)} -name "nvim.*.0" 2>/dev/null); do
            nvim --server "$server" --remote-expr "execute('set background=dark')" || true
          done
          
          # 2. 通过 emacsclient 向 Emacs Daemon 下发主题
          emacsclient -n -e "(load-theme 'solarized-dark t)" || true
          
        else
          echo "Syncing state: Light Mode"
          
          # 向 Neovim 实例发送亮色指令
          for server in $(find ''${XDG_RUNTIME_DIR:-/run/user/$(id -u)} -name "nvim.*.0" 2>/dev/null); do
            nvim --server "$server" --remote-expr "execute('set background=light')" || true
          done
          
          # 向 Emacs 下发亮色主题
          emacsclient -n -e "(load-theme 'solarized-light t)" || true
        fi
      }

      # 守护进程启动时先执行一次对齐基线状态
      apply_theme

      # 阻塞式监听 gsettings 事件
      gsettings monitor org.gnome.desktop.interface color-scheme | while read -r _; do
        apply_theme
      done
    ''}";
    
    # 异常退出时自动重启,保证系统的鲁棒性
    Restart = "on-failure";
    RestartSec = 2;
  };

  Install = {
    WantedBy = [ "graphical-session.target" ];
  };
};

细节说明与后续扩展

  1. Neovim 的依赖要求:该脚本利用了 Neovim 原生的 RPC 能力 (--server--remote-expr),无需在 Nvim 端安装任何额外插件,属于极低成本的集成方案。
  2. Emacs 的运行模式:由于 emacsclient 的调用机制,Emacs 必须以守护进程模式(Daemon)运行,或者在启动时主动执行 (server-start)
  3. 终端模拟器扩展:如果您使用的终端(如 Alacritty)未实现 Wayland 主题协议,可以通过查阅其官方文档中的 IPC 命令,在上述脚本的 if-else 分支中直接追加对应的配置热重载指令(例如 alacritty msg config)。

通过这种声明式的基础设施配置与旁路守护进程结合的架构,我们可以建立一套稳定、解耦且高容错的主题同步机制,在享受定制化窗口管理器的同时,不丢失商业桌面级别的连贯体验。