├── .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 | 
22 |
23 | #### Hyprland
24 |
25 | 
26 | 
27 | 
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 |
--------------------------------------------------------------------------------