├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── PotatoWidgets ├── Bash │ └── __init__.py ├── Cli │ ├── __init__.py │ ├── old__main__.py │ └── potatocli ├── Env │ └── __init__.py ├── Imports.py ├── Methods │ └── __init__.py ├── PotatoLoop.py ├── Services │ ├── Applications │ │ └── __init__.py │ ├── Battery │ │ └── __init__.py │ ├── Hyprland │ │ └── __init__.py │ ├── Notifications │ │ └── __init__.py │ ├── Service │ │ └── __init__.py │ ├── Style │ │ └── __init__.py │ ├── Tray │ │ └── __init__.py │ └── __init__.py ├── Variable.py ├── Widget │ ├── Box.py │ ├── Button.py │ ├── CenterBox.py │ ├── CheckBox.py │ ├── ComboBox.py │ ├── Common │ │ ├── BasicProps.py │ │ ├── Events.py │ │ └── __init__.py │ ├── Entry.py │ ├── EventBox.py │ ├── Fixed.py │ ├── FlowBox.py │ ├── Grid.py │ ├── Icon.py │ ├── Image.py │ ├── Label.py │ ├── Menu.py │ ├── Overlay.py │ ├── ProgressBar.py │ ├── Revealer.py │ ├── Scale.py │ ├── Scroll.py │ ├── Separator.py │ ├── Stack.py │ ├── ToggleButton.py │ ├── Window.py │ ├── _Window.py │ └── __init__.py ├── _Logger.py └── __init__.py ├── README.md ├── default_conf ├── __init__.py ├── __main__.py ├── modules │ ├── __init__.py │ ├── bspwm │ │ └── __init__.py │ ├── hyprland │ │ └── __init__.py │ ├── topbar.py │ └── utils │ │ └── __init__.py └── style.scss ├── examples ├── AppLauncher │ ├── AppLauncher.py │ └── AppLauncher.scss ├── BatteryModule │ ├── BatteryModule.py │ └── BatteryModule.scss ├── MediaPlayer │ └── MediaPlayer.py └── NotificationPopups │ ├── NotificationPopups.py │ └── NotificationPopups.scss ├── img ├── Preview.png ├── setup.png ├── setup2.png ├── setup3.png └── setup4.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | /PotatoWidgets.egg-info/ 3 | /build/ 4 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokyob0t/PotatoWidgets/5af1f29294c688fcdd6634827bb73dcf6f6cadf3/CHANGELOG.md -------------------------------------------------------------------------------- /PotatoWidgets/Bash/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility class for shell-related operations using GLib and Gio libraries. 3 | 4 | Methods: 5 | - expandvars: Expand environment variables and user home directory in a given path. 6 | - run: Run a command in the shell. 7 | - get_env: Get the value of an environment variable. 8 | - get_output: Get the output of a command run in the shell. 9 | - popen: Open a subprocess to run a command. 10 | - monitor_file: Monitor changes to a file. 11 | """ 12 | 13 | from .._Logger import Logger 14 | from ..Imports import * 15 | 16 | __all__ = ["Bash"] 17 | 18 | 19 | __monitors__ = [] 20 | 21 | T = TypeVar("T", str, int, dict, float, bool, list, bytes, tuple) 22 | 23 | 24 | class Bash: 25 | """ 26 | Utility class for shell-related operations using GLib and Gio libraries. 27 | 28 | Methods: 29 | - mkdir: Create a directory at the specified path. 30 | - touch: Create a file at the specified path. 31 | - cat: Read and return the contents of a file. 32 | - run: Run a command in the shell (BLOCKING operation). 33 | - run_async: Run a command in the shell and pass output to a callback (NON-BLOCKING operation). 34 | - get_output: Run a shell command and return its output (BLOCKING operation). 35 | - get_output_async: Run a shell command and pass its output to a callback (NON-BLOCKING operation). 36 | - dir_exists: Check if a directory exists at the specified path. 37 | - file_exists: Check if a file exists at the specified path. 38 | - subprocess: Create a new subprocess for running a command. 39 | - popen: Open a subprocess to run a command, especially useful for executing blocking commands. 40 | - expandvars: Expand environment variables and user home directory in a given path. 41 | - monitor_file: Monitor changes to a file. 42 | - get_env: Get the value of an environment variable. 43 | """ 44 | 45 | @staticmethod 46 | def dir_exists(path: str) -> bool: 47 | """ 48 | Check if a directory exists at the specified path. 49 | 50 | Args: 51 | path (str): The path to the directory. 52 | 53 | Returns: 54 | bool: True if the directory exists, False otherwise. 55 | """ 56 | return GLib.file_test(Bash.expandvars(path), GLib.FileTest.IS_DIR) 57 | 58 | @staticmethod 59 | def file_exists(path: str) -> bool: 60 | """ 61 | Check if a file exists at the specified path. 62 | 63 | Args: 64 | path (str): The path to the file. 65 | 66 | Returns: 67 | bool: True if the file exists, False otherwise. 68 | """ 69 | return GLib.file_test(Bash.expandvars(path), GLib.FileTest.EXISTS) 70 | 71 | @staticmethod 72 | def mkdir(path: str) -> bool: 73 | """ 74 | Create a directory at the specified path. By default, parent directories are created if they don't exist. 75 | 76 | Args: 77 | path (str): The path to the directory. 78 | 79 | Returns: 80 | bool: True if the directory creation is successful, False otherwise. 81 | """ 82 | return {0: True, -1: False}.get( 83 | GLib.mkdir_with_parents(Bash.expandvars(path), 0o755), False 84 | ) 85 | 86 | @staticmethod 87 | def touch(path: str) -> bool: 88 | """ 89 | Create a file at the specified path. 90 | 91 | Args: 92 | path (str): The path to the file. 93 | 94 | Returns: 95 | bool: True if the file creation is successful, False otherwise. 96 | """ 97 | return GLib.file_set_contents(filename=Bash.expandvars(path), contents="") 98 | 99 | @staticmethod 100 | def __to__( 101 | type: Type[T] = str, 102 | default: str = "", 103 | ) -> T: 104 | if default.endswith("\n"): 105 | default = default[:-1] 106 | 107 | match type.__name__: 108 | case "str": 109 | return str(default) 110 | case "dict": 111 | return json.loads(default) 112 | case "int": 113 | return int(default) 114 | case "float": 115 | return float(default) 116 | case "list": 117 | return list(str(default).splitlines()) 118 | case "tuple": 119 | return tuple(str(default)) 120 | case "bool": 121 | DictValue = { 122 | "[]": False, 123 | # 124 | "0": False, 125 | "1": True, 126 | # 127 | "yes": True, 128 | "no": False, 129 | # 130 | "true": True, 131 | "false": False, 132 | }.get(default, -1) 133 | 134 | if DictValue == -1: 135 | return bool(default) 136 | else: 137 | return DictValue 138 | 139 | case "bytes": 140 | return str(default).encode() 141 | case _: 142 | return str(default) 143 | 144 | @staticmethod 145 | def cat( 146 | file_or_path: Union[Gio.File, str], 147 | to: Type[T] = str, 148 | ) -> T: 149 | """ 150 | Read and return the contents of a file. This is a BLOCKING operation. 151 | 152 | Args: 153 | file_or_path (Union[Gio.File, str]): Either a Gio.File object or the path to the file as a string. 154 | 155 | Returns: 156 | str: The contents of the file as a string. 157 | """ 158 | 159 | content: bytes 160 | 161 | if not isinstance(file_or_path, (Gio.File)): 162 | file_or_path = Gio.File.new_for_path(Bash.expandvars(file_or_path)) 163 | 164 | _, content, _ = file_or_path.load_contents() 165 | _content = content.decode() 166 | 167 | return Bash.__to__(to, _content) 168 | 169 | @staticmethod 170 | def run(cmd: str) -> bool: 171 | """ 172 | Run a command in the shell. 173 | 174 | This function executes a command in the shell environment. It should be noted that this 175 | is a BLOCKING operation, meaning that it will halt the main thread until the command completes. 176 | 177 | Args: 178 | cmd (Union[list, str]): The command to be executed. If a string is provided, it will be parsed as 179 | a single command. If a list is provided, the elements will be joined with spaces to form a command. 180 | 181 | Returns: 182 | bool: True if the command ran successfully and returned an exit status of 0, False otherwise. 183 | """ 184 | proc = Bash.subprocess(cmd) 185 | proc.communicate() 186 | return proc.get_successful() 187 | 188 | @staticmethod 189 | def run_async(cmd: str, callback: Callable = lambda *_: ()) -> None: 190 | """ 191 | Run a command in the shell. 192 | 193 | This function executes a command in the shell environment and then passes the 194 | output to the callback. 195 | 196 | NON BLOCKING operation 197 | 198 | Args: 199 | cmd (Union[list, str]): The command to be executed. If a string is provided, it will be parsed as 200 | a single command. If a list is provided, the elements will be joined with spaces to form a command. 201 | 202 | Returns: 203 | bool: True if the command ran successfully and returned an exit status of 0, False otherwise. 204 | """ 205 | 206 | def internal_callback(proc: Gio.Subprocess, res: Gio.Task) -> None: 207 | nonlocal stderr, stdout 208 | _, stdout, stderr = proc.communicate_utf8_finish(res) 209 | if stdout: 210 | callback(True) 211 | else: 212 | callback(False) 213 | 214 | proc: Gio.Subprocess 215 | stderr: str 216 | stdout: str 217 | 218 | proc = Bash.subprocess(cmd) 219 | proc.communicate_utf8_async(callback=internal_callback) 220 | 221 | @staticmethod 222 | def get_output( 223 | cmd: str, 224 | to: Type[T] = str, 225 | ) -> T: 226 | """ 227 | Run a shell command and return its output. 228 | BLOCKING operation. 229 | 230 | Args: 231 | cmd (str): The command to run. 232 | 233 | Returns: 234 | str: The output of the command. 235 | """ 236 | 237 | proc: Gio.Subprocess 238 | stdout: str 239 | stderr: str 240 | 241 | proc = Bash.subprocess(cmd) 242 | 243 | _, stdout, stderr = proc.communicate_utf8() 244 | std = stdout or stderr 245 | 246 | return Bash.__to__(to, std) 247 | 248 | @staticmethod 249 | def get_output_async( 250 | cmd: str, 251 | callback: Callable = lambda *_: (), 252 | to: Type[T] = str, 253 | ) -> None: 254 | """ 255 | Run a shell command and pass its output to the callback. NON BLOCKING operation. 256 | 257 | Args: 258 | cmd (str): The command to run. 259 | 260 | Returns: 261 | str: The output of the command. 262 | """ 263 | 264 | def internal_callback(proc: Gio.Subprocess, res: Gio.Task) -> None: 265 | nonlocal stderr, stdout 266 | _, stdout, stderr = proc.communicate_utf8_finish(res) 267 | std = stdout or stderr 268 | callback(Bash.__to__(to, std)) 269 | 270 | proc: Gio.Subprocess 271 | stdout: str 272 | stderr: str 273 | 274 | proc = Bash.subprocess(cmd) 275 | proc.communicate_utf8_async(callback=internal_callback) 276 | 277 | @staticmethod 278 | def subprocess( 279 | cmd: str, 280 | stdout_flags: Gio.SubprocessFlags = Gio.SubprocessFlags.STDOUT_PIPE, 281 | stderr_flags: Gio.SubprocessFlags = Gio.SubprocessFlags.STDERR_PIPE, 282 | ) -> Gio.Subprocess: 283 | 284 | cmd = Bash.expandvars(cmd) 285 | 286 | return Gio.Subprocess.new( 287 | argv=["bash", "-c", f"{cmd}"], flags=stdout_flags | stderr_flags 288 | ) 289 | 290 | @staticmethod 291 | def popen( 292 | cmd: Union[List[str], str], 293 | stdout: Union[Callable, None] = None, 294 | stderr: Union[Callable, None] = None, 295 | ) -> Union[Gio.Subprocess, None]: 296 | """ 297 | Open a subprocess to run a command, especially useful for executing blocking commands 298 | like `pactl subscribe` or `playerctl --follow`. 299 | 300 | Args: 301 | cmd (Union[List[str], str]): The command to run. 302 | stdout (Union[Callable, None], optional): Callback function for stdout. Defaults to None. 303 | stderr (Union[Callable, None], optional): Callback function for stderr. Defaults to None. 304 | 305 | Returns: 306 | Union[Gio.Subprocess, None]: The subprocess object. 307 | 308 | """ 309 | output: str 310 | success: bool 311 | parsed_cmd: List[str] 312 | proc: Gio.Subprocess 313 | stdout_stream: Gio.DataInputStream 314 | stderr_stream: Gio.DataInputStream 315 | 316 | def read_stream(out: Gio.DataInputStream, callback: Callable): 317 | def internal_callback(stdout: Gio.DataInputStream, res: Gio.Task): 318 | nonlocal output 319 | try: 320 | output, _ = stdout.read_line_finish_utf8(res) 321 | 322 | callback(output) 323 | 324 | return stdout.read_line_async( 325 | io_priority=GLib.PRIORITY_LOW, callback=internal_callback 326 | ) 327 | 328 | except Exception as e: 329 | print(e) 330 | 331 | out.read_line_async( 332 | io_priority=GLib.PRIORITY_LOW, callback=internal_callback 333 | ) 334 | 335 | if isinstance(cmd, (str)): 336 | cmd = Bash.expandvars(cmd) 337 | success, parsed_cmd = GLib.shell_parse_argv(cmd) 338 | if success and parsed_cmd: 339 | cmd = parsed_cmd 340 | 341 | elif isinstance(cmd, (list)): 342 | parsed_cmd = [Bash.expandvars(i) for i in cmd] 343 | if parsed_cmd: 344 | cmd = parsed_cmd 345 | 346 | if cmd: 347 | 348 | proc = Gio.Subprocess.new( 349 | argv=cmd, 350 | flags=Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE, 351 | ) 352 | 353 | if stdout is not None: 354 | stdout_stream = Gio.DataInputStream.new( 355 | base_stream=proc.get_stdout_pipe() 356 | ) 357 | read_stream(stdout_stream, stdout) 358 | 359 | if stderr is not None: 360 | stderr_stream = Gio.DataInputStream.new( 361 | base_stream=proc.get_stderr_pipe() 362 | ) 363 | read_stream(stderr_stream, stderr) 364 | 365 | if stderr is None and stdout is None: 366 | _ = proc.communicate_async() 367 | 368 | return proc 369 | 370 | @staticmethod 371 | def expandvars(path: str) -> str: 372 | """ 373 | Expand environment variables and user home directory in a given path. 374 | 375 | Args: 376 | path (str): The path containing environment variables and user home directory. 377 | 378 | Returns: 379 | str: The path with expanded variables and user home directory. 380 | """ 381 | 382 | return os_expanduser(os_expandvars(path)) 383 | 384 | @staticmethod 385 | def monitor_file( 386 | file_or_path: Union[Gio.File, str], 387 | flags: Literal[ 388 | "none", 389 | "send_moved", 390 | "watch_moves", 391 | "watch_mounts", 392 | "hard_links", 393 | ] = "none", 394 | callback: Callable = lambda *_: (), 395 | call_when: List[ 396 | Union[ 397 | Literal["ALL"], 398 | Literal["changed"], 399 | Literal["renamed"], 400 | Literal["moved_in"], 401 | Literal["moved_out"], 402 | Literal["deleted"], 403 | Literal["created"], 404 | Literal["attribute_changed"], 405 | Literal["changes_done_hint"], 406 | Literal["unmounted"], 407 | Literal["pre_unmount"], 408 | ], 409 | ] = ["changed"], 410 | ) -> Gio.FileMonitor: 411 | """ 412 | Monitor changes to a file. 413 | 414 | Args: 415 | file_or_path (Union[Gio.File, str]): Either a Gio.File object or the path to the file to monitor. 416 | flags (str, optional): Flags to specify monitoring behavior. Defaults to "none". 417 | callback (Callable, optional): Callback function to be executed when a monitored event occurs. Defaults to lambda *_: (). 418 | call_when (str, optional): Indicates when the callback will be called depending on the type of event specified. 419 | 420 | Returns: 421 | None 422 | """ 423 | 424 | monitor: Gio.FileMonitor 425 | monitor_flags: Gio.FileMonitorFlags 426 | arg_count: int = callback.__code__.co_argcount 427 | 428 | _call_when: List[Gio.FileMonitorEvent] = [] 429 | 430 | _call_when_events: Dict[str, Gio.FileMonitorEvent] = { 431 | "changed": Gio.FileMonitorEvent.CHANGED, 432 | "renamed": Gio.FileMonitorEvent.RENAMED, 433 | "moved_in": Gio.FileMonitorEvent.MOVED_IN, 434 | "moved_out": Gio.FileMonitorEvent.MOVED_OUT, 435 | "deleted": Gio.FileMonitorEvent.DELETED, 436 | "created": Gio.FileMonitorEvent.CREATED, 437 | "attribute_changed": Gio.FileMonitorEvent.ATTRIBUTE_CHANGED, 438 | "changes_done_hint": Gio.FileMonitorEvent.CHANGES_DONE_HINT, 439 | "unmounted": Gio.FileMonitorEvent.UNMOUNTED, 440 | "pre_unmount": Gio.FileMonitorEvent.PRE_UNMOUNT, 441 | } 442 | 443 | if isinstance(flags, (str)): 444 | monitor_flags = { 445 | "none": Gio.FileMonitorFlags.NONE, 446 | "send_moved": Gio.FileMonitorFlags.SEND_MOVED, 447 | "watch_moves": Gio.FileMonitorFlags.WATCH_MOVES, 448 | "watch_mounts": Gio.FileMonitorFlags.WATCH_MOUNTS, 449 | "hard_links": Gio.FileMonitorFlags.WATCH_HARD_LINKS, 450 | }.get(flags, Gio.FileMonitorFlags.NONE) 451 | 452 | if not isinstance(file_or_path, (Gio.File)): 453 | file_or_path = Gio.File.new_for_path(Bash.expandvars(file_or_path)) 454 | 455 | if "ALL" in call_when: 456 | _call_when = [ 457 | Gio.FileMonitorEvent.PRE_UNMOUNT, 458 | Gio.FileMonitorEvent.ATTRIBUTE_CHANGED, 459 | Gio.FileMonitorEvent.CHANGES_DONE_HINT, 460 | Gio.FileMonitorEvent.MOVED_OUT, 461 | Gio.FileMonitorEvent.UNMOUNTED, 462 | Gio.FileMonitorEvent.MOVED_IN, 463 | Gio.FileMonitorEvent.RENAMED, 464 | Gio.FileMonitorEvent.CREATED, 465 | Gio.FileMonitorEvent.DELETED, 466 | Gio.FileMonitorEvent.CHANGED, 467 | ] 468 | else: 469 | _call_when = list( 470 | filter( 471 | lambda e: e != None, 472 | map(_call_when_events.get, call_when), 473 | ) 474 | ) 475 | 476 | def internal_callback( 477 | FileMonitor: Gio.FileMonitor, 478 | File: Gio.File, 479 | _: None, 480 | Event: Gio.FileMonitorEvent, 481 | ) -> None: 482 | 483 | if Event in _call_when: 484 | match arg_count: 485 | case 0: 486 | callback() 487 | case 1: 488 | callback(File) 489 | case 2: 490 | callback(File, Event) 491 | case _: 492 | callback(File, Event, FileMonitor) 493 | 494 | monitor = file_or_path.monitor(flags=monitor_flags) 495 | __monitors__.append(monitor) 496 | 497 | monitor.connect("notify::cancelled", lambda _: __monitors__.remove(monitor)) 498 | monitor.connect("changed", internal_callback) 499 | return monitor 500 | 501 | @staticmethod 502 | def get_env(var: str) -> str: 503 | """ 504 | Get the value of an environment variable. 505 | 506 | Args: 507 | var (str): The name of the environment variable. 508 | 509 | Returns: 510 | str: The value of the environment variable. 511 | """ 512 | return GLib.getenv(var) 513 | -------------------------------------------------------------------------------- /PotatoWidgets/Cli/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import Widget 2 | from .._Logger import Logger 3 | from ..Imports import * 4 | from ..Services.Service import Listener, Poll, Service, Variable 5 | 6 | __all__ = ["PotatoService"] 7 | 8 | com_T0kyoB0y_PotatoWidgets = """ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | """ 33 | 34 | NodeInfo = Gio.DBusNodeInfo.new_for_xml(com_T0kyoB0y_PotatoWidgets) 35 | 36 | 37 | class PotatoService(Service): 38 | def __init__(self, confdir: str) -> None: 39 | super().__init__() 40 | 41 | DATA = {"windows": [], "functions": [], "variables": []} 42 | try: 43 | module_name: str = confdir.split("/").pop(-1) 44 | init_file: str = confdir + "/" + "__init__.py" 45 | 46 | if dir not in sys.path: 47 | sys.path.append(confdir) 48 | 49 | spec: Union[ModuleSpec, None] = spec_from_file_location( 50 | module_name, init_file 51 | ) 52 | 53 | if spec: 54 | modulo: ModuleType = module_from_spec(spec) 55 | spec.loader.exec_module(modulo) 56 | 57 | if hasattr(modulo, "DATA"): 58 | DATA = modulo.DATA 59 | else: 60 | raise AttributeError 61 | else: 62 | raise FileNotFoundError 63 | 64 | except AttributeError: 65 | Logger.WARNING(f"DATA variable not found in {confdir}") 66 | 67 | except FileNotFoundError: 68 | Logger.WARNING(f"File __init__.py not found in {confdir}") 69 | 70 | except Exception as e: 71 | Logger.ERROR(f"Unexpected error in {confdir}:\n", e) 72 | 73 | self.data = { 74 | "windows": [ 75 | {"name": w.__name__, "window": w} 76 | for w in DATA.get("windows", []) 77 | if isinstance(w, (Widget.Window)) and w.__name__ 78 | ], 79 | "functions": [ 80 | {"name": f.__name__, "function": f} 81 | for f in DATA.get("functions", []) 82 | if isinstance(f, (Callable)) and f.__name__ != "" 83 | ], 84 | "variables": [ 85 | {"name": v.__name__, "variable": v} 86 | for v in DATA.get("variables", []) 87 | if isinstance(v, (Variable, Listener, Poll)) and v.__name__ 88 | ], 89 | } 90 | 91 | self.__register__() 92 | 93 | def ListWindows(self) -> GLib.Variant: 94 | if self.data["windows"]: 95 | return GLib.Variant( 96 | "(as)", 97 | ( 98 | ( 99 | f"{'*' if i['window'].get_visible() else ''}{i['name']}" 100 | for i in self.data["windows"] 101 | ), 102 | ), 103 | ) 104 | 105 | return GLib.Variant("(as)", (("none",),)) 106 | 107 | def ListFunctions(self) -> GLib.Variant: 108 | if self.data["functions"]: 109 | return GLib.Variant("(as)", ((i["name"] for i in self.data["functions"]),)) 110 | return GLib.Variant("(as)", (("none",),)) 111 | 112 | def ListVariables(self) -> GLib.Variant: 113 | if self.data["variables"]: 114 | return GLib.Variant("(as)", ((i["name"] for i in self.data["variables"]),)) 115 | return GLib.Variant("(as)", (("none",),)) 116 | 117 | def CallFunction(self, callback_name: str) -> GLib.Variant: 118 | callback: Union[Callable, None] = next( 119 | ( 120 | i["function"] 121 | for i in self.data["functions"] 122 | if i["name"] == callback_name 123 | ), 124 | None, 125 | ) 126 | if callback is not None: 127 | try: 128 | callback() 129 | return GLib.Variant("(s)", ("ok",)) 130 | except Exception as r: 131 | return GLib.Variant("(s)", (str(r),)) 132 | return GLib.Variant("(s)", ("notfound",)) 133 | 134 | def WindowAction(self, action: str, window_name: str) -> GLib.Variant: 135 | window: Union[Widget.Window, None] = next( 136 | (i["window"] for i in self.data["windows"] if i["name"] == window_name), 137 | None, 138 | ) 139 | if not window: 140 | return GLib.Variant("(s)", ("notfound",)) 141 | 142 | try: 143 | match action: 144 | case "toggle": 145 | window.toggle() 146 | case "open": 147 | window.open() 148 | case "close": 149 | window.close() 150 | case _: 151 | return GLib.Variant("(s)", ("invalid",)) 152 | return GLib.Variant("(s)", ("ok",)) 153 | except: 154 | return GLib.Variant("(s)", ("error",)) 155 | 156 | def __register__(self) -> None: 157 | Gio.bus_own_name( 158 | Gio.BusType.SESSION, 159 | "com.T0kyoB0y.PotatoWidgets", 160 | Gio.BusNameOwnerFlags.DO_NOT_QUEUE, 161 | self.__on_success__, 162 | None, 163 | self.__on_failed__, 164 | ) 165 | 166 | def __on_success__( 167 | self, 168 | Connection: Gio.DBusConnection, 169 | BusName: Literal["org.freedesktop.Notifications"], 170 | ): 171 | 172 | Connection.register_object( 173 | "/com/T0kyoB0y/PotatoWidgets", 174 | NodeInfo.interfaces[0], 175 | self.__on_call__, 176 | ) 177 | 178 | def __on_failed__( 179 | self, 180 | Connection: Gio.DBusConnection, 181 | BusName: Literal["org.freedesktop.Notifications"], 182 | ): 183 | print("error") 184 | 185 | def __on_call__( 186 | self, 187 | Connection: Gio.DBusConnection, 188 | Sender: str, 189 | Path: Literal["/org/freedesktop/Notifications"], 190 | BusName: Literal["org.freedesktop.Notifications"], 191 | Method: str, 192 | Parameters: tuple, 193 | MethodInvocation: Gio.DBusMethodInvocation, 194 | ): 195 | # print(Method, Parameters) 196 | try: 197 | match Method: 198 | case "CallFunction": 199 | MethodInvocation.return_value(self.CallFunction(*Parameters)) 200 | case "ListWindows": 201 | MethodInvocation.return_value(self.ListWindows()) 202 | case "ListFunctions": 203 | MethodInvocation.return_value(self.ListFunctions()) 204 | case "ListVariables": 205 | MethodInvocation.return_value(self.ListVariables()) 206 | case "WindowAction": 207 | MethodInvocation.return_value(self.WindowAction(*Parameters)) 208 | 209 | except Exception as r: 210 | print("ERRORRRRR: ", r) 211 | finally: 212 | return Connection.flush() 213 | -------------------------------------------------------------------------------- /PotatoWidgets/Cli/old__main__.py: -------------------------------------------------------------------------------- 1 | from .. import Bash 2 | from ..Imports import * 3 | 4 | 5 | def dbuscall(MethodName: str, *args): 6 | return literal_eval( 7 | Bash.get_output( 8 | """gdbus call --session --dest com.T0kyoB0y.PotatoWidgets --object-path /com/T0kyoB0y/PotatoWidgets --method com.T0kyoB0y.PotatoWidgets.{} {} """.format( 9 | MethodName, " ".join(args) 10 | ) 11 | ) 12 | )[0] 13 | 14 | 15 | def list_windows(): 16 | for i in dbuscall("ListWindows"): 17 | print(i) 18 | 19 | 20 | def list_functions(): 21 | for i in dbuscall("ListFunctions"): 22 | print(i) 23 | 24 | 25 | def list_variables(): 26 | for i in dbuscall("ListVariables"): 27 | print(i) 28 | 29 | 30 | def call_function(func_name): 31 | return print(dbuscall("CallFunction", func_name)) 32 | 33 | 34 | def window_action(action, window_name): 35 | return print(dbuscall("WindowAction", action, window_name)) 36 | 37 | 38 | def main(): 39 | parser = argparse.ArgumentParser(description="PotatoWidgets CLI") 40 | 41 | args_withoutmetavar: Tuple[List[str], ...] = ( 42 | ["--windows", "List all exported windows"], 43 | ["--functions", "List all exported functions"], 44 | ["--variables", "List all exported variables"], 45 | ) 46 | 47 | args_withmetavar: Tuple[List[str], ...] = ( 48 | ["--exec", "", "Execute an exported function"], 49 | ["--open", "", "Open a window"], 50 | ["--close", "", "Close a window"], 51 | ["--toggle", "", "Toggle a window"], 52 | ) 53 | 54 | for i in args_withmetavar: 55 | parser.add_argument(i[0], metavar=i[1], help=i[2]) 56 | 57 | for i in args_withoutmetavar: 58 | parser.add_argument(i[0], action="store_true", help=i[1]) 59 | 60 | args = parser.parse_args() 61 | 62 | if args.windows: 63 | list_windows() 64 | elif args.functions: 65 | list_functions() 66 | elif args.variables: 67 | list_variables() 68 | elif args.exec: 69 | call_function(args.exec) 70 | elif args.open: 71 | window_action(iface, "open", args.open) 72 | elif args.close: 73 | window_action(iface, "close", args.close) 74 | elif args.toggle: 75 | window_action(iface, "toggle", args.toggle) 76 | else: 77 | parser.print_help() 78 | 79 | 80 | if __name__ == "__main__": 81 | main() 82 | -------------------------------------------------------------------------------- /PotatoWidgets/Cli/potatocli: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dbuscall() { 4 | local MethodName="$1" 5 | shift 6 | local args=("$@") 7 | local result=$(gdbus call --session --dest com.T0kyoB0y.PotatoWidgets --object-path /com/T0kyoB0y/PotatoWidgets --method com.T0kyoB0y.PotatoWidgets."$MethodName" "${args[@]}") 8 | echo "$result" | sed "s/'//g" | awk -F '[][]' '{print $2}' | tr -d ' ' | tr ',' '\n' 9 | } 10 | 11 | list_windows() { 12 | dbuscall "ListWindows" | tr ',' '\n' 13 | } 14 | 15 | list_functions() { 16 | dbuscall "ListFunctions" | tr ',' '\n' 17 | } 18 | 19 | list_variables() { 20 | dbuscall "ListVariables" | tr ',' '\n' 21 | } 22 | 23 | call_function() { 24 | local func_name="$1" 25 | dbuscall "CallFunction" "$func_name" 26 | } 27 | 28 | window_action() { 29 | local action="$1" 30 | local window_name="$2" 31 | dbuscall "WindowAction" "$action" "$window_name" 32 | } 33 | 34 | show_help() { 35 | echo "Usage: $0 [options]" 36 | echo 37 | echo "Options:" 38 | echo " --windows List all exported windows" 39 | echo " --functions List all exported functions" 40 | echo " --variables List all exported variables" 41 | echo " --exec Execute an exported function" 42 | echo " --open Open a window" 43 | echo " --close Close a window" 44 | echo " --toggle Toggle a window" 45 | } 46 | 47 | main() { 48 | if [[ $# -eq 0 ]]; then 49 | show_help 50 | exit 1 51 | fi 52 | case $1 in 53 | --windows) 54 | list_windows 55 | ;; 56 | --functions) 57 | list_functions 58 | ;; 59 | --variables) 60 | list_variables 61 | ;; 62 | --exec) 63 | if [[ -n "$2" ]]; then 64 | call_function "$2" 65 | else 66 | echo "Error: --exec requires a function name" 67 | exit 1 68 | fi 69 | ;; 70 | --open) 71 | if [[ -n "$2" ]]; then 72 | window_action "open" "$2" 73 | else 74 | echo "Error: --open requires a window name" 75 | exit 1 76 | fi 77 | ;; 78 | --close) 79 | if [[ -n "$2" ]]; then 80 | window_action "close" "$2" 81 | else 82 | echo "Error: --close requires a window name" 83 | exit 1 84 | fi 85 | ;; 86 | --toggle) 87 | if [[ -n "$2" ]]; then 88 | window_action "toggle" "$2" 89 | else 90 | echo "Error: --toggle requires a window name" 91 | exit 1 92 | fi 93 | ;; 94 | *) 95 | echo "Unknown option: $key" 96 | show_help 97 | exit 1 98 | ;; 99 | esac 100 | } 101 | 102 | main "$@" 103 | -------------------------------------------------------------------------------- /PotatoWidgets/Env/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module includes various directories and files related to application caching, configuration, 3 | notifications, and styling in the Potato application. 4 | 5 | DIR_HOME: Home directory path fetched from GLib environment. 6 | DIR_CONFIG: Configuration directory path within the home directory. 7 | DIR_CONFIG_POTATO: Directory path specific to Potato within the configuration directory. 8 | DIR_CURRENT: Current directory path using GLib or falling back to Potato's configuration directory. 9 | DIR_CACHE: Cache directory path based on XDG_CACHE_HOME or defaulting to the home directory's cache. 10 | DIR_CACHE_NOTIF: Directory for notifications within the cache directory. 11 | DIR_CACHE_NOTIF_IMAGES: Directory for notification images within the notification cache directory. 12 | FILE_CACHE_APPS: JSON file path for caching application data. 13 | FILE_CACHE_NOTIF: JSON file path for caching notification data. 14 | FILE_CACHE_CSS: Path to the CSS file used for styling within the cache directory. 15 | """ 16 | 17 | from ..Imports import * 18 | 19 | __all__ = [ 20 | "DIR_CACHE", 21 | "DIR_CONFIG", 22 | "DIR_CURRENT", 23 | "DIR_HOME", 24 | "DIR_CACHE_TRAY", 25 | "DIR_CONFIG_POTATO", 26 | "DIR_CACHE_NOTIF", 27 | "DIR_CACHE_NOTIF_IMAGES", 28 | "FILE_CACHE_APPS", 29 | "FILE_CACHE_CSS", 30 | "FILE_CACHE_NOTIF", 31 | ] 32 | 33 | DIR_HOME: str = GLib.getenv("HOME") 34 | DIR_CONFIG: str = DIR_HOME + "/.config" 35 | DIR_CONFIG_POTATO: str = DIR_CONFIG + "/potato" 36 | DIR_CURRENT: str = GLib.get_current_dir() or (DIR_CONFIG + "/potato") 37 | 38 | 39 | DIR_CACHE: str = DIR_HOME + "/.cache/PotatoCache" 40 | DIR_CACHE_TRAY: str = DIR_CACHE + "/Tray" 41 | DIR_CACHE_NOTIF: str = f"{DIR_CACHE}/Notifications" 42 | DIR_CACHE_NOTIF_IMAGES: str = f"{DIR_CACHE_NOTIF}/Img" 43 | 44 | 45 | FILE_CACHE_APPS: str = f"{DIR_CACHE}/Applications.json" 46 | FILE_CACHE_NOTIF: str = f"{DIR_CACHE_NOTIF}/Notifications.json" 47 | FILE_CACHE_CSS: str = f"{DIR_CACHE}/Style.css" 48 | 49 | 50 | for _dir in [ 51 | DIR_HOME, 52 | DIR_CACHE, 53 | DIR_CONFIG, 54 | DIR_CACHE_NOTIF, 55 | DIR_CACHE_NOTIF_IMAGES, 56 | DIR_CACHE_TRAY, 57 | ]: 58 | if not GLib.file_test(_dir, GLib.FileTest.IS_DIR): 59 | GLib.mkdir_with_parents(_dir, 0o755) 60 | 61 | for _file in [FILE_CACHE_APPS, FILE_CACHE_NOTIF]: 62 | if not GLib.file_test(_file, GLib.FileTest.EXISTS): 63 | GLib.file_set_contents(filename=_file, contents="") 64 | -------------------------------------------------------------------------------- /PotatoWidgets/Imports.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module that imports all the required libraries in a single file 3 | """ 4 | 5 | __all__ = [ 6 | "argparse", 7 | "functools", 8 | "importlib", 9 | "io", 10 | "json", 11 | "sys", 12 | "threading", 13 | "dataclass", 14 | "ModuleSpec", 15 | "module_from_spec", 16 | "spec_from_file_location", 17 | "os_expanduser", 18 | "os_expandvars", 19 | "randint", 20 | "re_sub", 21 | "traceback_extract_stack", 22 | "ModuleType", 23 | "Any", 24 | "Callable", 25 | "Dict", 26 | "List", 27 | "Literal", 28 | "NoReturn", 29 | "Optional", 30 | "Tuple", 31 | "Type", 32 | "TypeVar", 33 | "Union", 34 | "dbus", 35 | "gi", 36 | "literal_eval", 37 | "SessionBus", 38 | "DBusGMainLoop", 39 | "G_MAXDOUBLE", 40 | "G_MAXINT", 41 | "G_MININT", 42 | "TYPE_STRING", 43 | "Gdk", 44 | "GdkPixbuf", 45 | "Gio", 46 | "GLib", 47 | "GObject", 48 | "Gtk", 49 | "GtkLayerShell", 50 | "Pango", 51 | "Playerctl", 52 | ] 53 | 54 | 55 | import argparse 56 | import functools 57 | import importlib 58 | import io 59 | import json 60 | import sys 61 | import threading 62 | from ast import literal_eval 63 | from dataclasses import dataclass 64 | from importlib.machinery import ModuleSpec 65 | from importlib.util import module_from_spec, spec_from_file_location 66 | from os.path import expanduser as os_expanduser 67 | from os.path import expandvars as os_expandvars 68 | from random import randint 69 | from re import sub as re_sub 70 | from traceback import extract_stack as traceback_extract_stack 71 | from types import ModuleType 72 | from typing import (Any, Callable, Dict, List, Literal, NoReturn, Optional, 73 | Tuple, Type, TypeVar, Union) 74 | 75 | import dbus 76 | import dbus.service 77 | import gi 78 | from dbus import SessionBus 79 | from dbus.mainloop.glib import DBusGMainLoop 80 | from gi._propertyhelper import G_MAXDOUBLE, G_MAXINT, G_MININT, TYPE_STRING 81 | 82 | for n, v in { 83 | "Gdk": "3.0", 84 | "GdkPixbuf": "2.0", 85 | "Gio": "2.0", 86 | "GLib": "2.0", 87 | "GObject": "2.0", 88 | "Gtk": "3.0", 89 | "GtkLayerShell": "0.1", 90 | "Pango": "1.0", 91 | "Playerctl": "2.0", 92 | }.items(): 93 | try: 94 | gi.require_version(n, v) 95 | except Exception as r: 96 | print(r) 97 | 98 | from gi.repository import (Gdk, GdkPixbuf, Gio, GLib, GObject, Gtk, 99 | GtkLayerShell, Pango, Playerctl) 100 | 101 | DBusGMainLoop(set_as_default=True) 102 | -------------------------------------------------------------------------------- /PotatoWidgets/Methods/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides utility functions for parsing intervals, getting screen sizes, 3 | waiting for timeouts, scheduling idle callbacks, looking up icons, and executing commands. 4 | 5 | wait: Waits for a specified time and executes a callback function. 6 | interval: Sets up recurring intervals to execute a callback function. 7 | idle: Schedules a callback function to be executed when the main event loop is idle. 8 | lookup_icon: Looks up icons by name and returns file paths or icon information. 9 | getoutput: Executes a command and returns its output or error message. 10 | parse_interval: Parses intervals in milliseconds or seconds/minutes/hours. 11 | get_screen_size: Retrieves the screen size of a specified monitor. 12 | parse_screen_size: Parses screen size values (percentage, integer, or boolean). 13 | 14 | Note: The getoutput method is deprecated and recommended to use Bash.get_output() instead. 15 | """ 16 | 17 | from ..Imports import * 18 | 19 | __all__ = [ 20 | "get_screen_size", 21 | "getoutput", 22 | "idle", 23 | "interval", 24 | "lookup_icon", 25 | "parse_interval", 26 | "parse_screen_size", 27 | "wait", 28 | "make_async", 29 | ] 30 | 31 | 32 | def parse_interval( 33 | interval: Union[int, str] = 1000, fallback_interval: int = 1000 34 | ) -> int: 35 | """Parse the interval in milliseconds. 36 | 37 | Args: 38 | interval (Union[int, str], optional): The interval to parse, can be in milliseconds (int) 39 | or in string format indicating seconds ('s'), minutes ('m'), or hours ('h'). 40 | Defaults to 1000 (1 second). 41 | 42 | Returns: 43 | int: The parsed interval in milliseconds. 44 | """ 45 | unit: str 46 | value: int 47 | try: 48 | if isinstance(interval, str): 49 | unit = interval[-1].lower() 50 | value = int(interval[:-1]) 51 | 52 | if unit == "s": 53 | return int(value * 1000) 54 | elif unit == "m": 55 | return int(value * 60 * 1000) 56 | elif unit == "h": 57 | return int(value * 60 * 60 * 1000) 58 | else: 59 | return int(interval) 60 | 61 | except (ValueError, IndexError): 62 | pass 63 | 64 | return fallback_interval 65 | 66 | 67 | def get_screen_size( 68 | monitor_index: int = 0, fallback_size: Tuple[int, int] = (1920, 1080) 69 | ) -> Tuple[int, int]: 70 | """Get the screen size. 71 | 72 | Args: 73 | monitor_index (int, optional): The index of the monitor to get the size of. Defaults to 0. 74 | fallback_size (tuple, optional): A tuple containing the width and height to return 75 | if the display is not available or the monitor index is out of range. 76 | Defaults to (1920, 1080). 77 | 78 | Returns: 79 | tuple: A tuple containing the width and height of the specified monitor, 80 | or the fallback size if the display is not available or the index is out of range. 81 | """ 82 | display: Gdk.Display 83 | monitor: Gdk.Monitor 84 | geometry: Gdk.Rectangle 85 | 86 | display = Gdk.Display.get_default() 87 | 88 | if display and 0 <= monitor_index < display.get_n_monitors(): 89 | monitor = display.get_monitor(monitor_index) 90 | geometry = monitor.get_geometry() 91 | if geometry: 92 | return geometry.width, geometry.height 93 | else: 94 | return fallback_size 95 | else: 96 | return fallback_size 97 | 98 | 99 | def parse_screen_size(value: Union[int, str], total: int = 0) -> int: 100 | """Parse the screen size. 101 | 102 | Args: 103 | value (Union[int, str, bool]): The screen size value, which can be a string with percentage, 104 | an integer, or a boolean. 105 | total (int, optional): Total value. Defaults to 0. 106 | 107 | Returns: 108 | int: The parsed screen size. 109 | """ 110 | if isinstance(value, str) and "%" in value: 111 | percentage = float(value.strip("%")) / 100 112 | return int(total * percentage) 113 | elif isinstance(value, (int, float)): 114 | return int(value) 115 | else: 116 | return 10 117 | 118 | 119 | def wait( 120 | time_ms: Union[str, int], callback: Callable, *args: Any, **kwargs: Any 121 | ) -> int: 122 | """Wait for a specified amount of time and then execute a callback function. 123 | 124 | Args: 125 | time_ms (Union[str, int]): The time to wait before executing the callback. 126 | callback (Callable): The function to call after the specified time has elapsed. 127 | 128 | Returns: 129 | int: The ID of the timeout source. 130 | """ 131 | 132 | def on_timeout() -> bool: 133 | callback(*args, **kwargs) 134 | return False 135 | 136 | return GLib.timeout_add(parse_interval(time_ms), on_timeout) 137 | 138 | 139 | def interval( 140 | time_ms: Union[str, int], callback: Callable, *args: Any, **kwargs: Any 141 | ) -> int: 142 | """Sets a function to be called at regular intervals. 143 | 144 | Args: 145 | time_ms (Union[str, int]): The interval between callback executions. 146 | callback (Callable): The function to call at each interval. 147 | 148 | Returns: 149 | int: The ID of the timeout source. 150 | """ 151 | 152 | def on_timeout() -> bool: 153 | callback(*args, **kwargs) 154 | return True 155 | 156 | return GLib.timeout_add(parse_interval(time_ms), on_timeout) 157 | 158 | 159 | def idle(callback: Callable, *args: Any, **kwargs: Any) -> int: 160 | """Schedule a callback function to be called whenever there 161 | are no higher priority events pending to the default PotatoLoop. 162 | Args: 163 | callback (Callable): The function to call when the main event loop is idle. 164 | 165 | Returns: 166 | int: The ID of the idle source. 167 | """ 168 | 169 | def on_idle() -> bool: 170 | callback(*args, **kwargs) 171 | return False 172 | 173 | return GLib.idle_add(on_idle) 174 | 175 | 176 | def lookup_icon( 177 | icon_name: str, 178 | size: Literal[8, 16, 32, 64, 128] = 128, 179 | path: bool = True, 180 | fallback: str = "application-x-addon-symbolic", 181 | ) -> Union[str, Gtk.IconInfo]: 182 | """Look up an icon by name and return its file path or icon info. 183 | 184 | Args: 185 | icon_name (str): The name of the icon to look up. 186 | size (Literal[8, 16, 32, 64, 128], optional): The size of the icon. Defaults to 128. 187 | path (bool, optional): Whether to return the file path of the icon. Defaults to True. 188 | fallback (str, optional): The name of the icon to use if the specified icon is not found. 189 | Defaults to "application-x-addon-symbolic". 190 | 191 | Returns: 192 | str: The file path of the icon if path=True, otherwise the icon info. 193 | """ 194 | icon_info: Gtk.IconInfo 195 | theme: Gtk.IconTheme 196 | filename: str 197 | name: str 198 | 199 | if icon_name: 200 | theme = Gtk.IconTheme.get_default() 201 | 202 | for name in [ 203 | icon_name, 204 | icon_name.lower(), 205 | icon_name.title(), 206 | icon_name.capitalize(), 207 | ]: 208 | icon_info = theme.lookup_icon( 209 | name, 210 | size, 211 | Gtk.IconLookupFlags.USE_BUILTIN, 212 | ) 213 | 214 | if not icon_info: 215 | continue 216 | 217 | filename = icon_info.get_filename() 218 | 219 | if not filename: 220 | continue 221 | 222 | if path: 223 | return filename 224 | else: 225 | return icon_info 226 | return ( 227 | lookup_icon(fallback, path=True) if path else lookup_icon(fallback, path=False) 228 | ) 229 | 230 | 231 | def getoutput(cmd: str) -> str: 232 | """Execute a command and return its output or error message. 233 | 234 | Args: 235 | cmd (str): The command to execute. 236 | 237 | Returns: 238 | str: The output of the command if successful, otherwise an empty string. 239 | """ 240 | stdout: bytes 241 | stderr: bytes 242 | state: int 243 | line: str = "" 244 | file: str = "" 245 | _line: int = -1 246 | 247 | try: 248 | file, _line, _, line = traceback_extract_stack()[-2] 249 | except: 250 | pass 251 | 252 | try: 253 | _, stdout, stderr, state = GLib.spawn_command_line_sync(cmd) 254 | 255 | if line: 256 | print( 257 | f"getoutput Method is deprecated, use Bash.get_output() instead, called on line {_line}, in file {file}" 258 | ) 259 | else: 260 | 261 | print(f"getoutput Method is deprecated, use Bash.get_output() instead") 262 | return stdout.decode() if state == 0 else stderr.decode() 263 | except: 264 | return "" 265 | 266 | 267 | def make_async(func: Callable): 268 | """ 269 | Decorator to execute a function in a separate thread without blocking the main thread. 270 | 271 | Args: 272 | func (Callable): The function to be decorated. 273 | 274 | Returns: 275 | Callable: The decorated function that runs asynchronously in a separate thread. 276 | 277 | Note: 278 | To update the interface, variables, or elements outside the thread, it's recommended 279 | to use `idle()` within the thread to ensure thread-safe operation. 280 | """ 281 | 282 | @functools.wraps(func) 283 | def wrapper(*args, **kwargs) -> GLib.Thread: 284 | def run_in_thread(*args, **kwargs) -> None: 285 | func(*args, **kwargs) 286 | 287 | return GLib.Thread.new(func.__name__, run_in_thread, *args, **kwargs) 288 | 289 | return wrapper 290 | -------------------------------------------------------------------------------- /PotatoWidgets/PotatoLoop.py: -------------------------------------------------------------------------------- 1 | from ._Logger import Logger 2 | from .Cli import PotatoService 3 | from .Env import * 4 | from .Imports import * 5 | from .Services.Style import Style 6 | 7 | __all__ = ["PotatoLoop"] 8 | 9 | 10 | def PotatoLoop( 11 | confdir: str = DIR_CONFIG_POTATO, *, run_without_services: bool = False 12 | ) -> NoReturn: 13 | """Starts the Potato application loop and initializes necessary services. 14 | 15 | Args: 16 | confdir (str, optional): The directory path for configuration. Defaults to DIR_CONFIG_POTATO. 17 | run_without_services (bool, optional): If True, the loop will start without initializing services. Defaults to False. 18 | 19 | Returns: 20 | NoReturn: This function does not return anything. 21 | 22 | Raises: 23 | KeyboardInterrupt: If the loop is interrupted by a keyboard event. 24 | Exception: If any other exception occurs during the loop execution. 25 | """ 26 | 27 | if confdir.endswith("/"): 28 | confdir = confdir[:-1] 29 | 30 | GLibLoop: GLib.MainLoop = GLib.MainLoop() 31 | 32 | def SpawnServices() -> None: 33 | """Initialize necessary services for Potato application.""" 34 | Style.load_css(f"{confdir}/style.scss") 35 | PotatoService(confdir) 36 | 37 | try: 38 | # Then run the MainLoop 39 | if not run_without_services: 40 | SpawnServices() 41 | GLibLoop.run() 42 | except KeyboardInterrupt: 43 | Logger.SUCCESS("\n\nBye :)") 44 | GLibLoop.quit() 45 | 46 | except Exception as r: 47 | Logger.ERROR(r) 48 | GLibLoop.quit() 49 | 50 | finally: 51 | exit(0) 52 | -------------------------------------------------------------------------------- /PotatoWidgets/Services/Applications/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file includes the following classes: 3 | 4 | - App: Represents a desktop application. 5 | - Applications: Represents a collection of desktop applications. 6 | 7 | Class App Methods: 8 | - app: Gets the desktop application information. 9 | - name: Gets the name of the application. 10 | - generic_name: Gets the generic name of the application. 11 | - display_name: Gets the display name of the application. 12 | - comment: Gets the comment of the application. 13 | - categories: Gets the categories of the application. 14 | - desktop: Gets the desktop identifier of the application. 15 | - icon_name: Gets the icon name of the application. 16 | - keywords: Gets the keywords of the application. 17 | - json: Returns a JSON representation of the application. 18 | - launch: Launches the application. 19 | 20 | Class Applications Methods: 21 | - get_all: Gets all the applications. 22 | - add_preferred: Adds an application to the preferred list. 23 | - get_preferred: Gets the preferred applications list. 24 | - add_blacklist: Adds an application to the blacklist. 25 | - get_blacklist: Gets the blacklist of applications. 26 | - add_whitelist: Adds an application to the whitelist. 27 | - get_whitelist: Gets the whitelist of applications. 28 | - query: Queries applications based on keywords. 29 | - json: Returns a JSON representation of the applications. 30 | - reload: Reloads the JSON data. 31 | """ 32 | 33 | from ...Env import * 34 | from ...Imports import * 35 | from ..Service import Service 36 | 37 | 38 | class App(dict): 39 | """ 40 | Represents a desktop application. 41 | """ 42 | 43 | def __init__(self, app: Gio.DesktopAppInfo) -> None: 44 | """ 45 | Initializes an instance of App. 46 | 47 | Args: 48 | app (Gio.DesktopAppInfo): The desktop application information. 49 | """ 50 | super().__init__() 51 | 52 | self._app: Gio.DesktopAppInfo = app 53 | # self._context = Gio.AppLaunchContext().new() 54 | 55 | self._keywords: str = " ".join( 56 | [ 57 | self.name, 58 | self.comment, 59 | self.categories, 60 | self.generic_name, 61 | self.display_name, 62 | self.icon_name, 63 | ] 64 | ) 65 | self["icon_name"] = self.icon_name 66 | self["name"] = self.name 67 | self["comment"] = self.comment 68 | self["desktop"] = self.desktop 69 | self["categories"] = self.categories 70 | self["keywords"] = self.keywords 71 | 72 | @property 73 | def app(self) -> Gio.DesktopAppInfo: 74 | """Gets the desktop application information.""" 75 | return self._app 76 | 77 | @property 78 | def name(self) -> str: 79 | """Gets the name of the application.""" 80 | return self._app.get_name() or "" 81 | 82 | @property 83 | def generic_name(self) -> str: 84 | """Gets the generic name of the application.""" 85 | return self._app.get_generic_name() or "" 86 | 87 | @property 88 | def display_name(self) -> str: 89 | """Gets the display name of the application.""" 90 | return self._app.get_display_name() or "" 91 | 92 | @property 93 | def comment(self) -> str: 94 | """Gets the comment of the application.""" 95 | return self._app.get_description() or "" 96 | 97 | @property 98 | def categories(self) -> str: 99 | """Gets the categories of the application.""" 100 | _cat = self._app.get_categories() 101 | return " ".join(_cat.split(";")) if _cat else "" 102 | 103 | @property 104 | def desktop(self) -> str: 105 | """Gets the desktop identifier of the application.""" 106 | return self._app.get_id() or "" 107 | 108 | @property 109 | def icon_name(self) -> str: 110 | """Gets the icon name of the application.""" 111 | return self._app.get_string("Icon") or "" 112 | 113 | @property 114 | def keywords(self) -> str: 115 | """Gets the keywords of the application.""" 116 | return self._keywords or "" 117 | 118 | def json(self) -> dict: 119 | """Returns a JSON representation of the application.""" 120 | return dict(super().items()) 121 | 122 | def launch(self) -> bool: 123 | """Launches the application.""" 124 | # return self._app.launch() 125 | 126 | _proc: Gio.Subprocess = Gio.Subprocess.new( 127 | flags=Gio.SubprocessFlags.STDERR_SILENCE 128 | | Gio.SubprocessFlags.STDOUT_SILENCE, 129 | argv=["gtk-launch", self.desktop], 130 | ) 131 | return _proc.wait_check() 132 | 133 | 134 | class _Applications(Service): 135 | """ 136 | Represents a collection of desktop applications. 137 | 138 | Methods: 139 | - get_all: Get all applications. 140 | - add_preferred: Add an application to the preferred list. 141 | - get_preferred: Get the preferred applications list. 142 | - add_blacklist: Add an application to the blacklist. 143 | - get_blacklist: Get the blacklist of applications. 144 | - add_whitelist: Add an application to the whitelist. 145 | - get_whitelist: Get the whitelist of applications. 146 | - query: Query applications based on keywords. 147 | - json: Get JSON representation of the applications. 148 | - reload: Reload the JSON data. 149 | """ 150 | 151 | def __init__(self) -> None: 152 | """Initializes an instance of Applications.""" 153 | 154 | self._json: Dict[str, List[Union[str, None]]] = self._load_json() 155 | self._preferred: List[Union[str, None]] = self._json.get("preferred", []) 156 | self._blacklist: List[Union[str, None]] = self._json.get("blacklist", []) 157 | self._whitelist: List[Union[str, None]] = self._json.get("whitelist", []) 158 | self._all = [] 159 | self.__reload__() 160 | 161 | def __reload__(self): 162 | self._all = [ 163 | App(i) 164 | for i in Gio.DesktopAppInfo.get_all() 165 | if ( 166 | i.should_show() 167 | and not any( 168 | j.lower() in i.get_name().lower() for j in self.get_blacklist() if j 169 | ) 170 | ) 171 | or any(j.lower() in i.get_name().lower() for j in self.get_whitelist() if j) 172 | ] 173 | 174 | self._all.sort(key=lambda app: app.name) 175 | 176 | def get_all(self) -> List[App]: 177 | """Gets all the applications.""" 178 | self.__reload__() 179 | return self._all 180 | 181 | def add_preferred(self, name: str) -> None: 182 | """ 183 | Adds an application to the preferred list. 184 | 185 | Args: 186 | name (str): The name of the application to add. 187 | """ 188 | if name not in self._preferred: 189 | self._preferred.append(name) 190 | self.reload() 191 | 192 | def get_preferred(self) -> List[Union[None, str]]: 193 | """Gets the preferred applications list.""" 194 | return self._preferred 195 | 196 | def add_blacklist(self, name: str) -> None: 197 | """ 198 | Adds an application to the blacklist. 199 | 200 | Args: 201 | name (str): The name of the application to add. 202 | """ 203 | if name not in self._blacklist: 204 | self._blacklist.append(name) 205 | self.reload() 206 | 207 | def get_blacklist(self) -> List[Union[str, None]]: 208 | """Gets the blacklist of applications.""" 209 | return self._blacklist 210 | 211 | def add_whitelist(self, name: str) -> None: 212 | """ 213 | Adds an application to the whitelist. 214 | 215 | Args: 216 | name (str): The name of the application to add. 217 | """ 218 | if name not in self._whitelist: 219 | self._whitelist.append(name) 220 | self.reload() 221 | 222 | def get_whitelist(self) -> List[Union[None, str]]: 223 | """Gets the whitelist of applications.""" 224 | return self._whitelist 225 | 226 | def query(self, keywords: str) -> List[Union[App, None]]: 227 | """ 228 | Queries applications based on keywords. 229 | 230 | Args: 231 | keywords (str): The keywords to search for. 232 | 233 | Returns: 234 | Union[List[App], List[None]]: List of matching applications. 235 | """ 236 | keywords = keywords.lower() 237 | return [ 238 | i for i in self.get_all() if i and keywords.lower() in i.keywords.lower() 239 | ] 240 | 241 | def _load_json(self) -> Dict[str, List[Union[str, None]]]: 242 | """ 243 | Loads JSON data from a file. 244 | 245 | Returns: 246 | dict: Loaded JSON data. 247 | """ 248 | try: 249 | with open(FILE_CACHE_APPS, "r") as file: 250 | return json.load(file) 251 | 252 | except json.decoder.JSONDecodeError: 253 | return {"preferred": [], "blacklist": [], "whitelist": []} 254 | 255 | def _save_json(self) -> None: 256 | """Saves JSON data to a file.""" 257 | data = { 258 | "preferred": self._preferred, 259 | "blacklist": self._blacklist, 260 | "whitelist": self._whitelist, 261 | } 262 | with open(FILE_CACHE_APPS, "w") as file: 263 | json.dump(data, file, indent=1) 264 | 265 | def reload(self) -> None: 266 | """Reloads the JSON data.""" 267 | self._save_json() 268 | self._json: Dict[str, List[Union[str, None]]] = self._load_json() 269 | self._preferred: List[Union[str, None]] = self._json["preferred"] 270 | self._blacklist: List[Union[str, None]] = self._json["blacklist"] 271 | self._whitelist: List[Union[str, None]] = self._json["whitelist"] 272 | 273 | def json(self) -> dict: 274 | """Returns a JSON representation of the applications.""" 275 | return self._json 276 | 277 | def __str__(self) -> str: 278 | """Returns a string representation of the JSON data.""" 279 | return str(self.json()) 280 | 281 | def __repr__(self) -> str: 282 | """Returns a string representation of the JSON data.""" 283 | return self.__str__() 284 | 285 | 286 | Applications = _Applications() 287 | -------------------------------------------------------------------------------- /PotatoWidgets/Services/Battery/__init__.py: -------------------------------------------------------------------------------- 1 | from ..._Logger import Logger 2 | from ...Imports import * 3 | from ..Service import Service 4 | 5 | 6 | class _BatteryService(Service): 7 | __gproperties__ = Service.properties( 8 | { 9 | "available": [bool], 10 | "percentage": [int], 11 | "state": [int], 12 | "icon-name": [str], 13 | "time-remaining": [int], 14 | "energy": [float], 15 | "energy-full": [float], 16 | "energy-rate": [float], 17 | } 18 | ) 19 | 20 | def __init__(self, battery: str = "/org/freedesktop/UPower/devices/battery_BAT1"): 21 | super().__init__() 22 | self._battery = battery 23 | 24 | self._available: bool = False 25 | self._percentage: int = -1 26 | self._state: int = 0 27 | self._icon_name: str = "battery-missing-symbolic" 28 | self._time_remaining: int = 0 29 | 30 | self._UPOWER_NAME = "org.freedesktop.UPower" 31 | self._UPOWER_PATH = "/org/freedesktop/UPower" 32 | 33 | self._DBUS_PROPERTIES = "org.freedesktop.DBus.Properties" 34 | self._bus = dbus.SystemBus() 35 | 36 | self._proxy = self._bus.get_object(self._UPOWER_NAME, self._battery) 37 | self._interface = dbus.Interface(self._proxy, self._DBUS_PROPERTIES) 38 | self._interface.connect_to_signal("PropertiesChanged", self._get_all) 39 | 40 | def bind( 41 | self, 42 | signal: Literal[ 43 | "available", 44 | "percentage", 45 | "state", 46 | "icon-name", 47 | "time-remaining", 48 | "energy", 49 | "energy-full", 50 | "energy-rate", 51 | ], 52 | format: Callable = lambda value: value, 53 | ): 54 | return super().bind(signal, format) 55 | 56 | def _get_all(self, *_) -> None: 57 | 58 | if not self._interface_prop("IsPresent"): 59 | self._available = False 60 | return 61 | 62 | data_key_value = { 63 | "_available": "IsPresent", 64 | "_percentage": "Percentage", 65 | "_state": "State", 66 | "_icon_name": "IconName", 67 | "_time_remaining": "TimeTo", 68 | } 69 | 70 | for key, value in data_key_value.items(): 71 | value = self._interface_prop(value) 72 | 73 | if value != getattr(self, key): 74 | setattr(self, key, value) 75 | 76 | _signal = key[1:].lower().replace("_", "-") # kebab-case for signals 77 | 78 | self.emit(_signal, value) 79 | 80 | else: 81 | continue 82 | 83 | def _interface_prop(self, prop: str) -> Any: 84 | if prop == "TimeTo": 85 | if self._interface_prop("State") == 1: 86 | _val = self._interface_prop("TimeToFull") 87 | else: 88 | _val = self._interface_prop("TimeToEmpty") 89 | else: 90 | _val = self._interface.Get(self._UPOWER_NAME + ".Device", prop) 91 | 92 | return _val 93 | 94 | @property 95 | def available(self) -> bool: 96 | return self._available 97 | 98 | @property 99 | def percentage(self) -> int: 100 | return self._percentage 101 | 102 | @property 103 | def icon_name(self) -> str: 104 | return self._icon_name 105 | 106 | @property 107 | def time_remaining(self) -> int: 108 | return self._time_remaining 109 | 110 | @property 111 | def state(self) -> int: 112 | return self._state 113 | 114 | 115 | BatteryService = _BatteryService() 116 | -------------------------------------------------------------------------------- /PotatoWidgets/Services/Hyprland/__init__.py: -------------------------------------------------------------------------------- 1 | from ..._Logger import Logger 2 | from ...Bash import Bash 3 | from ...Imports import * 4 | from ..Service import Service 5 | 6 | 7 | @dataclass 8 | class Client: 9 | address: str 10 | mapped: bool 11 | hidden: bool 12 | at: Tuple[int, int] 13 | size: Tuple[int, int] 14 | workspace: Dict[ 15 | Literal["id", "name"], 16 | int, 17 | ] 18 | floating: bool 19 | monitor: int 20 | class_: str 21 | title: str 22 | initialClass: str 23 | initialTitle: str 24 | pid: int 25 | xwayland: bool 26 | pinned: bool 27 | fullscreen: bool 28 | fullscreenMode: int 29 | fakeFullscreen: bool 30 | grouped: list 31 | swallowing: str 32 | focusHistoryID: int 33 | 34 | 35 | @dataclass 36 | class Workspace: 37 | id: int 38 | name: str 39 | monitor: str 40 | monitorId: int 41 | windows: int 42 | hasfullscreen: bool 43 | lastwindow: str 44 | lastwindowtitle: str 45 | 46 | 47 | @dataclass 48 | class Monitor: 49 | id: int 50 | name: str 51 | x: int 52 | y: int 53 | width: int 54 | height: int 55 | refreshRate: int 56 | reserved: Tuple[int, int, int, int] 57 | focused: bool 58 | description: str 59 | # make:str 60 | # model:str 61 | # serial:str 62 | 63 | 64 | # ToDo 65 | class _HyprlandService(Service): 66 | """ 67 | WIP; PLEASE DONT USE 68 | """ 69 | 70 | # https://wiki.hyprland.org/IPC/#events-list 71 | __gsignals__ = Service.signals( 72 | { 73 | "workspace": [[str]], 74 | "workspacev2": [[int, str]], 75 | "focusedmon": [[str, str]], 76 | "activewindow": [[str, str]], 77 | "activewindowv2": [[str]], 78 | # Monitor Things 79 | "fullscreen": [[bool]], 80 | "monitorremoved": [[str]], 81 | "monitoradded": [[str]], 82 | "monitoraddedv2": [[int, str, str]], 83 | # Workspace things 84 | "createworkspace": [[str]], 85 | "createworkspacev2": [[int, str]], 86 | "destroyworkspace": [[str]], 87 | "destroyworkspacev2": [[int, str]], 88 | "moveworkspace": [[str, str]], 89 | "moveworkspacev2": [[str, str, str]], 90 | "renameworkspace": [[str, str]], 91 | "activespecial": [[str, str]], 92 | # Windows 93 | "openwindow": [[str, str, str, str]], 94 | "closewindow": [[str]], 95 | "movewindow": [[str, str]], 96 | "movewindowv2": [[str, str, str]], 97 | "windowtitle": [[str]], 98 | "changefloatingmode": [[str, bool]], 99 | "minimize": [[str, bool]], 100 | "urgent": [[str]], 101 | "pin": [[str, bool]], 102 | # Layers 103 | "openlayer": [[str]], 104 | "closelayer": [[str]], 105 | # Other 106 | "activelayout": [[str, str]], 107 | "submap": [[str]], 108 | "screencast": [[bool, str]], 109 | "ignoregrouplock": [[bool]], 110 | "lockgroups": [[bool]], 111 | "configreloaded": [], 112 | } 113 | ) 114 | __gproperties__ = Service.properties( 115 | { 116 | "workspaces": [object], 117 | "monitors": [object], 118 | "clients": [object], 119 | } 120 | ) 121 | 122 | def __init__(self) -> None: 123 | super().__init__() 124 | self._XDG_RUNTIME_DIR: str = Bash.get_env("XDG_RUNTIME_DIR") 125 | self._SIGNATURE: str = Bash.get_env("HYPRLAND_INSTANCE_SIGNATURE") 126 | self._EVENTS_SOCKET: str = ( 127 | f"{self.XDG_RUNTIME_DIR}/hypr/{self.SIGNATURE}/.socket2.sock" 128 | ) 129 | self._COMMANDS_SOCKET: str = ( 130 | f"{self.XDG_RUNTIME_DIR}/hypr/{self.SIGNATURE}/.socket.sock" 131 | ) 132 | 133 | self._workspaces: List[Workspace] = [] 134 | self._monitors = [] 135 | self._windows = [] 136 | 137 | if not self.SIGNATURE: 138 | return Logger.ERROR("Hyprland Signature not found, is hyprland running?") 139 | 140 | self.__start__() 141 | for i in self.list_properties(): 142 | self.emit(i) 143 | 144 | def __on_connect(self, client, result, command, callback): 145 | def wrapper(stream, result, callback): 146 | data, _ = stream.read_upto_finish(result) 147 | callback(data) 148 | 149 | connection = client.connect_finish(result) 150 | output_stream = connection.get_output_stream() 151 | output_stream.write(command.encode()) 152 | output_stream.flush() 153 | 154 | input_stream = Gio.DataInputStream.new(connection.get_input_stream()) 155 | input_stream.read_upto_async( 156 | "\x04", -1, GLib.PRIORITY_DEFAULT, None, wrapper, callback 157 | ) 158 | 159 | def hyprctl_async(self, command: str, callback: Callable = lambda _: ()): 160 | if not self.SIGNATURE: 161 | return Logger.ERROR("Hyprland Signature not found, is hyprland running?") 162 | 163 | socket_address = Gio.UnixSocketAddress.new(self._COMMANDS_SOCKET) 164 | socket_client = Gio.SocketClient.new() 165 | socket_client.connect_async( 166 | socket_address, None, self.__on_connect, command, callback 167 | ) 168 | 169 | def hyprctl(self, command: str) -> str: 170 | if not self.SIGNATURE: 171 | Logger.ERROR("Hyprland Signature not found, is hyprland running?") 172 | return "" 173 | 174 | socket_address = Gio.UnixSocketAddress.new(self._COMMANDS_SOCKET) 175 | socket_client = Gio.SocketClient.new() 176 | connection = socket_client.connect(socket_address) 177 | output_stream = connection.get_output_stream() 178 | output_stream.write(command.encode()) 179 | output_stream.flush() 180 | 181 | input_stream = Gio.DataInputStream.new(connection.get_input_stream()) 182 | data, _ = input_stream.read_upto("\x04", -1) 183 | return data 184 | 185 | def connect( 186 | self, 187 | signal_name: Literal[ 188 | "workspace", 189 | "workspacev2", 190 | "focusedmon", 191 | "activewindow", 192 | "activewindowv2", 193 | "fullscreen", 194 | "monitorremoved", 195 | "monitoradded", 196 | "monitoraddedv2", 197 | "createworkspace", 198 | "createworkspacev2", 199 | "destroyworkspace", 200 | "destroyworkspacev2", 201 | "moveworkspace", 202 | "moveworkspacev2", 203 | "renameworkspace", 204 | "activespecial", 205 | "openwindow", 206 | "closewindow", 207 | "movewindow", 208 | "movewindowv2", 209 | "windowtitle", 210 | "changefloatingmode", 211 | "minimize", 212 | "urgent", 213 | "pin", 214 | "openlayer", 215 | "closelayer", 216 | "activelayout", 217 | "submap", 218 | "screencast", 219 | "ignoregrouplock", 220 | "lockgroups", 221 | "configreloaded", 222 | "workspaces", 223 | "monitors", 224 | "clients", 225 | ], 226 | callback: Callable, 227 | *args: Any, 228 | **kwargs: Any, 229 | ) -> Union[object, None]: 230 | return super().connect(signal_name, callback, *args, **kwargs) 231 | 232 | def __start__(self) -> None: 233 | def wrapper(datainput_stream: Gio.DataInputStream, res: Gio.Task) -> None: 234 | try: 235 | signal, data = datainput_stream.read_line_finish_utf8(res)[0].split( 236 | ">>" 237 | ) 238 | self.__handle_data_stream(signal, data) 239 | except Exception as r: 240 | Logger.ERROR(r) 241 | 242 | return datainput_stream.read_line_async( 243 | io_priority=GLib.PRIORITY_LOW, callback=wrapper 244 | ) 245 | 246 | socket_address: Gio.SocketAddress = Gio.UnixSocketAddress.new( 247 | self.EVENTS_SOCKET 248 | ) 249 | socket_client: Gio.SocketClient = Gio.SocketClient.new() 250 | socket_connection: Gio.SocketConnection = socket_client.connect(socket_address) 251 | datainput_stream: Gio.DataInputStream = Gio.DataInputStream.new( 252 | socket_connection.get_input_stream() 253 | ) 254 | 255 | return datainput_stream.read_line_async( 256 | io_priority=GLib.PRIORITY_LOW, callback=wrapper 257 | ) 258 | 259 | @property 260 | def XDG_RUNTIME_DIR(self) -> str: 261 | return self._XDG_RUNTIME_DIR 262 | 263 | @property 264 | def SIGNATURE(self) -> str: 265 | return self._SIGNATURE 266 | 267 | @property 268 | def COMMANDS_SOCKET(self) -> str: 269 | return self._EVENTS_SOCKET 270 | 271 | @property 272 | def EVENTS_SOCKET(self) -> str: 273 | return self._EVENTS_SOCKET 274 | 275 | def __handle_data_stream(self, signal: str, args: str) -> None: 276 | try: 277 | _args: Union[tuple, list] 278 | 279 | match signal: 280 | case "workspacev2": 281 | _args = args.split(",", 1) 282 | _args[0] = int(_args[0]) 283 | case "fullscreen" | "lockgroups" | "ignoregrouplock": 284 | _args = args.split(",") 285 | _args = [bool(_args[0])] 286 | case "openwindow": 287 | _args = args.split(",", 3) 288 | case "activewindow": 289 | _args = args.split(",", 1) 290 | case "createworkspacev2" | "destroyworkspacev2": 291 | _args = args.split(",", 1) 292 | _args[0] = int(_args[0]) 293 | case _: 294 | _args = args.split(",") 295 | 296 | # print(signal, ",".join(str(i) for i in _args)) 297 | self.emit(signal, *_args) 298 | except Exception as e: 299 | Logger.DEBUG("HyprlandService error, ignore this shit") 300 | print("------") 301 | print(e) 302 | print(f"SIGNAL: {signal}, ARGS: {args}") 303 | print("------") 304 | 305 | 306 | HyprlandService = _HyprlandService() 307 | -------------------------------------------------------------------------------- /PotatoWidgets/Services/Notifications/__init__.py: -------------------------------------------------------------------------------- 1 | from ..._Logger import Logger 2 | from ...Env import DIR_CACHE_NOTIF_IMAGES, FILE_CACHE_NOTIF 3 | from ...Imports import * 4 | from ...Methods import idle, make_async, parse_interval, wait 5 | from ..Service import BaseGObjectClass, Service 6 | 7 | # 8 | # Notification Structure 9 | # 10 | # notify-send -a "github" "Welcome to my setup" "This is a notification using potato, very cool huh?" -A Id_0=Pretty_Text -t 2000 11 | # 12 | # { 13 | # "name": "github", 14 | # "id": 10, 15 | # "image": "", 16 | # "summary": "Welcome to my setup", 17 | # "body": "This is a notification using potato, very cool huh?", 18 | # "urgency": "normal", 19 | # "actions": [{"id": "Id_0", "label": "Pretty_Text"}], 20 | # "hints": {"urgency": 1, "sender-pid": 23898}, 21 | # "timeout": 2000, 22 | # } 23 | 24 | org_freedesktop_Notifications_xml = """ 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | """ 66 | 67 | NodeInfo = Gio.DBusNodeInfo.new_for_xml(org_freedesktop_Notifications_xml) 68 | 69 | 70 | class Notification(BaseGObjectClass): 71 | __gsignals__ = Service.signals( 72 | { 73 | "dismiss": [], 74 | "closed": [], 75 | "action": [[str]], 76 | } 77 | ) 78 | 79 | __gproperties__ = Service.properties({}) 80 | 81 | def __init__( 82 | self, 83 | name: str, 84 | id: int, 85 | image: str, 86 | summary: str, 87 | body: str, 88 | actions: list, 89 | urgency: str, 90 | hints: Dict[str, Any], 91 | timeout: int, 92 | ) -> None: 93 | super().__init__() 94 | 95 | self._id: int = int(id) 96 | self._name: str = str(name) 97 | self._image: str = str(image) 98 | self._summary: str = str(summary) 99 | self._body: str = str(body) 100 | self._actions: list = list(actions) 101 | self._urgency: str = str(urgency) 102 | self._timeout: int = int(timeout) 103 | 104 | self._hints: Dict[str, Any] = dict(hints) 105 | 106 | if "image-data" in self._hints: 107 | del self._hints["image-data"] 108 | 109 | def bind( 110 | self, 111 | signal: Literal["dismiss", "close", "action"], 112 | format: Callable = lambda value: value, 113 | ): 114 | return super().bind(signal, format) 115 | 116 | def dismiss(self) -> None: 117 | self.emit("dismiss") 118 | 119 | def close(self) -> None: 120 | self.emit("closed") 121 | 122 | def action(self, action: str) -> None: 123 | self.emit("action", action) 124 | 125 | @property 126 | def id(self) -> int: 127 | return self._id 128 | 129 | @property 130 | def name(self) -> str: 131 | return self._name 132 | 133 | @property 134 | def image(self) -> str: 135 | return self._image 136 | 137 | @property 138 | def summary(self) -> str: 139 | return self._summary 140 | 141 | @property 142 | def body(self) -> str: 143 | return self._body 144 | 145 | @property 146 | def actions(self) -> list: 147 | return self._actions 148 | 149 | @property 150 | def urgency(self) -> str: 151 | return self._urgency 152 | 153 | @property 154 | def hints(self) -> Dict[str, Any]: 155 | return self._hints 156 | 157 | @property 158 | def timeout(self) -> int: 159 | return self._timeout 160 | 161 | def json(self) -> dict: 162 | return { 163 | "name": self.name, 164 | "id": self.id, 165 | "image": self.image, 166 | "summary": self.summary, 167 | "body": self.body, 168 | "urgency": self.urgency, 169 | "actions": self.actions, 170 | "hints": self.hints, 171 | "timeout": self.timeout, 172 | } 173 | 174 | 175 | class _NotificationsService(Service): 176 | 177 | __gproperties__ = Service.properties( 178 | { 179 | "notifications": [object], 180 | "popups": [object], 181 | "count": [int], 182 | "dnd": [bool], 183 | } 184 | ) 185 | __gsignals__ = Service.signals( 186 | { 187 | "notified": [[int]], 188 | "dismissed": [[int]], 189 | "closed": [[int]], 190 | "popup": [[int]], 191 | } 192 | ) 193 | 194 | def __init__(self, *args, **kwargs) -> None: 195 | super().__init__(*args, **kwargs) 196 | self._json: Dict[str, Any] = self.__load_json() 197 | self._dnd: bool = self._json["dnd"] 198 | self._count: int = self._json["count"] 199 | self._notifications: List[Notification] = self._json["notifications"] 200 | self._popups: List[Notification] = [] 201 | self._timeout: int = 4500 202 | self._connection: Gio.DBusConnection 203 | self.__register__() 204 | 205 | def bind( 206 | self, 207 | signal: Literal[ 208 | "closed", 209 | "action", 210 | "notified", 211 | "dismissed", 212 | "popup", 213 | "count", 214 | "dnd", 215 | "notifications", 216 | "popups", 217 | ], 218 | format: Callable = lambda value: value, 219 | ): 220 | return super().bind(signal, format) 221 | 222 | def emit( 223 | self, 224 | signal_name: Literal[ 225 | "closed", 226 | "action", 227 | "notified", 228 | "dismissed", 229 | "popup", 230 | "count", 231 | "dnd", 232 | "notifications", 233 | "popups", 234 | ], 235 | *args: Any, 236 | **kwargs: Any, 237 | ) -> Union[Any, None]: 238 | return super().emit(signal_name, *args, **kwargs) 239 | 240 | def __add_notif(self, notif: Notification) -> None: 241 | self._count += 1 242 | self.emit("count") 243 | self._popups.append(notif) 244 | self._notifications.append(notif) 245 | 246 | if not self.dnd: 247 | self.emit("popup", notif.id) 248 | self.emit("popups") 249 | 250 | if self.timeout > 0: 251 | wait(self.timeout, notif.dismiss) 252 | 253 | self.emit("notified", notif.id) 254 | self.emit("notifications") 255 | 256 | notif.connect("dismiss", self.__on_dismiss) 257 | notif.connect("closed", self.__on_close) 258 | notif.connect("action", self.__on_action) 259 | 260 | def __on_action(self, notif: Notification, action: str) -> None: 261 | self.InvokeAction(notif.id, action) 262 | 263 | 264 | def __on_dismiss(self, notif: Notification) -> None: 265 | if notif in self._popups: 266 | self.emit("dismissed", notif.id) 267 | self.emit("popups") 268 | del self._popups[self._popups.index(notif)] 269 | 270 | def __on_close(self, notif: Notification) -> None: 271 | 272 | if notif in self._popups: 273 | self.emit("dismissed", notif.id) 274 | self.emit("popups") 275 | del self._popups[self._popups.index(notif)] 276 | 277 | if notif in self._notifications: 278 | self._count -= 1 279 | self.emit("closed", notif.id) 280 | self.emit("count") 281 | del self._notifications[self._notifications.index(notif)] 282 | 283 | self.CloseNotification(notif.id) 284 | 285 | # 286 | # 287 | # 288 | # 289 | # 290 | 291 | @property 292 | def count(self) -> int: 293 | return self._count 294 | 295 | @property 296 | def timeout(self) -> int: 297 | return self._timeout 298 | 299 | @timeout.setter 300 | def timeout(self, new_timeout: Union[str, int]) -> None: 301 | new_timeout = parse_interval(new_timeout) 302 | if new_timeout > 0: 303 | self._timeout = new_timeout 304 | else: 305 | self._timeout = 4500 306 | 307 | @property 308 | def dnd(self) -> bool: 309 | return self._dnd 310 | 311 | @dnd.setter 312 | def dnd(self, new_value: bool) -> None: 313 | self._dnd = new_value 314 | self.emit("dnd") 315 | 316 | @property 317 | def notifications(self) -> List[Notification]: 318 | return self._notifications 319 | 320 | @property 321 | def popups(self) -> List[Notification]: 322 | return self._popups 323 | 324 | def get_popup(self, id: int) -> Union[Notification, None]: 325 | return next((i for i in self._popups if i.id == id), None) 326 | 327 | def get_notification(self, id: int) -> Union[Notification, None]: 328 | return next((i for i in self._notifications if id == i.id), None) 329 | 330 | def get_notifications(self) -> List[Notification]: 331 | return self.notifications 332 | 333 | def get_popups(self) -> List[Notification]: 334 | return self.popups 335 | 336 | @make_async 337 | def clear(self) -> None: 338 | if not self.notifications: 339 | return 340 | 341 | for notif in self._notifications[::]: 342 | if notif in self._popups: 343 | self.emit("dismissed", notif.id) 344 | 345 | if notif in self._notifications: 346 | self.emit("closed", notif.id) 347 | self.CloseNotification(notif.id) 348 | 349 | self._count = 0 350 | self._notifications = [] 351 | self._popups = [] 352 | 353 | self.__save_json() 354 | self.emit("count") 355 | self.emit("notifications") 356 | self.emit("popups") 357 | 358 | def __load_json( 359 | self, 360 | ) -> Dict[str, Union[bool, int, List[None], List[Notification]]]: 361 | try: 362 | with open(FILE_CACHE_NOTIF, "r") as file: 363 | data = json.load(file) 364 | 365 | return_data = { 366 | "dnd": False, 367 | "count": 0, 368 | "notifications": [], 369 | } 370 | 371 | return_data["dnd"] = data.get("dnd", False) 372 | return_data["count"] = data.get("count", 0) 373 | return_data["notifications"] = [ 374 | Notification( 375 | id=i["id"], 376 | name=i["name"], 377 | image=i["image"], 378 | summary=i["summary"], 379 | body=i["body"], 380 | urgency=i["urgency"], 381 | actions=i["actions"], 382 | hints=i["hints"], 383 | timeout=i["timeout"], 384 | ) 385 | for i in data.get("notifications", []) 386 | ] 387 | return return_data 388 | 389 | except json.decoder.JSONDecodeError: 390 | return { 391 | "dnd": False, 392 | "count": 0, 393 | "notifications": [], 394 | } 395 | 396 | def __save_json(self) -> None: 397 | data = { 398 | "dnd": self.dnd, 399 | "count": self.count, 400 | "notifications": [i.json() for i in self.notifications], 401 | } 402 | 403 | with open(FILE_CACHE_NOTIF, "w") as file: 404 | json.dump(data, file) 405 | 406 | # 407 | # Other 408 | # 409 | 410 | def __get_id(self, new_id) -> int: 411 | if new_id != 0: 412 | return new_id 413 | else: 414 | notifs = self.notifications 415 | if notifs and notifs[-1]: 416 | return notifs[-1].id + 1 417 | else: 418 | return 1 419 | 420 | def __get_urgency(self, hints: Dict[str, Any]) -> str: 421 | return {0: "low", 1: "normal", 2: "critical"}.get( 422 | hints.get("urgency", 0), "low" 423 | ) 424 | 425 | def __get_actions(self, actions: List[str]) -> List[Union[Dict[str, str], None]]: 426 | return [ 427 | { 428 | "id": actions[i], 429 | "label": actions[i + 1], 430 | } 431 | for i in range(0, len(actions), 2) 432 | ] 433 | 434 | def __decode_icon(self, hints: Dict[str, Any], id: int) -> str: 435 | _hint: Union[list, None] = hints.get("image-data") 436 | 437 | if _hint: 438 | image_path: str = f"{DIR_CACHE_NOTIF_IMAGES}/{id}.png" 439 | 440 | self.__save_icon_async(_hint, image_path) 441 | 442 | return image_path 443 | 444 | return "" 445 | 446 | @make_async 447 | def __save_icon_async(self, _hint, image_path) -> None: 448 | GdkPixbuf.Pixbuf.new_from_bytes( 449 | data=GLib.Bytes(_hint[6]), 450 | colorspace=GdkPixbuf.Colorspace.RGB, 451 | has_alpha=_hint[3], 452 | bits_per_sample=_hint[4], 453 | width=_hint[0], 454 | height=_hint[1], 455 | rowstride=_hint[2], 456 | ).savev(image_path, "png") 457 | 458 | ## Dbus Methods-Signals and that stuff 459 | 460 | # 461 | # Pair Method-Signal 462 | # 463 | 464 | def CloseNotification(self, id: int, reason:int=3) -> None: 465 | self.NotificationClosed(id, reason) 466 | 467 | def NotificationClosed(self, id: int, reason: int) -> None: 468 | self._connection.emit_signal( 469 | "org.freedesktop.Notifications", 470 | "/org/freedesktop/Notifications", 471 | "org.freedesktop.Notifications", 472 | "NotificationClosed", 473 | GLib.Variant("(uu)", (id, reason)), 474 | ) 475 | 476 | def InvokeAction(self, id: int, action: str) -> None: 477 | self.ActionInvoked(id, action) 478 | 479 | def ActionInvoked(self, id: int, action: str) -> None: 480 | self._connection.emit_signal( 481 | "org.freedesktop.Notifications", 482 | "/org/freedesktop/Notifications", 483 | "org.freedesktop.Notifications", 484 | "ActionInvoked", 485 | GLib.Variant("(us)", (id, action)), 486 | ) 487 | 488 | def GetServerInformation(self) -> GLib.Variant: 489 | return GLib.Variant( 490 | "(ssss)", 491 | ( 492 | "Potato Notification Daemon", 493 | "t0kyob0y", 494 | "0.1", 495 | "1.2" 496 | ) 497 | ) 498 | 499 | def GetCapabilities(self) -> GLib.Variant: 500 | return GLib.Variant( 501 | "(as)", 502 | ( 503 | ( 504 | "action-icons", 505 | "actions", 506 | "body", 507 | "body-hyperlinks", 508 | "body-markup", 509 | "icon-static", 510 | "persistence", 511 | ), 512 | ), 513 | ) 514 | 515 | def Notify( 516 | self, 517 | name: str, 518 | id: int, 519 | image: str, 520 | summary: str, 521 | body: str, 522 | actions: List[str], 523 | hints: Dict[str, Any], 524 | timeout: int, 525 | ) -> GLib.Variant: 526 | 527 | _id: int = self.__get_id(id) 528 | _urgency: str = self.__get_urgency(hints) 529 | _actions: list = self.__get_actions(actions) 530 | _image: str = image or self.__decode_icon(hints, id) 531 | 532 | notif = Notification( 533 | name=name, 534 | id=_id, 535 | image=_image, 536 | urgency=_urgency, 537 | summary=summary, 538 | body=body, 539 | actions=_actions, 540 | hints=hints, 541 | timeout=timeout, 542 | ) 543 | 544 | self.__add_notif(notif) 545 | self.__save_json() 546 | return GLib.Variant("(u)", (notif.id,)) 547 | 548 | ## DBus Stuff 549 | 550 | def __register__(self) -> None: 551 | # https://lazka.github.io/pgi-docs/index.html#Gio-2.0/functions.html#Gio.bus_own_name 552 | Gio.bus_own_name( 553 | Gio.BusType.SESSION, # Bus_Type 554 | "org.freedesktop.Notifications", # Name 555 | Gio.BusNameOwnerFlags.DO_NOT_QUEUE, # Flags 556 | self.__on_success__, # bus_acquired_closure 557 | None, # name_acquired_closure 558 | self.__on_failed__, # name_lost_closure 559 | ) 560 | 561 | def __on_success__( 562 | self, 563 | Connection: Gio.DBusConnection, 564 | BusName: Literal["org.freedesktop.Notifications"], 565 | ): 566 | self._connection = Connection 567 | Connection.register_object( 568 | "/org/freedesktop/Notifications", 569 | NodeInfo.interfaces[0], 570 | self.__on_call__, 571 | ) 572 | 573 | def __on_failed__( 574 | self, 575 | Connection: Gio.DBusConnection, 576 | BusName: Literal["org.freedesktop.Notifications"], 577 | ): 578 | Name = Connection.call_sync( 579 | bus_name=BusName, 580 | object_path="/org/freedesktop/Notifications", 581 | interface_name=BusName, 582 | method_name="GetServerInformation", 583 | parameters=GLib.Variant("()", ()), 584 | reply_type=GLib.VariantType.new("(ssss)"), 585 | flags=Gio.DBusCallFlags.NONE, 586 | timeout_msec=-1, 587 | ) 588 | 589 | Logger.ERROR("There is already a notifications daemon running:", *Name) 590 | 591 | def __on_call__( 592 | self, 593 | Connection: Gio.DBusConnection, 594 | Sender: str, 595 | Path: Literal["/org/freedesktop/Notifications"], 596 | BusName: Literal["org.freedesktop.Notifications"], 597 | Method: str, 598 | Parameters: tuple, 599 | MethodInvocation: Gio.DBusMethodInvocation, 600 | ): 601 | try: 602 | match Method: 603 | case "GetServerInformation": 604 | MethodInvocation.return_value(self.GetServerInformation()) 605 | case "GetCapabilities": 606 | MethodInvocation.return_value(self.GetCapabilities()) 607 | case "Notify": 608 | MethodInvocation.return_value(self.Notify(*Parameters)) 609 | case "CloseNotification": 610 | MethodInvocation.return_value(self.CloseNotification(*Parameters)) 611 | case "InvokeAction": 612 | MethodInvocation.return_value(self.InvokeAction(*Parameters)) 613 | case _: 614 | print("Cllback no llamado", Method, Parameters) 615 | except Exception as r: 616 | Logger.ERROR(r) 617 | #finally: 618 | # return Connection.flush() 619 | 620 | 621 | NotificationsService = _NotificationsService() 622 | -------------------------------------------------------------------------------- /PotatoWidgets/Services/Style/__init__.py: -------------------------------------------------------------------------------- 1 | from ...Env import FILE_CACHE_CSS 2 | from ...Imports import * 3 | from ..Service import Service 4 | 5 | 6 | class _Style(Service): 7 | @staticmethod 8 | def load_css(css_path) -> None: 9 | 10 | if css_path.endswith(".scss"): 11 | try: 12 | if GLib.file_test(FILE_CACHE_CSS, GLib.FileTest.EXISTS): 13 | GLib.spawn_command_line_sync(f"rm {FILE_CACHE_CSS}") 14 | 15 | GLib.spawn_command_line_sync(f"sassc {css_path} {FILE_CACHE_CSS} ") 16 | 17 | except Exception as e: 18 | print(f"Error transpiling SCSS:") 19 | print(e) 20 | else: 21 | pass 22 | 23 | try: 24 | # Load Provider 25 | css_provider = Gtk.CssProvider() 26 | # Load File 27 | css_provider.load_from_path(FILE_CACHE_CSS) 28 | 29 | # Get Default Screen 30 | screen: Gdk.Screen = Gdk.Screen.get_default() 31 | # Get StyleConext 32 | style_context: Gtk.StyleContext = Gtk.StyleContext() 33 | # Load Provider 34 | style_context.add_provider_for_screen( 35 | screen=screen, provider=css_provider, priority=600 36 | ) 37 | except Exception as e: 38 | print(f"Error loading CSS file:") 39 | print(e) 40 | 41 | @staticmethod 42 | def rgb(red, green, blue): 43 | return "#{:02x}{:02x}{:02x}".format(red, green, blue) 44 | 45 | @staticmethod 46 | def rgba(red, green, blue, alpha): 47 | alpha = round(alpha * 255) 48 | return "#{:02x}{:02x}{:02x}{:02x}".format(red, green, blue, alpha) 49 | 50 | @staticmethod 51 | def mix(color_1, color_2, percentage): 52 | if percentage < 0 or percentage > 1: 53 | raise ValueError("El porcentaje debe estar entre 0 y 1") 54 | 55 | r1, g1, b1 = int(color_1[1:3], 16), int(color_1[3:5], 16), int(color_1[5:7], 16) 56 | r2, g2, b2 = int(color_2[1:3], 16), int(color_2[3:5], 16), int(color_2[5:7], 16) 57 | 58 | r = round(r1 * (1 - percentage) + r2 * percentage) 59 | g = round(g1 * (1 - percentage) + g2 * percentage) 60 | b = round(b1 * (1 - percentage) + b2 * percentage) 61 | 62 | return _Style.rgb(r, g, b) 63 | 64 | 65 | Style = _Style() 66 | -------------------------------------------------------------------------------- /PotatoWidgets/Services/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module includes various classes related to application management, battery services, 3 | notification handling, service management, and styling. 4 | 5 | App: Class for managing applications. 6 | Applications: Module for managing multiple applications. 7 | BatteryService: Class for managing battery-related services. 8 | HyprlandService: Class for managing Hyprland services. 9 | Notification: Class for handling notifications. 10 | NotificationsDbusService: Class for managing notifications via D-Bus. 11 | NotificationsService: Class for managing notification services. 12 | Service: Base class for managing services. 13 | ServiceChildren: Class for managing child services. 14 | Style: Module for styling applications and widgets. 15 | """ 16 | 17 | __all__ = [ 18 | "Applications", 19 | "BatteryService", 20 | "NotificationsService", 21 | # "TrayService", 22 | ] 23 | from .Applications import App, Applications 24 | from .Battery import BatteryService 25 | from .Notifications import Notification, NotificationsService 26 | from .Service import Service 27 | from .Style import Style 28 | 29 | # from .Tray import TrayItem, TrayService 30 | -------------------------------------------------------------------------------- /PotatoWidgets/Variable.py: -------------------------------------------------------------------------------- 1 | from .Services.Service import Listener, Poll, Variable 2 | 3 | __all__ = ["Listener", "Poll", "Variable"] 4 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Box.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Box(Gtk.Box, BasicProps): 7 | def __init__( 8 | self, 9 | children: Union[List[Union[Gtk.Widget, None]], Gtk.Widget, Any] = [], 10 | orientation: Union[ 11 | Gtk.Orientation, Literal["h", "horizontal", "v", "vertical"] 12 | ] = "h", 13 | spacing: int = 0, 14 | homogeneous: bool = False, 15 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 16 | attributes: Callable = lambda self: self, 17 | css: str = "", 18 | halign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 19 | valign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 20 | hexpand: bool = False, 21 | vexpand: bool = False, 22 | visible: bool = True, 23 | classname="", 24 | ) -> None: 25 | Gtk.Box.__init__(self, spacing=spacing) 26 | 27 | BasicProps.__init__( 28 | self, 29 | size=size, 30 | css=css, 31 | halign=halign, 32 | valign=valign, 33 | hexpand=hexpand, 34 | vexpand=vexpand, 35 | active=True, 36 | visible=visible, 37 | classname=classname, 38 | ) 39 | 40 | self.set_children(children) 41 | self.set_orientation(orientation) 42 | self.set_visible(visible) 43 | self.set_homogeneous(homogeneous) if homogeneous else None 44 | attributes(self) 45 | 46 | for key, value in locals().items(): 47 | if key not in [ 48 | "self", 49 | "halign", 50 | "valign", 51 | "hexpand", 52 | "vexpand", 53 | "visible", 54 | "active", 55 | "visible", 56 | "classname", 57 | ] and isinstance(value, (Listener, Poll, Variable)): 58 | callback = { 59 | "orientation": self.set_orientation, 60 | "visible": self.set_visible, 61 | "spacing": self.set_spacing, 62 | "homogeneous": self.set_homogeneous, 63 | "children": self.set_children, 64 | }.get(key) 65 | 66 | if not callback: 67 | continue 68 | 69 | self.bind(value, callback) 70 | 71 | def add(self, widget: Union[Gtk.Widget, None]) -> None: 72 | if widget: 73 | return super().add(widget) 74 | 75 | def remove(self, widget: Union[Gtk.Widget, None]) -> None: 76 | if widget: 77 | return super().remove(widget) 78 | 79 | def set_orientation( 80 | self, 81 | orientation: Union[ 82 | Gtk.Orientation, Literal["h", "horizontal", "v", "vertical"] 83 | ] = Gtk.Orientation.HORIZONTAL, 84 | ) -> None: 85 | 86 | _orientation: Gtk.Orientation 87 | 88 | if isinstance(orientation, (Gtk.Orientation)): 89 | _orientation = orientation 90 | elif orientation in ["h", "horizontal"]: 91 | _orientation = Gtk.Orientation.HORIZONTAL 92 | elif orientation in ["v", "vertical"]: 93 | _orientation = Gtk.Orientation.VERTICAL 94 | else: 95 | return 96 | 97 | super().set_orientation(_orientation) 98 | 99 | def set_children( 100 | self, 101 | newChildren: Union[List[Union[Gtk.Widget, None]], Gtk.Widget, Any], 102 | ) -> None: 103 | 104 | if newChildren is None: 105 | return 106 | 107 | for children in self.get_children(): 108 | if children not in newChildren: 109 | children.destroy() 110 | self.remove(children) 111 | 112 | if isinstance(newChildren, (list)): 113 | for children in newChildren: 114 | if isinstance(children, (Gtk.Widget)): 115 | self.add(children) 116 | 117 | elif isinstance(newChildren, (Gtk.Widget)): 118 | self.add(newChildren) 119 | 120 | self.show_all() 121 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Button.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Button(Gtk.Button, BasicProps): 7 | def __init__( 8 | self, 9 | children: Union[Gtk.Widget, None] = None, 10 | onclick: Union[Callable, None] = None, 11 | onmiddleclick: Union[Callable, None] = None, 12 | onhover: Union[Callable, None] = None, 13 | onhoverlost: Union[Callable, None] = None, 14 | primaryhold: Union[Callable, None] = None, 15 | primaryrelease: Union[Callable, None] = None, 16 | secondaryhold: Union[Callable, None] = None, 17 | secondaryrelease: Union[Callable, None] = None, 18 | attributes: Callable = lambda self: self, 19 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 20 | css: str = "", 21 | classname: str = "", 22 | halign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 23 | valign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 24 | hexpand: bool = False, 25 | vexpand: bool = False, 26 | visible: bool = True, 27 | active: bool = True, 28 | ) -> None: 29 | 30 | Gtk.Button.__init__(self) 31 | 32 | BasicProps.__init__( 33 | self, 34 | css=css, 35 | size=size, 36 | halign=halign, 37 | valign=valign, 38 | hexpand=hexpand, 39 | vexpand=vexpand, 40 | active=active, 41 | visible=visible, 42 | classname=classname, 43 | ) 44 | 45 | attributes(self) if attributes else None 46 | 47 | if children: 48 | self.add(children) 49 | 50 | self.dict = { 51 | "onclick": onclick, 52 | "onmiddleclick": onmiddleclick, 53 | "onhover": onhover, 54 | "onhoverlost": onhoverlost, 55 | "primaryhold": primaryhold, 56 | "primaryrelease": primaryrelease, 57 | "secondaryhold": secondaryhold, 58 | "secondaryrelease": secondaryrelease, 59 | } 60 | 61 | self.connect("clicked", self.__click_event_idle) if onclick else None 62 | 63 | self.connect( 64 | "button-press-event", 65 | self.__press_event, 66 | ) 67 | 68 | self.connect( 69 | "button-release-event", 70 | self.__release_event, 71 | ) 72 | 73 | self.connect( 74 | "enter-notify-event", 75 | self.__enter_event, 76 | ) 77 | 78 | self.connect( 79 | "leave-notify-event", 80 | self.__leave_event, 81 | ) 82 | 83 | def __clasif_args(self, widget, event, callback: Callable) -> None: 84 | arg_num = callback.__code__.co_argcount 85 | arg_tuple = callback.__code__.co_varnames[:arg_num] 86 | 87 | match arg_num: 88 | case 2: 89 | callback(widget=widget, event=event) 90 | case 1: 91 | if "widget" in arg_tuple and widget: 92 | callback(widget=widget) 93 | elif "event" in arg_tuple and event: 94 | callback(event=event) 95 | else: 96 | callback(event) 97 | case _: 98 | callback() 99 | 100 | def __click_event_idle(self, event): 101 | callback = self.dict.get("onclick") 102 | 103 | if callback: 104 | self.__clasif_args(widget=False, event=event, callback=callback) 105 | 106 | def __press_event(self, widget, event): 107 | match event.button: 108 | case Gdk.BUTTON_PRIMARY: 109 | callback = self.dict.get("primaryhold") 110 | case Gdk.BUTTON_SECONDARY: 111 | callback = self.dict.get("secondaryhold") 112 | case Gdk.BUTTON_MIDDLE: 113 | callback = self.dict.get("onmiddleclick") 114 | case _: 115 | callback = None 116 | 117 | if callback: 118 | self.__clasif_args(widget=widget, event=event, callback=callback) 119 | 120 | def __release_event(self, widget, event): 121 | match event.button: 122 | case Gdk.BUTTON_PRIMARY: 123 | callback = self.dict.get("primaryrelease") 124 | case Gdk.BUTTON_SECONDARY: 125 | callback = self.dict.get("secondaryrelease") 126 | case _: 127 | callback = None 128 | 129 | if callback: 130 | self.__clasif_args(widget=widget, event=event, callback=callback) 131 | 132 | def __enter_event(self, widget, event): 133 | callback = self.dict.get("onhover") 134 | if callback: 135 | self.__clasif_args(widget=widget, event=event, callback=callback) 136 | 137 | def __leave_event(self, widget, event): 138 | callback = self.dict.get("onhoverlost") 139 | 140 | if callback: 141 | self.__clasif_args(widget=widget, event=event, callback=callback) 142 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/CenterBox.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from .Box import Box 3 | 4 | 5 | class CenterBox(Box): 6 | def __init__( 7 | self, 8 | start: Gtk.Widget, 9 | center: Gtk.Widget, 10 | end: Gtk.Widget, 11 | orientation: str = "h", 12 | size: Union[int, str, List[Union[int, str]]] = [5, 5], 13 | attributes: Callable = lambda self: self, 14 | css: str = "", 15 | halign: str = "fill", 16 | valign: str = "fill", 17 | hexpand: bool = False, 18 | vexpand: bool = False, 19 | visible: bool = True, 20 | classname: str = "", 21 | ) -> None: 22 | super().__init__( 23 | orientation=orientation, 24 | size=size, 25 | attributes=attributes, 26 | css=css, 27 | halign=halign, 28 | valign=valign, 29 | hexpand=hexpand, 30 | vexpand=vexpand, 31 | visible=visible, 32 | classname=classname, 33 | ) 34 | 35 | self._start_widget = None 36 | self._center_widget = None 37 | self._end_widget = None 38 | 39 | self.start_widget = start 40 | self.center_widget = center 41 | self.end_widget = end 42 | 43 | @property 44 | def start_widget(self) -> Union[Gtk.Widget, None]: 45 | return self._start_widget 46 | 47 | @start_widget.setter 48 | def start_widget(self, start: Gtk.Widget) -> None: 49 | if self._start_widget: 50 | self._start_widget.destroy() 51 | 52 | self._start_widget = start 53 | 54 | if start: 55 | self.pack_start(start, False, False, 0) 56 | 57 | @property 58 | def end_widget(self) -> Union[Gtk.Widget, None]: 59 | return self._end_widget 60 | 61 | @end_widget.setter 62 | def end_widget(self, end: Gtk.Widget) -> None: 63 | if self._end_widget: 64 | self._end_widget.destroy() 65 | 66 | self._end_widget = end 67 | 68 | if end: 69 | self.pack_end(self._end_widget, False, False, 0) 70 | 71 | @property 72 | def center_widget(self) -> Union[Gtk.Widget, None]: 73 | return self._center_widget 74 | 75 | @center_widget.setter 76 | def center_widget(self, center: Gtk.Widget) -> None: 77 | if self._center_widget: 78 | self._center_widget.destroy() 79 | self._center_widget = center 80 | 81 | super().set_center_widget(center) 82 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/CheckBox.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Button import Button 4 | from .Common import BasicProps 5 | 6 | 7 | class CheckBox(Gtk.CheckButton, Button): 8 | def __init__( 9 | self, 10 | children: Gtk.Widget, 11 | onclick: Union[Callable, None] = None, 12 | halign: str = "fill", 13 | valign: str = "fill", 14 | hexpand: bool = False, 15 | vexpand: bool = False, 16 | active: bool = True, 17 | visible: bool = True, 18 | classname: str = "", 19 | ): 20 | super().__init__() 21 | 22 | Button.__init__( 23 | self, 24 | children=children, 25 | onclick=onclick, 26 | halign=halign, 27 | valign=valign, 28 | hexpand=hexpand, 29 | vexpand=vexpand, 30 | active=active, 31 | visible=visible, 32 | classname=classname, 33 | ) 34 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/ComboBox.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from .Common import BasicProps 3 | 4 | 5 | class ComboBox(Gtk.ComboBoxText, BasicProps): 6 | def __init__( 7 | self, 8 | children: List[Any] = [], 9 | css: str = "", 10 | halign: str = "fill", 11 | valign: str = "fill", 12 | hexpand: bool = False, 13 | vexpand: bool = False, 14 | active: bool = True, 15 | visible: bool = True, 16 | classname: str = "", 17 | ): 18 | Gtk.ComboBoxText.__init__(self) 19 | 20 | BasicProps.__init__( 21 | self, 22 | css=css, 23 | halign=halign, 24 | valign=valign, 25 | hexpand=hexpand, 26 | vexpand=vexpand, 27 | active=active, 28 | visible=visible, 29 | classname=classname, 30 | ) 31 | for i in children: 32 | try: 33 | self.append_text(str(i)) 34 | except: 35 | continue 36 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Common/BasicProps.py: -------------------------------------------------------------------------------- 1 | from ...Imports import * 2 | from ...Methods import get_screen_size, parse_screen_size 3 | from ...Variable import Listener, Poll, Variable 4 | 5 | 6 | class BasicProps(Gtk.Widget): 7 | def __init__( 8 | self, 9 | halign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 10 | valign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 11 | hexpand: bool = False, 12 | vexpand: bool = False, 13 | classname: str = "", 14 | # tooltip, 15 | css: str = "", 16 | visible: bool = True, 17 | active: bool = True, 18 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 19 | attributes: Callable = lambda self: self, 20 | ) -> None: 21 | Gtk.Widget.__init__(self) 22 | self._default_classnames = self.get_style_context().list_classes() 23 | self.set_hexpand(hexpand) 24 | self.set_vexpand(vexpand) 25 | self.set_halign(halign) 26 | self.set_valign(valign) 27 | self.set_visible(visible) 28 | self.set_active(active) 29 | self.set_size(size) if size else None 30 | self.set_classname(classname) 31 | self._rand_classname = "" 32 | 33 | self.set_css(css) if css else None 34 | 35 | for key, value in locals().items(): 36 | callback = { 37 | "halign": self.set_halign, 38 | "valign": self.set_valign, 39 | "hexpand": self.set_hexpand, 40 | "vexpand": self.set_vexpand, 41 | "active": self.set_sensitive, 42 | "visible": self.set_visible, 43 | "size": self.set_size, 44 | "classname": self.set_classname, 45 | }.get(key) 46 | if callback: 47 | if isinstance(value, (Listener, Poll, Variable)): 48 | self.bind(value, callback) 49 | 50 | attributes(self) 51 | 52 | def set_size(self, size: Union[int, str, List[Union[int, str]], List[int]]) -> None: 53 | if size is not None: 54 | if isinstance(size, (int, str)): 55 | size = [size, size] 56 | elif isinstance(size, (list)): 57 | if len(size) == 1: 58 | size = [size[0], size[0]] 59 | elif len(size) >= 2: 60 | size = size[:2] 61 | 62 | width, height = get_screen_size() 63 | _width = parse_screen_size(size[0], width) 64 | _height = parse_screen_size(size[1], height) 65 | 66 | self.set_size_request(_width, _height) 67 | 68 | def set_halign(self, align: Union[str, Gtk.Align] = "fill") -> None: 69 | 70 | if isinstance(align, (str)): 71 | _alignment = { 72 | "fill": Gtk.Align.FILL, 73 | "start": Gtk.Align.START, 74 | "end": Gtk.Align.END, 75 | "center": Gtk.Align.CENTER, 76 | "baseline": Gtk.Align.BASELINE, 77 | }.get(align, Gtk.Align.FILL) 78 | else: 79 | _alignment = align 80 | 81 | super().set_halign(_alignment) 82 | 83 | def set_valign(self, align: Union[str, Gtk.Align] = Gtk.Align.FILL) -> None: 84 | 85 | if isinstance(align, (str)): 86 | _alignment = { 87 | "fill": Gtk.Align.FILL, 88 | "start": Gtk.Align.START, 89 | "end": Gtk.Align.END, 90 | "center": Gtk.Align.CENTER, 91 | "baseline": Gtk.Align.BASELINE, 92 | }.get(align, Gtk.Align.FILL) 93 | else: 94 | _alignment = align 95 | 96 | super().set_valign(_alignment) 97 | 98 | def set_active(self, param: bool) -> None: 99 | super().set_sensitive(param) 100 | 101 | def set_classname(self, classname: str) -> None: 102 | 103 | if isinstance(classname, (str)): 104 | context = self.get_style_context() 105 | [ 106 | context.remove_class(i) 107 | for i in context.list_classes() 108 | if i not in self._default_classnames 109 | ] 110 | 111 | for j in classname.split(" "): 112 | if j != " ": 113 | context.add_class(j) 114 | 115 | def get_classname(self) -> str: 116 | return " ".join( 117 | i 118 | for i in self.get_style_context().list_classes() 119 | if i not in self._default_classnames 120 | ) 121 | 122 | def _add_randclassname(self) -> None: 123 | if not self._rand_classname: 124 | context = self.get_style_context() 125 | 126 | self._rand_classname = ( 127 | self.get_name().replace("+", "_") + "_" + str(randint(1111, 9999)) 128 | ) 129 | context.add_class(self._rand_classname) 130 | 131 | def set_css(self, css_rules) -> None: 132 | self._add_randclassname() 133 | 134 | if css_rules and self._rand_classname: 135 | context = self.get_style_context() 136 | 137 | try: 138 | css_style = f".{self._rand_classname} {{{css_rules}}}" 139 | 140 | provider = Gtk.CssProvider() 141 | provider.load_from_data(css_style.encode()) 142 | 143 | context.add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) 144 | 145 | except Exception as e: 146 | print(e) 147 | 148 | def bind( 149 | self, 150 | variable: Union[Listener, Poll, Variable], 151 | callback: Callable, 152 | *args, 153 | **kwargs, 154 | ) -> None: 155 | variable.bind(callback, *args, **kwargs) 156 | # self.connect("destroy", lambda _: variable.run_dispose()) 157 | 158 | def attributes(self, callback: Callable) -> None: 159 | callback(self) 160 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Common/Events.py: -------------------------------------------------------------------------------- 1 | from ...Imports import * 2 | 3 | 4 | class Events(Gtk.Widget): 5 | def __init__( 6 | self, 7 | onclick=None, 8 | onmiddleclick=None, 9 | onhover=None, 10 | onhoverlost=None, 11 | primaryhold=None, 12 | primaryrelease=None, 13 | secondaryhold=None, 14 | secondaryrelease=None, 15 | ): 16 | Gtk.Widget.__init__(self) 17 | 18 | self.dict = { 19 | "onclick": onclick, 20 | "onmiddleclick": onmiddleclick, 21 | "onhover": onhover, 22 | "onhoverlost": onhoverlost, 23 | "primaryhold": primaryhold, 24 | "primaryrelease": primaryrelease, 25 | "secondaryhold": secondaryhold, 26 | "secondaryrelease": secondaryrelease, 27 | } 28 | 29 | self.connect("clicked", self.__click_event) if onclick else None 30 | self.connect("button-press-event", self.__press_event) 31 | self.connect("button-release-event", self.__release_event) 32 | self.connect("enter-notify-event", self.__enter_event) 33 | self.connect("leave-notify-event", self.__leave_event) 34 | 35 | def __click_event(self, _): 36 | callback = self.dict.get("onclick", None) 37 | if callback: 38 | callback() 39 | 40 | def __press_event(self, _, event): 41 | match event.button: 42 | case Gdk.EventButton.BUTTON_PRIMARY: 43 | callback = self.dict.get("primaryhold", None) 44 | if callback: 45 | callback() 46 | case Gdk.EventButton.BUTTON_SECONDARY: 47 | callback = self.dict.get("secondaryhold", None) 48 | if callback: 49 | callback() 50 | case Gdk.BUTTON_MIDDLE: 51 | callback = self.dict.get("onmiddleclick", None) 52 | if callback: 53 | callback() 54 | case _: 55 | pass 56 | 57 | def __release_event(self, _, event): 58 | match event: 59 | case Gdk.BUTTON_PRIMARY: 60 | callback = self.dict.get("primaryrelease", None) 61 | if callback: 62 | callback() 63 | case Gdk.BUTTON_SECONDARY: 64 | callback = self.dict.get("secondaryrelease", None) 65 | if callback: 66 | callback() 67 | case _: 68 | pass 69 | 70 | def __enter_event(self, *_): 71 | callback = self.dict.get("onhover", None) 72 | if callback: 73 | callback() 74 | 75 | def __leave_event(self, *_): 76 | callback = self.dict.get("onhoverlost", None) 77 | if callback: 78 | callback() 79 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Common/__init__.py: -------------------------------------------------------------------------------- 1 | from .BasicProps import BasicProps 2 | from .Events import Events 3 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Entry.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Entry(Gtk.Entry, BasicProps): 7 | def __init__( 8 | self, 9 | placeholder: str = "", 10 | onchange: Union[Callable, None] = None, 11 | onenter: Union[Callable, None] = None, 12 | css: str = "", 13 | attributes: Callable = lambda self: self, 14 | halign: str = "fill", 15 | valign: str = "fill", 16 | hexpand: bool = False, 17 | vexpand: bool = False, 18 | visible: bool = True, 19 | active: bool = True, 20 | classname: str = "", 21 | ): 22 | Gtk.Entry.__init__(self) 23 | BasicProps.__init__( 24 | self, 25 | css=css, 26 | halign=halign, 27 | valign=valign, 28 | hexpand=hexpand, 29 | vexpand=vexpand, 30 | active=active, 31 | visible=visible, 32 | classname=classname, 33 | ) 34 | 35 | self.set_placeholder_text(placeholder) 36 | 37 | ( 38 | self.connect("changed", lambda _: onchange(self.get_text())) 39 | if onchange 40 | else None 41 | ) 42 | 43 | ( 44 | self.connect("activate", lambda _: onenter(self.get_text())) 45 | if onenter 46 | else None 47 | ) 48 | 49 | attributes(self) if attributes else None 50 | 51 | def set_placeholder_text(self, text: Any = "") -> None: 52 | super().set_placeholder_text(str(text)) 53 | 54 | def set_text(self, text: Any = "") -> None: 55 | super().set_text(str(text)) 56 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/EventBox.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps, Events 4 | 5 | 6 | class EventBox(Gtk.EventBox, Events, BasicProps): 7 | def __init__( 8 | self, 9 | children: Union[Gtk.Widget, None] = None, 10 | onclick: Union[Callable, None] = None, 11 | onmiddleclick: Union[Callable, None] = None, 12 | onhover: Union[Callable, None] = None, 13 | onhoverlost: Union[Callable, None] = None, 14 | primaryhold: Union[Callable, None] = None, 15 | primaryrelease: Union[Callable, None] = None, 16 | secondaryhold: Union[Callable, None] = None, 17 | secondaryrelease: Union[Callable, None] = None, 18 | onscrollup: Union[Callable, None] = None, 19 | onscrolldown: Union[Callable, None] = None, 20 | attributes=lambda self: self, 21 | size: Union[int, str, List[Union[int, str]]] = 0, 22 | css: str = "", 23 | classname: str = "", 24 | halign: str = "fill", 25 | valign: str = "fill", 26 | hexpand: bool = False, 27 | vexpand: bool = False, 28 | visible: bool = True, 29 | ): 30 | Gtk.EventBox.__init__(self) 31 | 32 | BasicProps.__init__( 33 | self, 34 | css=css, 35 | halign=halign, 36 | valign=valign, 37 | hexpand=hexpand, 38 | vexpand=vexpand, 39 | active=True, 40 | visible=visible, 41 | classname=classname, 42 | size=size, 43 | ) 44 | 45 | self.add(children) if children else None 46 | attributes(self) if attributes else None 47 | self.dict = { 48 | "onclick": onclick, 49 | "onmiddleclick": onmiddleclick, 50 | "onhover": onhover, 51 | "onhoverlost": onhoverlost, 52 | "primaryhold": primaryhold, 53 | "primaryrelease": primaryrelease, 54 | "secondaryhold": secondaryhold, 55 | "secondaryrelease": secondaryrelease, 56 | "onscrollup": onscrollup, 57 | "onscrolldown": onscrolldown, 58 | } 59 | 60 | self.connect("scroll-event", self.__clasif_scroll) 61 | self.connect("button-press-event", self.__press_event) 62 | self.connect("button-release-event", self.__release_event) 63 | self.connect("enter-notify-event", self.__enter_event) 64 | self.connect("leave-notify-event", self.__leave_event) 65 | # self.connect("key-press-event", self.__press_event) 66 | # self.connect("key-release-event", self.__release_event) 67 | 68 | # Classification 69 | def __clasif_args(self, widget, event, callback): 70 | arg_num = callback.__code__.co_argcount 71 | arg_tuple = callback.__code__.co_varnames[:arg_num] 72 | 73 | match arg_num: 74 | 75 | case 2: 76 | callback(widget=widget, event=event) 77 | case 1: 78 | if "widget" in arg_tuple and widget: 79 | callback(widget=widget) 80 | elif "event" in arg_tuple and event: 81 | callback(event=event) 82 | else: 83 | callback(event) 84 | case _: 85 | callback() 86 | 87 | # def __click_event(self, _): 88 | # callback = self.dict.get("onclick", None) 89 | # if callback: 90 | # self.__clasif_args(self, None, callback) 91 | 92 | def __clasif_scroll(self, widget, event): 93 | match event: 94 | case Gdk.ScrollDirection.UP: 95 | callback = self.dict.get("onscrollup", None) 96 | case Gdk.ScrollDirection.DOWN: 97 | callback = self.dict.get("onscrolldown", None) 98 | case _: 99 | callback = None 100 | 101 | if callback: 102 | self.__clasif_args(widget, event, callback) 103 | 104 | def __press_event(self, widget, event): 105 | match event.button: 106 | case Gdk.BUTTON_PRIMARY: 107 | callback = self.dict.get("primaryhold", None) 108 | case Gdk.BUTTON_SECONDARY: 109 | callback = self.dict.get("secondaryhold", None) 110 | case Gdk.BUTTON_MIDDLE: 111 | callback = self.dict.get("onmiddleclick", None) 112 | case _: 113 | callback = None 114 | 115 | if callback: 116 | self.__clasif_args(widget, event, callback) 117 | 118 | def __release_event(self, widget, event): 119 | match event.button: 120 | case Gdk.BUTTON_PRIMARY: 121 | callback = self.dict.get("primaryrelease", None) 122 | case Gdk.BUTTON_SECONDARY: 123 | callback = self.dict.get("secondaryrelease", None) 124 | case _: 125 | callback = None 126 | 127 | if callback: 128 | self.__clasif_args(widget, event, callback) 129 | 130 | def __enter_event(self, widget, event): 131 | callback = self.dict.get("onhover", None) 132 | if callback: 133 | self.__clasif_args(widget, event, callback) 134 | 135 | def __leave_event(self, widget, event): 136 | callback = self.dict.get("onhoverlost", None) 137 | if callback: 138 | self.__clasif_args(widget, event, callback) 139 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Fixed.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Fixed(Gtk.Fixed, BasicProps): 7 | def __init__( 8 | self, 9 | attributes=lambda self: self, 10 | halign: str = "fill", 11 | valign: str = "fill", 12 | classname: str = "", 13 | css: str = "", 14 | hexpand: bool = False, 15 | vexpand: bool = False, 16 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 17 | ) -> None: 18 | Gtk.Fixed.__init__(self) 19 | BasicProps.__init__( 20 | self, 21 | active=True, 22 | visible=True, 23 | classname=classname, 24 | css=css, 25 | halign=halign, 26 | valign=valign, 27 | hexpand=hexpand, 28 | vexpand=vexpand, 29 | size=size, 30 | ) 31 | attributes(self) if attributes else None 32 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/FlowBox.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | # https://lazka.github.io/pgi-docs/Gtk-3.0/classes/FlowBox.html#Gtk.FlowBox.set_activate_on_single_click 7 | class FlowBox(Gtk.FlowBox, BasicProps): 8 | def __init__( 9 | self, 10 | children: List[Union["FlowBoxChild", Gtk.FlowBoxChild]] = [], 11 | spacing: Tuple[int, int] = (0, 0), 12 | orientation: Union[ 13 | Gtk.Orientation, Literal["h", "horizontal", "v", "vertical"] 14 | ] = "h", 15 | single_click: bool = False, 16 | halign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 17 | valign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 18 | hexpand: bool = False, 19 | vexpand: bool = False, 20 | classname: str = "", 21 | css: str = "", 22 | visible: bool = True, 23 | active: bool = True, 24 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 25 | attributes: Callable = lambda self: self, 26 | ) -> None: 27 | Gtk.FlowBox.__init__(self) 28 | BasicProps.__init__( 29 | self, 30 | halign, 31 | valign, 32 | hexpand, 33 | vexpand, 34 | classname, 35 | css, 36 | visible, 37 | active, 38 | size, 39 | attributes, 40 | ) 41 | self.set_column_spacing(spacing[0]) 42 | self.set_row_spacing(spacing[1]) 43 | self.set_activate_on_single_click(single_click) 44 | self.set_orientation(orientation) 45 | for i in children: 46 | self.insert(i) 47 | 48 | def set_orientation( 49 | self, 50 | orientation: Union[ 51 | Gtk.Orientation, Literal["h", "horizontal", "v", "vertical"] 52 | ] = Gtk.Orientation.HORIZONTAL, 53 | ) -> None: 54 | 55 | _orientation: Gtk.Orientation 56 | 57 | if isinstance(orientation, (Gtk.Orientation)): 58 | _orientation = orientation 59 | elif orientation in ["h", "horizontal"]: 60 | _orientation = Gtk.Orientation.HORIZONTAL 61 | elif orientation in ["v", "vertical"]: 62 | _orientation = Gtk.Orientation.VERTICAL 63 | else: 64 | return 65 | 66 | super().set_orientation(_orientation) 67 | 68 | def set_activate_on_single_click(self, single: bool) -> None: 69 | return super().set_activate_on_single_click(single) 70 | 71 | def insert( 72 | self, widget: Union["FlowBoxChild", Gtk.FlowBoxChild], position: int = -1 73 | ) -> None: 74 | return super().insert(widget, position) 75 | 76 | def set_max_children_per_line(self, n_children: int) -> None: 77 | return super().set_max_children_per_line(n_children) 78 | 79 | def set_min_children_per_line(self, n_children: int) -> None: 80 | return super().set_min_children_per_line(n_children) 81 | 82 | def set_row_spacing(self, spacing: int) -> None: 83 | return super().set_row_spacing(spacing) 84 | 85 | def set_column_spacing(self, spacing: int) -> None: 86 | return super().set_column_spacing(spacing) 87 | 88 | 89 | class FlowBoxChild(Gtk.FlowBoxChild, BasicProps): 90 | def __init__( 91 | self, 92 | onactivate: Callable = lambda *_: _, 93 | children: Gtk.Widget = Gtk.Box(), 94 | halign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 95 | valign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 96 | hexpand: bool = False, 97 | vexpand: bool = False, 98 | classname: str = "", 99 | css: str = "", 100 | visible: bool = True, 101 | active: bool = True, 102 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 103 | attributes: Callable = lambda self: self, 104 | ) -> None: 105 | Gtk.FlowBoxChild.__init__(self) 106 | BasicProps.__init__( 107 | self, 108 | halign, 109 | valign, 110 | hexpand, 111 | vexpand, 112 | classname, 113 | css, 114 | visible, 115 | active, 116 | size, 117 | attributes, 118 | ) 119 | self.dict = {"onactivate": onactivate} 120 | self.add(children) 121 | self.connect("activate", self.__activate_event) 122 | 123 | def __activate_event(self, widget, event): 124 | callback = self.dict.get("onactivate") 125 | if callback: 126 | self.__clasif_args(widget, event, callback) 127 | 128 | def __clasif_args(self, widget, event, callback: Callable) -> None: 129 | arg_num = callback.__code__.co_argcount 130 | arg_tuple = callback.__code__.co_varnames[:arg_num] 131 | 132 | match arg_num: 133 | case 2: 134 | callback(widget=widget, event=event) 135 | case 1: 136 | if "widget" in arg_tuple and widget: 137 | callback(widget=widget) 138 | elif "event" in arg_tuple and event: 139 | callback(event=event) 140 | else: 141 | callback(event) 142 | case _: 143 | callback() 144 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Grid.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from .Common import BasicProps 3 | 4 | 5 | class Grid(Gtk.Grid, BasicProps): 6 | def __init__( 7 | self, 8 | spacing: Tuple[int, int] = (0, 0), 9 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 10 | attributes: Callable = lambda self: self, 11 | css: str = "", 12 | halign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 13 | valign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 14 | hexpand: bool = False, 15 | vexpand: bool = False, 16 | visible: bool = True, 17 | classname="", 18 | ) -> None: 19 | Gtk.Grid.__init__(self) 20 | BasicProps.__init__( 21 | self, 22 | size=size, 23 | css=css, 24 | halign=halign, 25 | valign=valign, 26 | hexpand=hexpand, 27 | vexpand=vexpand, 28 | active=True, 29 | visible=visible, 30 | classname=classname, 31 | ) 32 | 33 | attributes(self) 34 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Icon.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Icon(Gtk.Image, BasicProps): 7 | def __init__( 8 | self, 9 | icon: Union[str, Listener, Poll, Variable] = "", 10 | size: int = 20, 11 | attributes: Callable = lambda self: self, 12 | css: str = "", 13 | halign: str = "fill", 14 | valign: str = "fill", 15 | hexpand: bool = False, 16 | vexpand: bool = False, 17 | visible: bool = True, 18 | classname: str = "", 19 | ): 20 | Gtk.Image.__init__(self) 21 | BasicProps.__init__( 22 | self, 23 | css=css, 24 | halign=halign, 25 | valign=valign, 26 | hexpand=hexpand, 27 | vexpand=vexpand, 28 | active=True, 29 | visible=visible, 30 | classname=classname, 31 | size=0, 32 | ) 33 | self.__size = size 34 | self.__icon = icon 35 | if isinstance(icon, (Listener, Poll, Variable)): 36 | self.set_icon(icon.get_value()) 37 | else: 38 | self.set_icon(icon) 39 | 40 | self.set_size(size) 41 | 42 | for key, value in locals().items(): 43 | if key not in [ 44 | "self", 45 | "halign", 46 | "valign", 47 | "hexpand", 48 | "vexpand", 49 | "visible", 50 | "active", 51 | "visible", 52 | "classname", 53 | ] and isinstance(value, (Listener, Poll, Variable)): 54 | callback = { 55 | "icon": self.set_icon, 56 | "size": self.set_size, 57 | }.get(key) 58 | 59 | if callback: 60 | self.bind(value, callback) 61 | 62 | attributes(self) if attributes else None 63 | 64 | def set_icon(self, icon: str) -> None: 65 | self.__icon = icon 66 | self.set_from_icon_name(self.__icon, Gtk.IconSize.DIALOG) 67 | 68 | def set_size(self, size: int) -> None: 69 | self.__size = size 70 | self.set_pixel_size(self.__size) 71 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Image.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Image(Gtk.Image, BasicProps): 7 | def __init__( 8 | self, 9 | path: Union[str, Variable, Listener, Poll, GdkPixbuf.Pixbuf, Gtk.IconInfo] = "", 10 | size: Union[list, int] = 20, 11 | hexpand=False, 12 | vexpand=False, 13 | halign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 14 | valign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 15 | visible=True, 16 | classname="", 17 | attributes: Callable = lambda self: self, 18 | css="", 19 | ) -> None: 20 | Gtk.Image.__init__(self) 21 | BasicProps.__init__( 22 | self, 23 | css=css, 24 | halign=halign, 25 | valign=valign, 26 | hexpand=hexpand, 27 | vexpand=vexpand, 28 | active=True, 29 | visible=visible, 30 | classname=classname, 31 | size=0, 32 | ) 33 | 34 | self.__size = [0, 0] 35 | self.__path = "" 36 | 37 | self.set_size(size) 38 | if isinstance(path, (str, GdkPixbuf.Pixbuf, Gtk.IconInfo)): 39 | self.set_image(path) 40 | 41 | for key, value in locals().items(): 42 | if key not in [ 43 | "self", 44 | "halign", 45 | "valign", 46 | "hexpand", 47 | "vexpand", 48 | "visible", 49 | "active", 50 | "visible", 51 | "classname", 52 | ] and isinstance(value, (Listener, Poll, Variable)): 53 | callback = { 54 | "path": self.set_image, 55 | "size": self.set_size, 56 | }.get(key) 57 | 58 | if not callback: 59 | continue 60 | 61 | self.bind(value, callback) 62 | attributes(self) if attributes else None 63 | 64 | def set_size( 65 | self, 66 | size: Union[int, List[int]], 67 | *, 68 | resize_method: GdkPixbuf.InterpType = GdkPixbuf.InterpType.BILINEAR, 69 | ) -> None: 70 | 71 | self.__size = [size, size] if isinstance(size, (int)) else size 72 | if self.__path: 73 | self.set_image(self.__path, resize_method=resize_method) 74 | 75 | def set_image( 76 | self, 77 | path: Union[GdkPixbuf.Pixbuf, str, Gtk.IconInfo], 78 | *, 79 | resize_method: GdkPixbuf.InterpType = GdkPixbuf.InterpType.BILINEAR, 80 | ) -> None: 81 | if not path: 82 | return 83 | 84 | if isinstance(path, (str)): 85 | self.__path = path 86 | self.set_image( 87 | GdkPixbuf.Pixbuf.new_from_file_at_scale( 88 | filename=self.__path, 89 | width=self.__size[0], 90 | height=self.__size[1], 91 | preserve_aspect_ratio=True, 92 | ), 93 | resize_method=resize_method, 94 | ) 95 | 96 | elif isinstance(path, (Gtk.IconInfo)): 97 | 98 | def wrapper(_: Gtk.IconInfo, task: Gio.Task): 99 | self.set_image(path.load_icon_finish(task)) 100 | 101 | return path.load_icon_async(callback=wrapper) 102 | 103 | elif isinstance(path, (GdkPixbuf.Pixbuf)): 104 | self.set_pixel_size(sum(self.__size) // 2) 105 | _w, _h = self.__preserve_aspect_ratio(path, *self.__size) 106 | path = path.scale_simple(_w, _h, resize_method) 107 | 108 | self.set_from_pixbuf(path) 109 | self.__path = path 110 | 111 | self.show() 112 | 113 | def __preserve_aspect_ratio( 114 | self, pixbuf: GdkPixbuf.Pixbuf, new_width: int, new_height: int 115 | ) -> Tuple[int, int]: 116 | original_width, original_height = pixbuf.get_width(), pixbuf.get_height() 117 | 118 | width_ratio = new_width / original_width 119 | height_ratio = new_height / original_height 120 | 121 | scale_factor = min(width_ratio, height_ratio) 122 | 123 | new_width_scaled = int(original_width * scale_factor) 124 | new_height_scaled = int(original_height * scale_factor) 125 | 126 | return new_width_scaled, new_height_scaled 127 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Label.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Label(Gtk.Label, BasicProps): 7 | def __init__( 8 | self, 9 | text: Any = "", 10 | yalign: float = 0.5, 11 | xalign: float = 0.5, 12 | angle: float = 0.0, 13 | maxchars: int = -1, 14 | wrap: bool = False, 15 | attributes: Callable = lambda self: self, 16 | css: str = "", 17 | halign: str = "fill", 18 | valign: str = "fill", 19 | hexpand: bool = False, 20 | vexpand: bool = False, 21 | visible: bool = True, 22 | classname: str = "", 23 | justify: str = "justified", 24 | ) -> None: 25 | Gtk.Label.__init__(self) 26 | BasicProps.__init__( 27 | self, 28 | css=css, 29 | halign=halign, 30 | valign=valign, 31 | hexpand=hexpand, 32 | vexpand=vexpand, 33 | active=True, 34 | visible=visible, 35 | classname=classname, 36 | ) 37 | 38 | self.set_text(text) 39 | self.set_yalign(yalign) 40 | self.set_xalign(xalign) 41 | self.set_selectable(False) 42 | self.set_angle(angle) 43 | self.set_maxchars(maxchars) 44 | self.set_wrap(wrap) 45 | self.set_justify(justify) 46 | 47 | attributes(self) if attributes else None 48 | 49 | for key, value in locals().items(): 50 | if key not in [ 51 | "self", 52 | "halign", 53 | "valign", 54 | "hexpand", 55 | "vexpand", 56 | "visible", 57 | "active", 58 | "visible", 59 | "classname", 60 | ] and isinstance(value, (Listener, Poll, Variable)): 61 | callback = { 62 | "text": self.set_text, 63 | "yalign": self.set_yalign, 64 | "xalign": self.set_xalign, 65 | "angle": self.set_angle, 66 | "maxchars": self.set_maxchars, 67 | "justify": self.set_justify, 68 | }.get(key) 69 | 70 | self.bind(value, callback) if callback else None 71 | 72 | def set_text(self, text: Any) -> None: 73 | return super().set_text(str(text)) 74 | 75 | def set_markup(self, text: Any) -> None: 76 | return super().set_markup(str(text)) 77 | 78 | def set_wrap(self, wrap: bool) -> None: 79 | super().set_line_wrap(wrap) 80 | 81 | def set_maxchars(self, chars: int) -> None: 82 | if chars > 0: 83 | super().set_max_width_chars(chars) 84 | super().set_ellipsize(Pango.EllipsizeMode.END) 85 | 86 | def set_justify(self, jtype: Union[str, Gtk.Justification]) -> None: 87 | if isinstance(jtype, (str)): 88 | if jtype == "left": 89 | super().set_justify(Gtk.Justification.LEFT) 90 | elif jtype == "center": 91 | super().set_justify(Gtk.Justification.CENTER) 92 | elif jtype == "right": 93 | super().set_justify(Gtk.Justification.RIGHT) 94 | elif jtype == "justified": 95 | super().set_justify(Gtk.Justification.FILL) 96 | else: 97 | super().set_justify(jtype) 98 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Menu.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Menu(Gtk.Menu, BasicProps): 7 | def __init__( 8 | self, 9 | children: List[Union[Gtk.MenuItem, Gtk.SeparatorMenuItem]] = [], 10 | css: str = "", 11 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 12 | valign: str = "fill", 13 | halign: str = "fill", 14 | hexpand: bool = False, 15 | vexpand: bool = False, 16 | classname: str = "", 17 | ): 18 | Gtk.Menu.__init__(self) 19 | BasicProps.__init__( 20 | self, 21 | size=size, 22 | css=css, 23 | halign=halign, 24 | valign=valign, 25 | hexpand=hexpand, 26 | vexpand=vexpand, 27 | classname=classname, 28 | active=True, 29 | ) 30 | 31 | for i in children: 32 | if i: 33 | self.append(i) 34 | 35 | self.show_all() 36 | 37 | def popup_at_widget( 38 | self, 39 | parent_widget: Gtk.Widget, 40 | trigger_event: Gdk.EventButton, 41 | parent_anchor: Literal[ 42 | "top", "top_left", "left", "bottom", "bottom_left", "bottom_right" 43 | ] = "top", 44 | menu_anchor: Literal[ 45 | "top", "top_left", "left", "bottom", "bottom_left", "bottom_right" 46 | ] = "top", 47 | ) -> None: 48 | _gravity_mapping: Dict[str, Gdk.Gravity] = { 49 | "top": Gdk.Gravity.NORTH, 50 | "top_left": Gdk.Gravity.NORTH_WEST, 51 | "top_right": Gdk.Gravity.NORTH_EAST, 52 | "left": Gdk.Gravity.WEST, 53 | "right": Gdk.Gravity.EAST, 54 | "bottom": Gdk.Gravity.SOUTH, 55 | "bottom_left": Gdk.Gravity.SOUTH_WEST, 56 | "bottom_right": Gdk.Gravity.SOUTH_EAST, 57 | } 58 | _from = _gravity_mapping.get(parent_anchor, Gdk.Gravity.NORTH) 59 | _to = _gravity_mapping.get(menu_anchor, Gdk.Gravity.NORTH) 60 | 61 | return super().popup_at_widget( 62 | widget=parent_widget, 63 | widget_anchor=_from, 64 | menu_anchor=_to, 65 | trigger_event=trigger_event, 66 | ) 67 | 68 | def popup_at_pointer(self, trigger_event: Gdk.EventButton) -> None: 69 | return super().popup_at_pointer(trigger_event) 70 | 71 | 72 | class MenuItem(Gtk.MenuItem, BasicProps): 73 | def __init__( 74 | self, 75 | children: Gtk.Widget, 76 | submenu: Union[Gtk.Menu, None] = None, 77 | onactivate: Callable = lambda callback: callback, 78 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 79 | halign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 80 | valign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 81 | hexpand: bool = False, 82 | vexpand: bool = False, 83 | classname: str = "", 84 | css: str = "", 85 | active: bool = True, 86 | ): 87 | Gtk.MenuItem.__init__(self) 88 | BasicProps.__init__( 89 | self, 90 | size=size, 91 | css=css, 92 | halign=halign, 93 | valign=valign, 94 | hexpand=hexpand, 95 | vexpand=vexpand, 96 | classname=classname, 97 | active=active, 98 | ) 99 | self.add(children) if children else None 100 | self.set_submenu(submenu) if submenu else None 101 | 102 | self.connect( 103 | "activate", 104 | lambda widget: GLib.idle_add( 105 | lambda: self.__clasif_args( 106 | callback=onactivate, 107 | widget=widget, 108 | event=False, 109 | ) 110 | ), 111 | ) 112 | 113 | def __clasif_args(self, widget, event, callback) -> None: 114 | arg_num = callback.__code__.co_argcount 115 | arg_tuple = callback.__code__.co_varnames[:arg_num] 116 | 117 | if arg_num == 2: 118 | callback(widget=widget, event=event) 119 | 120 | elif arg_num == 1: 121 | if "widget" in arg_tuple and widget: 122 | callback(widget=widget) 123 | elif "event" in arg_tuple and event: 124 | callback(event=event) 125 | else: 126 | callback(event) 127 | else: 128 | callback() 129 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Overlay.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Overlay(Gtk.Overlay, BasicProps): 7 | def __init__( 8 | self, 9 | children: Union[List[Union[Gtk.Widget, None]], Any] = [], 10 | attributes=lambda self: self, 11 | css: str = "", 12 | halign: str = "fill", 13 | valign: str = "fill", 14 | hexpand: bool = False, 15 | vexpand: bool = False, 16 | visible: bool = True, 17 | classname: str = "", 18 | ): 19 | Gtk.Overlay.__init__(self) 20 | 21 | BasicProps.__init__( 22 | self, 23 | css=css, 24 | halign=halign, 25 | valign=valign, 26 | hexpand=hexpand, 27 | vexpand=vexpand, 28 | active=True, 29 | visible=visible, 30 | classname=classname, 31 | ) 32 | 33 | if not children: 34 | return 35 | 36 | BaseWidget: Union[Gtk.Widget, None] = children.pop( 37 | children.index(next(i for i in children if isinstance(i, Gtk.Widget))) 38 | ) 39 | OverlayWidgets: Union[List[None], List[Gtk.Widget]] = [ 40 | i for i in children if isinstance(i, (Gtk.Widget)) 41 | ] 42 | 43 | if BaseWidget: 44 | self.add(BaseWidget) 45 | 46 | if OverlayWidgets: 47 | for i in OverlayWidgets: 48 | self.add_overlay(i) 49 | 50 | attributes(self) if attributes else None 51 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/ProgressBar.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class ProgressBar(Gtk.ProgressBar, BasicProps): 7 | def __init__( 8 | self, 9 | value: Union[int, float, Poll, Listener, Variable] = 50, 10 | showtext: bool = False, 11 | inverted: bool = False, 12 | orientation: str = "h", 13 | attributes: Callable = lambda self: self, 14 | css: str = "", 15 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 16 | halign: str = "fill", 17 | valign: str = "fill", 18 | hexpand: bool = False, 19 | vexpand: bool = False, 20 | visible: bool = True, 21 | classname: str = "", 22 | ): 23 | Gtk.ProgressBar.__init__(self) 24 | 25 | BasicProps.__init__( 26 | self, 27 | css=css, 28 | halign=halign, 29 | valign=valign, 30 | hexpand=hexpand, 31 | vexpand=vexpand, 32 | active=True, 33 | visible=visible, 34 | classname=classname, 35 | size=size, 36 | ) 37 | if isinstance(value, (Variable, Listener, Poll)): 38 | self.bind(value, self.set_value) 39 | else: 40 | self.set_value(value) 41 | 42 | self.set_show_text(showtext) 43 | self.set_inverted(inverted) 44 | self.set_orientation(orientation) 45 | 46 | attributes(self) if attributes else None 47 | 48 | def set_value(self, value: Union[int, float]) -> None: 49 | if 0 <= value <= 100: 50 | self.set_fraction(value / 100) 51 | self.set_text(str(value)) 52 | 53 | def set_orientation( 54 | self, orientation: Union[Gtk.Orientation, str] = Gtk.Orientation.HORIZONTAL 55 | ) -> None: 56 | _orientation = Gtk.Orientation.HORIZONTAL 57 | 58 | if isinstance(orientation, (Gtk.Orientation)): 59 | _orientation = orientation 60 | elif orientation == "v": 61 | _orientation = Gtk.Orientation.VERTICAL 62 | else: 63 | _orientation = Gtk.Orientation.HORIZONTAL 64 | 65 | super().set_orientation(_orientation) 66 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Revealer.py: -------------------------------------------------------------------------------- 1 | from PotatoWidgets.Methods import parse_interval 2 | 3 | from ..Imports import * 4 | from ..Variable import Listener, Poll, Variable 5 | from .Common import BasicProps 6 | 7 | 8 | class Revealer(Gtk.Revealer, BasicProps): 9 | def __init__( 10 | self, 11 | children: Gtk.Widget, 12 | reveal: Union[bool, Variable, Listener, Poll] = True, 13 | transition: Literal[ 14 | "crossfade", "slideleft", "slideup", "slideright", "slidedown", "none" 15 | ] = "crossfade", 16 | duration: Union[int, str] = 500, 17 | css: str = "", 18 | size: List[Union[int, str]] = [], 19 | attributes: Callable = lambda self: self, 20 | halign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 21 | valign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 22 | hexpand: bool = False, 23 | vexpand: bool = False, 24 | classname: str = "", 25 | ): 26 | 27 | Gtk.Revealer.__init__(self) 28 | 29 | BasicProps.__init__( 30 | self, 31 | css=css, 32 | size=size, 33 | halign=halign, 34 | valign=valign, 35 | hexpand=hexpand, 36 | vexpand=vexpand, 37 | classname=classname, 38 | ) 39 | self.add(children) if children else None 40 | self.set_duration(duration) 41 | self.set_transition(transition) 42 | if isinstance(reveal, bool): 43 | self.set_revealed(reveal) 44 | 45 | attributes(self) if attributes else None 46 | 47 | for key, value in locals().items(): 48 | if key not in [ 49 | "self", 50 | "halign", 51 | "valign", 52 | "hexpand", 53 | "vexpand", 54 | "visible", 55 | "active", 56 | "visible", 57 | "classname", 58 | ] and isinstance(value, (Listener, Poll, Variable)): 59 | callback = { 60 | "reveal": self.set_revealed, 61 | "transition": self.set_transition, 62 | "duration": self.set_duration, 63 | }.get(key) 64 | if callback: 65 | callback(value.value) 66 | self.bind(value, callback) 67 | 68 | def set_transition( 69 | self, transition: Union[str, Gtk.RevealerTransitionType] 70 | ) -> None: 71 | if isinstance(transition, (str)): 72 | anim = { 73 | "none": Gtk.RevealerTransitionType.NONE, 74 | "crossfade": Gtk.RevealerTransitionType.CROSSFADE, 75 | "slideright": Gtk.RevealerTransitionType.SLIDE_RIGHT, 76 | "slideleft": Gtk.RevealerTransitionType.SLIDE_LEFT, 77 | "slideright": Gtk.RevealerTransitionType.SLIDE_RIGHT, 78 | "slideup": Gtk.RevealerTransitionType.SLIDE_UP, 79 | "slidedown": Gtk.RevealerTransitionType.SLIDE_DOWN, 80 | }.get(transition.lower(), Gtk.RevealerTransitionType.NONE) 81 | else: 82 | anim = transition 83 | 84 | super().set_transition_type(anim) 85 | 86 | def set_duration(self, duration_ms: Union[int, str]) -> None: 87 | _duration = parse_interval(interval=duration_ms, fallback_interval=500) 88 | super().set_transition_duration(_duration) 89 | 90 | def set_revealed(self, reveal: bool) -> None: 91 | super().set_reveal_child(reveal) 92 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Scale.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Scale(Gtk.Scale, BasicProps): 7 | def __init__( 8 | self, 9 | min: int = 0, 10 | max: int = 100, 11 | value: int = 50, 12 | inverted: bool = False, 13 | draw_value: bool = False, 14 | decimals: int = 0, 15 | onchange: Union[Callable, None] = lambda value: value, 16 | attributes: Callable = lambda self: self, 17 | orientation: str = "h", 18 | css: str = "", 19 | size: Union[int, str, List[Union[int, str]], List[int]] = [100, 10], 20 | halign: str = "fill", 21 | valign: str = "fill", 22 | hexpand: bool = False, 23 | vexpand: bool = False, 24 | visible: bool = True, 25 | classname: str = "", 26 | ): 27 | Gtk.Scale.__init__(self) 28 | BasicProps.__init__( 29 | self, 30 | css=css, 31 | halign=halign, 32 | valign=valign, 33 | hexpand=hexpand, 34 | vexpand=vexpand, 35 | active=True, 36 | visible=visible, 37 | classname=classname, 38 | size=size, 39 | ) 40 | self._min = min 41 | self._max = max 42 | self._reload_values() 43 | self.set_value(value) 44 | self.set_decimals(decimals) 45 | self.set_inverted(inverted) 46 | self.set_orientation(orientation) 47 | self.set_draw_value(draw_value) 48 | 49 | ( 50 | self.connect( 51 | "value-changed", 52 | # lambda x: onchange(round(x.get_value(), self._decimals)), 53 | lambda x: onchange(x.get_value()), 54 | ) 55 | if onchange 56 | else None 57 | ) 58 | 59 | attributes(self) if attributes else None 60 | 61 | def set_min(self, min: int) -> None: 62 | self._min = min 63 | self._reload_values() 64 | 65 | def set_max(self, max: int) -> None: 66 | self._max = max 67 | self._reload_values() 68 | 69 | def set_decimals(self, value: int) -> None: 70 | if value >= 0: 71 | self._decimals = value 72 | self.set_digits(value) 73 | 74 | def _reload_values(self) -> None: 75 | super().set_range(self._min, self._max) 76 | 77 | def set_orientation( 78 | self, orientation: Union[Gtk.Orientation, str] = Gtk.Orientation.HORIZONTAL 79 | ) -> None: 80 | _orientation = Gtk.Orientation.HORIZONTAL 81 | 82 | if isinstance(orientation, (Gtk.Orientation)): 83 | _orientation = orientation 84 | elif orientation == "v": 85 | _orientation = Gtk.Orientation.VERTICAL 86 | else: 87 | _orientation = Gtk.Orientation.HORIZONTAL 88 | 89 | super().set_orientation(_orientation) 90 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Scroll.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from .Common import BasicProps 3 | 4 | 5 | class Scroll(Gtk.ScrolledWindow, BasicProps): 6 | def __init__( 7 | self, 8 | children: Gtk.Widget, 9 | attributes: Callable = lambda self: self, 10 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 11 | css: str = "", 12 | halign: str = "fill", 13 | valign: str = "fill", 14 | hexpand: bool = False, 15 | vexpand: bool = False, 16 | visible: bool = True, 17 | classname: str = "", 18 | ): 19 | Gtk.ScrolledWindow.__init__(self) 20 | BasicProps.__init__( 21 | self, 22 | size=size, 23 | css=css, 24 | halign=halign, 25 | valign=valign, 26 | hexpand=hexpand, 27 | vexpand=vexpand, 28 | active=True, 29 | visible=visible, 30 | classname=classname, 31 | ) 32 | 33 | self.add_with_viewport(children) if children else None 34 | attributes(self) 35 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Separator.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | from .Common import BasicProps 4 | 5 | 6 | class Separator(Gtk.Separator, BasicProps): 7 | def __init__( 8 | self, 9 | orientation: str = "h", 10 | attributes: Callable = lambda self: self, 11 | halign: str = "fill", 12 | valign: str = "fill", 13 | hexpand: bool = False, 14 | vexpand: bool = False, 15 | classname: str = "", 16 | css: str = "", 17 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 18 | ): 19 | Gtk.Separator.__init__(self) 20 | BasicProps.__init__( 21 | self, 22 | halign=halign, 23 | valign=valign, 24 | hexpand=hexpand, 25 | vexpand=vexpand, 26 | active=True, 27 | visible=True, 28 | classname=classname, 29 | css=css, 30 | size=size, 31 | ) 32 | attributes(self) if attributes else None 33 | self.set_orientation(orientation) 34 | 35 | def set_orientation( 36 | self, orientation: Union[Gtk.Orientation, str] = Gtk.Orientation.HORIZONTAL 37 | ) -> None: 38 | _orientation = Gtk.Orientation.HORIZONTAL 39 | 40 | if isinstance(orientation, (Gtk.Orientation)): 41 | _orientation = orientation 42 | elif orientation == "v": 43 | _orientation = Gtk.Orientation.VERTICAL 44 | else: 45 | _orientation = Gtk.Orientation.HORIZONTAL 46 | 47 | super().set_orientation(_orientation) 48 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Stack.py: -------------------------------------------------------------------------------- 1 | from PotatoWidgets.Widget.Common.BasicProps import BasicProps 2 | 3 | from ..Imports import * 4 | from .Common import BasicProps 5 | 6 | 7 | class Stack(Gtk.Stack, BasicProps): 8 | def __init__( 9 | self, 10 | children: Dict[str, Gtk.Widget] = {}, 11 | size: Union[int, str, List[Union[int, str]], List[int]] = 0, 12 | attributes: Callable = lambda self: self, 13 | css: str = "", 14 | halign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 15 | valign: Literal["fill", "start", "center", "end", "baseline"] = "fill", 16 | hexpand: bool = False, 17 | vexpand: bool = False, 18 | visible: bool = True, 19 | classname="", 20 | ) -> None: 21 | Gtk.Stack.__init__(self) 22 | 23 | BasicProps.__init__( 24 | self, 25 | size=size, 26 | css=css, 27 | halign=halign, 28 | valign=valign, 29 | hexpand=hexpand, 30 | vexpand=vexpand, 31 | active=True, 32 | visible=visible, 33 | classname=classname, 34 | ) 35 | for name, child in children.items(): 36 | self.add_named(child, name) 37 | 38 | attributes(self) 39 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/ToggleButton.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from .Common import BasicProps 3 | from ..Variable import Listener, Poll, Variable 4 | 5 | 6 | class ToggleButton(Gtk.ToggleButton, BasicProps): 7 | def __init__( 8 | self, 9 | children=None, 10 | onclick=None, 11 | css="", 12 | halign="fill", 13 | valign="fill", 14 | hexpand=False, 15 | vexpand=False, 16 | active=True, 17 | visible=True, 18 | classname="", 19 | ): 20 | Gtk.ToggleButton.__init__(self) 21 | 22 | BasicProps.__init__( 23 | self, 24 | halign=halign, 25 | valign=valign, 26 | hexpand=hexpand, 27 | vexpand=vexpand, 28 | active=active, 29 | visible=visible, 30 | classname=classname, 31 | css=css, 32 | ) 33 | 34 | self.add(children) if children else None 35 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/Window.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Methods import get_screen_size, parse_interval, parse_screen_size 3 | from ..Variable import Listener, Poll, Variable 4 | 5 | 6 | class Window(Gtk.Window): 7 | def __init__( 8 | self, 9 | size: List[Union[str, int]] = [-1, -1], 10 | at: Dict[Literal["left", "right", "top", "bottom"], Union[int, str]] = {}, 11 | position: str = "center", 12 | layer: str = "top", 13 | exclusive: Union[bool, int] = False, 14 | children: Union[Gtk.Widget, None] = None, 15 | focusable: Literal[True, False, "none", "on-demand", "exclusive"] = "none", 16 | monitor: int = 0, 17 | namespace: str = "gtk-layer-shell", 18 | attributes: Callable = lambda self: self, 19 | disable_layer: bool = False, 20 | ) -> None: 21 | Gtk.Window.__init__(self) 22 | 23 | self._wayland_display: bool = bool(GLib.getenv("WAYLAND_DISPLAY")) 24 | self._disable_layer: bool = disable_layer 25 | self._monitor: int = monitor 26 | 27 | screen: tuple = get_screen_size(self.monitor) 28 | 29 | _width: int = parse_screen_size(size[0], screen[0]) 30 | _height: int = parse_screen_size( 31 | size[1] if len(size) >= 2 else size[0], screen[1] 32 | ) 33 | self._size: List[int] = [max(_width, 1), max(_height, 1)] 34 | 35 | if namespace != "gtk-layer-shell": 36 | self._name: str = namespace 37 | else: 38 | # Hacky Stuff Again 39 | line: str 40 | index: int 41 | 42 | _, _, _, line = traceback_extract_stack()[-2] 43 | index = line.find("=") 44 | 45 | if index != -1: 46 | self._name: str = line[:index].strip() 47 | else: 48 | self._name: str = namespace 49 | 50 | if self._wayland_display and not self._disable_layer: 51 | GtkLayerShell.init_for_window(self) 52 | GtkLayerShell.set_namespace(self, self.__name__) 53 | else: 54 | self.set_wmclass("potatowindow", "PotatoWindow") 55 | self.set_app_paintable(True) 56 | self.set_visual( 57 | Gdk.Display.get_default().get_default_screen().get_rgba_visual() 58 | ) 59 | 60 | if layer not in ["normal"] and not self._disable_layer: 61 | self.set_skip_pager_hint(True) 62 | self.set_skip_taskbar_hint(True) 63 | self.set_decorated(False) 64 | self.set_resizable(False) 65 | 66 | if layer in [ 67 | "dock", 68 | "top", 69 | "bottom", 70 | "background", 71 | "notification", 72 | "dialog", 73 | ]: 74 | self.stick() 75 | 76 | if layer in [ 77 | "dialog", 78 | "tooltip", 79 | "notification", 80 | "combo", 81 | "dnd", 82 | "menu", 83 | "toolbar", 84 | "dock", 85 | "splashscreen", 86 | "utility", 87 | "dropdown", 88 | "popup", 89 | "top", 90 | "overlay", 91 | ]: 92 | self.set_keep_above(True) 93 | elif layer in [ 94 | "desktop", 95 | "background", 96 | "bottom", 97 | ]: 98 | self.set_keep_below(True) 99 | 100 | self.add(children) if children else None 101 | self.set_title(self.__name__) 102 | self.set_size(size[0], size[1 if len(size) == 2 else 0]) 103 | if self._disable_layer: 104 | self.set_layer("normal") 105 | else: 106 | self.set_layer(layer) 107 | 108 | self.set_position(position) 109 | self.set_exclusive(exclusive) 110 | self.set_margin(at) 111 | self.set_focusable(focusable) 112 | self.close() 113 | attributes(self) 114 | 115 | def set_focusable(self, focusable: Union[str, bool]) -> None: 116 | if self._disable_layer: 117 | return 118 | if self._wayland_display: 119 | _mode = { 120 | True: GtkLayerShell.KeyboardMode.EXCLUSIVE, 121 | False: GtkLayerShell.KeyboardMode.NONE, 122 | "none": GtkLayerShell.KeyboardMode.NONE, 123 | "on-demand": GtkLayerShell.KeyboardMode.ON_DEMAND, 124 | "exclusive": GtkLayerShell.KeyboardMode.EXCLUSIVE, 125 | }.get(focusable, GtkLayerShell.KeyboardMode.NONE) 126 | GtkLayerShell.set_keyboard_mode(self, _mode) 127 | else: 128 | self.set_can_focus(bool(focusable)) 129 | 130 | def set_position(self, position: str) -> None: 131 | if self._disable_layer: 132 | return 133 | if self._wayland_display: 134 | if position == "center": 135 | for j in [ 136 | GtkLayerShell.Edge.TOP, 137 | GtkLayerShell.Edge.RIGHT, 138 | GtkLayerShell.Edge.LEFT, 139 | GtkLayerShell.Edge.BOTTOM, 140 | ]: 141 | GtkLayerShell.set_anchor(self, j, False) 142 | else: 143 | for j in position.split(): 144 | GtkLayerShell.set_anchor( 145 | self, 146 | { 147 | "top": GtkLayerShell.Edge.TOP, 148 | "right": GtkLayerShell.Edge.RIGHT, 149 | "left": GtkLayerShell.Edge.LEFT, 150 | "bottom": GtkLayerShell.Edge.BOTTOM, 151 | }.get(j, GtkLayerShell.Edge.TOP), 152 | True, 153 | ) 154 | else: 155 | _size = self.get_size() or [10, 10] 156 | 157 | width, height = get_screen_size(self.monitor) 158 | width -= _size[0] 159 | height -= _size[1] 160 | 161 | if position == "center": 162 | self.move(width // 2, height // 2) 163 | else: 164 | 165 | if "top" in position: 166 | y = 0 167 | elif "bottom" in position: 168 | y = height 169 | elif "top" in position and "bottom" in position: 170 | y = height // 2 171 | else: 172 | y = height // 2 173 | 174 | if "left" in position: 175 | x = 0 176 | elif "right" in position: 177 | x = width 178 | elif "left" in position and "right" in position: 179 | x = width // 2 180 | else: 181 | x = width // 2 182 | 183 | self.move(x, y) 184 | 185 | def set_margin(self, margins: dict) -> None: 186 | if self._disable_layer: 187 | return 188 | 189 | if self._wayland_display: 190 | width, height = get_screen_size(self.monitor) 191 | 192 | for key, value in margins.items(): 193 | _key = { 194 | "top": GtkLayerShell.Edge.TOP, 195 | "bottom": GtkLayerShell.Edge.BOTTOM, 196 | "left": GtkLayerShell.Edge.LEFT, 197 | "right": GtkLayerShell.Edge.RIGHT, 198 | }.get(key) 199 | 200 | if not _key: 201 | continue 202 | 203 | if key in ["bottom", "top"]: 204 | value = parse_screen_size(value, height) 205 | GtkLayerShell.set_margin(self, _key, value) 206 | elif key in ["left", "right"]: 207 | value = parse_screen_size(value, width) 208 | GtkLayerShell.set_margin(self, _key, value) 209 | else: 210 | _size = self.get_size() or [10, 10] 211 | 212 | width, height = get_screen_size(self.monitor) 213 | width -= _size[0] 214 | height -= _size[1] 215 | 216 | posx, posy = self.get_position() 217 | 218 | for key, value in margins.items(): 219 | _key = { 220 | "top": posy == 0, 221 | "bottom": posy == height, 222 | "left": posx == 0, 223 | "right": posx == width, 224 | }.get(key) 225 | if _key: 226 | value = parse_screen_size(value, height) 227 | 228 | if key in ["bottom", "right"]: 229 | value = abs(value) * -1 230 | 231 | if key in ["top", "bottom"]: 232 | value = parse_screen_size(value, height) 233 | self.move_relative(y=value) 234 | elif key in ["left", "right"]: 235 | value = parse_screen_size(value, width) 236 | self.move_relative(x=value) 237 | 238 | def move_relative(self, x: int = 0, y: int = 0) -> None: 239 | if self._disable_layer: 240 | return 241 | if x != 0: 242 | _x, _y = self.get_position() 243 | self.move(_x + x, _y) 244 | if y != 0: 245 | _x, _y = self.get_position() 246 | self.move(_x, _y + y) 247 | 248 | def set_exclusive(self, exclusivity: Union[int, bool]) -> None: 249 | 250 | if self._wayland_display and not self._disable_layer: 251 | if exclusivity == True: 252 | GtkLayerShell.auto_exclusive_zone_enable(self) 253 | elif isinstance(exclusivity, int): 254 | GtkLayerShell.set_exclusive_zone(self, exclusivity) 255 | else: 256 | return 257 | else: 258 | pass 259 | 260 | def set_layer(self, layer: str) -> None: 261 | if self._wayland_display and not self._disable_layer: 262 | _layer = { 263 | "background": GtkLayerShell.Layer.BACKGROUND, 264 | "bottom": GtkLayerShell.Layer.BOTTOM, 265 | "top": GtkLayerShell.Layer.TOP, 266 | "overlay": GtkLayerShell.Layer.OVERLAY, 267 | # 268 | "desktop": GtkLayerShell.Layer.BACKGROUND, 269 | "menu": GtkLayerShell.Layer.BOTTOM, 270 | "dock": GtkLayerShell.Layer.TOP, 271 | "popup": GtkLayerShell.Layer.OVERLAY, 272 | }.get(layer, GtkLayerShell.Layer.TOP) 273 | 274 | GtkLayerShell.set_layer(self, _layer) 275 | 276 | else: 277 | _layer = { 278 | "normal": Gdk.WindowTypeHint.NORMAL, 279 | "dialog": Gdk.WindowTypeHint.DIALOG, 280 | "tooltip": Gdk.WindowTypeHint.TOOLTIP, 281 | "notification": Gdk.WindowTypeHint.NOTIFICATION, 282 | "combo": Gdk.WindowTypeHint.COMBO, 283 | "dnd": Gdk.WindowTypeHint.DND, 284 | "menu": Gdk.WindowTypeHint.MENU, 285 | "toolbar": Gdk.WindowTypeHint.TOOLBAR, 286 | "dock": Gdk.WindowTypeHint.DOCK, 287 | "splashscreen": Gdk.WindowTypeHint.SPLASHSCREEN, 288 | "utility": Gdk.WindowTypeHint.UTILITY, 289 | "desktop": Gdk.WindowTypeHint.DESKTOP, 290 | "dropdown": Gdk.WindowTypeHint.DROPDOWN_MENU, 291 | "popup": Gdk.WindowTypeHint.POPUP_MENU, 292 | # 293 | "background": Gdk.WindowTypeHint.DESKTOP, 294 | "bottom": Gdk.WindowTypeHint.DOCK, 295 | "top": Gdk.WindowTypeHint.DOCK, 296 | "overlay": Gdk.WindowTypeHint.NOTIFICATION, 297 | }.get(layer, Gdk.WindowTypeHint.DOCK) 298 | 299 | self.set_type_hint(_layer) 300 | 301 | def set_size(self, width: Union[int, str], height: Union[int, str]) -> None: 302 | screen = get_screen_size(self.monitor) 303 | 304 | _width = parse_screen_size(width, screen[0]) 305 | _height = parse_screen_size(height, screen[1]) 306 | 307 | self._size = [max(_width, 1), max(_height, 1)] 308 | 309 | self.set_default_size(self._size[0], self._size[1]) 310 | self.set_size_request(self._size[0], self._size[1]) 311 | self.resize(self._size[0], self._size[1]) 312 | 313 | def bind(self, var: Union[Listener, Variable, Poll], callback: Callable) -> None: 314 | var.bind(callback) 315 | 316 | def open(self, duration: Union[int, str] = 0) -> None: 317 | self.show() 318 | 319 | if bool(duration): 320 | GLib.timeout_add(parse_interval(duration), self.close) 321 | 322 | def close(self) -> None: 323 | self.hide() 324 | 325 | def toggle(self) -> None: 326 | if self.get_visible(): 327 | self.close() 328 | else: 329 | self.open() 330 | 331 | @property 332 | def monitor(self) -> int: 333 | return self._monitor 334 | 335 | @property 336 | def __name__(self) -> str: 337 | return self._name 338 | 339 | def __str__(self) -> str: 340 | return self.__name__ 341 | 342 | def __repr__(self) -> str: 343 | return self.__str__() 344 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/_Window.py: -------------------------------------------------------------------------------- 1 | from ..Imports import * 2 | from ..Variable import Listener, Poll, Variable 3 | 4 | cleantextX = lambda x, perheight: ( 5 | perheight(str(x).replace("%", "")) 6 | if "%" in str(x) 7 | else float(str(x).replace("px", "")) 8 | ) 9 | cleantextY = lambda x, perwidth: ( 10 | perwidth(str(x).replace("%", "")) 11 | if "%" in str(x) 12 | else float(str(x).replace("px", "")) 13 | ) 14 | 15 | 16 | class Window(Gtk.Window): 17 | def __init__( 18 | self, 19 | size=[0, 0], 20 | at={}, 21 | position="center", 22 | layer="top", 23 | exclusive=False, 24 | children=None, 25 | monitor=0, 26 | parent=None, 27 | focusable="none", 28 | popup=False, 29 | namespace="gtk-layer-shell", 30 | attributes=None, 31 | **kwargs, 32 | ): 33 | Gtk.Window.__init__(self) 34 | self.monitor = monitor 35 | self._screen_width, self._screen_height = self.__calculateResolution( 36 | self.monitor 37 | ) 38 | self._perheight = lambda x: (float(x) * self._screen_height) / 100 39 | self._perwidth = lambda x: (float(x) * self._screen_width) / 100 40 | self.properties = self.__adjustProps( 41 | { 42 | "size": size, 43 | "at": at, 44 | "position": position, 45 | "layer": layer, 46 | "exclusive": exclusive, 47 | "namespace": namespace, 48 | } 49 | ) 50 | self.add(children) if children else None 51 | # Other settings for the window 52 | # Useful for popups or something like that 53 | self.set_transient_for(parent) if parent else None 54 | self.set_destroy_with_parent(True if parent else False) 55 | 56 | # GtkLayerShell SETTING, etc... 57 | if not locals().get("disable_gtklayershell", False): 58 | GtkLayerShell.init_for_window(self) 59 | GtkLayerShell.set_namespace(self, self.properties.get("namespace")) 60 | GtkLayerShell.set_layer( 61 | self, self.__clasif_layer(self.properties.get("layer", "top")) 62 | ) 63 | 64 | self.__clasif_position(self.properties.get("position", "center")) 65 | self.__clasif_exclusive(self.properties.get("exclusive", False)) 66 | self.__clasif_at(self.properties.get("at", False)) 67 | self.set_size_request( 68 | max(self.properties["size"][0], 10), max(self.properties["size"][1], 10) 69 | ) 70 | 71 | # self.connect("destroy", Gtk.main_quit) 72 | 73 | self.set_focusable(focusable) 74 | self.set_popup(popup) 75 | attributes(self) if attributes else None 76 | 77 | if self.popup: 78 | # Connect the key-press-event signal to handle the Escape key 79 | self.connect("key-press-event", self.on_key_press) 80 | # Connect the button-press-event signal to handle clicks outside the window 81 | self.connect("button-press-event", self.on_button_press) 82 | 83 | self.close() 84 | 85 | def on_key_press(self, _, event): 86 | # Handle key-press-event signal (Escape key) 87 | if event.keyval == Gdk.KEY_Escape: 88 | self.close() 89 | 90 | def on_button_press(self, _, event): 91 | # Handle button-press-event signal (click outside the window) 92 | if ( 93 | event.type == Gdk.EventType.BUTTON_PRESS and event.button == 1 94 | ): # Left mouse button 95 | x, y = event.x_root, event.y_root 96 | frame_extents = self.get_window().get_frame_extents() 97 | if ( 98 | x < frame_extents.x 99 | or x >= frame_extents.x + frame_extents.width 100 | or y < frame_extents.y 101 | or y >= frame_extents.y + frame_extents.height 102 | ): 103 | self.close() 104 | 105 | def set_focusable(self, focusable): 106 | focusable_mode = { 107 | "onfocus": GtkLayerShell.KeyboardMode.ON_DEMAND, 108 | "force": GtkLayerShell.KeyboardMode.EXCLUSIVE, 109 | "none": GtkLayerShell.KeyboardMode.NONE, 110 | }.get(focusable, GtkLayerShell.KeyboardMode.NONE) 111 | 112 | GtkLayerShell.set_keyboard_mode(self, focusable_mode) 113 | 114 | def set_popup(self, popup): 115 | self.popup = popup 116 | if popup: 117 | # Set the window type hint to POPUP 118 | self.set_type_hint(Gdk.WindowTypeHint.POPUP_MENU) 119 | else: 120 | # Set the window type hint to NORMAL 121 | self.set_type_hint(Gdk.WindowTypeHint.NORMAL) 122 | 123 | def __adjustProps(self, props): 124 | at = props.get("at", {"top": 0, "bottom": 0, "left": 0, "right": 0}) 125 | 126 | at["top"] = cleantextY(at.get("top", 0), self._perwidth) 127 | at["bottom"] = cleantextY(at.get("bottom", 0), self._perwidth) 128 | at["left"] = cleantextX(at.get("left", 0), self._perheight) 129 | at["right"] = cleantextX(at.get("right", 0), self._perheight) 130 | 131 | size = props.get("size", [0, 0]) 132 | 133 | props["size"] = [ 134 | cleantextX(size[0], self._perwidth), 135 | cleantextY(size[1], self._perheight), 136 | ] 137 | props["at"] = at 138 | 139 | return props 140 | 141 | def __calculateResolution(self, monitor): 142 | display = Gdk.Display.get_default() 143 | n_monitors = display.get_n_monitors() 144 | 145 | if monitor < 0 or monitor >= n_monitors: 146 | raise ValueError(f"Invalid monitor index: {monitor}") 147 | 148 | monitors = [display.get_monitor(i).get_geometry() for i in range(n_monitors)] 149 | selected_monitor = monitors[monitor] 150 | 151 | return selected_monitor.width, selected_monitor.height 152 | 153 | def __clasif_layer(self, layer): 154 | if layer.lower() in ["background", "bg"]: 155 | return GtkLayerShell.Layer.BACKGROUND 156 | elif layer.lower() in ["bottom", "bt"]: 157 | return GtkLayerShell.Layer.BOTTOM 158 | elif layer.lower() in ["top", "tp"]: 159 | return GtkLayerShell.Layer.TOP 160 | elif layer.lower() in ["overlay", "ov"]: 161 | return GtkLayerShell.Layer.OVERLAY 162 | 163 | def __clasif_position(self, position): 164 | for i in position.lower().split(" "): 165 | if i in ["top", "tp"]: 166 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, True) 167 | 168 | elif i in ["bottom", "bt"]: 169 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.BOTTOM, True) 170 | 171 | elif i in ["left", "lf"]: 172 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, True) 173 | 174 | elif i in ["right", "rg"]: 175 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.RIGHT, True) 176 | 177 | elif i in ["center", "ct"]: 178 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, False) 179 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.RIGHT, False) 180 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, False) 181 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.BOTTOM, False) 182 | 183 | def __clasif_exclusive(self, exclusivity): 184 | if exclusivity == True: 185 | return GtkLayerShell.auto_exclusive_zone_enable(self) 186 | elif isinstance(exclusivity, int): 187 | return GtkLayerShell.set_exclusive_zone(self, exclusivity) 188 | else: 189 | return 190 | 191 | def __clasif_at(self, at): 192 | if at: 193 | for key, value in at.items(): 194 | if key in ["top", "tp"]: 195 | GtkLayerShell.set_margin(self, GtkLayerShell.Edge.TOP, value) 196 | 197 | elif key in ["bottom", "bt"]: 198 | GtkLayerShell.set_margin(self, GtkLayerShell.Edge.BOTTOM, value) 199 | 200 | elif key in ["left", "lf"]: 201 | GtkLayerShell.set_margin(self, GtkLayerShell.Edge.LEFT, value) 202 | 203 | elif key in ["right", "rg"]: 204 | GtkLayerShell.set_margin(self, GtkLayerShell.Edge.RIGHT, value) 205 | 206 | elif key in ["center", "ct"]: 207 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, False) 208 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.RIGHT, False) 209 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, False) 210 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.BOTTOM, False) 211 | 212 | def bind(self, var, callback): 213 | if isinstance(var, (Listener, Variable, Poll)): 214 | var.connect( 215 | "valuechanged", lambda x: GLib.idle_add(lambda: callback(x.get_value())) 216 | ) 217 | 218 | def open(self, duration=0): 219 | self.show() 220 | 221 | if duration > 0: 222 | GLib.timeout_add(duration, lambda: self.close()) 223 | 224 | def close(self): 225 | self.hide() 226 | 227 | def toggle(self): 228 | if self.get_visible(): 229 | self.close() 230 | else: 231 | self.open() 232 | 233 | def __str__(self) -> str: 234 | return str(self.properties["namespace"]) 235 | 236 | def __repr__(self) -> str: 237 | return str(self.properties["namespace"]) 238 | -------------------------------------------------------------------------------- /PotatoWidgets/Widget/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains various GTK widgets that inherit from the BasicProps class. 3 | 4 | Box: Container widget for organizing other widgets. 5 | Button: Interactive button. 6 | CenterBox: Container widget that aligns its content to the center. 7 | CheckBox: Checkbox for binary selection. 8 | ComboBox: Dropdown box for selection of options. 9 | Entry: Text entry field. 10 | EventBox: Widget for handling input events. 11 | Fixed: Container widget with fixed positioning. 12 | Icon: Widget for displaying icons. 13 | Image: Widget for displaying images. 14 | Label: Widget for displaying non-editable text. 15 | Menu: Dropdown menu. 16 | MenuItem: Menu item. 17 | Overlay: Widget for overlaying content. 18 | ProgressBar: Progress bar. 19 | Revealer: Widget for showing or hiding content. 20 | Scale: Slider control for numerical selection. 21 | Scroll: Widget for adding scrollbars. 22 | Separator: Visual separator between widgets. 23 | Window: Main application window. 24 | """ 25 | 26 | __all__ = [ 27 | "Box", 28 | "Button", 29 | "CenterBox", 30 | "CheckBox", 31 | "ComboBox", 32 | "Entry", 33 | "EventBox", 34 | "Fixed", 35 | "Icon", 36 | "Image", 37 | "Label", 38 | "Menu", 39 | "MenuItem", 40 | "Overlay", 41 | "ProgressBar", 42 | "Revealer", 43 | "Scale", 44 | "Scroll", 45 | "Separator", 46 | "Window", 47 | "Grid", 48 | "Stack", 49 | "FlowBox", 50 | "FlowBoxChild", 51 | ] 52 | 53 | 54 | from .Box import Box 55 | from .Button import Button 56 | from .CenterBox import CenterBox 57 | from .CheckBox import CheckBox 58 | from .ComboBox import ComboBox 59 | from .Entry import Entry 60 | from .EventBox import EventBox 61 | from .Fixed import Fixed 62 | from .FlowBox import FlowBox, FlowBoxChild 63 | from .Grid import Grid 64 | from .Icon import Icon 65 | from .Image import Image 66 | from .Label import Label 67 | from .Menu import Menu, MenuItem 68 | from .Overlay import Overlay 69 | from .ProgressBar import ProgressBar 70 | from .Revealer import Revealer 71 | from .Scale import Scale 72 | from .Scroll import Scroll 73 | from .Separator import Separator 74 | from .Stack import Stack 75 | from .Window import Window 76 | -------------------------------------------------------------------------------- /PotatoWidgets/_Logger.py: -------------------------------------------------------------------------------- 1 | class Logger: 2 | class Colors: 3 | RESET = "\033[0m" 4 | BOLD = "\033[1m" 5 | 6 | # Warning Scale 7 | ERROR = "\033[1;91m" # Bold red for critical errors 8 | WARNING = "\033[1;33m" # Bold yellow for important warnings 9 | DEPRECATED = "\033[1;35m" # Bold magenta for deprecated messages 10 | DEBUG = "\033[1;94m" # Bold light blue for debugging messages 11 | SUCCESS = "\033[1;92m" # Bold green for indicating success 12 | 13 | @staticmethod 14 | def _log(color_code, *args, **kwargs): 15 | print(color_code, *args, **kwargs) 16 | print(Logger.Colors.RESET, end="") 17 | 18 | @staticmethod 19 | def DEPRECATED(*args, **kwargs): 20 | Logger._log(Logger.Colors.DEPRECATED, "[DEPRECATED]", *args, **kwargs) 21 | 22 | @staticmethod 23 | def WARNING(*args, **kwargs): 24 | Logger._log(Logger.Colors.WARNING, "[WARNING]", *args, **kwargs) 25 | 26 | @staticmethod 27 | def SUCCESS(*args, **kwargs): 28 | Logger._log(Logger.Colors.SUCCESS, "[SUCCESS]", *args, **kwargs) 29 | 30 | @staticmethod 31 | def DEBUG(*args, **kwargs): 32 | Logger._log(Logger.Colors.DEBUG, "[DEBUG]", *args, **kwargs) 33 | 34 | @staticmethod 35 | def ERROR(*args, **kwargs): 36 | Logger._log(Logger.Colors.ERROR, "[ERROR]", *args, **kwargs) 37 | -------------------------------------------------------------------------------- /PotatoWidgets/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Welcome to PotatoWidgets :D 3 | """ 4 | 5 | __all__ = [ 6 | "Widget", 7 | "Bash", 8 | "PotatoLoop", 9 | "Listener", 10 | "Poll", 11 | "Variable", 12 | "Gdk", 13 | "GdkPixbuf", 14 | "Gio", 15 | "GObject", 16 | "Gtk", 17 | "GtkLayerShell", 18 | "Pango", 19 | "Playerctl", 20 | "GLib", 21 | ] 22 | 23 | from . import Widget 24 | from .Bash import * 25 | from .Env import * 26 | from .Imports import (Gdk, GdkPixbuf, Gio, GLib, GObject, Gtk, GtkLayerShell, 27 | Pango, Playerctl) 28 | from .Methods import * 29 | from .PotatoLoop import * 30 | from .Variable import Listener, Poll, Variable 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Welcome to PotatoWidgets :potato::sparkles: 2 | 3 | PotatoWidgets simplifies Python's interaction with GTK, offering a straightforward framework for creating graphical user interfaces effortlessly. 4 | 5 | ### Why PotatoWidgets? 6 | 7 | - **Pythonic Configuration**: Entirely configured in Python (and SCSS for added beauty), PotatoWidgets leverages Python's extensive library ecosystem, empowering you with all the tools you need for seamless development. 8 | - **Built-in Functions and Services**: PotatoWidgets makes development easier by providing built-in functions and services to interact with your system, eliminating the need for external scripting. 9 | - **Various Examples**: Find illustrative examples for various functionalities on the project's wiki. If not available, you're welcome to contribute and help expand the library of examples. Check out the contributing guide to get started! 10 | 11 | ### Installation 12 | 13 | ```bash 14 | pip install git+https://github.com/T0kyoB0y/PotatoWidgets.git 15 | ``` 16 | 17 | ### Peek into [My Setup](https://github.com/T0kyoB0y/dotfiles/) 18 | 19 | #### BSPWM 20 | 21 | ![BSPWM Setup](./img/setup4.png) 22 | 23 | #### Hyprland 24 | 25 | ![Hyprland Setup 1](./img/setup.png) 26 | ![Hyprland Setup 2](./img/setup2.png) 27 | ![Hyprland Setup 3](./img/setup3.png) 28 | 29 | ### Ready to Contribute? 30 | 31 | Thank you for considering contributing to PotatoWidgets! Here's how you can get started: 32 | 33 | #### Contributing to the Code 34 | 35 | 1. **Fork the repository**: Click on the 'Fork' button on the top-right corner of this page. 36 | 2. **Code**: Make your desired changes to the codebase. 37 | 3. **Push your changes**: Push your changes to your forked repository. 38 | 4. **Submit a pull request**: Go to the [original repository](https://github.com/T0kyoB0y/PotatoWidgets) and click on the 'New pull request' button. Describe your changes and submit the pull request. 39 | 40 | #### Contributing to the Wiki 41 | 42 | Wanna contribute to the wiki? Here are some ways you can help: 43 | 44 | - **Document new features**: Add documentation for new features. 45 | - **Update existing documentation**: Ensure that existing documentation is accurate and up-to-date. 46 | - **Write tutorials**: Create tutorials or guides to help users understand how to use PotatoWidgets effectively. 47 | - **Provide examples**: Add code snippets or full examples to illustrate usage. 48 | 49 | Feel free to add any examples or detailed explanations you think would be helpful! 50 | 51 | Thank you for your contributions! 🥔🚀 52 | 53 | > "potato widgets can be resumed as 'the joke went to far, cannot stop now'" 54 | > -- λ.midnight 55 | -------------------------------------------------------------------------------- /default_conf/__init__.py: -------------------------------------------------------------------------------- 1 | from modules import * 2 | -------------------------------------------------------------------------------- /default_conf/__main__.py: -------------------------------------------------------------------------------- 1 | from modules import * 2 | 3 | from PotatoWidgets import PotatoLoop 4 | 5 | 6 | def main() -> None: 7 | 8 | MyTopbar.open() 9 | PotatoLoop(run_without_services=True) 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /default_conf/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from .topbar import * 2 | -------------------------------------------------------------------------------- /default_conf/modules/bspwm/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from PotatoWidgets import Bash, Variable, Widget 4 | 5 | keys: List[str] = Bash.get_output("bspc query --desktops", list) 6 | values: List[int] = [i for i in range(1, len(keys) + 1)] 7 | 8 | ActiveWorkspace = Variable(1) 9 | # doing this bc by default bspwm handles workspace names in hex 10 | WorkspaceMapping: Dict[str, int] = {keys[i]: int(values[i]) for i in range(len(keys))} 11 | 12 | 13 | # bspc subscribe desktop_focus 14 | # desktop_focus 15 | # hex id 16 | Bash.popen( 17 | "bspc subscribe desktop_focus", 18 | stdout=lambda stdout: ActiveWorkspace.set_value( 19 | # desktop_focus 0x00200002 0x00200006 20 | # ["desktop_focus", "0x00200002", "x00200006"] 21 | # 0 1 2 22 | WorkspaceMapping.get(stdout.split()[2], 0) 23 | ), 24 | ) 25 | 26 | 27 | # Topbar Elements 28 | def WorkspaceButton(id): 29 | my_button = Widget.Button( 30 | onclick=lambda: Bash.run_async(f"bspc desktop --focus {id}"), 31 | valign="center", 32 | children=Widget.Label(id), 33 | attributes=lambda self: ( 34 | setattr(self, "id", id), 35 | self.bind( 36 | ActiveWorkspace, 37 | lambda out: ( 38 | self.set_css("background: blue;") 39 | if out == self.id 40 | else self.set_css("background: unset;") 41 | ), 42 | ), 43 | ), 44 | ) 45 | my_button.set_css("background: unset;") 46 | return my_button 47 | 48 | 49 | def Workspaces(): 50 | return list(map(WorkspaceButton, WorkspaceMapping.values())) 51 | # return [WorkspaceButton(i) for i in WorkspaceMapping.values()] 52 | -------------------------------------------------------------------------------- /default_conf/modules/hyprland/__init__.py: -------------------------------------------------------------------------------- 1 | # from PotatoWidgets import HyprlandService 2 | from PotatoWidgets import Variable, Widget 3 | from PotatoWidgets.Services.Hyprland import HyprlandService 4 | 5 | ActiveWorkspace = Variable(0) 6 | 7 | 8 | # Sorry, I havent fully implemented the Hyprland service yet 9 | HyprlandService.connect( 10 | "workspacev2", lambda _, id, name: ActiveWorkspace.set_value(id) 11 | ) 12 | 13 | 14 | def WorkspaceButton(id): 15 | my_button = Widget.Button( 16 | onclick=lambda: HyprlandService.hyprctl_async(f"dispatch workspace {id}"), 17 | valign="center", 18 | children=Widget.Label(id), 19 | ) 20 | setattr(my_button, "id", id) 21 | ActiveWorkspace.bind( 22 | lambda v: ( 23 | my_button.set_css("background: blue") 24 | if v == getattr(my_button, "id") 25 | else my_button.set_css("background: unset") 26 | ) 27 | ) 28 | 29 | my_button.set_css("background: unset;") 30 | 31 | return my_button 32 | 33 | 34 | def Workspaces(): 35 | return list(map(WorkspaceButton, range(1, 11))) 36 | -------------------------------------------------------------------------------- /default_conf/modules/topbar.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from PotatoWidgets import Bash, Poll, Widget, interval 4 | from PotatoWidgets.Services import BatteryService 5 | 6 | from .utils import * 7 | 8 | if Bash.get_env("HYPRLAND_INSTANCE_SIGNATURE"): 9 | print("hyprñand detected") 10 | 11 | from .hyprland import Workspaces 12 | elif Bash.get_env("DESKTOP_SESSION") == "bspwm": 13 | print("bspwm detected") 14 | from .bspwm import Workspaces 15 | else: 16 | 17 | def Workspaces(): 18 | return [] 19 | 20 | 21 | def Brightness(): 22 | icon = Widget.Icon("display-brightness-symbolic", 20) 23 | scale = Widget.Scale( 24 | onchange=lambda b: Bash.run_async(f"brightnessctl set {b}%"), 25 | value=BRIGHTNESS.value, 26 | valign="center", 27 | vexpand=True, 28 | css="min-width: 100px;", 29 | ) 30 | 31 | interval("1s", lambda: scale.set_value(BRIGHTNESS.value)) 32 | 33 | return [icon, scale] 34 | 35 | 36 | def Volume(): 37 | def get_icon(value: int) -> str: 38 | if value == 0: 39 | return "audio-volume-muted-symbolic" 40 | elif value < 30: 41 | return "audio-volume-low-symbolic" 42 | elif value < 60: 43 | return "audio-volume-medium-symbolic" 44 | else: 45 | return "audio-volume-high-symbolic" 46 | 47 | icon = Widget.Icon(get_icon(VOLUME.value), 20) 48 | icon.bind(VOLUME, lambda out: icon.set_icon(get_icon(out))) 49 | 50 | scale = Widget.Scale( 51 | attributes=lambda self: self.bind(VOLUME, self.set_value), 52 | value=VOLUME.value, 53 | valign="center", 54 | vexpand=True, 55 | css="min-width: 100px;", 56 | onchange=lambda v: Bash.run_async(f"pactl set-sink-volume @DEFAULT_SINK@ {v}%"), 57 | ) 58 | 59 | return [icon, scale] 60 | 61 | 62 | def Battery(): 63 | icon = Widget.Icon( 64 | icon=BatteryService.bind("icon-name"), 65 | size=20, 66 | ) 67 | progressbar = Widget.ProgressBar(BatteryService.bind("percentage"), valign="center") 68 | return [icon, progressbar] 69 | 70 | 71 | def NotifIndicator(): 72 | def wrapper(count: int): 73 | if count: 74 | return "user-available-symbolic" 75 | return "user-idle-symbolic" 76 | 77 | icon = Widget.Icon("user-available-symbolic", 20) 78 | label = Widget.Label(NotificationsService.bind("count")) 79 | 80 | NotificationsService.connect( 81 | "count", lambda _, count: icon.set_icon(wrapper(count)) 82 | ) 83 | 84 | return Widget.Button( 85 | Widget.Box([icon, label], spacing=10), 86 | onclick=lambda: NotificationsService.clear(), 87 | ) 88 | 89 | 90 | def Topbar(): 91 | return Widget.Window( 92 | position="top left right", 93 | size=["100%", 50], 94 | children=Widget.CenterBox( 95 | css="background-color: #161616; padding: 0 20px;", 96 | start=NotifIndicator(), 97 | center=Widget.Box( 98 | spacing=10, 99 | children=Workspaces() + Volume() + Brightness() + Battery(), 100 | ), 101 | end=Widget.Box( 102 | children=Widget.Label( 103 | text=Poll( 104 | "1m", lambda: datetime.now().strftime("%H:%M %p %d/%m/%Y") 105 | ) 106 | ), 107 | ), 108 | ), 109 | ) 110 | 111 | 112 | MyTopbar = Topbar() 113 | -------------------------------------------------------------------------------- /default_conf/modules/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from PotatoWidgets import Bash, Variable 2 | from PotatoWidgets.Services import NotificationsService 3 | 4 | VOLUME = Variable(0) 5 | BRIGHTNESS = Variable(0) 6 | NOTIFICATIONS = NotificationsService.bind("count") 7 | 8 | 9 | def UpdateVolume() -> None: 10 | volume = Bash.get_output( 11 | "pactl get-sink-volume @DEFAULT_SINK@ | grep -Po '[0-9]{1,3}(?=%)' | head -1", 12 | int, 13 | ) 14 | if volume != VOLUME.value: 15 | VOLUME.value = int(volume) 16 | 17 | 18 | def UpdateBrightness() -> None: 19 | brightness = Bash.get_output( 20 | "brightnessctl | grep Current | awk '{print $4}' | sed 's/[(%)]//g'", 21 | int, 22 | ) 23 | if brightness != BRIGHTNESS.value: 24 | BRIGHTNESS.value = brightness 25 | 26 | 27 | BRIGHTNESS_FILE = Bash.get_output("ls -1 /sys/class/backlight", list)[0] 28 | 29 | Bash.popen( 30 | """bash -c 'pactl subscribe | grep --line-buffered "on sink"' """, 31 | stdout=lambda _: UpdateVolume(), 32 | ) 33 | 34 | Bash.monitor_file( 35 | f"/sys/class/backlight/{BRIGHTNESS_FILE}/brightness", 36 | flags="watch_moves", 37 | call_when=["changed"], 38 | callback=lambda: UpdateBrightness(), 39 | ) 40 | -------------------------------------------------------------------------------- /default_conf/style.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/AppLauncher/AppLauncher.py: -------------------------------------------------------------------------------- 1 | from PotatoWidgets import Variable, Widget, lookup_icon 2 | from PotatoWidgets.Services import Applications 3 | 4 | 5 | def GenerateApp(entry): 6 | _actioned = lambda: ( 7 | entry.launch(), 8 | AppLauncher.close(), 9 | AppQuery.set_value(""), 10 | AppEntry.set_text(""), 11 | ) 12 | 13 | _app = Widget.Revealer( 14 | valign="start", 15 | transition="slideup", 16 | duration=250, 17 | reveal=True, 18 | attributes=lambda self: ( 19 | setattr(self, "keywords", entry.keywords), 20 | setattr(self, "launch", _actioned), 21 | self.bind( 22 | AppQuery, 23 | lambda query: self.set_revealed(str(query).lower() in self.keywords), 24 | ), 25 | ), 26 | children=Widget.Button( 27 | classname="app", 28 | valign="start", 29 | onclick=_actioned, 30 | children=Widget.Box( 31 | spacing=10, 32 | children=[ 33 | Widget.Image(lookup_icon(entry.icon_name), 35), 34 | Widget.Box( 35 | orientation="v", 36 | vexpand=True, 37 | children=[ 38 | Widget.Label( 39 | entry.name.title(), 40 | wrap=True, 41 | halign="start", 42 | valign=("center" if not entry.comment else "start"), 43 | vexpand=(True if not entry.comment else False), 44 | classname="name", 45 | ), 46 | Widget.Label( 47 | entry.comment or entry.generic_name, 48 | wrap=True, 49 | visible=bool(entry.comment or entry.generic_name), 50 | classname="comment", 51 | ), 52 | ], 53 | ), 54 | ], 55 | ), 56 | ), 57 | ) 58 | return _app 59 | 60 | 61 | AppQuery = Variable("") 62 | 63 | SortedAppsByName = Applications.get_all() 64 | SortedAppsByName.sort(key=lambda app: app.name) 65 | 66 | 67 | AppsList = Widget.Scroll( 68 | hexpand=True, 69 | vexpand=True, 70 | children=Widget.Box( 71 | orientation="v", 72 | children=[GenerateApp(app) for app in SortedAppsByName], 73 | ), 74 | ) 75 | 76 | AppEntry = Widget.Entry( 77 | onchange=AppQuery.set_value, 78 | onenter=lambda text: next( 79 | app.launch() 80 | for app in AppsList.get_children()[0].get_children()[0].get_children() 81 | if str(text).lower() in app.keywords 82 | ), 83 | ) 84 | 85 | 86 | AppLauncher = Widget.Window( 87 | size=[500, 600], 88 | layer="dialog", 89 | namespace="AppLauncher", 90 | children=Widget.Box( 91 | classname="launcher", 92 | orientation="v", 93 | spacing=20, 94 | children=[ 95 | AppEntry, 96 | AppsList, 97 | ], 98 | ), 99 | ) 100 | -------------------------------------------------------------------------------- /examples/AppLauncher/AppLauncher.scss: -------------------------------------------------------------------------------- 1 | 2 | .launcher { 3 | padding: 40px; 4 | border-radius: 20px; 5 | background: #111; 6 | 7 | .app { 8 | border-radius: 20px; 9 | padding: 10px 20px; 10 | &:hover { 11 | background: #232323; 12 | } 13 | .name { 14 | color: #fff; 15 | font-weight: 700; 16 | } 17 | .comment { 18 | color: #525252; 19 | font-weight: 500, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/BatteryModule/BatteryModule.py: -------------------------------------------------------------------------------- 1 | from PotatoWidgets import Widget 2 | from PotatoWidgets.Services import BatteryService 3 | 4 | BatteryModule = Widget.Overlay( 5 | [ 6 | Widget.ProgressBar( 7 | value=BatteryService.bind("percentage"), 8 | orientation="v", 9 | halign="center", 10 | classname="battery-progress", 11 | inverted=True, 12 | ), 13 | # Nerd Font as icon 14 | Widget.Label(text="󱐋", css="color: #111;", classname="nf-icon"), 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /examples/BatteryModule/BatteryModule.scss: -------------------------------------------------------------------------------- 1 | 2 | .nf-icon { 3 | font-family: "Symbols Nerd Font"; 4 | font-size: 20px; 5 | min-width: 1em; 6 | min-height: 1em; 7 | } 8 | .battery-progress { 9 | background: #232323; 10 | &, 11 | & > trough, 12 | & > trough > progress { 13 | min-width: 20px; 14 | border-radius: 20px; 15 | } 16 | trough progress { 17 | background-color: #43b365; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/MediaPlayer/MediaPlayer.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from PotatoWidgets import Playerctl, Variable 4 | 5 | # First we create a var to store our players as widgets 6 | PlayersList = Variable([]) 7 | 8 | 9 | # This handles all the players available 10 | # https://lazka.github.io/pgi-docs/Playerctl-2.0/classes/PlayerManager.html 11 | Manager = Playerctl.PlayerManager() 12 | 13 | # First add current players 14 | for name in Manager.get_property("player_names") or []: 15 | Manager.manage_player( 16 | Playerctl.Player.new_from_name(name), 17 | ) 18 | 19 | # Then connect to signals to update players 20 | Manager.connect("name-appeared", lambda *args: UpdatePlayers(*args, new_player=True)) 21 | Manager.connect("name-vanished", lambda *args: UpdatePlayers(*args)) 22 | 23 | 24 | # Now make a callback to handle appear/vanish players 25 | def UpdatePlayers(_, player: Playerctl.PlayerName, new_player: bool = False): 26 | if new_player: 27 | Manager.manage_player(Playerctl.Player.new_from_name(player)) 28 | 29 | # Reload PlayersList var with new players as widgets 30 | PlayersList.value = list( 31 | map( 32 | lambda p: GenerateWidget(*ConnectPlayerToWidgets(p)), 33 | _manager.props.players, 34 | ) 35 | ) 36 | -------------------------------------------------------------------------------- /examples/NotificationPopups/NotificationPopups.py: -------------------------------------------------------------------------------- 1 | from PotatoWidgets import Widget, lookup_icon 2 | from PotatoWidgets.Services import Notification as NotificationClass 3 | from PotatoWidgets.Services import NotificationsService 4 | 5 | 6 | def Notification(notif: NotificationClass): # to have autocompletion 7 | 8 | if not notif: 9 | return 10 | 11 | if notif.image: 12 | if notif.image.endswith((".png", ".jpg")): 13 | IMAGE_WIDGET = Widget.Box( 14 | css=f""" 15 | background-size: cover; 16 | background-repeat: no-repeat; 17 | background-position: center; 18 | background-image: url("{notif.image}"); 19 | """, 20 | size=60, 21 | halign="center", 22 | valign="center", 23 | classname="image", 24 | ) 25 | else: 26 | IMAGE_WIDGET = Widget.Icon(notif.image, 60) 27 | else: 28 | IMAGE_WIDGET = Widget.Image(lookup_icon(notif.name), 60) 29 | 30 | if notif.actions: 31 | _actions = Widget.Box( 32 | homogeneous=True, 33 | spacing=5, 34 | children=[ 35 | ( 36 | lambda action: Widget.Button( 37 | onclick=lambda: notif.action(action["id"]), 38 | classname="actionbutton", 39 | children=Widget.Label(action["label"]), 40 | ) 41 | )(i) 42 | for i in notif.actions 43 | ], 44 | ) 45 | else: 46 | _actions = None 47 | 48 | return Widget.Button( 49 | classname="notification", 50 | primaryrelease=lambda: notif.dismiss(), 51 | attributes=lambda self: setattr(self, "id", notif.id), 52 | children=Widget.Box( 53 | orientation="v", 54 | spacing=10, 55 | children=[ 56 | Widget.Box( 57 | spacing=10, 58 | children=[ 59 | IMAGE_WIDGET, 60 | Widget.Box( 61 | orientation="v", 62 | valign="center", 63 | spacing=5, 64 | children=[ 65 | Widget.Label( 66 | notif.summary, 67 | justify="left", 68 | classname="header", 69 | xalign=0, 70 | ), 71 | Widget.Label( 72 | notif.body, 73 | wrap=True, 74 | justify="left", 75 | xalign=0, 76 | classname="content", 77 | ), 78 | ], 79 | ), 80 | ], 81 | ), 82 | _actions, 83 | ], 84 | ), 85 | ) 86 | 87 | 88 | NotificationsPopup = Widget.Window( 89 | layer="notification", 90 | position="top right", 91 | size=[450, 50], 92 | at={"right": 10, "top": 10}, 93 | children=Widget.Box( 94 | orientation="v", 95 | spacing=10, 96 | classname="notifications-container", 97 | attributes=lambda self: ( 98 | NotificationsService.connect( 99 | "popup", 100 | lambda instance, id: self.add( 101 | Notification(instance.get_notification(id)) 102 | ), 103 | ), 104 | NotificationsService.connect( 105 | "dismissed", 106 | lambda _, id: self.set_children( 107 | [i for i in self.get_children() if getattr(i, "id") != id] 108 | ), 109 | ), 110 | ), 111 | ), 112 | ) 113 | -------------------------------------------------------------------------------- /examples/NotificationPopups/NotificationPopups.scss: -------------------------------------------------------------------------------- 1 | .notifications-container { 2 | padding:10px; 3 | .notification { 4 | background: #111; 5 | padding: 10px; 6 | border-radius: 20px; 7 | .image { 8 | border-radius: 20px; 9 | } 10 | 11 | .header { 12 | color: #fff; 13 | font-size: 15px; 14 | font-weight: 600; 15 | } 16 | .content { 17 | color: #525252; 18 | font-size: 14px; 19 | font-weight: 500; 20 | 21 | } 22 | 23 | .actionbutton { 24 | background: #232323; 25 | padding: 7.5px; 26 | border-radius: 20px; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /img/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokyob0t/PotatoWidgets/5af1f29294c688fcdd6634827bb73dcf6f6cadf3/img/Preview.png -------------------------------------------------------------------------------- /img/setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokyob0t/PotatoWidgets/5af1f29294c688fcdd6634827bb73dcf6f6cadf3/img/setup.png -------------------------------------------------------------------------------- /img/setup2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokyob0t/PotatoWidgets/5af1f29294c688fcdd6634827bb73dcf6f6cadf3/img/setup2.png -------------------------------------------------------------------------------- /img/setup3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokyob0t/PotatoWidgets/5af1f29294c688fcdd6634827bb73dcf6f6cadf3/img/setup3.png -------------------------------------------------------------------------------- /img/setup4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tokyob0t/PotatoWidgets/5af1f29294c688fcdd6634827bb73dcf6f6cadf3/img/setup4.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | from setuptools.command.install import install 3 | 4 | 5 | class InitConfigFiles(install): 6 | def run(self): 7 | print("Helo") 8 | install.run(self) 9 | 10 | 11 | setup( 12 | name="PotatoWidgets", 13 | version="1.2.8", 14 | packages=find_packages(), 15 | install_requires=["PyGObject"], 16 | cmdclass={ 17 | "install": InitConfigFiles, 18 | }, 19 | entry_points={ 20 | "console_scripts": [ 21 | # "potatocli = PotatoWidgets.Cli.main:main", 22 | # old 23 | # "potatocli = PotatoWidgets.PotatoCLI:main", 24 | ], 25 | }, 26 | scripts=["PotatoWidgets/Cli/potatocli"], 27 | author="T0kyoB0y", 28 | description="Widget system written in Python, using GTK+ and the GtkLayerShell.", 29 | long_description=open("README.md").read(), 30 | long_description_content_type="text/markdown", 31 | url="https://github.com/T0kyoB0y/PotatoWidgets", 32 | classifiers=[ 33 | "Development Status :: 3 - Alpha", 34 | "Intended Audience :: Developers", 35 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 36 | "Programming Language :: Python :: 3", 37 | "Programming Language :: Python :: 3.6", 38 | "Programming Language :: Python :: 3.7", 39 | ], 40 | keywords="GTK+ GtkLayerShell widget python", 41 | project_urls={ 42 | "Source": "https://github.com/T0kyoB0y/PotatoWidgets", 43 | }, 44 | ) 45 | --------------------------------------------------------------------------------