Life is a math equation. In order to gain the most, you have to know how to convert negatives into positives, Anonymous

In the pursuit of a seamless desktop workflow, launching terminal windows to perform specific tasks —like opening an SSH session, running a script on a remote server, or simply moving to a specific workspace —often requires complex Bash workarounds. These scripts can become brittle, struggling with shell quoting, background process management, and keeping the window open after a command completes.
Enter myrofi_terminal.py, a robust Python utility designed to replace these fragile Bash patterns. This script introduces a unified launch_terminal function that intelligently handles three common scenarios: plain interactive terminals, commands that require the window to stay open (via a . By leveraging Python’s subprocess and tempfile modules, it solves the pain of command escaping and ensures proper process detachment, while also integrating with the Niri compositor to switch workspaces before the terminal appears.
#!/usr/bin/env python3
"""
myrofi_terminal.py — Terminal launcher for rofi scripts
────────────────────────────────────────────────────────
Provides launch_terminal(command) which replicates three bash patterns:
PATTERN A — plain detached terminal (no command):
_goto_workspace
sleep 0.1
alacritty &
PATTERN B — terminal that runs a command and waits (run-in-term):
tmpscript=$(mktemp)
printf '%s' "$cmd" > "$tmpscript"
printf 'read -rp "Press Enter to close..."' >> "$tmpscript"
alacritty --title "..." -e bash "$tmpscript" &
PATTERN C — terminal running an SSH session that stays open :
alacritty --title "SSH · user@host" -e bash -c "ssh -X user@host; exec bash" &
All three collapse into one function:
launch_terminal() # plain terminal
launch_terminal("ssh -X user@host") # SSH session, keeps shell after
launch_terminal("python script.py", # command + wait-for-enter footer
title="My Script",
wait=True)
Usage from rofi-launcher.sh:
# Plain terminal
9) python3 ~/dotfiles/scripts/myrofi_terminal.py ;;
# SSH session
17) python3 ~/dotfiles/scripts/myrofi_terminal.py --ssh ;;
# Run a remote script and wait
18) python3 ~/dotfiles/scripts/myrofi_terminal.py --check-links ;;
Or import from another Python rofi script:
from myrofi_terminal import launch_terminal
launch_terminal("python greetings.py", title="Greetings", wait=True)
"""
import subprocess
import tempfile
import time
import os
import sys
import stat
# ── Configuration ─────────────────────────────────────────────────────────────
TERMINAL = "alacritty"
TERMINAL_FONT = "Hack"
TERMINAL_SIZE = 14 # font size in points
USER = "nmaximo7"
SERVER = "192.168.1.49"
PYTHON_DIR = "/home/nmaximo7/myNewPython"
PYTHON_LOCAL = "/mnt/data/justtothepoint/mypython"
SCRIPTS_DIR = f"/home/{USER}/dotfiles/myArch/admin"
TERM_WORKSPACE = "1"
WORKSPACE_SETTLE_DELAY = 0.15 # seconds to wait after workspace switch
# ── Workspace helper ──────────────────────────────────────────────────────────
def goto_workspace(workspace: str = TERM_WORKSPACE) -> None:
"""
Switch to a workspace in the Niri compositor.
Niri uses 0‑based indices: workspace "1" → index 0.
If the `niri` command is not available, this does nothing (prints a warning).
"""
workspace = int(workspace) - 1
subprocess.run(
["niri", "msg", "action", "workspace-focus", str(workspace)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False, # Don't crash if niri fails (e.g., not running)
)
# ── Core launcher ─────────────────────────────────────────────────────────────
def launch_terminal(
command: str | None = None,
*,
title: str = "Terminal",
wait: bool = False,
workspace: str = TERM_WORKSPACE,
) -> None:
"""
Open a terminal window, optionally running a shell command inside it.
Args:
command: Shell command string to run inside the terminal.
None → opens an interactive shell (plain alacritty).
String → passed to bash -c "command" inside alacritty.
title: Window title shown in the window title bar.
wait: When True, append a "Press Enter to close..." footer so
the terminal window stays open after the command finishes.
Mirrors the _run_in_term bash helper.
Ignored when command is None (interactive shell never needs it).
workspace: If False, skip workspace switching entirely.
Implementation notes
────────────────────
WHY a temp script file instead of alacritty -e bash -c "...":
Long or multi-line commands passed via -c require careful shell
quoting. Every quote, dollar sign, and backslash must be escaped.
Writing the command to a temp file avoids ALL quoting — the file
content is verbatim, no shell interpretation of the string happens.
This is exactly what _run_in_term does in bash.
WHY start_new_session=True:
alacritty stays in our process group without this. When rofi exits
it holds an X11 keyboard grab; if alacritty is in rofi's process
group, X11 can leave the grab dangling. start_new_session=True calls
os.setsid() in the child before exec — equivalent to bash ( cmd & ).
WHY Popen instead of run:
subprocess.run() blocks until the child exits.
subprocess.Popen() returns immediately — the terminal runs
independently. This mirrors bash's & background operator.
WHY the temp file is NOT deleted here:
The temp file must outlive this Python script. We exit immediately
after Popen(). The terminal reads the file after we are gone.
We append rm -f "$file" as the last line of the script so the
terminal cleans it up after the user presses Enter to close.
"""
# Step 1 — switch workspace if requested
goto_workspace(workspace)
time.sleep(WORKSPACE_SETTLE_DELAY)
# Step 2 — build the alacritty command list
if command is None:
# ── PATTERN A: plain interactive terminal ─────────────────────────────
# alacritty with no -e flag opens a shell directly.
alacritty_cmd = [
TERMINAL,
"--title", title,
]
else:
# ── PATTERN B / C: run a command, optionally wait for keypress ────────
#
# Write the command (and optional footer) to a temp file.
# NamedTemporaryFile with delete=False keeps the file on disk after
# we close it — Popen will read it after this script exits.
#
tmp = tempfile.NamedTemporaryFile(
mode="w",
prefix="rofi-term-",
suffix=".sh",
delete=False, # ← do NOT delete when we close() it
)
# Write shebang + the actual command
tmp.write("#!/usr/bin/env bash\n")
tmp.write(f"{command}\n")
if wait:
# Append a visual separator and a blocking read so the window
# stays open after the command finishes — user can read output.
tmp.write('\necho\necho "─── done ───"\n')
tmp.write('read -rp "Press Enter to close..."\n')
# Self-destruct: the script deletes itself as its very last action.
tmp.write(f'rm -f "{tmp.name}"\n')
tmp.close()
# Make the temp file executable so bash can run it directly
os.chmod(tmp.name, os.stat(tmp.name).st_mode | stat.S_IEXEC)
alacritty_cmd = [
TERMINAL,
"--title", title,
"-e", "bash", tmp.name, # alacritty runs: bash /tmp/rofi-term-XXXX.sh
]
# Step 3 — spawn the terminal, fully detached from our process group
subprocess.Popen(
alacritty_cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True, # os.setsid() in child — releases X11 grab
)
# Popen returns immediately. Terminal runs independently.
# This Python script exits now, rofi's grab is released cleanly.
# ── Pre-built actions ─────────────────────────────────────────────────────────
# Each function composes the right launch_terminal() call for a specific task.
# These are what rofi-launcher.sh calls via python3 myrofi_terminal.py --flag.
def open_terminal() -> None:
"""Plain interactive Alacritty on the terminal workspace."""
launch_terminal(
command=None,
title="Terminal",
)
def open_ssh() -> None:
"""
SSH session to SERVER.
Mirrors the bash pattern:
alacritty --title "SSH · user@host" \
-e bash -c "ssh -X user@host; exec bash" &
WHY ssh ... ; exec bash:
After the SSH connection closes (logout or network drop), the
terminal would immediately close too — no time to read the last
output. The trailing ; exec bash replaces the ssh process with
an interactive bash shell, keeping the window open.
"""
launch_terminal(
command=f"ssh -X {USER}@{SERVER}; exec bash",
title=f"SSH · {USER}@{SERVER}",
wait=False, # exec bash keeps the window open itself
)
def _remote_python(script_name: str, title: str) -> None:
"""
Helper: SSH to SERVER, activate a nix environment, run a Python script, then wait.
Mirrors _run_in_term with an SSH + nix-shell command.
Args:
script_name: Filename of the Python script in PYTHON_DIR on the server.
title: Window title.
"""
# The command is a single bash string that:
# 1. SSHes to the server
# 2. Changes directory and uses nix-shell to execute the python script.
# Using ssh -t forces a pseudo-TTY so colours and interactive output work.
command = (
f"ssh -t {USER}@{SERVER} "
f"'cd {PYTHON_DIR} && nix-shell && python {script_name}'"
# That works for Arch
# f"'cd {PYTHON_DIR} && source venv/bin/activate && python {script_name}'"
)
launch_terminal(command=command, title=title, wait=True)
def _local_python(script_name: str, title: str) -> None:
"""
Helper: Activate a local venv, run a Python script, then wait.
Args:
script_name: Filename of the Python script in PYTHON_DIR on the server.
title: Window title.
"""
# The command is a single bash string that runs a here-string as a remote shell command (cd + venv + python)
command = (
f"cd {PYTHON_LOCAL} && source venv/bin/activate && python3 {script_name}"
)
launch_terminal(command=command, title=title, wait=True)
def check_links() -> None:
"""Run link_checker.py locally and keep window open to read results."""
_local_python("link_checker.py", f"Check Links · {SERVER}")
def greetings() -> None:
"""Run greetings.py on the server."""
_remote_python("greetings.py", f"Greetings · {SERVER}")
def deploy() -> None:
"""
Run deploy.sh locally, keep window open.
Mirrors:
_run_in_term "Deploy" "sh deploy.sh"
"""
command = (
f"sh {SCRIPTS_DIR}/deploy.sh"
)
launch_terminal(command=command, title="Deploy", wait=True)
# ── CLI entry point ───────────────────────────────────────────────────────────
#
# rofi-launcher.sh calls this script with a --flag argument:
#
# 9) python3 ~/dotfiles/scripts/myrofi_terminal.py
# 17) python3 ~/dotfiles/scripts/myrofi_terminal.py --ssh
# 18) python3 ~/dotfiles/scripts/myrofi_terminal.py --check-links
# 20) python3 ~/dotfiles/scripts/myrofi_terminal.py --greetings
# 21) python3 ~/dotfiles/scripts/myrofi_terminal.py --deploy
#
# Mapping from flag → function:
ACTIONS = {
"--ssh": open_ssh,
"--check-links": check_links,
"--greetings": greetings,
"--deploy": deploy,
}
def main() -> None:
flag = sys.argv[1] if len(sys.argv) > 1 else None
if flag is None:
# No argument → plain terminal
open_terminal()
elif flag in ACTIONS:
ACTIONS[flag]()
else:
print(f"myrofi_terminal: unknown flag: {flag}", file=sys.stderr)
print(f"Valid flags: {', '.join(ACTIONS)}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()