desklab 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. desklab-0.1.0/PKG-INFO +121 -0
  2. desklab-0.1.0/README.md +98 -0
  3. desklab-0.1.0/pyproject.toml +38 -0
  4. desklab-0.1.0/setup.cfg +4 -0
  5. desklab-0.1.0/src/desklab/__init__.py +9 -0
  6. desklab-0.1.0/src/desklab/_assets/__init__.py +0 -0
  7. desklab-0.1.0/src/desklab/_assets/desklab.png +0 -0
  8. desklab-0.1.0/src/desklab/_check/__init__.py +10 -0
  9. desklab-0.1.0/src/desklab/_check/_check_operations/__init__.py +9 -0
  10. desklab-0.1.0/src/desklab/_check/_check_operations/_check.py +14 -0
  11. desklab-0.1.0/src/desklab/_check/_check_operations/_length.py +33 -0
  12. desklab-0.1.0/src/desklab/_check/_check_operations/_range.py +41 -0
  13. desklab-0.1.0/src/desklab/_check/_value.py +31 -0
  14. desklab-0.1.0/src/desklab/_utils/__init__.py +10 -0
  15. desklab-0.1.0/src/desklab/_utils/_geometry.py +36 -0
  16. desklab-0.1.0/src/desklab/_utils/_imageio_ffmpeg_exe.py +163 -0
  17. desklab-0.1.0/src/desklab/_utils/_regex.py +5 -0
  18. desklab-0.1.0/src/desklab/areas/__init__.py +9 -0
  19. desklab-0.1.0/src/desklab/areas/_area_interface.py +17 -0
  20. desklab-0.1.0/src/desklab/areas/_clickable_area.py +27 -0
  21. desklab-0.1.0/src/desklab/areas/_rectangular_area.py +102 -0
  22. desklab-0.1.0/src/desklab/components/__init__.py +15 -0
  23. desklab-0.1.0/src/desklab/components/_button.py +61 -0
  24. desklab-0.1.0/src/desklab/components/_drag_drop.py +58 -0
  25. desklab-0.1.0/src/desklab/components/_drawing_area.py +205 -0
  26. desklab-0.1.0/src/desklab/components/_text.py +117 -0
  27. desklab-0.1.0/src/desklab/components/_text_input.py +344 -0
  28. desklab-0.1.0/src/desklab/components/_window.py +124 -0
  29. desklab-0.1.0/src/desklab/containers/__init__.py +9 -0
  30. desklab-0.1.0/src/desklab/containers/_constants.py +18 -0
  31. desklab-0.1.0/src/desklab/containers/_flexbox.py +61 -0
  32. desklab-0.1.0/src/desklab/containers/_flexbox_interface.py +205 -0
  33. desklab-0.1.0/src/desklab/containers/_protected_flexbox.py +133 -0
  34. desklab-0.1.0/src/desklab/entity_types/__init__.py +20 -0
  35. desklab-0.1.0/src/desklab/entity_types/_colorable.py +26 -0
  36. desklab-0.1.0/src/desklab/entity_types/_containable.py +12 -0
  37. desklab-0.1.0/src/desklab/entity_types/_copiable.py +19 -0
  38. desklab-0.1.0/src/desklab/entity_types/_dimensionable.py +25 -0
  39. desklab-0.1.0/src/desklab/entity_types/_displayable.py +14 -0
  40. desklab-0.1.0/src/desklab/entity_types/_entity.py +5 -0
  41. desklab-0.1.0/src/desklab/entity_types/_event_sensitive.py +27 -0
  42. desklab-0.1.0/src/desklab/entity_types/_positionable.py +22 -0
  43. desklab-0.1.0/src/desklab/exceptions/__init__.py +12 -0
  44. desklab-0.1.0/src/desklab/exceptions/_exceptions.py +37 -0
  45. desklab-0.1.0/src/desklab/listeners/__init__.py +38 -0
  46. desklab-0.1.0/src/desklab/listeners/_clipboard.py +18 -0
  47. desklab-0.1.0/src/desklab/listeners/_default.py +21 -0
  48. desklab-0.1.0/src/desklab/listeners/_hover.py +36 -0
  49. desklab-0.1.0/src/desklab/listeners/_keyboard.py +71 -0
  50. desklab-0.1.0/src/desklab/listeners/_mouse.py +26 -0
  51. desklab-0.1.0/src/desklab/listeners/_protected_listener.py +89 -0
  52. desklab-0.1.0/src/desklab/listeners/_system_listener.py +46 -0
  53. desklab-0.1.0/src/desklab/media/__init__.py +7 -0
  54. desklab-0.1.0/src/desklab/media/_audio.py +55 -0
  55. desklab-0.1.0/src/desklab/media/_image.py +64 -0
  56. desklab-0.1.0/src/desklab/primitives/__init__.py +7 -0
  57. desklab-0.1.0/src/desklab/primitives/_color.py +142 -0
  58. desklab-0.1.0/src/desklab/primitives/_font.py +86 -0
  59. desklab-0.1.0/src/desklab/system/__init__.py +12 -0
  60. desklab-0.1.0/src/desklab/system/_clipboard.py +248 -0
  61. desklab-0.1.0/src/desklab/system/_keyboard.py +148 -0
  62. desklab-0.1.0/src/desklab/system/_mouse.py +63 -0
  63. desklab-0.1.0/src/desklab/system/_system_input.py +23 -0
  64. desklab-0.1.0/src/desklab.egg-info/PKG-INFO +121 -0
  65. desklab-0.1.0/src/desklab.egg-info/SOURCES.txt +66 -0
  66. desklab-0.1.0/src/desklab.egg-info/dependency_links.txt +1 -0
  67. desklab-0.1.0/src/desklab.egg-info/requires.txt +11 -0
  68. desklab-0.1.0/src/desklab.egg-info/top_level.txt +1 -0
