├── LICENSE
├── README.md
├── config.d
├── 10-systemd-cgroups.conf.in
├── 10-systemd-session.conf.in
├── 95-system-keyboard-config.conf.in
└── 95-xdg-desktop-autostart.conf.in
├── meson.build
├── meson_options.txt
├── rpkg.conf
├── setup.cfg
├── src
├── assign-cgroups.py
├── locale1-xkb-config
├── session.sh
└── wait-sni-ready
├── srpm
├── rpkg.macros
└── sway-systemd.spec.rpkg
└── units
├── sway-session-shutdown.target
├── sway-session.target
└── sway-xdg-autostart.target
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Aleksei Bavshin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Systemd integration for Sway
2 |
3 | ## Goals and requirements
4 |
5 | The goal of this project is to provide a minimal set of configuration files and
6 | scripts required for running [Sway] in a systemd environment.
7 | This includes several areas of integration:
8 |
9 | - Propagate required variables to the systemd user session environment.
10 | - Define sway-session.target for starting user services.
11 | - Place GUI applications into systemd scopes for systemd-oomd compatibility.
12 |
13 | ## Non-goals
14 |
15 | - Running the compositor itself as a user service.
16 | [sway-services] already exists and does exactly that.
17 |
18 | - Managing Sway environment.
19 | It's hard, opinionated and depends on the way user starts Sway, so I don't
20 | have a solution that works for everyone and is acceptable for default
21 | configuration. See also [#6].
22 |
23 | The common solutions are `~/.profile` (if your display manager supports that)
24 | or a wrapper script that sets the variables before starting Sway.
25 |
26 | - Supporting multiple concurrent Sway sessions for the same user.
27 | It's uncommon and doing so would cause problems for which there are no easy
28 | solutions:
29 |
30 | As a part of the integration, we set `WAYLAND_DISPLAY` and `DISPLAY` for a
31 | systemd user session.
32 | The variables are only accurate per-session, while the systemd user sessions
33 | are per-user.
34 | So if the user starts a second Sway instance on the same machine, the new
35 | instance would overwrite the variables, potentially causing some services to
36 | break for the first session.
37 |
38 | ## Components
39 |
40 | ### Session targets
41 |
42 | Systemd forbids starting the `graphical-session.target` directly and encourages
43 | use of an environment-specific target units. Thus, the package here defines
44 | [`sway-session.target`] that binds to the `graphical-session.target` and starts
45 | user services enabled for a graphical session.
46 | `sway-session.target` should be started when the compositor is ready and the
47 | user session environment is set, and stopped before the compositor exits.
48 |
49 | An user service may depend on or reference `sway-session.target` only if it is
50 | specific for Sway. Otherwise, it's recommended to use `graphical-session.target`.
51 |
52 | A special `sway-session-shutdown.target` can be used to stop the
53 | `graphical-session.target` and the `sway-session.target` with all the contained
54 | services.\
55 | `systemctl start sway-session-shutdown.target` will apply the `Conflicts=`
56 | statements in the unit file and ensure that everything is exited, something that
57 | `systemctl stop sway-session.target` is unable to guarantee.
58 |
59 | ### Session script
60 |
61 | The [`session.sh`](./src/session.sh) script is responsible for importing
62 | variables into systemd and dbus activation environments and starting session
63 | target. It also stays in the background until the compositor exits, stops
64 | the session target and unsets variables for systemd user session
65 | (this can be disabled by passing `--no-cleanup`).
66 |
67 | The script itself does not set any variables except `XDG_CURRENT_DESKTOP`/
68 | `XDG_SESSION_TYPE`; it simply passes the values received from Sway.
69 | The list of variables and the name of the session target are currently
70 | hardcoded and could be changed by editing the script.
71 |
72 | For a better description see [comments in the code](./src/session.sh).
73 |
74 | ### Cgroups assignment script
75 |
76 | The [`assign-cgroups.py`](./src/assign-cgroups.py) script subscribes to a new
77 | window i3 ipc event and automatically creates a transient scope unit
78 | (with path `app.slice/app-${app_id}.slice/app-${app_id}-${pid}.scope`) for each
79 | GUI application launched in the same cgroup as the compositor.
80 | Existing child processes of the application are assigned to the same scope.
81 |
82 | The script is necessary to overcome a limitation of `systemd-oomd`:
83 | it only tracks resource usage by cgroups and kills the whole group when
84 | a single application misbehaves and exceeds resource usage limits.
85 | By placing individual apps into isolated cgroups we are decreasing the chance
86 | that the oomd killer would target the group with the compositor and accidentally
87 | terminate the session.
88 |
89 | It can also be used to impose resource usage limits on a specific application,
90 | because transient units are still loading override configs. For example,
91 | by creating `$XDG_CONFIG_HOME/systemd/user/app-firefox.slice.d/override.conf`
92 | with content
93 |
94 | ```ini
95 | [Slice]
96 | MemoryHigh=2G
97 | ```
98 |
99 | you can tell systemd that all the Firefox processes combined are not allowed to
100 | exceed 2 Gb of memory. See [`systemd.resource-control(5)`] for other available
101 | resource control options.
102 |
103 | ### Keyboard layout configuration
104 |
105 | The [`locale1-xkb-config`] script reads the system-wide input configuration
106 | from [`org.freedesktop.locale1`] systemd interface, translates it into a Sway
107 | configuration and applies to all devices with type:keyboard.
108 |
109 | The main motivation for this component was an ability to apply system-wide
110 | keyboard mappings configured in the OS installer to a greetd or SDDM greeter
111 | running with Sway as a display server.
112 |
113 | The component is not enabled by default. Use `-Dautoload-configs=locale1,...`
114 | to install the configuration file to Sway's default config drop-in directory or
115 | check [`95-system-keyboard-config.conf`] for necessary configuration.
116 |
117 | ### XDG Desktop autostart target
118 |
119 | The `sway-xdg-autostart.target` wraps systemd bultin
120 | [`xdg-desktop-autostart.target`] to allow delayed start from a script.
121 |
122 | The `xdg-desktop-autostart.target` contains units generated by
123 | [`systemd-xdg-autostart-generator(8)`] from XDG desktop files in autostart
124 | directories.
125 | The recommended way to start it is a `Wants=xdg-desktop-autostart.target`
126 | in a Desktop Environment session target (`sway-session.target` in our case),
127 | but there are some issues with that approach.
128 |
129 | Most notably, there's a race between the autostarted applications and the panel
130 | with StatusNotifierHost implementation.
131 | SNI specification is very clear on that point; if the `StatusNotifierWatcher`
132 | is unavailable or `IsStatusNotifierHostRegistered` is not set, the application
133 | should fallback to XEmbed tray.
134 | There are even known implementations that follow this requirement (Qt...) and
135 | will fail to create a status icon if started before the panel.
136 |
137 | The component is not enabled by default. Use `-Dautoload-configs=autostart,...`
138 | to install the configuration file to Sway's default config drop-in directory or
139 | check [`95-xdg-desktop-autostart.conf`] for necessary configuration.
140 |
141 | ## Installation
142 |
143 |
144 |
146 |
147 |
148 | ### Dependencies
149 |
150 | Session script calls these commands:
151 | `swaymsg`, `systemctl`, `dbus-update-activation-environment`.
152 |
153 | Cgroups script uses following python packages:
154 | [`dbus-next`](https://pypi.org/project/dbus-next/),
155 | [`i3ipc`](https://pypi.org/project/i3ipc/),
156 | [`psutil`](https://pypi.org/project/psutil/),
157 | [`tenacity`](https://pypi.org/project/tenacity/),
158 | [`python-xlib`](https://pypi.org/project/python-xlib/)
159 |
160 | ### Installing with meson
161 |
162 | ```
163 | meson setup --sysconfidir=/etc [-Dautoload-configs=...,...] build
164 | sudo meson install -C build
165 | ```
166 |
167 | The command will install configuration files from [`config.d`](./config.d/)
168 | to the `/etc/sway/config.d/` directory included from the default Sway config.
169 | The `autoload-config` option allows you to specify the configuration files that
170 | are loaded by default, with the rest being installed to
171 | `$PREFIX/share/sway-systemd`.
172 |
173 | If you are using a custom Sway configuration file and already removed the
174 | `include /etc/sway/config.d/*` line, you will need to edit your config and
175 | include the installed files.
176 |
177 | > [!NOTE]
178 | > It's not advised to enable everything system-wide, as behavior of certain
179 | > integration components can be unexpected and confusing for the users.
180 | > E.g. `locale1` can overwrite the keyboard options set in Sway config ([#21]),
181 | > and `autostart` can conflict with existing autostart configuration.
182 |
183 | ### Installing manually/using directly from git checkout
184 |
185 | 1. Clone repository.
186 | 2. Copy `units/*.target` to the systemd user unit directory
187 | (`/usr/lib/systemd/user/`, `$XDG_CONFIG_HOME/systemd/user/` or
188 | `~/.config/systemd/user` are common locations).
189 | 3. Run `systemctl --user daemon-reload` to make systemd rescan the service files.
190 | 4. Add `exec /path/to/cloned/repo/src/session.sh` to your Sway config for
191 | environment and session configuration.
192 | 5. Add `exec /path/to/cloned/repo/src/assign-cgroups.py` to your Sway config
193 | to enable cgroup assignment script.
194 | 6. Restart your Sway session or run `swaymsg` with the commands above.
195 | Simple config reload is insufficient as it does not execute `exec` commands.
196 |
197 | [Sway]: https://swaywm.org
198 | [sway-services]: https://github.com/xdbob/sway-services/
199 |
200 | [`systemd.resource-control(5)`]: https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html
201 | [`org.freedesktop.locale1`]: https://www.freedesktop.org/software/systemd/man/org.freedesktop.locale1.html
202 | [`xdg-desktop-autostart.target`]: https://www.freedesktop.org/software/systemd/man/systemd.special.html#xdg-desktop-autostart.target
203 | [`systemd-xdg-autostart-generator(8)`]: https://www.freedesktop.org/software/systemd/man/systemd-xdg-autostart-generator.html
204 |
205 | [`95-system-keyboard-config.conf`]: ./config.d/95-system-keyboard-config.conf.in
206 | [`95-xdg-desktop-autostart.conf`]: ./config.d/95-xdg-desktop-autostart.conf.in
207 | [`locale1-xkb-config`]: ./src/locale1-xkb-config
208 | [`sway-session.target`]: ./units/sway-session.target
209 |
210 | [#6]: https://github.com/alebastr/sway-systemd/issues/6
211 | [#21]: https://github.com/alebastr/sway-systemd/issues/21
212 |
--------------------------------------------------------------------------------
/config.d/10-systemd-cgroups.conf.in:
--------------------------------------------------------------------------------
1 | # Automatically assign a dedicated systemd scope to the GUI applications
2 | # launched in the same cgroup as the compositor. This could be helpful for
3 | # implementing cgroup-based resource management and would be necessary when
4 | # `systemd-oomd` is in use.
5 | #
6 | # Limitations: The script is using i3ipc window:new event to detect application
7 | # launches and would fail to detect background apps or special surfaces.
8 | # Therefore it's recommended to supplement the script with use of systemd user
9 | # services for such background apps.
10 | #
11 | exec @execdir@/assign-cgroups.py
12 |
--------------------------------------------------------------------------------
/config.d/10-systemd-session.conf.in:
--------------------------------------------------------------------------------
1 | # Address several issues with DBus activation and systemd user sessions
2 | #
3 | # 1. DBus-activated and systemd services do not share the environment with user
4 | # login session. In order to make the applications that have GUI or interact
5 | # with the compositor work as a systemd user service, certain variables must
6 | # be propagated to the systemd and dbus.
7 | # Possible (but not exhaustive) list of variables:
8 | # - DISPLAY - for X11 applications that are started as user session services
9 | # - WAYLAND_DISPLAY - similarly, this is needed for wayland-native services
10 | # - I3SOCK/SWAYSOCK - allow services to talk with sway using i3 IPC protocol
11 | #
12 | # 2. `xdg-desktop-portal` requires XDG_CURRENT_DESKTOP to be set in order to
13 | # select the right implementation for screenshot and screencast portals.
14 | # With all the numerous ways to start sway, it's not possible to rely on the
15 | # right value of the XDG_CURRENT_DESKTOP variable within the login session,
16 | # therefore the script will ensure that it is always set to `sway`.
17 | #
18 | # 3. GUI applications started as a systemd service (or via xdg-autostart-generator)
19 | # may rely on the XDG_SESSION_TYPE variable to select the backend.
20 | # Ensure that it is always set to `wayland`.
21 | #
22 | # 4. The common way to autostart a systemd service along with the desktop
23 | # environment is to add it to a `graphical-session.target`. However, systemd
24 | # forbids starting the graphical session target directly and encourages use
25 | # of an environment-specific target units. Therefore, the integration
26 | # package here provides and uses `sway-session.target` which would bind to
27 | # the `graphical-session.target`.
28 | #
29 | # 5. Stop the target and unset the variables when the compositor exits.
30 | #
31 | exec @execdir@/session.sh
32 |
--------------------------------------------------------------------------------
/config.d/95-system-keyboard-config.conf.in:
--------------------------------------------------------------------------------
1 | # Apply system-wide XKB configuration stored in systemd-localed.
2 | #
3 | # The configuration can be viewed with `localectl` and modified
4 | # with `localectl set-x11-keymap`.
5 | #
6 | # Normal mode will pick up the configuration changes immediately
7 | # and oneshot mode will require a Sway config reload.
8 |
9 | # exec @execdir@/locale1-xkb-config
10 | exec_always @execdir@/locale1-xkb-config --oneshot
11 |
--------------------------------------------------------------------------------
/config.d/95-xdg-desktop-autostart.conf.in:
--------------------------------------------------------------------------------
1 | # Wait until a StatusNotifierItem tray implementation is available and
2 | # process XDG autostart entries.
3 | #
4 | # This horror has to exist because
5 | #
6 | # - SNI spec mandates that if `IsStatusNotifierHostRegistered` is not set,
7 | # the client should fall back to the Freedesktop System Tray specification
8 | # (XEmbed).
9 | # - There are actual implementations that take this seriously and implement
10 | # a fallback *even if* StatusNotifierWatcher is already DBus-activated.
11 | # - https://github.com/systemd/systemd/issues/3750
12 | #
13 | exec @execdir@/wait-sni-ready && \
14 | systemctl --user start sway-xdg-autostart.target
15 |
--------------------------------------------------------------------------------
/meson.build:
--------------------------------------------------------------------------------
1 | project('sway-systemd', [],
2 | meson_version: '>= 0.51',
3 | license: 'MIT',
4 | )
5 |
6 | enabled = get_option('autoload-configs')
7 | configs = {
8 | 'config.d/10-systemd-session.conf.in': true,
9 | 'config.d/10-systemd-cgroups.conf.in': enabled.contains('all') or enabled.contains('cgroups'),
10 | 'config.d/95-system-keyboard-config.conf.in': enabled.contains('all') or enabled.contains('locale1'),
11 | 'config.d/95-xdg-desktop-autostart.conf.in': enabled.contains('all') or enabled.contains('autostart'),
12 | }
13 |
14 | scripts = [
15 | 'src/session.sh',
16 | 'src/assign-cgroups.py',
17 | 'src/wait-sni-ready',
18 | 'src/locale1-xkb-config',
19 | ]
20 |
21 | unit_files = [
22 | 'units/sway-session.target',
23 | 'units/sway-session-shutdown.target',
24 | 'units/sway-xdg-autostart.target',
25 | ]
26 |
27 | systemd = dependency('systemd')
28 | conf_dir = get_option('sysconfdir') / 'sway' / 'config.d'
29 | data_dir = get_option('datadir') / meson.project_name()
30 | # must be absolute path for configuration_data
31 | exec_dir = get_option('prefix') / get_option('libexecdir') / meson.project_name()
32 |
33 | install_data(
34 | scripts,
35 | install_dir: exec_dir,
36 | install_mode: 'rwxr-xr-x',
37 | )
38 |
39 | install_data(
40 | unit_files,
41 | install_dir: systemd.get_variable(pkgconfig: 'systemduserunitdir'),
42 | )
43 |
44 | conf_data = configuration_data()
45 | conf_data.set('execdir', exec_dir)
46 |
47 | foreach config, enabled : configs
48 | configure_file(
49 | configuration: conf_data,
50 | input: config,
51 | output: '@BASENAME@',
52 | install_dir: enabled ? conf_dir : data_dir,
53 | )
54 | endforeach
55 |
--------------------------------------------------------------------------------
/meson_options.txt:
--------------------------------------------------------------------------------
1 | option('autoload-configs', type: 'array',
2 | choices: ['all', 'autostart', 'cgroups', 'locale1'], value: [],
3 | description: 'Install configuration for selected components into a system-wide Sway include directory (/etc/sway/config.d)')
4 |
--------------------------------------------------------------------------------
/rpkg.conf:
--------------------------------------------------------------------------------
1 | [rpkg]
2 | user_macros = "${git_props:root}/srpm/rpkg.macros"
3 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 88
3 | extend-ignore = E203, W292, W503
4 |
5 | [mypy]
6 | # Too many libraries without type hints :'(
7 | ignore_missing_imports = True
8 |
9 | [pycodestyle]
10 | max-line-length = 88
11 | ignore = E203, W292, W503
12 |
13 | [pylint]
14 | max-line-length = 88
15 |
16 | [pylint.messages_control]
17 | # silence C0103 (invalid-name) for script module name
18 | good-names = assign-cgroups, locale1-xkb-config, wait-sni-ready
19 |
--------------------------------------------------------------------------------
/src/assign-cgroups.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | """
3 | Automatically assign a dedicated systemd scope to the GUI applications
4 | launched in the same cgroup as the compositor. This could be helpful for
5 | implementing cgroup-based resource management and would be necessary when
6 | `systemd-oomd` is in use.
7 |
8 | Limitations: The script is using i3ipc window:new event to detect application
9 | launches and would fail to detect background apps or special surfaces.
10 | Therefore it's recommended to supplement the script with use of systemd user
11 | services for such background apps.
12 |
13 | Dependencies: dbus-next, i3ipc, psutil, tenacity, python-xlib
14 | """
15 | import argparse
16 | import asyncio
17 | import logging
18 | import re
19 | import socket
20 | import struct
21 | import sys
22 | from functools import lru_cache
23 | from typing import Optional
24 |
25 | from dbus_next import Variant
26 | from dbus_next.aio import MessageBus
27 | from dbus_next.errors import DBusError
28 | from i3ipc import Event
29 | from i3ipc.aio import Con, Connection
30 | from psutil import Process
31 | from tenacity import retry, retry_if_exception_type, stop_after_attempt
32 |
33 | if sys.version_info[:2] >= (3, 9):
34 | from collections.abc import Callable
35 | else:
36 | from typing import Callable
37 |
38 |
39 | LOG = logging.getLogger("assign-cgroups")
40 | SD_BUS_NAME = "org.freedesktop.systemd1"
41 | SD_OBJECT_PATH = "/org/freedesktop/systemd1"
42 | SD_SLICE_FORMAT = "app-{app_id}.slice"
43 | SD_UNIT_FORMAT = "app-{app_id}-{unique}.scope"
44 | # Ids of known launcher applications that are not special surfaces. When the app is
45 | # started using one of those, it should be moved to a new cgroup.
46 | # Launcher should only be listed here if it creates cgroup of its own.
47 | LAUNCHER_APPS = ["nwgbar", "nwgdmenu", "nwggrid", "onagre"]
48 |
49 | SD_UNIT_ESCAPE_RE = re.compile(r"[^\w:.\\]", re.ASCII)
50 |
51 |
52 | def escape_app_id(app_id: str) -> str:
53 | """Escape app_id for systemd APIs.
54 |
55 | The "unit prefix" must consist of one or more valid characters (ASCII letters,
56 | digits, ":", "-", "_", ".", and "\"). The total length of the unit name including
57 | the suffix must not exceed 256 characters. [systemd.unit(5)]
58 |
59 | We also want to escape "-" to avoid creating extra slices.
60 | """
61 |
62 | def repl(match):
63 | return "".join([f"\\x{x:02x}" for x in match.group().encode()])
64 |
65 | return SD_UNIT_ESCAPE_RE.sub(repl, app_id)
66 |
67 |
68 | LAUNCHER_APP_CGROUPS = [
69 | SD_SLICE_FORMAT.format(app_id=escape_app_id(app)) for app in LAUNCHER_APPS
70 | ]
71 |
72 |
73 | def get_cgroup(pid: int) -> Optional[str]:
74 | """
75 | Get cgroup identifier for the process specified by pid.
76 | Assumes cgroups v2 unified hierarchy.
77 | """
78 | try:
79 | with open(f"/proc/{pid}/cgroup", "r") as file:
80 | cgroup = file.read()
81 | return cgroup.strip().split(":")[-1]
82 | except OSError:
83 | LOG.exception("Error geting cgroup info")
84 | return None
85 |
86 |
87 | def get_pid_by_socket(sockpath: str) -> int:
88 | """
89 | getsockopt (..., SO_PEERCRED, ...) returns the following structure
90 | struct ucred
91 | {
92 | pid_t pid; /* s32: PID of sending process. */
93 | uid_t uid; /* u32: UID of sending process. */
94 | gid_t gid; /* u32: GID of sending process. */
95 | };
96 | See also: socket(7), unix(7)
97 | """
98 | with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
99 | sock.connect(sockpath)
100 | ucred = sock.getsockopt(
101 | socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("iII")
102 | )
103 | pid, _, _ = struct.unpack("iII", ucred)
104 | return pid
105 |
106 |
107 | def create_x11_pid_getter() -> Callable[[int], int]:
108 | """Create fallback X11 PID getter.
109 |
110 | Sway 1.6.1/wlroots 0.14 can use XRes to get the PID for Xwayland apps from
111 | the server and won't ever reach that. The fallback is preserved for
112 | compatibility with i3 and earlier versions of Sway.
113 | """
114 | # pylint: disable=import-outside-toplevel
115 | # Defer Xlib import until we really need it.
116 | from Xlib import X
117 | from Xlib.display import Display
118 |
119 | try:
120 | # requires python-xlib >= 0.30
121 | from Xlib.ext import res as XRes
122 | except ImportError:
123 | XRes = None
124 |
125 | display = Display()
126 |
127 | def get_net_wm_pid(wid: int) -> int:
128 | """Get PID from _NET_WM_PID property of X11 window"""
129 | window = display.create_resource_object("window", wid)
130 | net_wm_pid = display.get_atom("_NET_WM_PID")
131 | pid = window.get_full_property(net_wm_pid, X.AnyPropertyType)
132 |
133 | if pid is None:
134 | raise RuntimeError("Failed to get PID from _NET_WM_PID")
135 | return int(pid.value.tolist()[0])
136 |
137 | def get_xres_client_id(wid: int) -> int:
138 | """Get PID from X server via X-Resource extension"""
139 | res = display.res_query_client_ids(
140 | [{"client": wid, "mask": XRes.LocalClientPIDMask}]
141 | )
142 | for cid in res.ids:
143 | if cid.spec.client > 0 and cid.spec.mask == XRes.LocalClientPIDMask:
144 | for value in cid.value:
145 | return value
146 | raise RuntimeError("Failed to get PID via X-Resource extension")
147 |
148 | if XRes is None or display.query_extension(XRes.extname) is None:
149 | LOG.warning(
150 | "X-Resource extension is not supported. "
151 | "Process identification for X11 applications will be less reliable."
152 | )
153 | return get_net_wm_pid
154 |
155 | ver = display.res_query_version()
156 | LOG.info(
157 | "X-Resource version %d.%d",
158 | ver.server_major,
159 | ver.server_minor,
160 | )
161 | if (ver.server_major, ver.server_minor) < (1, 2):
162 | return get_net_wm_pid
163 |
164 | return get_xres_client_id
165 |
166 |
167 | class CGroupHandler:
168 | """Main logic: handle i3/sway IPC events and start systemd transient units."""
169 |
170 | def __init__(self, bus: MessageBus, conn: Connection):
171 | self._bus = bus
172 | self._conn = conn
173 |
174 | @property
175 | @lru_cache(maxsize=1)
176 | def get_x11_window_pid(self) -> Optional[Callable[[int], int]]:
177 | """On-demand initialization of X11 PID getter"""
178 | try:
179 | return create_x11_pid_getter()
180 | # pylint: disable=broad-except
181 | except Exception as exc:
182 | LOG.warning("Failed to create X11 PID getter: %s", exc)
183 | return None
184 |
185 | async def connect(self):
186 | """asynchronous initialization code"""
187 | # pylint: disable=attribute-defined-outside-init
188 | introspection = await self._bus.introspect(SD_BUS_NAME, SD_OBJECT_PATH)
189 | self._sd_proxy = self._bus.get_proxy_object(
190 | SD_BUS_NAME, SD_OBJECT_PATH, introspection
191 | )
192 | self._sd_manager = self._sd_proxy.get_interface(f"{SD_BUS_NAME}.Manager")
193 |
194 | self._compositor_pid = get_pid_by_socket(self._conn.socket_path)
195 | self._compositor_cgroup = get_cgroup(self._compositor_pid)
196 | assert self._compositor_cgroup is not None
197 | LOG.info("compositor:%s %s", self._compositor_pid, self._compositor_cgroup)
198 |
199 | self._conn.on(Event.WINDOW_NEW, self._on_new_window)
200 | return self
201 |
202 | def get_pid(self, con: Con) -> Optional[int]:
203 | """Get PID from IPC response (sway), X-Resource or _NET_WM_PID (i3)"""
204 | if isinstance(con.pid, int) and con.pid > 0:
205 | return con.pid
206 |
207 | if con.window is not None and self.get_x11_window_pid is not None:
208 | return self.get_x11_window_pid(con.window)
209 |
210 | return None
211 |
212 | def cgroup_change_needed(self, cgroup: Optional[str]) -> bool:
213 | """Check criteria for assigning current app into an isolated cgroup"""
214 | if cgroup is None:
215 | return False
216 | for launcher in LAUNCHER_APP_CGROUPS:
217 | if launcher in cgroup:
218 | return True
219 | return cgroup == self._compositor_cgroup
220 |
221 | @retry(
222 | reraise=True,
223 | retry=retry_if_exception_type(DBusError),
224 | stop=stop_after_attempt(3),
225 | )
226 | async def assign_scope(self, app_id: str, proc: Process):
227 | """
228 | Assign process (and all unassigned children) to the
229 | app-{app_id}.slice/app{app_id}-{pid}.scope cgroup
230 | """
231 | app_id = escape_app_id(app_id)
232 | sd_slice = SD_SLICE_FORMAT.format(app_id=app_id)
233 | sd_unit = SD_UNIT_FORMAT.format(app_id=app_id, unique=proc.pid)
234 | # Collect child processes as systemd assigns a scope only to explicitly
235 | # specified PIDs.
236 | # There's a risk of race as the child processes may exit by the time dbus call
237 | # reaches systemd, hence the @retry decorator is applied to the method.
238 | pids = [proc.pid] + [
239 | x.pid
240 | for x in proc.children(recursive=True)
241 | if self.cgroup_change_needed(get_cgroup(x.pid))
242 | ]
243 |
244 | await self._sd_manager.call_start_transient_unit(
245 | sd_unit,
246 | "fail",
247 | [["PIDs", Variant("au", pids)], ["Slice", Variant("s", sd_slice)]],
248 | [],
249 | )
250 | LOG.debug(
251 | "window %s successfully assigned to cgroup %s/%s", app_id, sd_slice, sd_unit
252 | )
253 |
254 | async def _on_new_window(self, _: Connection, event: Event):
255 | """window:new IPC event handler"""
256 | con = event.container
257 | app_id = con.app_id if con.app_id else con.window_class
258 | try:
259 | pid = self.get_pid(con)
260 | if pid is None:
261 | LOG.warning("Failed to get pid for %s", app_id)
262 | return
263 | proc = Process(pid)
264 | cgroup = get_cgroup(proc.pid)
265 | # some X11 apps don't set WM_CLASS. fallback to process name
266 | if app_id is None:
267 | app_id = proc.name()
268 | LOG.debug("window %s(%s) cgroup %s", app_id, proc.pid, cgroup)
269 | if self.cgroup_change_needed(cgroup):
270 | await self.assign_scope(app_id, proc)
271 | # pylint: disable=broad-except
272 | except Exception as exc:
273 | LOG.error("Failed to modify cgroup for %s: %s", app_id, exc)
274 |
275 |
276 | async def main():
277 | """Async entrypoint"""
278 | try:
279 | bus = await MessageBus().connect()
280 | conn = await Connection(auto_reconnect=False).connect()
281 | await CGroupHandler(bus, conn).connect()
282 | await conn.main()
283 | except DBusError as exc:
284 | LOG.error("DBus connection error: %s", exc)
285 | except (ConnectionError, EOFError) as exc:
286 | LOG.error("Sway IPC connection error: %s", exc)
287 |
288 |
289 | if __name__ == "__main__":
290 | parser = argparse.ArgumentParser(
291 | description="Assign CGroups to apps in compositors with i3 IPC protocol support"
292 | )
293 | parser.add_argument(
294 | "-l",
295 | "--loglevel",
296 | choices=["critical", "error", "warning", "info", "debug"],
297 | default="info",
298 | dest="loglevel",
299 | help="set logging level",
300 | )
301 | args = parser.parse_args()
302 | logging.basicConfig(level=args.loglevel.upper())
303 | asyncio.run(main())
304 |
--------------------------------------------------------------------------------
/src/locale1-xkb-config:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | """
3 | Sync Sway input configuration with org.freedesktop.locale1.
4 |
5 | Usage:
6 | Configure keyboard mappings with `localectl set-x11-keymap`.
7 | Add `exec /path/to/script` to your Sway config.
8 |
9 | See also:
10 | https://www.freedesktop.org/software/systemd/man/org.freedesktop.locale1.html
11 |
12 | Dependencies: dbus-next, i3ipc
13 | """
14 | import argparse
15 | import asyncio
16 | import logging
17 | from typing import Any, Dict
18 |
19 | from dbus_next import BusType, DBusError, Variant
20 | from dbus_next.aio import MessageBus
21 | from i3ipc.aio import Connection
22 |
23 | DEFAULT_DEVICE = 'type:keyboard'
24 | LOG = logging.getLogger("sway.locale1")
25 | LOCALE1_BUS_NAME = "org.freedesktop.locale1"
26 | LOCALE1_OBJECT_PATH = "/org/freedesktop/locale1"
27 | LOCALE1_INTERFACE = "org.freedesktop.locale1"
28 | PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties"
29 | PROPERTIES = {
30 | 'X11Layout': 'layout',
31 | 'X11Model': 'model',
32 | 'X11Variant': 'variant',
33 | 'X11Options': 'options'
34 | }
35 |
36 |
37 | class Locale1Client:
38 | """Handle org.freedesktop.locale1 updates and pass XKB configuration to Sway"""
39 |
40 | layout: str = ''
41 | model: str = ''
42 | variant: str = ''
43 | options: str = ''
44 |
45 | def __init__(self,
46 | bus: MessageBus,
47 | conn: Connection,
48 | device: str = DEFAULT_DEVICE):
49 | self._bus = bus
50 | self._conn = conn
51 | self._proxy = None
52 | self._device = device
53 |
54 | async def connect(self):
55 | """asynchronous initialization code"""
56 | introspection = await self._bus.introspect(LOCALE1_BUS_NAME,
57 | LOCALE1_OBJECT_PATH)
58 | self._proxy = self._bus.get_proxy_object(LOCALE1_BUS_NAME,
59 | LOCALE1_OBJECT_PATH,
60 | introspection)
61 | self._proxy.get_interface(PROPERTIES_INTERFACE).on_properties_changed(
62 | self.on_properties_changed)
63 |
64 | locale1 = self._proxy.get_interface(LOCALE1_INTERFACE)
65 | self.layout = await locale1.get_x11_layout()
66 | self.model = await locale1.get_x11_model()
67 | self.variant = await locale1.get_x11_variant()
68 | self.options = await locale1.get_x11_options()
69 |
70 | await self.update()
71 |
72 | async def on_properties_changed(self,
73 | interface: str,
74 | changed: Dict[str, Any],
75 | _invalidated=None):
76 | """Handle updates from localed"""
77 | if interface != LOCALE1_INTERFACE:
78 | return
79 |
80 | apply = False
81 |
82 | for name, value in changed.items():
83 | if name not in PROPERTIES:
84 | continue
85 | if isinstance(value, Variant):
86 | value = value.value
87 | self.__dict__[PROPERTIES[name]] = value
88 | apply = True
89 |
90 | if apply:
91 | await self.update()
92 |
93 | async def update(self):
94 | """Pass the updated xkb configuration to Sway"""
95 | LOG.info("xkb(%s): layout '%s' model '%s', variant '%s' options '%s'",
96 | self._device, self.layout, self.model, self.variant,
97 | self.options)
98 | cmd = [f"input {self._device} xkb_variant ''"]
99 | cmd.extend([
100 | f"input {self._device} xkb_{name} '{self.__dict__[name]}'"
101 | for name in PROPERTIES.values()
102 | ])
103 | replies = await self._conn.command(', '.join(cmd))
104 | for cmd, reply in zip(cmd, replies):
105 | if reply.error is not None:
106 | LOG.error("command '%s' failed: %s", cmd, reply.error)
107 |
108 |
109 | async def main(args: argparse.Namespace):
110 | """Async entrypoint"""
111 | try:
112 | bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
113 | conn = await Connection(auto_reconnect=False).connect()
114 | await Locale1Client(bus, conn, device=args.device).connect()
115 |
116 | if not args.oneshot:
117 | await conn.main()
118 | except DBusError as exc:
119 | LOG.error("DBus connection error: %s", exc)
120 | except (ConnectionError, EOFError) as exc:
121 | LOG.error("Sway IPC connection error: %s", exc)
122 |
123 |
124 | if __name__ == "__main__":
125 | parser = argparse.ArgumentParser(
126 | description="Sync Sway input configuration with org.freedesktop.locale1"
127 | )
128 | parser.add_argument(
129 | "-l",
130 | "--loglevel",
131 | choices=["critical", "error", "warning", "info", "debug"],
132 | default="info",
133 | dest="loglevel",
134 | help="set logging level",
135 | )
136 | parser.add_argument(
137 | "--device",
138 | default=DEFAULT_DEVICE,
139 | metavar='identifier',
140 | nargs='?',
141 | type=str,
142 | help="control settings for a specific device "
143 | "(see man sway-input; default: %(default)s)",
144 | )
145 | parser.add_argument("--oneshot",
146 | action='store_true',
147 | help="apply current settings and exit immediately")
148 | args = parser.parse_args()
149 | logging.basicConfig(level=args.loglevel.upper())
150 | asyncio.run(main(args))
151 |
--------------------------------------------------------------------------------
/src/session.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # Address several issues with DBus activation and systemd user sessions
4 | #
5 | # 1. DBus-activated and systemd services do not share the environment with user
6 | # login session. In order to make the applications that have GUI or interact
7 | # with the compositor work as a systemd user service, certain variables must
8 | # be propagated to the systemd and dbus.
9 | # Possible (but not exhaustive) list of variables:
10 | # - DISPLAY - for X11 applications that are started as user session services
11 | # - WAYLAND_DISPLAY - similarly, this is needed for wayland-native services
12 | # - I3SOCK/SWAYSOCK - allow services to talk with sway using i3 IPC protocol
13 | #
14 | # 2. `xdg-desktop-portal` requires XDG_CURRENT_DESKTOP to be set in order to
15 | # select the right implementation for screenshot and screencast portals.
16 | # With all the numerous ways to start sway, it's not possible to rely on the
17 | # right value of the XDG_CURRENT_DESKTOP variable within the login session,
18 | # therefore the script will ensure that it is always set to `sway`.
19 | #
20 | # 3. GUI applications started as a systemd service (or via xdg-autostart-generator)
21 | # may rely on the XDG_SESSION_TYPE variable to select the backend.
22 | # Ensure that it is always set to `wayland`.
23 | #
24 | # 4. The common way to autostart a systemd service along with the desktop
25 | # environment is to add it to a `graphical-session.target`. However, systemd
26 | # forbids starting the graphical session target directly and encourages use
27 | # of an environment-specific target units. Therefore, the integration
28 | # package here provides and uses `sway-session.target` which would bind to
29 | # the `graphical-session.target`.
30 | #
31 | # 5. Stop the target and unset the variables when the compositor exits.
32 | #
33 | # References:
34 | # - https://github.com/swaywm/sway/wiki#gtk-applications-take-20-seconds-to-start
35 | # - https://github.com/emersion/xdg-desktop-portal-wlr/wiki/systemd-user-services,-pam,-and-environment-variables
36 | # - https://www.freedesktop.org/software/systemd/man/systemd.special.html#graphical-session.target
37 | # - https://systemd.io/DESKTOP_ENVIRONMENTS/
38 | #
39 | export XDG_CURRENT_DESKTOP=sway
40 | export XDG_SESSION_DESKTOP="${XDG_SESSION_DESKTOP:-sway}"
41 | export XDG_SESSION_TYPE=wayland
42 | VARIABLES="DESKTOP_SESSION XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE"
43 | VARIABLES="${VARIABLES} DISPLAY I3SOCK SWAYSOCK WAYLAND_DISPLAY"
44 | VARIABLES="${VARIABLES} XCURSOR_THEME XCURSOR_SIZE"
45 | SESSION_TARGET="sway-session.target"
46 | SESSION_SHUTDOWN_TARGET="sway-session-shutdown.target"
47 | WITH_CLEANUP=1
48 |
49 | print_usage() {
50 | cat <&2
82 | print_usage
83 | exit 1 ;;
84 | *)
85 | break ;;
86 | esac
87 | shift
88 | done
89 |
90 | # Check if another Sway session is already active.
91 | #
92 | # Ignores all other kinds of parallel or nested sessions
93 | # (Sway on Gnome/KDE/X11/etc.), as the only way to detect these is to check
94 | # for (WAYLAND_)?DISPLAY and that is know to be broken on Arch.
95 | if systemctl --user -q is-active "$SESSION_TARGET"; then
96 | echo "Another session found; refusing to overwrite the variables"
97 | exit 1
98 | fi
99 |
100 | # DBus activation environment is independent from systemd. While most of
101 | # dbus-activated services are already using `SystemdService` directive, some
102 | # still don't and thus we should set the dbus environment with a separate
103 | # command.
104 | if hash dbus-update-activation-environment 2>/dev/null; then
105 | # shellcheck disable=SC2086
106 | dbus-update-activation-environment --systemd ${VARIABLES:- --all}
107 | fi
108 |
109 | # reset failed state of all user units
110 | systemctl --user reset-failed
111 |
112 | # shellcheck disable=SC2086
113 | systemctl --user import-environment $VARIABLES
114 | systemctl --user start "$SESSION_TARGET"
115 |
116 | # Optionally, wait until the compositor exits and cleanup variables and services.
117 | if [ -z "$WITH_CLEANUP" ] ||
118 | [ -z "$SWAYSOCK" ] ||
119 | ! hash swaymsg 2>/dev/null
120 | then
121 | exit 0;
122 | fi
123 |
124 | # declare cleanup handler and run it on script termination via kill or Ctrl-C
125 | session_cleanup () {
126 | # stop the session target and unset the variables
127 | systemctl --user start --job-mode=replace-irreversibly "$SESSION_SHUTDOWN_TARGET"
128 | if [ -n "$VARIABLES" ]; then
129 | # shellcheck disable=SC2086
130 | systemctl --user unset-environment $VARIABLES
131 | fi
132 | }
133 | trap session_cleanup INT TERM
134 | # wait until the compositor exits
135 | swaymsg -t subscribe '["shutdown"]'
136 | # run cleanup handler on normal exit
137 | session_cleanup
138 |
--------------------------------------------------------------------------------
/src/wait-sni-ready:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | """
3 | A simple script for waiting until an org.kde.StatusNotifierItem host implementation
4 | is available and ready.
5 |
6 | Dependencies: dbus-next, tenacity
7 | """
8 | import asyncio
9 | import logging
10 | import os
11 |
12 | from dbus_next.aio import MessageBus
13 | from tenacity import retry, stop_after_delay, wait_fixed
14 |
15 | LOG = logging.getLogger("wait-sni-host")
16 | TIMEOUT = int(os.environ.get("SNI_WAIT_TIMEOUT", default=25))
17 |
18 |
19 | @retry(reraise=True, stop=stop_after_delay(TIMEOUT), wait=wait_fixed(0.5))
20 | async def get_service(bus, name, object_path, interface_name):
21 | """Wait until the service appears on the bus"""
22 | introspection = await bus.introspect(name, object_path)
23 | proxy = bus.get_proxy_object(name, object_path, introspection)
24 | return proxy.get_interface(interface_name)
25 |
26 |
27 | async def wait_sni_host(bus: MessageBus):
28 | """Wait until a StatusNotifierWatcher service is available and has a
29 | StatusNotifierHost instance"""
30 | future = asyncio.get_event_loop().create_future()
31 |
32 | async def on_host_registered():
33 | value = await sni_watcher.get_is_status_notifier_host_registered()
34 | LOG.debug("StatusNotifierHostRegistered: %s", value)
35 | if value:
36 | future.set_result(value)
37 |
38 | sni_watcher = await get_service(bus, "org.kde.StatusNotifierWatcher",
39 | "/StatusNotifierWatcher",
40 | "org.kde.StatusNotifierWatcher")
41 | sni_watcher.on_status_notifier_host_registered(on_host_registered)
42 | # fetch initial value
43 | await on_host_registered()
44 | return await asyncio.wait_for(future, timeout=TIMEOUT)
45 |
46 |
47 | async def main():
48 | """asyncio entrypoint"""
49 | bus = await MessageBus().connect()
50 | try:
51 | await wait_sni_host(bus)
52 | LOG.info("Successfully waited for org.kde.StatusNotifierHost")
53 | # pylint: disable=broad-except
54 | except Exception as err:
55 | LOG.error("Failed to wait for org.kde.StatusNotifierHost: %s",
56 | str(err))
57 |
58 |
59 | if __name__ == "__main__":
60 | logging.basicConfig(level=logging.INFO)
61 | asyncio.run(main())
62 |
--------------------------------------------------------------------------------
/srpm/rpkg.macros:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # vim ft:sh
3 |
4 | function git_tag {
5 | git describe --tags --abbrev=0 2>/dev/null | head -n 1
6 | }
7 |
8 | function git_commit_count {
9 | local tag=$1
10 | if [ -n "$tag" ]; then
11 | git rev-list "$tag"..HEAD --count 2>/dev/null || printf 0
12 | else
13 | git rev-list HEAD --count 2>/dev/null || printf 0
14 | fi
15 | }
16 |
17 | function git_version {
18 | tag="$(git_tag)"
19 | tag_version="$(echo "$tag" | sed -E -n "s/^v?([^-]+)/\1/p")"
20 | if [ -z "$tag_version" ]; then
21 | tag_version=0
22 | fi
23 | commit_count="$(git_commit_count "$tag")"
24 | if [ "$commit_count" -eq 0 ]; then
25 | output "$tag_version"
26 | else
27 | shortcommit="$(git rev-parse --short HEAD)"
28 | output "$tag_version^${commit_count}.git${shortcommit}"
29 | fi
30 | }
31 |
32 | function git_release {
33 | output "1"
34 | }
35 |
36 | function git_dir_release {
37 | git_release "$@"
38 | }
39 |
--------------------------------------------------------------------------------
/srpm/sway-systemd.spec.rpkg:
--------------------------------------------------------------------------------
1 | # vim: ft=spec
2 | %global srcname {{{ git_name }}}
3 |
4 | Name: {{{ git_name append="-git" }}}
5 | Version: {{{ git_version }}}
6 | Release: {{{ git_release }}}%{?dist}
7 | Summary: Systemd integration for Sway session
8 |
9 | License: MIT
10 | URL: https://github.com/alebastr/sway-systemd
11 | Source0: {{{ git_pack path=$(git rev-parse --show-toplevel) }}}
12 |
13 | BuildArch: noarch
14 |
15 | BuildRequires: meson
16 | BuildRequires: pkgconfig(systemd)
17 | BuildRequires: systemd-rpm-macros
18 |
19 | Conflicts: %{srcname}
20 |
21 | Requires: python3dist(dbus-next)
22 | Requires: python3dist(i3ipc)
23 | Requires: python3dist(psutil)
24 | Requires: python3dist(python-xlib)
25 | Requires: python3dist(tenacity)
26 | Requires: sway
27 | Requires: systemd
28 | Recommends: /usr/bin/dbus-update-activation-environment
29 |
30 | %description
31 | %{summary}.
32 |
33 | The goal of this project is to provide a minimal set of configuration files
34 | and scripts required for running Sway in a systemd environment.
35 |
36 | This includes several areas of integration:
37 | - Propagate required variables to the systemd user session environment.
38 | - Define sway-session.target for starting user services.
39 | - Place GUI applications into a systemd scopes for systemd-oomd compatibility.
40 |
41 | %prep
42 | {{{ git_setup_macro path=$(git rev-parse --show-toplevel) }}}
43 |
44 |
45 | %build
46 | %meson \
47 | -Dautoload-configs='cgroups'
48 | %meson_build
49 |
50 |
51 | %install
52 | %meson_install
53 |
54 |
55 | %files
56 | %license LICENSE
57 | %doc README.md
58 | %config(noreplace) %{_sysconfdir}/sway/config.d/10-systemd-session.conf
59 | %config(noreplace) %{_sysconfdir}/sway/config.d/10-systemd-cgroups.conf
60 | %{_datadir}/%{srcname}/*.conf
61 | %dir %{_libexecdir}/%{srcname}
62 | %{_libexecdir}/%{srcname}/assign-cgroups.py
63 | %{_libexecdir}/%{srcname}/locale1-xkb-config
64 | %{_libexecdir}/%{srcname}/session.sh
65 | %{_libexecdir}/%{srcname}/wait-sni-ready
66 | %{_userunitdir}/sway-session.target
67 | %{_userunitdir}/sway-session-shutdown.target
68 | %{_userunitdir}/sway-xdg-autostart.target
69 |
70 |
71 | %changelog
72 | {{{ git_changelog }}}
73 |
--------------------------------------------------------------------------------
/units/sway-session-shutdown.target:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Shutdown running Sway session
3 | DefaultDependencies=no
4 | StopWhenUnneeded=true
5 |
6 | Conflicts=graphical-session.target graphical-session-pre.target
7 | After=graphical-session.target graphical-session-pre.target
8 |
9 | Conflicts=sway-session.target
10 | After=sway-session.target
11 |
--------------------------------------------------------------------------------
/units/sway-session.target:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Sway session
3 | Documentation=man:systemd.special(7)
4 | BindsTo=graphical-session.target
5 | Wants=graphical-session-pre.target
6 | After=graphical-session-pre.target
7 |
--------------------------------------------------------------------------------
/units/sway-xdg-autostart.target:
--------------------------------------------------------------------------------
1 | # Systemd provides xdg-desktop-autostart.target as a way to process XDG autostart
2 | # desktop files. The target sets RefuseManualStart though, and thus cannot be
3 | # used from scripts.
4 | #
5 | [Unit]
6 | Description=XDG autostart for Sway session
7 | Documentation=man:systemd.special(7) man:systemd-xdg-autostart-generator(8)
8 | Documentation=https://github.com/alebastr/sway-systemd
9 | BindsTo=xdg-desktop-autostart.target
10 | PartOf=sway-session.target
11 | After=sway-session.target
12 |
--------------------------------------------------------------------------------