JustToThePoint English Website Version
JustToThePoint en español

Robust App Launching: Solving Rofi Process Detachment

If you find yourself in a hole, stop digging, Will Rogers.

Topology and Limits

Introduction

Launching graphical applications from a menu like Rofi seems trivial —just add an ampersand & and move on, right? In reality, process management in a tiling window manager session is fraught with edge cases. A naive & keeps the child process in the same process group as the launcher, often causing X11 to hang onto the keyboard grab long after the menu disappears, or causing the app to terminate unexpectedly if the script exits. Conversely, tools like nohup solve the persistence problem but often strip essential environment variables like DBUS_SESSION_BUS_ADDRESS, breaking the application’s integration with the desktop session.

In this article, we introduce myrofi_launch.py, a robust Python utility designed to replace fragile Bash backgrounding helpers. By leveraging subprocess.Popen with start_new_session=True, we create a clean process group for every application, allowing the kernel to immediately re-parent the app to systemd (PID 1). This ensures that applications like Google Chrome or VS Code launch completely independently, preserving their full environment and releasing the input focus instantly.

#!/usr/bin/env python3
"""
myrofi_launch.py — Drop-in replacement for bash's _launch() helper
───────────────────────────────────────────────────────────────────
Launches any app fully detached, exactly like  `( cmd & )`  in bash.

USAGE FROM rofi-launcher.sh:
    Replace every  _launch  [args]  call with:
        python3 "$HOME/dotfiles/scripts/myrofi_launch.py"  [args]

    Examples — direct replacements:
        # bash                              # python
        _launch pcmanfm                 →   python3 .../myrofi_launch.py pcmanfm
        _launch keepassxc               →   python3 .../myrofi_launch.py keepassxc
        _launch google-chrome-stable    →   python3 .../myrofi_launch.py google-chrome-stable
        _launch code                    →   python3 .../myrofi_launch.py code
        _launch rofi -show drun         →   python3 .../myrofi_launch.py rofi -show drun

    Or import and call from another Python script:
        from myrofi_launch import launch
        launch(["pcmanfm"])
        launch(["google-chrome-stable", "--app=https://mail.google.com"])

WHY NOT just use  &  or  nohup  in bash:
    - Plain `cmd &` keeps the child in the same process group as rofi.
      When rofi exits, X11 may not release its keyboard grab cleanly
      because the child is still in the same session.
    - nohup strips environment variables that GUI apps need to connect to the session bus.
    - subprocess.Popen with start_new_session=True forks cleanly AND
      preserves the full environment. Best of both worlds.

HOW DETACHMENT WORKS (same as  ( cmd & )  in bash):
    1. Popen() forks the child and returns immediately — no waiting.
    2. close_fds=True  → child does not inherit our file descriptors.
    3. start_new_session=True  → child gets its own process group (setsid).
       When this script exits, the kernel re-parents the child to PID 1
       (systemd). The child is now fully independent.
    4. stdout/stderr → /dev/null  → child output does not pollute the journal.
    5. The parent (this script) exits with code 0 immediately after forking.
       Rofi sees a clean exit and releases the X11 keyboard grab.
"""

import subprocess
import sys
import os


def launch(command: list[str]) -> None:
    """
    Fork a process and return immediately, leaving it fully detached.

    Args:
        command: Command as a list of strings. Never use shell=True —
                 a list avoids all quoting and injection issues.
                 Example: ["google-chrome-stable", "--app=https://mail.google.com"]

    Raises:
        FileNotFoundError: if the binary is not found on PATH.
                           The error is caught in main() and printed to stderr.
    """
    subprocess.Popen(
        command,
        stdout=subprocess.DEVNULL,   # suppress child stdout → not in journal
        stderr=subprocess.DEVNULL,   # suppress child stderr → not in journal
        close_fds=True,              # don't leak our file descriptors to child
        start_new_session=True,      # new session: child survives our exit,
                                     # kernel re-parents it to PID 1 (systemd)
    )
    # Popen() returns immediately — we do NOT call .wait() or .communicate().
    # The child is now running independently in the background.


def main() -> None:
    """
    Entry point when called from the command line / rofi-launcher.sh.

    sys.argv[1:]  is everything after the script name, which becomes
    the command list passed to launch().

    Example:
        python3 myrofi_launch.py google-chrome-stable --app=https://mail.google.com
        → launch(["google-chrome-stable", "--app=https://mail.google.com"])
    """
    if len(sys.argv) < 2:
        print(
            "Usage: myrofi_launch.py  [args...]\n"
            "Example: myrofi_launch.py google-chrome-stable\n"
            "Example: myrofi_launch.py rofi -show drun",
            file=sys.stderr,
        )
        sys.exit(1)

    command = sys.argv[1:]   # ["google-chrome-stable"] or ["rofi", "-show", "drun"]

    try:
        launch(command)
    except FileNotFoundError:
        print(
            f"myrofi_launch: command not found: {command[0]!r}\n"
            f"Check it is installed and on $PATH with:  which {command[0]}",
            file=sys.stderr,
        )
        sys.exit(127)   # 127 = standard shell "command not found" exit code


if __name__ == "__main__":
    main()
Bitcoin donation

JustToThePoint Copyright © 2011 - 2026 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.