desklab-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: desklab
3
+ Version: 0.1.0
4
+ Summary: Simple and easy to use interface package
5
+ Author: Jonatas Cortes
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: pygame==2.6.1
15
+ Requires-Dist: pynput==1.8.1
16
+ Requires-Dist: pyobjc-core==12.1; sys_platform == "darwin"
17
+ Requires-Dist: pyobjc-framework-ApplicationServices==12.1; sys_platform == "darwin"
18
+ Requires-Dist: pyobjc-framework-Cocoa==12.1; sys_platform == "darwin"
19
+ Requires-Dist: pyobjc-framework-CoreText==12.1; sys_platform == "darwin"
20
+ Requires-Dist: pyobjc-framework-Quartz==12.1; sys_platform == "darwin"
21
+ Requires-Dist: pyperclip==1.11.0
22
+ Requires-Dist: six==1.17.0
23
+
24
+ ![DeskLab-banner](./desklab_banner.png)
25
+
26
+ DeskLab is a Python interface library designed for small projects that prioritize fast development over extensive customization.
27
+
28
+ It simulates the web development programing style, with separation of responsibilities and reusable components.
29
+
30
+ ## 🛠️ Installation & Development Setup
31
+
32
+ If you want to clone this repository to contribute to the code, run tests, or develop features locally, follow the steps below using **`uv`**, a fast Python package installer and environment manager.
33
+
34
+
35
+ ### Windows
36
+
37
+ Open **PowerShell** and run:
38
+
39
+ ```powershell
40
+ powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
41
+ ```
42
+
43
+ Alternative:
44
+
45
+ ```powershell
46
+ winget install astral-sh.uv
47
+ ```
48
+
49
+ ---
50
+
51
+ ### Linux / macOS
52
+
53
+ Open a terminal and run:
54
+
55
+ ```bash
56
+ curl -LsSf https://astral.sh/uv/install.sh | sh
57
+ ```
58
+
59
+ Alternative on macOS using Homebrew:
60
+
61
+ ```bash
62
+ brew install uv
63
+ ```
64
+
65
+ Alternative on Linux using pip:
66
+
67
+ ```bash
68
+ pip install uv
69
+ ```
70
+
71
+ After installation, restart your terminal so the `uv` command becomes available.
72
+
73
+ Verify installation:
74
+
75
+ ```bash
76
+ uv --version
77
+ ```
78
+
79
+ ---
80
+
81
+ ### Clone the Repository
82
+
83
+ Clone the project and enter its directory:
84
+
85
+ ```bash
86
+ git clone https://github.com/your-username/desklab.git
87
+ cd desklab
88
+ ```
89
+
90
+ ---
91
+
92
+ ### Install Dependencies & Create Environment
93
+
94
+ You do **not** need to manually create a virtual environment or run `pip install`.
95
+
96
+ Simply execute:
97
+
98
+ ```bash
99
+ uv sync
100
+ ```
101
+
102
+ This command automatically:
103
+
104
+ - Creates a local virtual environment (`.venv/`)
105
+ - Installs all project dependencies
106
+ - Installs desklab in editable mode
107
+ - Synchronizes dependencies from `pyproject.toml`
108
+
109
+ ---
110
+
111
+ ### Running the Project
112
+
113
+ Run your application inside the managed environment:
114
+
115
+ ```bash
116
+ uv run <your_file>.py
117
+ ```
118
+
119
+ This guarantees execution inside the project's environment and avoids dependency conflicts.
120
+
121
+ ---
@@ -0,0 +1,98 @@
1
+ ![DeskLab-banner](./desklab_banner.png)
2
+
3
+ DeskLab is a Python interface library designed for small projects that prioritize fast development over extensive customization.
4
+
5
+ It simulates the web development programing style, with separation of responsibilities and reusable components.
6
+
7
+ ## 🛠️ Installation & Development Setup
8
+
9
+ If you want to clone this repository to contribute to the code, run tests, or develop features locally, follow the steps below using **`uv`**, a fast Python package installer and environment manager.
10
+
11
+
12
+ ### Windows
13
+
14
+ Open **PowerShell** and run:
15
+
16
+ ```powershell
17
+ powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
18
+ ```
19
+
20
+ Alternative:
21
+
22
+ ```powershell
23
+ winget install astral-sh.uv
24
+ ```
25
+
26
+ ---
27
+
28
+ ### Linux / macOS
29
+
30
+ Open a terminal and run:
31
+
32
+ ```bash
33
+ curl -LsSf https://astral.sh/uv/install.sh | sh
34
+ ```
35
+
36
+ Alternative on macOS using Homebrew:
37
+
38
+ ```bash
39
+ brew install uv
40
+ ```
41
+
42
+ Alternative on Linux using pip:
43
+
44
+ ```bash
45
+ pip install uv
46
+ ```
47
+
48
+ After installation, restart your terminal so the `uv` command becomes available.
49
+
50
+ Verify installation:
51
+
52
+ ```bash
53
+ uv --version
54
+ ```
55
+
56
+ ---
57
+
58
+ ### Clone the Repository
59
+
60
+ Clone the project and enter its directory:
61
+
62
+ ```bash
63
+ git clone https://github.com/your-username/desklab.git
64
+ cd desklab
65
+ ```
66
+
67
+ ---
68
+
69
+ ### Install Dependencies & Create Environment
70
+
71
+ You do **not** need to manually create a virtual environment or run `pip install`.
72
+
73
+ Simply execute:
74
+
75
+ ```bash
76
+ uv sync
77
+ ```
78
+
79
+ This command automatically:
80
+
81
+ - Creates a local virtual environment (`.venv/`)
82
+ - Installs all project dependencies
83
+ - Installs desklab in editable mode
84
+ - Synchronizes dependencies from `pyproject.toml`
85
+
86
+ ---
87
+
88
+ ### Running the Project
89
+
90
+ Run your application inside the managed environment:
91
+
92
+ ```bash
93
+ uv run <your_file>.py
94
+ ```
95
+
96
+ This guarantees execution inside the project's environment and avoids dependency conflicts.
97
+
98
+ ---
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "desklab"
7
+ version = "0.1.0"
8
+ description = "Simple and easy to use interface package"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Jonatas Cortes"}
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ ]
22
+ dependencies = [
23
+ "pygame==2.6.1",
24
+ "pynput==1.8.1",
25
+ "pyobjc-core==12.1; sys_platform == 'darwin'",
26
+ "pyobjc-framework-ApplicationServices==12.1; sys_platform == 'darwin'",
27
+ "pyobjc-framework-Cocoa==12.1; sys_platform == 'darwin'",
28
+ "pyobjc-framework-CoreText==12.1; sys_platform == 'darwin'",
29
+ "pyobjc-framework-Quartz==12.1; sys_platform == 'darwin'",
30
+ "pyperclip==1.11.0",
31
+ "six==1.17.0"
32
+ ]
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.setuptools.package-data]
38
+ "desklab._assets" = ["*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ from .primitives import *
2
+ from .areas import *
3
+ from .components import *
4
+ from .containers import *
5
+ from .entity_types import *
6
+ from .listeners import *
7
+ from .media import *
8
+ from .system import *
9
+ from .exceptions import *
File without changes
@@ -0,0 +1,10 @@
1
+ from ._value import value_check
2
+ from ._check_operations import (Check, CheckRange,
3
+ CheckLength)
4
+
5
+ __all__ = [
6
+ 'value_check',
7
+ 'Check',
8
+ 'CheckRange',
9
+ 'CheckLength'
10
+ ]
@@ -0,0 +1,9 @@
1
+ from ._check import Check
2
+ from ._range import CheckRange
3
+ from ._length import CheckLength
4
+
5
+ __all__ = [
6
+ 'Check',
7
+ 'CheckRange',
8
+ 'CheckLength'
9
+ ]
@@ -0,0 +1,14 @@
1
+ from typing import Any
2
+ from typing import Any, Callable
3
+
4
+
5
+ class Check:
6
+ def __init__(self, rule: Callable[[Any], bool], description: str | None = None) -> None:
7
+ self.__rule = rule
8
+ self.__description = description
9
+
10
+ def __call__(self, *args: Any, **kwargs: Any) -> bool:
11
+ return self.__rule(*args, **kwargs)
12
+
13
+ def __str__(self) -> str:
14
+ return self.__description or self.__rule.__name__
@@ -0,0 +1,33 @@
1
+ from typing import Any
2
+
3
+ from desklab._check._check_operations import Check
4
+ from desklab.exceptions import InvalidParameterValue
5
+
6
+
7
+ class CheckLength(Check):
8
+
9
+ def __init__(self, reference_length: int, comparison: str = "=", variable_name: str | None = None) -> None:
10
+
11
+ operators = {"=", ">", "<", ">=", "<="}
12
+ if comparison not in operators:
13
+ raise InvalidParameterValue(f"comparison", comparison,
14
+ f"'comparison' must be one of: {operators}")
15
+
16
+ def compare(value: Any) -> bool:
17
+ if comparison == "=":
18
+ return len(value) == reference_length
19
+ elif comparison == ">":
20
+ return len(value) > reference_length
21
+ elif comparison == "<":
22
+ return len(value) < reference_length
23
+ elif comparison == ">=":
24
+ return len(value) >= reference_length
25
+ elif comparison == "<=":
26
+ return len(value) <= reference_length
27
+ raise InvalidParameterValue(f"comparison", comparison,
28
+ f"'comparison' must be one of: {operators}")
29
+
30
+ name = variable_name or "Value"
31
+ message = f"length({name}) {comparison} {reference_length}"
32
+
33
+ super().__init__(compare, message)
@@ -0,0 +1,41 @@
1
+ from typing import Any
2
+ from typing import Any, cast
3
+ from desklab.exceptions import MissingParameters
4
+ from desklab._check._check_operations import Check
5
+ from collections.abc import Iterable
6
+
7
+
8
+ class CheckRange(Check):
9
+
10
+ def __init__(self, min_value: Any = None, max_value: Any = None, variable_name: str | None = None) -> None:
11
+
12
+ if min_value is None and max_value is None:
13
+ raise MissingParameters(["min_value", "max_value"],
14
+ "At least one of 'min_value' or 'max_value' must be provided")
15
+
16
+ def is_valid(value: Any) -> bool:
17
+
18
+ if isinstance(value, Iterable) and not isinstance(value, (str, bytes)):
19
+ iterable = cast(Iterable[Any], value)
20
+ return all(is_valid(v) for v in iterable)
21
+
22
+ if min_value is not None and value < min_value:
23
+ return False
24
+
25
+ if max_value is not None and value > max_value:
26
+ return False
27
+
28
+ return True
29
+
30
+ name = variable_name or "Value"
31
+
32
+ if min_value is not None and max_value is not None:
33
+ message = f"{name} must be between {min_value} and {max_value}"
34
+
35
+ elif min_value is not None:
36
+ message = f"{name} must be greater than or equal to {min_value}"
37
+
38
+ else:
39
+ message = f"{name} must be less than or equal to {max_value}"
40
+
41
+ super().__init__(is_valid, message)
@@ -0,0 +1,31 @@
1
+ import inspect
2
+ from typing import Callable, TypeVar, ParamSpec
3
+ from functools import wraps
4
+ from desklab.exceptions import InvalidParameterValue
5
+ from desklab._check._check_operations import Check
6
+
7
+
8
+ P = ParamSpec("P")
9
+ R = TypeVar("R")
10
+
11
+
12
+ def value_check(**validations: Check) -> Callable[[Callable[P, R]], Callable[P, R]]:
13
+ def decorator(function: Callable[P, R]) -> Callable[P, R]:
14
+ signature = inspect.signature(function)
15
+
16
+ @wraps(function)
17
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
18
+ bound_arguments = signature.bind(*args, **kwargs)
19
+ bound_arguments.apply_defaults()
20
+
21
+ for param_name, param_value in bound_arguments.arguments.items():
22
+ if param_name in ("self", "cls"):
23
+ continue
24
+ validation_rule = validations.get(param_name)
25
+ if validation_rule is not None and not validation_rule(param_value):
26
+ raise InvalidParameterValue(param_name, param_value,
27
+ str(validation_rule))
28
+
29
+ return function(*args, **kwargs)
30
+ return wrapper
31
+ return decorator
@@ -0,0 +1,10 @@
1
+ from ._geometry import point_to_segment_distance, is_inside_circle
2
+ from ._regex import camel_case_to_snake_case
3
+ from ._imageio_ffmpeg_exe import get_ffmpeg_exe
4
+
5
+ __all__ = [
6
+ "point_to_segment_distance",
7
+ "is_inside_circle",
8
+ "camel_case_to_snake_case",
9
+ "get_ffmpeg_exe"
10
+ ]
@@ -0,0 +1,36 @@
1
+ import math
2
+
3
+
4
+ def point_to_segment_distance(point: tuple[int, int], segment_start: tuple[int, int], segment_end: tuple[int, int]) -> float:
5
+ point_x, point_y = point
6
+ start_x, start_y = segment_start
7
+ end_x, end_y = segment_end
8
+
9
+ vector_segment_x = end_x - start_x
10
+ vector_segment_y = end_y - start_y
11
+
12
+ vector_point_x = point_x - start_x
13
+ vector_point_y = point_y - start_y
14
+
15
+ segment_length_squared = vector_segment_x ** 2 + vector_segment_y ** 2
16
+
17
+ if segment_length_squared == 0:
18
+ return math.sqrt((point_x - start_x) ** 2 + (point_y - start_y) ** 2)
19
+
20
+ dot_product = vector_point_x * vector_segment_x + \
21
+ vector_point_y * vector_segment_y
22
+ projection_ratio = max(0.0, min(1.0, dot_product / segment_length_squared))
23
+
24
+ closest_x = start_x + vector_segment_x * projection_ratio
25
+ closest_y = start_y + vector_segment_y * projection_ratio
26
+
27
+ distance_x = point_x - closest_x
28
+ distance_y = point_y - closest_y
29
+
30
+ return math.sqrt(distance_x ** 2 + distance_y ** 2)
31
+
32
+
33
+ def is_inside_circle(coordinates: tuple[int, int], circle_center: tuple[int, int], circle_radius: int):
34
+ x, y = coordinates
35
+ cx, cy = circle_center
36
+ return (x - cx)**2 + (y - cy)**2 <= circle_radius**2
@@ -0,0 +1,163 @@
1
+ # ==============================================================================
2
+ # The following code block or function incorporates code from 'imageio'.
3
+ #
4
+ # Copyright (c) 2019-2025, imageio
5
+ # All rights reserved.
6
+ #
7
+ # Redistribution and use in source and binary forms, with or without
8
+ # modification, are permitted provided that the following conditions are met:
9
+ #
10
+ # * Redistributions of source code must retain the above copyright notice, this
11
+ # list of conditions and the following disclaimer.
12
+ #
13
+ # * Redistributions in binary form must reproduce the above copyright notice,
14
+ # this list of conditions and the following disclaimer in the documentation
15
+ # and/or other materials provided with the distribution.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLD
21
+
22
+ import os
23
+ import platform
24
+ import subprocess
25
+ import sys
26
+ from functools import lru_cache
27
+ from pathlib import Path
28
+ from typing import Any, Optional, no_type_check
29
+
30
+ FNAME_PER_PLATFORM = {
31
+ "macos-aarch64": "ffmpeg-macos-aarch64-v7.1",
32
+ "macos-x86_64": "ffmpeg-macos-x86_64-v7.1",
33
+ "windows-x86_64": "ffmpeg-win-x86_64-v7.1.exe",
34
+ "windows-i686": "ffmpeg-win32-v4.2.2.exe",
35
+ "linux-aarch64": "ffmpeg-linux-aarch64-v7.0.2",
36
+ "linux-x86_64": "ffmpeg-linux-x86_64-v7.0.2",
37
+ }
38
+
39
+
40
+ def _get_os_string() -> str:
41
+ if sys.platform.startswith("win"):
42
+ return "windows"
43
+ elif sys.platform.startswith("darwin"):
44
+ return "macos"
45
+ elif sys.platform.startswith("linux"):
46
+ return "linux"
47
+ return sys.platform
48
+
49
+
50
+ def _get_arch() -> str:
51
+ is_64_bit = sys.maxsize > 2**32
52
+ machine = platform.machine()
53
+
54
+ if machine == "armv7l":
55
+ return "armv7"
56
+ elif is_64_bit and machine.startswith(("arm", "aarch64")):
57
+ return "aarch64"
58
+ elif is_64_bit:
59
+ return "x86_64"
60
+ return "i686"
61
+
62
+
63
+ def _get_platform() -> str:
64
+ return _get_os_string() + "-" + _get_arch()
65
+
66
+
67
+ @no_type_check
68
+ def _popen_kwargs() -> dict[str, Any]:
69
+ startupinfo = None
70
+ creationflags = 0
71
+ if sys.platform.startswith("win"):
72
+ startupinfo = subprocess.STARTUPINFO()
73
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
74
+ return {
75
+ "startupinfo": startupinfo,
76
+ "creationflags": creationflags,
77
+ }
78
+
79
+
80
+ def _is_valid_exe(exe: str) -> bool:
81
+ cmd = [exe, "-version"]
82
+ try:
83
+ with open(os.devnull, "w") as null:
84
+ subprocess.check_call(
85
+ cmd, stdout=null, stderr=subprocess.STDOUT, **_popen_kwargs()
86
+ )
87
+ return True
88
+ except (OSError, ValueError, subprocess.CalledProcessError):
89
+ return False
90
+
91
+
92
+ @lru_cache(maxsize=1)
93
+ def _find_ffmpeg_path() -> Optional[Path]:
94
+ plat = _get_platform()
95
+ os_str = _get_os_string()
96
+ expected_filename = FNAME_PER_PLATFORM.get(plat, "ffmpeg")
97
+
98
+ possible_paths: list[Path] = []
99
+ home = Path.home()
100
+
101
+ imageio_cache_dir = home / ".imageio" / "ffmpeg"
102
+ desklab_cache_dir = home / ".desklab" / "ffmpeg"
103
+
104
+ possible_paths.extend([
105
+ imageio_cache_dir / expected_filename,
106
+ desklab_cache_dir / expected_filename,
107
+ ])
108
+
109
+ prefix_path = Path(sys.prefix)
110
+ if os_str == "windows":
111
+ possible_paths.append(prefix_path / "Library" / "bin" / "ffmpeg.exe")
112
+ else:
113
+ possible_paths.append(prefix_path / "bin" / "ffmpeg")
114
+
115
+ if os_str == "windows":
116
+ appdata_local = Path(os.environ.get(
117
+ "LOCALAPPDATA", home / "AppData/Local"))
118
+ possible_paths.extend([
119
+ appdata_local / "Microsoft/WindowsApps/ffmpeg.exe",
120
+ home / "ffmpeg" / "bin" / "ffmpeg.exe",
121
+ Path("C:/Program Files/ffmpeg/bin/ffmpeg.exe"),
122
+ Path("C:/ffmpeg/bin/ffmpeg.exe"),
123
+ ])
124
+ elif os_str == "macos":
125
+ possible_paths.extend([
126
+ Path("/opt/homebrew/bin/ffmpeg"),
127
+ Path("/usr/local/bin/ffmpeg"),
128
+ Path("/usr/bin/ffmpeg"),
129
+ home / "bin" / "ffmpeg",
130
+ ])
131
+ elif os_str == "linux":
132
+ possible_paths.extend([
133
+ Path("/usr/bin/ffmpeg"),
134
+ Path("/usr/local/bin/ffmpeg"),
135
+ Path("/snap/bin/ffmpeg"),
136
+ home / "bin" / "ffmpeg",
137
+ home / ".local" / "bin" / "ffmpeg",
138
+ ])
139
+
140
+ for path in possible_paths:
141
+ if path.is_file() and _is_valid_exe(str(path)):
142
+ return path
143
+
144
+ if _is_valid_exe("ffmpeg"):
145
+ return Path("ffmpeg")
146
+
147
+ return None
148
+
149
+
150
+ def get_ffmpeg_exe() -> Path:
151
+ exe_env = os.getenv("DESKLAB_FFMPEG_EXE") or os.getenv(
152
+ "IMAGEIO_FFMPEG_EXE")
153
+ if exe_env:
154
+ return Path(exe_env)
155
+
156
+ detected_path = _find_ffmpeg_path()
157
+ if detected_path:
158
+ return detected_path
159
+
160
+ raise RuntimeError(
161
+ "No ffmpeg exe could be found. Install ffmpeg on your system, "
162
+ "or set the DESKLAB_FFMPEG_EXE environment variable."
163
+ )
@@ -0,0 +1,5 @@
1
+ import re
2
+
3
+
4
+ def camel_case_to_snake_case(text: str) -> str:
5
+ return re.sub(r'(?<!^)(?=[A-Z])', '_', text).lower()
@@ -0,0 +1,9 @@
1
+ from ._area_interface import AreaInterface
2
+ from ._rectangular_area import RectangularArea
3
+ from ._clickable_area import ClickableArea
4
+
5
+ __all__ = [
6
+ "AreaInterface",
7
+ "RectangularArea",
8
+ "ClickableArea"
9
+ ]
@@ -0,0 +1,17 @@
1
+ from desklab.entity_types import ContainableEntity, DisplayableEntity, CopiableEntity, ColorableEntity
2
+ from desklab.primitives import Color
3
+ from abc import abstractmethod
4
+
5
+
6
+ class AreaInterface(ContainableEntity, DisplayableEntity, ColorableEntity, CopiableEntity):
7
+
8
+ def __init__(self, width: int, height: int, color: Color | tuple[int, ...] | str = "BLACK") -> None:
9
+ super().__init__(x=0, y=0, width=width, height=height, color=color)
10
+
11
+ @abstractmethod
12
+ def contains(self, coordinates: tuple[int, int]) -> bool:
13
+ pass
14
+
15
+ def get_rect(self) -> tuple[int, int, int, int]:
16
+ return (self.get_x(), self.get_y(),
17
+ self.get_width(), self.get_height())