aboutsummaryrefslogtreecommitdiff
path: root/home-manager/modules/services/emacs.nix
blob: a73b750c5137b22fa30524871b88be00f9f854c5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
{ config, lib, pkgs, ... }:

with lib;

let

  cfg = config.services.emacs;
  emacsCfg = config.programs.emacs;
  emacsBinPath = "${emacsCfg.finalPackage}/bin";
  emacsVersion = getVersion emacsCfg.finalPackage;

  # Adapted from upstream emacs.desktop
  clientDesktopItem = pkgs.makeDesktopItem rec {
    name = "emacsclient";
    desktopName = "Emacs Client";
    genericName = "Text Editor";
    comment = "Edit text";
    mimeType =
      "text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;";
    exec = "${emacsBinPath}/emacsclient ${
        concatStringsSep " " cfg.client.arguments
      } %F";
    icon = "emacs";
    type = "Application";
    terminal = "false";
    categories = "Utility;TextEditor;";
    extraEntries = ''
      StartupWMClass=Emacs
    '';
  };

  # Match the default socket path for the Emacs version so emacsclient continues
  # to work without wrapping it. It might be worthwhile to allow customizing the
  # socket path, but we would want to wrap emacsclient in the user profile to
  # connect to the alternative socket by default for Emacs 26, and set
  # EMACS_SOCKET_NAME for Emacs 27.
  #
  # As systemd doesn't perform variable expansion for the ListenStream param, we
  # would also have to solve the problem of matching the shell path to the path
  # used in the socket unit, which would likely involve templating. It seems of
  # little value for the most common use case of one Emacs daemon per user
  # session.
  socketPath = if versionAtLeast emacsVersion "27" then
    "%t/emacs/server"
  else
    "%T/emacs%U/server";

in {
  meta.maintainers = [ maintainers.tadfisher ];

  options.services.emacs = {
    enable = mkEnableOption "the Emacs daemon";

    client = {
      enable = mkEnableOption "generation of Emacs client desktop file";
      arguments = mkOption {
        type = with types; listOf str;
        default = [ "-c" ];
        description = ''
          Command-line arguments to pass to <command>emacsclient</command>.
        '';
      };
    };

    # Attrset for forward-compatibility; there may be a need to customize the
    # socket path, though allowing for such is not easy to do as systemd socket
    # units don't perform variable expansion for 'ListenStream'.
    socketActivation = {
      enable = mkEnableOption "systemd socket activation for the Emacs service";
    };
  };

  config = mkIf cfg.enable (mkMerge [
    {
      assertions = [
        {
          assertion = emacsCfg.enable;
          message = "The Emacs service module requires"
            + " 'programs.emacs.enable = true'.";
        }
        {
          assertion = cfg.socketActivation.enable
            -> versionAtLeast emacsVersion "26";
          message = "Socket activation requires Emacs 26 or newer.";
        }
      ];

      systemd.user.services.emacs = {
        Unit = {
          Description = "Emacs: the extensible, self-documenting text editor";
          Documentation =
            "info:emacs man:emacs(1) https://gnu.org/software/emacs/";

          # Avoid killing the Emacs session, which may be full of
          # unsaved buffers.
          X-RestartIfChanged = false;
        };

        Service = {
          # We wrap ExecStart in a login shell so Emacs starts with the user's
          # environment, most importantly $PATH and $NIX_PROFILES. It may be
          # worth investigating a more targeted approach for user services to
          # import the user environment.
          ExecStart = ''
            ${pkgs.runtimeShell} -l -c "${emacsBinPath}/emacs --fg-daemon${
            # In case the user sets 'server-directory' or 'server-name' in
            # their Emacs config, we want to specify the socket path explicitly
            # so launching 'emacs.service' manually doesn't break emacsclient
            # when using socket activation.
              optionalString cfg.socketActivation.enable
              "=${escapeShellArg socketPath}"
            }"'';
          # We use '(kill-emacs 0)' to avoid exiting with a failure code, which
          # would restart the service immediately.
          ExecStop = "${emacsBinPath}/emacsclient --eval '(kill-emacs 0)'";
          Restart = "on-failure";
        };
      } // optionalAttrs (!cfg.socketActivation.enable) {
        Install = { WantedBy = [ "default.target" ]; };
      };

      home.packages = optional cfg.client.enable clientDesktopItem;
    }

    (mkIf cfg.socketActivation.enable {
      systemd.user.sockets.emacs = {
        Unit = {
          Description = "Emacs: the extensible, self-documenting text editor";
          Documentation =
            "info:emacs man:emacs(1) https://gnu.org/software/emacs/";
        };

        Socket = {
          ListenStream = socketPath;
          FileDescriptorName = "server";
          SocketMode = "0600";
          DirectoryMode = "0700";
        };

        Install = { WantedBy = [ "sockets.target" ]; };
      };
    })
  ]);
}