#!/usr/bin/python3 """ Launch a Wayland client with an optional tag using Mutter's ServiceChannel. This script connects to the Mutter ServiceChannel D-Bus service to create a Wayland connection and runs the specified command on it. """ import argparse import sys import re import subprocess from contextlib import contextmanager from os import environ from typing import List, Iterator, Optional import dbus import dbus.exceptions # Service channel constants SERVICE_NAME = "org.gnome.Mutter.ServiceChannel" SERVICE_INTERFACE = "org.gnome.Mutter.ServiceChannel" SERVICE_OBJECT_PATH = "/org/gnome/Mutter/ServiceChannel" # Exit codes EXIT_SUCCESS = 0 EXIT_FAILURE = 1 EXIT_DBUS_ERROR = 2 EXIT_MISSING_ARGS = 3 def get_service_channel() -> tuple[dbus.Interface, dbus.Bus]: """ Get the Mutter ServiceChannel D-Bus interface and own the sender name. Returns: Tuple of (D-Bus interface for the service channel, D-Bus bus) Raises: SystemExit: If the service is not available """ try: bus = dbus.SessionBus() service_channel = bus.get_object(SERVICE_NAME, SERVICE_OBJECT_PATH) return dbus.Interface(service_channel, dbus_interface=SERVICE_INTERFACE), bus except dbus.exceptions.DBusException as error: print(f"Error: Unable to connect to Mutter ServiceChannel: {error}", file=sys.stderr) print("Make sure you're running under a compatible Wayland compositor " "(GNOME Shell, GNOME Kiosk, etc.)", file=sys.stderr) sys.exit(EXIT_DBUS_ERROR) def get_wayland_connection_fd(service_channel: dbus.Interface, tag: Optional[str]) -> dbus.types.UnixFd: """ Request a new Wayland connection file descriptor with an optional window tag. Args: service_channel: The D-Bus service channel interface tag: The optional tag to associate with the Wayland client Returns: File descriptor for the Wayland connection Raises: SystemExit: If the service call fails """ try: options = {} if tag: options['window-tag'] = tag fd = service_channel.OpenWaylandConnection(options) return fd except dbus.exceptions.DBusException as error: print(f"Error: Failed to create Wayland connection: {error}", file=sys.stderr) sys.exit(EXIT_DBUS_ERROR) @contextmanager def wayland_socket_fd(fd: dbus.types.UnixFd) -> Iterator[int]: """ Context manager for handling the Wayland socket file descriptor. Args: fd: The Unix file descriptor from D-Bus Yields: The file descriptor number as an integer """ fd_num = fd.take() try: yield fd_num finally: try: import os os.close(fd_num) except OSError: # fd might already be closed by subprocess pass def run_command_with_wayland_socket(command: List[str], fd_num: int) -> int: """ Run the specified command with the Wayland socket. Args: command: Command and arguments to execute fd_num: File descriptor number for the Wayland socket Returns: The exit code from the executed command """ # Set up environment for the subprocess env = environ.copy() env["WAYLAND_SOCKET"] = str(fd_num) try: with subprocess.Popen(command, env=env, pass_fds=[fd_num]) as proc: return proc.wait() except FileNotFoundError: print(f"Error: Command not found: {command[0]}", file=sys.stderr) return EXIT_FAILURE except PermissionError: print(f"Error: Permission denied executing: {command[0]}", file=sys.stderr) return EXIT_FAILURE except KeyboardInterrupt: # Handle Ctrl+C gracefully print("\nInterrupted by user", file=sys.stderr) return EXIT_SUCCESS except Exception as error: print(f"Error: Failed to execute command: {error}", file=sys.stderr) return EXIT_FAILURE def parse_arguments() -> argparse.Namespace: """ Parse command line arguments. Returns: Parsed arguments namespace """ parser = argparse.ArgumentParser( description='Launch a Wayland client with an optional tag', epilog='Example: %(prog)s -t demo -- gnome-tour\n' ' %(prog)s -- gnome-calculator' ) parser.add_argument( '-t', '--tag', required=False, help='Optional tag to associate with the Wayland client' ) parser.add_argument( 'command', nargs='+', help='Command to run and its arguments' ) return parser.parse_args() def main() -> None: """Main entry point.""" try: args = parse_arguments() except SystemExit as e: # argparse handles --help and invalid args sys.exit(e.code) # Validate arguments if args.tag is not None and not args.tag.strip(): print("Error: Tag cannot be empty", file=sys.stderr) sys.exit(EXIT_MISSING_ARGS) if not args.command: print("Error: No command specified", file=sys.stderr) sys.exit(EXIT_MISSING_ARGS) # Connect to service and run command service_channel, bus = get_service_channel() try: wayland_fd = get_wayland_connection_fd(service_channel, args.tag) with wayland_socket_fd(wayland_fd) as fd_num: exit_code = run_command_with_wayland_socket(args.command, fd_num) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(EXIT_DBUS_ERROR) sys.exit(exit_code) if __name__ == '__main__': main()