The real problem of humanity is the following: We have Paleolithic emotions, medieval institutions and godlike technology. And it is terrifically dangerous, and it is now approaching a point of crisis overall, Edward O. Wilson.

System power management is a critical, yet often disjointed, aspect of the desktop workflow. Moving between terminal commands to shut down the system or relying on compositor-specific shortcuts to log out can break the immersion of a graphical session. In this guide, we introduce myrofi_power.py, a Python utility designed to unify these controls into a single, elegant interface.
By integrating with Rofi, this script provides a consistent, keyboard-driven menu for system actions. It bridges the gap between low-level systemctl commands for hardware power states and the Niri compositor’s messaging API for session termination. This approach ensures that your power options are always accessible, visually consistent, and safely executed without the need for confirmation dialogs.
#!/usr/bin/env python3
"""
myrofi_power.py — Power management menu for rofi
─────────────────────────────────────────────────
DESCRIPTION
This script provides a clean, keyboard‑driven power menu (shutdown, reboot, log out) using rofi’s dmenu mode.
It is designed to be called from a launcher script (e.g., rofi-launcher.sh) and presents three options.
Selection is immediate – no confirmation dialog.
Usage (from rofi-launcher.sh):
python /path/to/myrofi_power.py
Requirements:
- rofi must be installed and on $PATH
- systemctl must be available (standard on systemd‑based distributions, including NixOS and Arch)
- DISPLAY or WAYLAND_DISPLAY must be set (runs inside X11/i3 session)
"""
import subprocess
import os
import sys
# ── Configuration ─────────────────────────────────────────────────────────────
# Path to the rofi theme file – must match the theme used in rofi-launcher.sh
# so the power menu looks consistent with the rest of the launcher.
ROFI_THEME = os.path.expanduser("~/dotfiles/myArch/config/rofi/rounded-dark")
# Font used in the rofi menu (size and family). Change to your preferred font.
ROFI_FONT = "hack 20"
# The three power options. The order defines the display order in rofi
# (top to bottom). No “Cancel” entry is needed – pressing Escape exits.
MENU_OPTIONS = [
" ⏻ Shutdown",
" 🗘 Reboot",
" ⏏ Log Out",
]
# Map each menu label to the system command that executes it.
# systemctl poweroff/reboot perform a clean systemd shutdown/restart
# Log Out — compositor-specific:
# Niri: niri msg action quit
# i3: i3-msg exit
# Hyprland: hyprctl dispatch exit
# GNOME: gnome-session-quit --no-prompt
#
# We use "niri msg action quit" here because this script runs on Arch/Niri.
ACTION_MAP = {
" ⏻ Shutdown": ["systemctl", "poweroff"],
" 🗘 Reboot": ["systemctl", "reboot"],
" ⏏ Log Out": ["niri", "msg", "action", "quit"],
}
# ── Menu display ──────────────────────────────────────────────────────────────
def show_menu(options: list[str]) -> str | None:
"""
Display the power options in a rofi dmenu and return the selected string.
Args:
options: List of strings to present as selectable entries.
Returns:
The exact string the user selected (matches a key in ACTION_MAP),
or None if the user pressed Escape or rofi exited with no selection.
rofi flags explained:
-dmenu read options from stdin, output selection to stdout
-i case-insensitive filtering (user can type partial names).
-p prompt text shown on the left
-no-custom prevent typing arbitrary text that isn't in the list
-lines N fix the visible height to exactly N lines
-theme / -font apply custom theme and font for visual consistency.
"""
menu_input = "\n".join(options)
result = subprocess.run(
[
"rofi",
"-dmenu",
"-i",
"-p", " Power",
"-no-custom",
"-lines", str(len(options)),
"-theme", ROFI_THEME,
"-font", ROFI_FONT,
],
input=menu_input, # feed options via stdin
capture_output=True, # capture stdout (the selection)
text=True, # Decode stdout/stderr as text (not bytes)
)
# rofi returns exit code 1 when the user presses Escape (no selection).
# Any other non-zero exit is unexpected — we treat it the same: do nothing.
if result.returncode != 0:
return None
# Strip trailing newline that rofi appends to its stdout output.
selected = result.stdout.strip()
return selected if selected else None
# ── Action execution ──────────────────────────────────────────────────────────
def execute_action(selected: str) -> None:
"""
Execute the system command corresponding to the selected menu label.
Args:
selected: The exact string returned by rofi
subprocess.run() is used instead of os.system() because:
- It does not invoke a shell.
- The command is passed as a list of strings (no quoting issues).
- It is the modern, recommended stdlib approach.
"""
# Rofi strips leading/trailing whitespace before writing to stdout, so
# " ⏻ Shutdown" comes back as "⏻ Shutdown". However, the keys in
# in ACTION_MAP contain padding for alignment.
# Build a stripped lookup so padding in MENU_OPTIONS never causes misses.
stripped_map = {k.strip(): v for k, v in ACTION_MAP.items()}
command = stripped_map.get(selected.strip())
if command is None:
print(f"myrofi_power: unknown option: {selected!r}", file=sys.stderr)
sys.exit(1)
# Execute the command.
# subprocess.run() with a list — no shell, no quoting issues.
subprocess.run(command, check=True)
# ── Entry point ───────────────────────────────────────────────────────────────
def main() -> None:
"""
Main entry point.
1. Show the rofi menu.
2. If the user selected something, execute the corresponding command.
3. If the user pressed Escape, exit silently with code 0.
"""
selected = show_menu(MENU_OPTIONS)
if selected is None:
# User cancelled — exit cleanly, nothing to do.
sys.exit(0)
execute_action(selected)
if __name__ == "__main__":
main()