|
| 1 | +import os |
| 2 | +import shlex |
| 3 | +import subprocess |
1 | 4 | from typing import TYPE_CHECKING |
2 | 5 |
|
3 | 6 | from fabric.widgets.box import Box |
4 | 7 | from fabric.widgets.button import Button |
5 | | -from gi.repository import GLib |
6 | 8 | from loguru import logger |
7 | 9 |
|
8 | 10 | from mewline.config import cfg |
| 11 | +from mewline.utils.config_structure import PowerMenuCommands |
9 | 12 | from mewline.utils.widget_utils import setup_cursor_hover |
10 | 13 | from mewline.utils.widget_utils import text_icon |
11 | 14 | from mewline.widgets.dynamic_island.base import BaseDiWidget |
@@ -102,27 +105,83 @@ def __init__(self, di: "DynamicIsland", **kwargs): |
102 | 105 | def close_menu(self): |
103 | 106 | self.dynamic_island.close() |
104 | 107 |
|
| 108 | + def _get_commands(self) -> PowerMenuCommands: |
| 109 | + """Return commands for the current desktop environment. |
| 110 | +
|
| 111 | + Looks up ``XDG_CURRENT_DESKTOP`` in ``config.commands`` |
| 112 | + (case-insensitive). Falls back to the first entry in the dict, or |
| 113 | + to built-in defaults when the dict is empty. |
| 114 | + """ |
| 115 | + desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower() |
| 116 | + commands_map = self.config.commands |
| 117 | + |
| 118 | + for key, cmds in commands_map.items(): |
| 119 | + if key.lower() == desktop: |
| 120 | + return cmds |
| 121 | + |
| 122 | + if commands_map: |
| 123 | + first_key = next(iter(commands_map)) |
| 124 | + logger.warning( |
| 125 | + f"[PowerMenu] No commands configured for desktop {desktop!r}, " |
| 126 | + f"falling back to {first_key!r} commands." |
| 127 | + ) |
| 128 | + return commands_map[first_key] |
| 129 | + |
| 130 | + logger.warning( |
| 131 | + f"[PowerMenu] No commands configured for desktop {desktop!r} and " |
| 132 | + "no fallback available - using built-in defaults." |
| 133 | + ) |
| 134 | + return PowerMenuCommands() |
| 135 | + |
| 136 | + @staticmethod |
| 137 | + def _run_command(command: str) -> None: |
| 138 | + """Execute *command* safely without a shell. |
| 139 | +
|
| 140 | + ``$VAR`` / ``${VAR}`` references are expanded via |
| 141 | + :func:`os.path.expandvars` before the string is split with |
| 142 | + :func:`shlex.split`. The resulting argument list is handed directly |
| 143 | + to :class:`subprocess.Popen` (``shell=False``), so no shell injection |
| 144 | + is possible regardless of the command string's content. |
| 145 | + """ |
| 146 | + expanded = os.path.expandvars(command) |
| 147 | + try: |
| 148 | + args = shlex.split(expanded) |
| 149 | + except ValueError as exc: |
| 150 | + logger.error(f"[PowerMenu] Failed to parse command {command!r}: {exc}") |
| 151 | + return |
| 152 | + |
| 153 | + if not args: |
| 154 | + logger.warning(f"[PowerMenu] Empty command after parsing: {command!r}") |
| 155 | + return |
| 156 | + |
| 157 | + try: |
| 158 | + subprocess.Popen(args) |
| 159 | + except FileNotFoundError: |
| 160 | + logger.error(f"[PowerMenu] Command not found: {args[0]!r}") |
| 161 | + except OSError as exc: |
| 162 | + logger.error(f"[PowerMenu] Failed to start {args!r}: {exc}") |
| 163 | + |
105 | 164 | def lock(self, *args): |
106 | | - logger.info("Locking screen...") |
107 | | - GLib.spawn_command_line_async("swaylock") |
| 165 | + logger.info("[PowerMenu] Locking screen...") |
| 166 | + self._run_command(self._get_commands().lock) |
108 | 167 | self.close_menu() |
109 | 168 |
|
110 | 169 | def suspend(self, *args): |
111 | | - logger.info("Suspending screen...") |
112 | | - GLib.spawn_command_line_async("systemctl suspend") |
| 170 | + logger.info("[PowerMenu] Suspending system...") |
| 171 | + self._run_command(self._get_commands().suspend) |
113 | 172 | self.close_menu() |
114 | 173 |
|
115 | 174 | def logout(self, *args): |
116 | | - logger.info("Logging out...") |
117 | | - GLib.spawn_command_line_async("hyprctl dispatch exit") |
| 175 | + logger.info("[PowerMenu] Logging out...") |
| 176 | + self._run_command(self._get_commands().logout) |
118 | 177 | self.close_menu() |
119 | 178 |
|
120 | 179 | def reboot(self, *args): |
121 | | - logger.info("Rebooting system...") |
122 | | - GLib.spawn_command_line_async("systemctl reboot") |
| 180 | + logger.info("[PowerMenu] Rebooting system...") |
| 181 | + self._run_command(self._get_commands().reboot) |
123 | 182 | self.close_menu() |
124 | 183 |
|
125 | 184 | def poweroff(self, *args): |
126 | | - logger.info("Powering off system...") |
127 | | - GLib.spawn_command_line_async("systemctl poweroff") |
| 185 | + logger.info("[PowerMenu] Powering off system...") |
| 186 | + self._run_command(self._get_commands().shutdown) |
128 | 187 | self.close_menu() |
0 commit comments