├── 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 | Packaging status 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 | --------------------------------------------------------------------------------