Full Code of alebastr/sway-systemd for AI

main f8b36c53927e cached
19 files
41.8 KB
10.8k tokens
13 symbols
1 requests
Download .txt
Repository: alebastr/sway-systemd
Branch: main
Commit: f8b36c53927e
Files: 19
Total size: 41.8 KB

Directory structure:
gitextract_u67umdq4/

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

================================================
FILE CONTENTS
================================================

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 Aleksei Bavshin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Systemd integration for Sway

## Goals and requirements

The goal of this project is to provide a minimal set of configuration files and
scripts required for running [Sway] in a systemd environment.
This includes several areas of integration:

- Propagate required variables to the systemd user session environment.
- Define sway-session.target for starting user services.
- Place GUI applications into systemd scopes for systemd-oomd compatibility.

## Non-goals

- Running the compositor itself as a user service.
  [sway-services] already exists and does exactly that.

- Managing Sway environment.
  It's hard, opinionated and depends on the way user starts Sway, so I don't
  have a solution that works for everyone and is acceptable for default
  configuration.  See also [#6].

  The common solutions are `~/.profile` (if your display manager supports that)
  or a wrapper script that sets the variables before starting Sway.

- Supporting multiple concurrent Sway sessions for the same user.
  It's uncommon and doing so would cause problems for which there are no easy
  solutions:

  As a part of the integration, we set `WAYLAND_DISPLAY` and `DISPLAY` for a
  systemd user session.
  The variables are only accurate per-session, while the systemd user sessions
  are per-user.
  So if the user starts a second Sway instance on the same machine, the new
  instance would overwrite the variables, potentially causing some services to
  break for the first session.

## Components

### Session targets

Systemd forbids starting the `graphical-session.target` directly and encourages
use of an environment-specific target units.  Thus, the package here defines
[`sway-session.target`] that binds to the `graphical-session.target` and starts
user services enabled for a graphical session.
`sway-session.target` should be started when the compositor is ready and the
user session environment is set, and stopped before the compositor exits.

An user service may depend on or reference `sway-session.target` only if it is
specific for Sway. Otherwise, it's recommended to use `graphical-session.target`.

A special `sway-session-shutdown.target` can be used to stop the
`graphical-session.target` and the `sway-session.target` with all the contained
services.\
`systemctl start sway-session-shutdown.target` will apply the `Conflicts=`
statements in the unit file and ensure that everything is exited, something that
`systemctl stop sway-session.target` is unable to guarantee.

### Session script

The [`session.sh`](./src/session.sh) script is responsible for importing
variables into systemd and dbus activation environments and starting session
target.  It also stays in the background until the compositor exits, stops
the session target and unsets variables for systemd user session
(this can be disabled by passing `--no-cleanup`).

The script itself does not set any variables except `XDG_CURRENT_DESKTOP`/
`XDG_SESSION_TYPE`; it simply passes the values received from Sway.
The list of variables and the name of the session target are currently
hardcoded and could be changed by editing the script.

For a better description see [comments in the code](./src/session.sh).

### Cgroups assignment script

The [`assign-cgroups.py`](./src/assign-cgroups.py) script subscribes to a new
window i3 ipc event and automatically creates a transient scope unit
(with path `app.slice/app-${app_id}.slice/app-${app_id}-${pid}.scope`) for each
GUI application launched in the same cgroup as the compositor.
Existing child processes of the application are assigned to the same scope.

The script is necessary to overcome a limitation of `systemd-oomd`:
it only tracks resource usage by cgroups and kills the whole group when
a single application misbehaves and exceeds resource usage limits.
By placing individual apps into isolated cgroups we are decreasing the chance
that the oomd killer would target the group with the compositor and accidentally
terminate the session.

It can also be used to impose resource usage limits on a specific application,
because transient units are still loading override configs.  For example,
by creating `$XDG_CONFIG_HOME/systemd/user/app-firefox.slice.d/override.conf`
with content

```ini
[Slice]
MemoryHigh=2G
```

you can tell systemd that all the Firefox processes combined are not allowed to
exceed 2 Gb of memory.  See [`systemd.resource-control(5)`] for other available
resource control options.

### Keyboard layout configuration

The [`locale1-xkb-config`] script reads the system-wide input configuration
from [`org.freedesktop.locale1`] systemd interface, translates it into a Sway
configuration and applies to all devices with type:keyboard.

The main motivation for this component was an ability to apply system-wide
keyboard mappings configured in the OS installer to a greetd or SDDM greeter
running with Sway as a display server.

The component is not enabled by default. Use `-Dautoload-configs=locale1,...`
to install the configuration file to Sway's default config drop-in directory or
check [`95-system-keyboard-config.conf`] for necessary configuration.

### XDG Desktop autostart target

The `sway-xdg-autostart.target` wraps systemd bultin
[`xdg-desktop-autostart.target`] to allow delayed start from a script.

The `xdg-desktop-autostart.target` contains units generated by
[`systemd-xdg-autostart-generator(8)`] from XDG desktop files in autostart
directories.
The recommended way to start it is a `Wants=xdg-desktop-autostart.target`
in a Desktop Environment session target (`sway-session.target` in our case),
but there are some issues with that approach.

Most notably, there's a race between the autostarted applications and the panel
with StatusNotifierHost implementation.
SNI specification is very clear on that point; if the `StatusNotifierWatcher`
is unavailable or `IsStatusNotifierHostRegistered` is not set, the application
should fallback to XEmbed tray.
There are even known implementations that follow this requirement (Qt...) and
will fail to create a status icon if started before the panel.

The component is not enabled by default. Use `-Dautoload-configs=autostart,...`
to install the configuration file to Sway's default config drop-in directory or
check [`95-xdg-desktop-autostart.conf`] for necessary configuration.

## Installation

<a href="https://repology.org/project/sway-systemd/versions">
    <img src="https://repology.org/badge/vertical-allrepos/sway-systemd.svg?exclude_unsupported=1"
        alt="Packaging status" align="right">
</a>

### Dependencies

Session script calls these commands:
`swaymsg`, `systemctl`, `dbus-update-activation-environment`.

Cgroups script uses following python packages:
[`dbus-fast`](https://pypi.org/project/dbus-fast/),
[`i3ipc`](https://pypi.org/project/i3ipc/),
[`psutil`](https://pypi.org/project/psutil/),
[`tenacity`](https://pypi.org/project/tenacity/),
[`python-xlib`](https://pypi.org/project/python-xlib/)

### Installing with meson

```
meson setup --sysconfdir=/etc [-Dautoload-configs=...,...] build
sudo meson install -C build
```

The command will install configuration files from [`config.d`](./config.d/)
to the `/etc/sway/config.d/` directory included from the default Sway config.
The `autoload-config` option allows you to specify the configuration files that
are loaded by default, with the rest being installed to
`$PREFIX/share/sway-systemd`.

If you are using a custom Sway configuration file and already removed the
`include /etc/sway/config.d/*` line, you will need to edit your config and
include the installed files.

> [!NOTE]
> It's not advised to enable everything system-wide, as behavior of certain
> integration components can be unexpected and confusing for the users.
> E.g. `locale1` can overwrite the keyboard options set in Sway config ([#21]),
> and `autostart` can conflict with existing autostart configuration.

### Installing manually/using directly from git checkout

1. Clone repository.
2. Copy `units/*.target` to the systemd user unit directory
   (`/usr/lib/systemd/user/`, `$XDG_CONFIG_HOME/systemd/user/` or
   `~/.config/systemd/user` are common locations).
3. Run `systemctl --user daemon-reload` to make systemd rescan the service files.
4. Add `exec /path/to/cloned/repo/src/session.sh` to your Sway config for
   environment and session configuration.
5. Add `exec /path/to/cloned/repo/src/assign-cgroups.py` to your Sway config
   to enable cgroup assignment script.
6. Restart your Sway session or run `swaymsg` with the commands above.
   Simple config reload is insufficient as it does not execute `exec` commands.

[Sway]: https://swaywm.org
[sway-services]: https://github.com/xdbob/sway-services/

[`systemd.resource-control(5)`]: https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html
[`org.freedesktop.locale1`]: https://www.freedesktop.org/software/systemd/man/org.freedesktop.locale1.html
[`xdg-desktop-autostart.target`]: https://www.freedesktop.org/software/systemd/man/systemd.special.html#xdg-desktop-autostart.target
[`systemd-xdg-autostart-generator(8)`]: https://www.freedesktop.org/software/systemd/man/systemd-xdg-autostart-generator.html

[`95-system-keyboard-config.conf`]: ./config.d/95-system-keyboard-config.conf.in
[`95-xdg-desktop-autostart.conf`]: ./config.d/95-xdg-desktop-autostart.conf.in
[`locale1-xkb-config`]: ./src/locale1-xkb-config
[`sway-session.target`]: ./units/sway-session.target

[#6]: https://github.com/alebastr/sway-systemd/issues/6
[#21]: https://github.com/alebastr/sway-systemd/issues/21


================================================
FILE: config.d/10-systemd-cgroups.conf.in
================================================
# Automatically assign a dedicated systemd scope to the GUI applications
# launched in the same cgroup as the compositor. This could be helpful for
# implementing cgroup-based resource management and would be necessary when
# `systemd-oomd` is in use.
#
# Limitations: The script is using i3ipc window:new event to detect application
# launches and would fail to detect background apps or special surfaces.
# Therefore it's recommended to supplement the script with use of systemd user
# services for such background apps.
#
exec @execdir@/assign-cgroups.py


================================================
FILE: config.d/10-systemd-session.conf.in
================================================
# Address several issues with DBus activation and systemd user sessions
#
# 1. DBus-activated and systemd services do not share the environment with user
#    login session. In order to make the applications that have GUI or interact
#    with the compositor work as a systemd user service, certain variables must
#    be propagated to the systemd and dbus.
#    Possible (but not exhaustive) list of variables:
#    - DISPLAY - for X11 applications that are started as user session services
#    - WAYLAND_DISPLAY - similarly, this is needed for wayland-native services
#    - I3SOCK/SWAYSOCK - allow services to talk with sway using i3 IPC protocol
#
# 2. `xdg-desktop-portal` requires XDG_CURRENT_DESKTOP to be set in order to
#    select the right implementation for screenshot and screencast portals.
#    With all the numerous ways to start sway, it's not possible to rely on the
#    right value of the XDG_CURRENT_DESKTOP variable within the login session,
#    therefore the script will ensure that it is always set to `sway`.
#
# 3. GUI applications started as a systemd service (or via xdg-autostart-generator)
#    may rely on the XDG_SESSION_TYPE variable to select the backend.
#    Ensure that it is always set to `wayland`.
#
# 4. The common way to autostart a systemd service along with the desktop
#    environment is to add it to a `graphical-session.target`. However, systemd
#    forbids starting the graphical session target directly and encourages use
#    of an environment-specific target units. Therefore, the integration
#    package here provides and uses `sway-session.target` which would bind to
#    the `graphical-session.target`.
#
# 5. Stop the target and unset the variables when the compositor exits.
#
exec @execdir@/session.sh


================================================
FILE: config.d/95-system-keyboard-config.conf.in
================================================
# Apply system-wide XKB configuration stored in systemd-localed.
#
# The configuration can be viewed with `localectl` and modified
# with `localectl set-x11-keymap`.
#
# Normal mode will pick up the configuration changes immediately
# and oneshot mode will require a Sway config reload.

# exec @execdir@/locale1-xkb-config
exec_always @execdir@/locale1-xkb-config --oneshot


================================================
FILE: config.d/95-xdg-desktop-autostart.conf.in
================================================
# Wait until a StatusNotifierItem tray implementation is available and
# process XDG autostart entries.
#
# This horror has to exist because
#
#  - SNI spec mandates that if `IsStatusNotifierHostRegistered` is not set,
#    the client should fall back to the Freedesktop System Tray specification
#    (XEmbed).
#  - There are actual implementations that take this seriously and implement
#    a fallback *even if* StatusNotifierWatcher is already DBus-activated.
#  - https://github.com/systemd/systemd/issues/3750
#
exec @execdir@/wait-sni-ready && \
    systemctl --user start sway-xdg-autostart.target


================================================
FILE: meson.build
================================================
project('sway-systemd', [],
  meson_version: '>= 0.51',
  license: 'MIT',
)

enabled = get_option('autoload-configs')
configs = {
  'config.d/10-systemd-session.conf.in':        true,
  'config.d/10-systemd-cgroups.conf.in':        enabled.contains('all') or enabled.contains('cgroups'),
  'config.d/95-system-keyboard-config.conf.in': enabled.contains('all') or enabled.contains('locale1'),
  'config.d/95-xdg-desktop-autostart.conf.in':  enabled.contains('all') or enabled.contains('autostart'),
}

scripts = [
  'src/session.sh',
  'src/assign-cgroups.py',
  'src/wait-sni-ready',
  'src/locale1-xkb-config',
]

unit_files = [
  'units/sway-session.target',
  'units/sway-session-shutdown.target',
  'units/sway-xdg-autostart.target',
]

systemd = dependency('systemd')
conf_dir = get_option('sysconfdir') / 'sway' / 'config.d'
data_dir = get_option('datadir') / meson.project_name()
# must be absolute path for configuration_data
exec_dir = get_option('prefix') / get_option('libexecdir') / meson.project_name()

install_data(
  scripts,
  install_dir: exec_dir,
  install_mode: 'rwxr-xr-x',
)

install_data(
  unit_files,
  install_dir: systemd.get_variable(pkgconfig: 'systemduserunitdir'),
)

conf_data = configuration_data()
conf_data.set('execdir', exec_dir)

foreach config, enabled : configs
  configure_file(
    configuration: conf_data,
    input: config,
    output: '@BASENAME@',
    install_dir: enabled ? conf_dir : data_dir,
  )
endforeach


================================================
FILE: meson_options.txt
================================================
option('autoload-configs', type: 'array',
       choices: ['all', 'autostart', 'cgroups', 'locale1'], value: [],
       description: 'Install configuration for selected components into a system-wide Sway include directory (/etc/sway/config.d)')


================================================
FILE: rpkg.conf
================================================
[rpkg]
user_macros = "${git_props:root}/srpm/rpkg.macros"


================================================
FILE: setup.cfg
================================================
[flake8]
max-line-length = 88
extend-ignore = E203, W292, W503

[mypy]
# Too many libraries without type hints :'(
ignore_missing_imports = True

[pycodestyle]
max-line-length = 88
ignore = E203, W292, W503

[pylint]
max-line-length = 88

[pylint.messages_control]
# silence C0103 (invalid-name) for script module name
good-names = assign-cgroups, locale1-xkb-config, wait-sni-ready


================================================
FILE: src/assign-cgroups.py
================================================
#!/usr/bin/python3
"""
Automatically assign a dedicated systemd scope to the GUI applications
launched in the same cgroup as the compositor. This could be helpful for
implementing cgroup-based resource management and would be necessary when
`systemd-oomd` is in use.

Limitations: The script is using i3ipc window:new event to detect application
launches and would fail to detect background apps or special surfaces.
Therefore it's recommended to supplement the script with use of systemd user
services for such background apps.

Dependencies: dbus-fast, i3ipc, psutil, tenacity, python-xlib
"""
import argparse
import asyncio
import logging
import re
import socket
import struct
import sys
from functools import lru_cache
from typing import Optional

from dbus_fast import Variant
from dbus_fast.aio import MessageBus
from dbus_fast.errors import DBusError
from i3ipc import Event
from i3ipc.aio import Con, Connection
from psutil import Process
from tenacity import retry, retry_if_exception_type, stop_after_attempt

if sys.version_info[:2] >= (3, 9):
    from collections.abc import Callable
else:
    from typing import Callable


LOG = logging.getLogger("assign-cgroups")
SD_BUS_NAME = "org.freedesktop.systemd1"
SD_OBJECT_PATH = "/org/freedesktop/systemd1"
SD_SLICE_FORMAT = "app-{app_id}.slice"
SD_UNIT_FORMAT = "app-{app_id}-{unique}.scope"
# Ids of known launcher applications that are not special surfaces. When the app is
# started using one of those, it should be moved to a new cgroup.
# Launcher should only be listed here if it creates cgroup of its own.
LAUNCHER_APPS = ["nwgbar", "nwgdmenu", "nwggrid", "onagre"]

SD_UNIT_ESCAPE_RE = re.compile(r"[^\w:.\\]", re.ASCII)


def escape_app_id(app_id: str) -> str:
    """Escape app_id for systemd APIs.

    The "unit prefix" must consist of one or more valid characters (ASCII letters,
    digits, ":", "-", "_", ".", and "\"). The total length of the unit name including
    the suffix must not exceed 256 characters. [systemd.unit(5)]

    We also want to escape "-" to avoid creating extra slices.
    """

    def repl(match):
        return "".join([f"\\x{x:02x}" for x in match.group().encode()])

    return SD_UNIT_ESCAPE_RE.sub(repl, app_id)


LAUNCHER_APP_CGROUPS = [
    SD_SLICE_FORMAT.format(app_id=escape_app_id(app)) for app in LAUNCHER_APPS
]


def get_cgroup(pid: int) -> Optional[str]:
    """
    Get cgroup identifier for the process specified by pid.
    Assumes cgroups v2 unified hierarchy.
    """
    try:
        with open(f"/proc/{pid}/cgroup", "r") as file:
            cgroup = file.read()
        return cgroup.strip().split(":")[-1]
    except OSError:
        LOG.exception("Error geting cgroup info")
    return None


def get_pid_by_socket(sockpath: str) -> int:
    """
    getsockopt (..., SO_PEERCRED, ...) returns the following structure
    struct ucred
    {
      pid_t pid; /* s32: PID of sending process.  */
      uid_t uid; /* u32: UID of sending process.  */
      gid_t gid; /* u32: GID of sending process.  */
    };
    See also: socket(7), unix(7)
    """
    with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
        sock.connect(sockpath)
        ucred = sock.getsockopt(
            socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("iII")
        )
    pid, _, _ = struct.unpack("iII", ucred)
    return pid


def create_x11_pid_getter() -> Callable[[int], int]:
    """Create fallback X11 PID getter.

    Sway 1.6.1/wlroots 0.14 can use XRes to get the PID for Xwayland apps from
    the server and won't ever reach that. The fallback is preserved for
    compatibility with i3 and earlier versions of Sway.
    """
    # pylint: disable=import-outside-toplevel
    # Defer Xlib import until we really need it.
    from Xlib import X
    from Xlib.display import Display

    try:
        # requires python-xlib >= 0.30
        from Xlib.ext import res as XRes
    except ImportError:
        XRes = None

    display = Display()

    def get_net_wm_pid(wid: int) -> int:
        """Get PID from _NET_WM_PID property of X11 window"""
        window = display.create_resource_object("window", wid)
        net_wm_pid = display.get_atom("_NET_WM_PID")
        pid = window.get_full_property(net_wm_pid, X.AnyPropertyType)

        if pid is None:
            raise RuntimeError("Failed to get PID from _NET_WM_PID")
        return int(pid.value.tolist()[0])

    def get_xres_client_id(wid: int) -> int:
        """Get PID from X server via X-Resource extension"""
        res = display.res_query_client_ids(
            [{"client": wid, "mask": XRes.LocalClientPIDMask}]
        )
        for cid in res.ids:
            if cid.spec.client > 0 and cid.spec.mask == XRes.LocalClientPIDMask:
                for value in cid.value:
                    return value
        raise RuntimeError("Failed to get PID via X-Resource extension")

    if XRes is None or display.query_extension(XRes.extname) is None:
        LOG.warning(
            "X-Resource extension is not supported. "
            "Process identification for X11 applications will be less reliable."
        )
        return get_net_wm_pid

    ver = display.res_query_version()
    LOG.info(
        "X-Resource version %d.%d",
        ver.server_major,
        ver.server_minor,
    )
    if (ver.server_major, ver.server_minor) < (1, 2):
        return get_net_wm_pid

    return get_xres_client_id


class CGroupHandler:
    """Main logic: handle i3/sway IPC events and start systemd transient units."""

    def __init__(self, bus: MessageBus, conn: Connection):
        self._bus = bus
        self._conn = conn

    @property
    @lru_cache(maxsize=1)
    def get_x11_window_pid(self) -> Optional[Callable[[int], int]]:
        """On-demand initialization of X11 PID getter"""
        try:
            return create_x11_pid_getter()
        # pylint: disable=broad-except
        except Exception as exc:
            LOG.warning("Failed to create X11 PID getter: %s", exc)
            return None

    async def connect(self):
        """asynchronous initialization code"""
        # pylint: disable=attribute-defined-outside-init
        introspection = await self._bus.introspect(SD_BUS_NAME, SD_OBJECT_PATH)
        self._sd_proxy = self._bus.get_proxy_object(
            SD_BUS_NAME, SD_OBJECT_PATH, introspection
        )
        self._sd_manager = self._sd_proxy.get_interface(f"{SD_BUS_NAME}.Manager")

        self._compositor_pid = get_pid_by_socket(self._conn.socket_path)
        self._compositor_cgroup = get_cgroup(self._compositor_pid)
        assert self._compositor_cgroup is not None
        LOG.info("compositor:%s %s", self._compositor_pid, self._compositor_cgroup)

        self._conn.on(Event.WINDOW_NEW, self._on_new_window)
        return self

    def get_pid(self, con: Con) -> Optional[int]:
        """Get PID from IPC response (sway), X-Resource or _NET_WM_PID (i3)"""
        if isinstance(con.pid, int) and con.pid > 0:
            return con.pid

        if con.window is not None and self.get_x11_window_pid is not None:
            return self.get_x11_window_pid(con.window)

        return None

    def cgroup_change_needed(self, cgroup: Optional[str]) -> bool:
        """Check criteria for assigning current app into an isolated cgroup"""
        if cgroup is None:
            return False
        for launcher in LAUNCHER_APP_CGROUPS:
            if launcher in cgroup:
                return True
        return cgroup == self._compositor_cgroup

    @retry(
        reraise=True,
        retry=retry_if_exception_type(DBusError),
        stop=stop_after_attempt(3),
    )
    async def assign_scope(self, app_id: str, proc: Process):
        """
        Assign process (and all unassigned children) to the
        app-{app_id}.slice/app{app_id}-{pid}.scope cgroup
        """
        app_id = escape_app_id(app_id)
        sd_slice = SD_SLICE_FORMAT.format(app_id=app_id)
        sd_unit = SD_UNIT_FORMAT.format(app_id=app_id, unique=proc.pid)
        # Collect child processes as systemd assigns a scope only to explicitly
        # specified PIDs.
        # There's a risk of race as the child processes may exit by the time dbus call
        # reaches systemd, hence the @retry decorator is applied to the method.
        pids = [proc.pid] + [
            x.pid
            for x in proc.children(recursive=True)
            if self.cgroup_change_needed(get_cgroup(x.pid))
        ]

        await self._sd_manager.call_start_transient_unit(
            sd_unit,
            "fail",
            [["PIDs", Variant("au", pids)], ["Slice", Variant("s", sd_slice)]],
            [],
        )
        LOG.debug(
            "window %s successfully assigned to cgroup %s/%s", app_id, sd_slice, sd_unit
        )

    async def _on_new_window(self, _: Connection, event: Event):
        """window:new IPC event handler"""
        con = event.container
        app_id = con.app_id if con.app_id else con.window_class
        try:
            pid = self.get_pid(con)
            if pid is None:
                LOG.warning("Failed to get pid for %s", app_id)
                return
            proc = Process(pid)
            cgroup = get_cgroup(proc.pid)
            # some X11 apps don't set WM_CLASS. fallback to process name
            if app_id is None:
                app_id = proc.name()
            LOG.debug("window %s(%s) cgroup %s", app_id, proc.pid, cgroup)
            if self.cgroup_change_needed(cgroup):
                await self.assign_scope(app_id, proc)
        # pylint: disable=broad-except
        except Exception as exc:
            LOG.error("Failed to modify cgroup for %s: %s", app_id, exc)


async def main():
    """Async entrypoint"""
    try:
        bus = await MessageBus().connect()
        conn = await Connection(auto_reconnect=False).connect()
        await CGroupHandler(bus, conn).connect()
        await conn.main()
    except DBusError as exc:
        LOG.error("DBus connection error: %s", exc)
    except (ConnectionError, EOFError) as exc:
        LOG.error("Sway IPC connection error: %s", exc)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Assign CGroups to apps in compositors with i3 IPC protocol support"
    )
    parser.add_argument(
        "-l",
        "--loglevel",
        choices=["critical", "error", "warning", "info", "debug"],
        default="info",
        dest="loglevel",
        help="set logging level",
    )
    args = parser.parse_args()
    logging.basicConfig(level=args.loglevel.upper())
    asyncio.run(main())


================================================
FILE: src/locale1-xkb-config
================================================
#!/usr/bin/python3
"""
Sync Sway input configuration with org.freedesktop.locale1.

Usage:
    Configure keyboard mappings with `localectl set-x11-keymap`.
    Add `exec /path/to/script` to your Sway config.

See also:
    https://www.freedesktop.org/software/systemd/man/org.freedesktop.locale1.html

Dependencies: dbus-fast, i3ipc
"""
import argparse
import asyncio
import logging
from typing import Any, Dict

from dbus_fast import BusType, DBusError, Variant
from dbus_fast.aio import MessageBus
from i3ipc.aio import Connection

DEFAULT_DEVICE = 'type:keyboard'
LOG = logging.getLogger("sway.locale1")
LOCALE1_BUS_NAME = "org.freedesktop.locale1"
LOCALE1_OBJECT_PATH = "/org/freedesktop/locale1"
LOCALE1_INTERFACE = "org.freedesktop.locale1"
PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties"
PROPERTIES = {
    'X11Layout': 'layout',
    'X11Model': 'model',
    'X11Variant': 'variant',
    'X11Options': 'options'
}


class Locale1Client:
    """Handle org.freedesktop.locale1 updates and pass XKB configuration to Sway"""

    layout: str = ''
    model: str = ''
    variant: str = ''
    options: str = ''

    def __init__(self,
                 bus: MessageBus,
                 conn: Connection,
                 device: str = DEFAULT_DEVICE):
        self._bus = bus
        self._conn = conn
        self._proxy = None
        self._device = device

    async def connect(self):
        """asynchronous initialization code"""
        introspection = await self._bus.introspect(LOCALE1_BUS_NAME,
                                                   LOCALE1_OBJECT_PATH)
        self._proxy = self._bus.get_proxy_object(LOCALE1_BUS_NAME,
                                                 LOCALE1_OBJECT_PATH,
                                                 introspection)
        self._proxy.get_interface(PROPERTIES_INTERFACE).on_properties_changed(
            self.on_properties_changed)

        locale1 = self._proxy.get_interface(LOCALE1_INTERFACE)
        self.layout = await locale1.get_x11_layout()
        self.model = await locale1.get_x11_model()
        self.variant = await locale1.get_x11_variant()
        self.options = await locale1.get_x11_options()

        await self.update()

    async def on_properties_changed(self,
                                    interface: str,
                                    changed: Dict[str, Any],
                                    _invalidated=None):
        """Handle updates from localed"""
        if interface != LOCALE1_INTERFACE:
            return

        apply = False

        for name, value in changed.items():
            if name not in PROPERTIES:
                continue
            if isinstance(value, Variant):
                value = value.value
            self.__dict__[PROPERTIES[name]] = value
            apply = True

        if apply:
            await self.update()

    async def update(self):
        """Pass the updated xkb configuration to Sway"""
        LOG.info("xkb(%s): layout '%s' model '%s', variant '%s' options '%s'",
                 self._device, self.layout, self.model, self.variant,
                 self.options)
        cmd = [f"input {self._device} xkb_variant ''"]
        cmd.extend([
            f"input {self._device} xkb_{name} '{self.__dict__[name]}'"
            for name in PROPERTIES.values()
        ])
        replies = await self._conn.command(', '.join(cmd))
        for cmd, reply in zip(cmd, replies):
            if reply.error is not None:
                LOG.error("command '%s' failed: %s", cmd, reply.error)


async def main(args: argparse.Namespace):
    """Async entrypoint"""
    try:
        bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
        conn = await Connection(auto_reconnect=False).connect()
        await Locale1Client(bus, conn, device=args.device).connect()

        if not args.oneshot:
            await conn.main()
    except DBusError as exc:
        LOG.error("DBus connection error: %s", exc)
    except (ConnectionError, EOFError) as exc:
        LOG.error("Sway IPC connection error: %s", exc)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Sync Sway input configuration with org.freedesktop.locale1"
    )
    parser.add_argument(
        "-l",
        "--loglevel",
        choices=["critical", "error", "warning", "info", "debug"],
        default="info",
        dest="loglevel",
        help="set logging level",
    )
    parser.add_argument(
        "--device",
        default=DEFAULT_DEVICE,
        metavar='identifier',
        nargs='?',
        type=str,
        help="control settings for a specific device "
        "(see man sway-input; default: %(default)s)",
    )
    parser.add_argument("--oneshot",
                        action='store_true',
                        help="apply current settings and exit immediately")
    args = parser.parse_args()
    logging.basicConfig(level=args.loglevel.upper())
    asyncio.run(main(args))


================================================
FILE: src/session.sh
================================================
#!/bin/sh
#
# Address several issues with DBus activation and systemd user sessions
#
# 1. DBus-activated and systemd services do not share the environment with user
#    login session. In order to make the applications that have GUI or interact
#    with the compositor work as a systemd user service, certain variables must
#    be propagated to the systemd and dbus.
#    Possible (but not exhaustive) list of variables:
#    - DISPLAY - for X11 applications that are started as user session services
#    - WAYLAND_DISPLAY - similarly, this is needed for wayland-native services
#    - I3SOCK/SWAYSOCK - allow services to talk with sway using i3 IPC protocol
#
# 2. `xdg-desktop-portal` requires XDG_CURRENT_DESKTOP to be set in order to
#    select the right implementation for screenshot and screencast portals.
#    With all the numerous ways to start sway, it's not possible to rely on the
#    right value of the XDG_CURRENT_DESKTOP variable within the login session,
#    therefore the script will ensure that it is always set to `sway`.
#
# 3. GUI applications started as a systemd service (or via xdg-autostart-generator)
#    may rely on the XDG_SESSION_TYPE variable to select the backend.
#    Ensure that it is always set to `wayland`.
#
# 4. The common way to autostart a systemd service along with the desktop
#    environment is to add it to a `graphical-session.target`. However, systemd
#    forbids starting the graphical session target directly and encourages use
#    of an environment-specific target units. Therefore, the integration
#    package here provides and uses `sway-session.target` which would bind to
#    the `graphical-session.target`.
#
# 5. Stop the target and unset the variables when the compositor exits.
#
# References:
#  - https://github.com/swaywm/sway/wiki#gtk-applications-take-20-seconds-to-start
#  - https://github.com/emersion/xdg-desktop-portal-wlr/wiki/systemd-user-services,-pam,-and-environment-variables
#  - https://www.freedesktop.org/software/systemd/man/systemd.special.html#graphical-session.target
#  - https://systemd.io/DESKTOP_ENVIRONMENTS/
#
export XDG_CURRENT_DESKTOP=sway
export XDG_SESSION_DESKTOP="${XDG_SESSION_DESKTOP:-sway}"
export XDG_SESSION_TYPE=wayland
VARIABLES="DESKTOP_SESSION XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE"
VARIABLES="${VARIABLES} DISPLAY I3SOCK SWAYSOCK WAYLAND_DISPLAY"
VARIABLES="${VARIABLES} XCURSOR_THEME XCURSOR_SIZE"
SESSION_TARGET="sway-session.target"
SESSION_SHUTDOWN_TARGET="sway-session-shutdown.target"
WITH_CLEANUP=1

print_usage() {
    cat <<EOH
Usage:
  --help            Show this help message and exit.
  --add-env NAME, -E NAME
                    Add a variable name to the subset of environment passed
                    to the user session. Can be specified multiple times.
  --no-cleanup      Skip cleanup code at compositor exit.
EOH
}

while [ $# -gt 0 ]; do
    case "$1" in
    --help)
        print_usage
        exit 0 ;;
    # The following flag is intentionally not exposed in the usage info:
    #  - I don't believe that's the right or safe thing to do;
    #  - systemd upstream is of the same opinion and has already deprecated
    #    the ability to import the full environment (systemd/systemd#18137)
    --all-environment)
        VARIABLES="" ;;
    --add-env=?*)
        VARIABLES="${VARIABLES} ${1#*=}" ;;
    --add-env | -E)
        shift
        VARIABLES="${VARIABLES} ${1}" ;;
    --with-cleanup)
        ;; # ignore (enabled by default)
    --no-cleanup)
        unset WITH_CLEANUP ;;
    -*)
        echo "Unexpected option: $1" 1>&2
        print_usage
        exit 1 ;;
    *)
        break ;;
    esac
    shift
done

# Check if another Sway session is already active.
#
# Ignores all other kinds of parallel or nested sessions
# (Sway on Gnome/KDE/X11/etc.), as the only way to detect these is to check
# for (WAYLAND_)?DISPLAY and that is know to be broken on Arch.
if systemctl --user -q is-active "$SESSION_TARGET"; then
    echo "Another session found; refusing to overwrite the variables"
    exit 1
fi

# DBus activation environment is independent from systemd. While most of
# dbus-activated services are already using `SystemdService` directive, some
# still don't and thus we should set the dbus environment with a separate
# command.
if hash dbus-update-activation-environment 2>/dev/null; then
    # shellcheck disable=SC2086
    dbus-update-activation-environment --systemd ${VARIABLES:- --all}
fi

# reset failed state of all user units
systemctl --user reset-failed

# shellcheck disable=SC2086
systemctl --user import-environment $VARIABLES
systemctl --user start "$SESSION_TARGET"

# Optionally, wait until the compositor exits and cleanup variables and services.
if [ -z "$WITH_CLEANUP" ] ||
    [ -z "$SWAYSOCK" ] ||
    ! hash swaymsg 2>/dev/null
then
    exit 0;
fi

# declare cleanup handler and run it on script termination via kill or Ctrl-C
session_cleanup () {
    # stop the session target and unset the variables
    systemctl --user start --job-mode=replace-irreversibly "$SESSION_SHUTDOWN_TARGET"
    if [ -n "$VARIABLES" ]; then
        # shellcheck disable=SC2086
        systemctl --user unset-environment $VARIABLES
    fi
}
trap session_cleanup INT TERM
# wait until the compositor exits
swaymsg -t subscribe '["shutdown"]'
# run cleanup handler on normal exit
session_cleanup


================================================
FILE: src/wait-sni-ready
================================================
#!/usr/bin/python3
"""
A simple script for waiting until an org.kde.StatusNotifierItem host implementation
is available and ready.

Dependencies: dbus-fast, tenacity
"""
import asyncio
import logging
import os

from dbus_fast.aio import MessageBus
from tenacity import retry, stop_after_delay, wait_fixed

LOG = logging.getLogger("wait-sni-host")
TIMEOUT = int(os.environ.get("SNI_WAIT_TIMEOUT", default=25))


@retry(reraise=True, stop=stop_after_delay(TIMEOUT), wait=wait_fixed(0.5))
async def get_service(bus, name, object_path, interface_name):
    """Wait until the service appears on the bus"""
    introspection = await bus.introspect(name, object_path)
    proxy = bus.get_proxy_object(name, object_path, introspection)
    return proxy.get_interface(interface_name)


async def wait_sni_host(bus: MessageBus):
    """Wait until a StatusNotifierWatcher service is available and has a
    StatusNotifierHost instance"""
    future = asyncio.get_event_loop().create_future()

    async def on_host_registered():
        value = await sni_watcher.get_is_status_notifier_host_registered()
        LOG.debug("StatusNotifierHostRegistered: %s", value)
        if value:
            future.set_result(value)

    sni_watcher = await get_service(bus, "org.kde.StatusNotifierWatcher",
                                    "/StatusNotifierWatcher",
                                    "org.kde.StatusNotifierWatcher")
    sni_watcher.on_status_notifier_host_registered(on_host_registered)
    # fetch initial value
    await on_host_registered()
    return await asyncio.wait_for(future, timeout=TIMEOUT)


async def main():
    """asyncio entrypoint"""
    bus = await MessageBus().connect()
    try:
        await wait_sni_host(bus)
        LOG.info("Successfully waited for org.kde.StatusNotifierHost")
    # pylint: disable=broad-except
    except Exception as err:
        LOG.error("Failed to wait for org.kde.StatusNotifierHost: %s",
                  str(err))


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    asyncio.run(main())


================================================
FILE: srpm/rpkg.macros
================================================
#!/bin/bash
# vim ft:sh

function git_tag {
    git describe --tags --abbrev=0 2>/dev/null | head -n 1
}

function git_commit_count {
    local tag=$1
    if [ -n "$tag" ]; then
        git rev-list "$tag"..HEAD --count 2>/dev/null || printf 0
    else
        git rev-list HEAD --count 2>/dev/null || printf 0
    fi
}

function git_version {
    tag="$(git_tag)"
    tag_version="$(echo "$tag" | sed -E -n "s/^v?([^-]+)/\1/p")"
    if [ -z "$tag_version" ]; then
        tag_version=0
    fi
    commit_count="$(git_commit_count "$tag")"
    if [ "$commit_count" -eq 0 ]; then
        output "$tag_version"
    else
        shortcommit="$(git rev-parse --short HEAD)"
        output "$tag_version^${commit_count}.git${shortcommit}"
    fi
}

function git_release {
    output "1"
}

function git_dir_release {
    git_release "$@"
}


================================================
FILE: srpm/sway-systemd.spec.rpkg
================================================
# vim: ft=spec
%global srcname {{{ git_name }}}

Name:           {{{ git_name append="-git" }}}
Version:        {{{ git_version }}}
Release:        {{{ git_release }}}%{?dist}
Summary:        Systemd integration for Sway session

License:        MIT
URL:            https://github.com/alebastr/sway-systemd
Source0:        {{{ git_pack path=$(git rev-parse --show-toplevel) }}}

BuildArch:      noarch

BuildRequires:  meson
BuildRequires:  pkgconfig(systemd)
BuildRequires:  systemd-rpm-macros

Conflicts:      %{srcname}

Requires:       python3dist(dbus-fast)
Requires:       python3dist(i3ipc)
Requires:       python3dist(psutil)
Requires:       python3dist(python-xlib)
Requires:       python3dist(tenacity)
Requires:       sway
Requires:       systemd
Recommends:     /usr/bin/dbus-update-activation-environment

%description
%{summary}.

The goal of this project is to provide a minimal set of configuration files
and scripts required for running Sway in a systemd environment.

This includes several areas of integration:
 - Propagate required variables to the systemd user session environment.
 - Define sway-session.target for starting user services.
 - Place GUI applications into a systemd scopes for systemd-oomd compatibility.

%prep
{{{ git_setup_macro path=$(git rev-parse --show-toplevel) }}}


%build
%meson \
    -Dautoload-configs='cgroups'
%meson_build


%install
%meson_install


%files
%license LICENSE
%doc README.md
%config(noreplace) %{_sysconfdir}/sway/config.d/10-systemd-session.conf
%config(noreplace) %{_sysconfdir}/sway/config.d/10-systemd-cgroups.conf
%{_datadir}/%{srcname}/*.conf
%dir %{_libexecdir}/%{srcname}
%{_libexecdir}/%{srcname}/assign-cgroups.py
%{_libexecdir}/%{srcname}/locale1-xkb-config
%{_libexecdir}/%{srcname}/session.sh
%{_libexecdir}/%{srcname}/wait-sni-ready
%{_userunitdir}/sway-session.target
%{_userunitdir}/sway-session-shutdown.target
%{_userunitdir}/sway-xdg-autostart.target


%changelog
{{{ git_changelog }}}


================================================
FILE: units/sway-session-shutdown.target
================================================
[Unit]
Description=Shutdown running Sway session
DefaultDependencies=no
StopWhenUnneeded=true

Conflicts=graphical-session.target graphical-session-pre.target
After=graphical-session.target graphical-session-pre.target

Conflicts=sway-session.target
After=sway-session.target


================================================
FILE: units/sway-session.target
================================================
[Unit]
Description=Sway session
Documentation=man:systemd.special(7)
BindsTo=graphical-session.target
Wants=graphical-session-pre.target
After=graphical-session-pre.target


================================================
FILE: units/sway-xdg-autostart.target
================================================
# Systemd provides xdg-desktop-autostart.target as a way to process XDG autostart
# desktop files. The target sets RefuseManualStart though, and thus cannot be
# used from scripts.
#
[Unit]
Description=XDG autostart for Sway session
Documentation=man:systemd.special(7) man:systemd-xdg-autostart-generator(8)
Documentation=https://github.com/alebastr/sway-systemd
BindsTo=xdg-desktop-autostart.target
PartOf=sway-session.target
After=sway-session.target
Download .txt
gitextract_u67umdq4/

├── 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
Download .txt
SYMBOL INDEX (13 symbols across 1 files)

FILE: src/assign-cgroups.py
  function escape_app_id (line 52) | def escape_app_id(app_id: str) -> str:
  function get_cgroup (line 73) | def get_cgroup(pid: int) -> Optional[str]:
  function get_pid_by_socket (line 87) | def get_pid_by_socket(sockpath: str) -> int:
  function create_x11_pid_getter (line 107) | def create_x11_pid_getter() -> Callable[[int], int]:
  class CGroupHandler (line 167) | class CGroupHandler:
    method __init__ (line 170) | def __init__(self, bus: MessageBus, conn: Connection):
    method get_x11_window_pid (line 176) | def get_x11_window_pid(self) -> Optional[Callable[[int], int]]:
    method connect (line 185) | async def connect(self):
    method get_pid (line 202) | def get_pid(self, con: Con) -> Optional[int]:
    method cgroup_change_needed (line 212) | def cgroup_change_needed(self, cgroup: Optional[str]) -> bool:
    method assign_scope (line 226) | async def assign_scope(self, app_id: str, proc: Process):
    method _on_new_window (line 254) | async def _on_new_window(self, _: Connection, event: Event):
  function main (line 276) | async def main():
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (45K chars).
[
  {
    "path": "LICENSE",
    "chars": 1072,
    "preview": "MIT License\n\nCopyright (c) 2021 Aleksei Bavshin\n\nPermission is hereby granted, free of charge, to any person obtaining a"
  },
  {
    "path": "README.md",
    "chars": 9546,
    "preview": "# Systemd integration for Sway\n\n## Goals and requirements\n\nThe goal of this project is to provide a minimal set of confi"
  },
  {
    "path": "config.d/10-systemd-cgroups.conf.in",
    "chars": 558,
    "preview": "# Automatically assign a dedicated systemd scope to the GUI applications\n# launched in the same cgroup as the compositor"
  },
  {
    "path": "config.d/10-systemd-session.conf.in",
    "chars": 1765,
    "preview": "# Address several issues with DBus activation and systemd user sessions\n#\n# 1. DBus-activated and systemd services do no"
  },
  {
    "path": "config.d/95-system-keyboard-config.conf.in",
    "chars": 375,
    "preview": "# Apply system-wide XKB configuration stored in systemd-localed.\n#\n# The configuration can be viewed with `localectl` an"
  },
  {
    "path": "config.d/95-xdg-desktop-autostart.conf.in",
    "chars": 606,
    "preview": "# Wait until a StatusNotifierItem tray implementation is available and\n# process XDG autostart entries.\n#\n# This horror "
  },
  {
    "path": "meson.build",
    "chars": 1459,
    "preview": "project('sway-systemd', [],\n  meson_version: '>= 0.51',\n  license: 'MIT',\n)\n\nenabled = get_option('autoload-configs')\nco"
  },
  {
    "path": "meson_options.txt",
    "chars": 245,
    "preview": "option('autoload-configs', type: 'array',\n       choices: ['all', 'autostart', 'cgroups', 'locale1'], value: [],\n       "
  },
  {
    "path": "rpkg.conf",
    "chars": 58,
    "preview": "[rpkg]\nuser_macros = \"${git_props:root}/srpm/rpkg.macros\"\n"
  },
  {
    "path": "setup.cfg",
    "chars": 383,
    "preview": "[flake8]\nmax-line-length = 88\nextend-ignore = E203, W292, W503\n\n[mypy]\n# Too many libraries without type hints :'(\nignor"
  },
  {
    "path": "src/assign-cgroups.py",
    "chars": 10577,
    "preview": "#!/usr/bin/python3\n\"\"\"\nAutomatically assign a dedicated systemd scope to the GUI applications\nlaunched in the same cgrou"
  },
  {
    "path": "src/locale1-xkb-config",
    "chars": 4977,
    "preview": "#!/usr/bin/python3\n\"\"\"\nSync Sway input configuration with org.freedesktop.locale1.\n\nUsage:\n    Configure keyboard mappin"
  },
  {
    "path": "src/session.sh",
    "chars": 5376,
    "preview": "#!/bin/sh\n#\n# Address several issues with DBus activation and systemd user sessions\n#\n# 1. DBus-activated and systemd se"
  },
  {
    "path": "src/wait-sni-ready",
    "chars": 2063,
    "preview": "#!/usr/bin/python3\n\"\"\"\nA simple script for waiting until an org.kde.StatusNotifierItem host implementation\nis available "
  },
  {
    "path": "srpm/rpkg.macros",
    "chars": 835,
    "preview": "#!/bin/bash\n# vim ft:sh\n\nfunction git_tag {\n    git describe --tags --abbrev=0 2>/dev/null | head -n 1\n}\n\nfunction git_c"
  },
  {
    "path": "srpm/sway-systemd.spec.rpkg",
    "chars": 1971,
    "preview": "# vim: ft=spec\n%global srcname {{{ git_name }}}\n\nName:           {{{ git_name append=\"-git\" }}}\nVersion:        {{{ git_"
  },
  {
    "path": "units/sway-session-shutdown.target",
    "chars": 276,
    "preview": "[Unit]\nDescription=Shutdown running Sway session\nDefaultDependencies=no\nStopWhenUnneeded=true\n\nConflicts=graphical-sessi"
  },
  {
    "path": "units/sway-session.target",
    "chars": 172,
    "preview": "[Unit]\nDescription=Sway session\nDocumentation=man:systemd.special(7)\nBindsTo=graphical-session.target\nWants=graphical-se"
  },
  {
    "path": "units/sway-xdg-autostart.target",
    "chars": 454,
    "preview": "# Systemd provides xdg-desktop-autostart.target as a way to process XDG autostart\n# desktop files. The target sets Refus"
  }
]

About this extraction

This page contains the full source code of the alebastr/sway-systemd GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (41.8 KB), approximately 10.8k tokens, and a symbol index with 13 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!