Skip to content

Controller Reference

Bases: ABC

Abstract base controller that orchestrates device interaction.

The controller acts as the central coordinator for: - Managing registered applications (PymordialApp). - Delegating UI interactions (clicks, finds, text reading) to the device. - Handling plugin resolution.

Attributes:

Name Type Description
apps dict[str, PymordialApp]

Dictionary of registered PymordialApp instances, keyed by sanitized name.

Source code in src/pymordial/core/controller.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 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
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
class PymordialController(ABC):
    """Abstract base controller that orchestrates device interaction.

    The controller acts as the central coordinator for:
    - Managing registered applications (`PymordialApp`).
    - Delegating UI interactions (clicks, finds, text reading) to the device.
    - Handling plugin resolution.

    Attributes:
        apps: Dictionary of registered PymordialApp instances, keyed by sanitized name.
    """

    def __init__(
        self,
        apps: list[PymordialApp] | None = None,
    ):
        """Initializes the PymordialController.

        Args:
            apps: Optional list of PymordialApp instances to register immediately.
        """
        self._apps: dict[str, PymordialApp] = {}

        if apps:
            for app in apps:
                self.add_app(app)

    @abstractmethod
    def _resolve_plugin(
        self,
        name: str,
        default_factory: Callable[[], PymordialPlugin],
        configure_found_plugin: Callable[[PymordialPlugin], None] | None = None,
    ) -> PymordialPlugin:
        """Resolves a plugin from the registry or falls back to a default.

        Args:
            name: The name of the plugin to look up.
            default_factory: Factory function to create a default instance if not found.
            configure_found_plugin: Optional callback to configure the plugin if found.

        Returns:
            The resolved or default plugin instance.
        """
        pass

    def __getattr__(self, name: str) -> PymordialApp:
        """Enables dot-notation access to registered apps.

        Args:
            name: The name of the app to retrieve (sanitized).

        Returns:
            The registered PymordialApp instance.

        Raises:
            AttributeError: If the app name is not registered.
        """
        if name in self._apps:
            return self._apps[name]
        raise AttributeError(
            f"'{type(self).__name__}' object has no attribute '{name}'. "
            f"Available apps: {list(self._apps.keys())}"
        )

    # --- Convenience Methods (delegate to sub-controllers) ---
    ## --- App Management ---
    def add_app(self, app: PymordialApp) -> None:
        """Registers a PymordialApp instance with this controller.

        This method adds the app to the internal registry using a sanitized name.
        If an app with the same sanitized name already exists, it will be overwritten.

        Args:
            app: The PymordialApp instance to register.
        """
        # Sanitize app_name for attribute access
        sanitized_name = app.app_name.replace("-", "_").replace(" ", "_")

        # Store in registry
        self._apps[sanitized_name] = app

    def list_apps(self) -> list[str]:
        """Returns a list of registered app names.

        Returns:
            A list of strings representing the names of registered apps.
        """
        return list(self._apps.keys())

    @property
    def apps(self) -> dict[str, PymordialApp]:
        """Returns the dictionary of registered apps.

        Returns:
            A dictionary mapping sanitized app names to PymordialApp instances.
        """
        return self._apps

    @abstractmethod
    def capture_screen(self) -> bytes | None:
        """Captures the current screen.

        Returns:
            The raw image bytes of the screenshot, or None if capture failed.
        """
        pass

    # --- Click Methods ---
    @abstractmethod
    def click_coord(self, coords: tuple[int, int], times: int = 1) -> bool:
        """Clicks specific coordinates on the screen.

        Args:
            coords: A tuple of (x, y) integer coordinates.
            times: The number of times to click. Defaults to 1.

        Returns:
            True if the click action was successful, False otherwise.
        """
        pass

    @abstractmethod
    def click_element(
        self,
        pymordial_element: "PymordialElement",
        times: int = 1,
        screenshot_img_bytes: bytes | None = None,
        max_tries: int = 1,
    ) -> bool:
        """Clicks a UI element on the screen.

        Args:
            pymordial_element: The element blueprint to find and click.
            times: The number of times to click. Defaults to 1.
            screenshot_img_bytes: Optional pre-captured screenshot to optimize finding.
            max_tries: Maximum number of attempts to find the element. Defaults to 1.

        Returns:
            True if the element was found and clicked, False otherwise.
        """
        pass

    @abstractmethod
    def click_elements(
        self,
        pymordial_elements: list["PymordialElement"],
        screenshot_img_bytes: bytes | None = None,
        max_tries: int = 1,
    ) -> bool:
        """Clicks any of the elements in the list.

        Iterates through the list and clicks the first element found.

        Args:
            pymordial_elements: List of element blueprints to search for.
            screenshot_img_bytes: Optional pre-captured screenshot.
            max_tries: Maximum attempts to find any element.

        Returns:
            True if any element from the list was clicked, False otherwise.
        """
        pass

    @abstractmethod
    def find_element(
        self,
        pymordial_element: "PymordialElement",
        pymordial_screenshot: bytes | None = None,
        max_tries: int = 1,
    ) -> tuple[int, int] | None:
        """Finds the coordinates of a UI element on the screen.

        Args:
            pymordial_element: The element blueprint to search for.
            pymordial_screenshot: Optional pre-captured screenshot.
            max_tries: Maximum number of search attempts. Defaults to 1.

        Returns:
            A tuple of (x, y) coordinates if found, None otherwise.
        """
        pass

    @abstractmethod
    def is_element_visible(
        self,
        pymordial_element: "PymordialElement",
        pymordial_screenshot: bytes | None = None,
        max_tries: int | None = None,
    ) -> bool:
        """Checks if a UI element is visible on the screen.

        Args:
            pymordial_element: The element blueprint to check.
            pymordial_screenshot: Optional pre-captured screenshot.
            max_tries: Maximum number of checks. If None, defaults to 1.

        Returns:
            True if the element is visible, False otherwise.
        """
        pass

    # --- App Lifecycle Methods ---
    @abstractmethod
    def open_app(
        self,
        app_name: str,
        package_name: str,
        timeout: int,
        wait_time: int,
    ) -> bool:
        """Opens an app on the device.

        Args:
            app_name: The display name of the app.
            package_name: The platform-specific package identifier (e.g. bundle ID).
            timeout: Maximum seconds to wait for launch.
            wait_time: Seconds to wait after launch command.

        Returns:
            True if the app launched successfully, False otherwise.
        """
        pass

    @abstractmethod
    def close_app(
        self,
        package_name: str,
        timeout: int,
        wait_time: int,
    ) -> bool:
        """Closes an app on the device.

        Args:
            package_name: The platform-specific package identifier.
            timeout: Maximum seconds to wait for closure.
            wait_time: Seconds to wait after close command.

        Returns:
            True if the app closed successfully, False otherwise.
        """
        pass

    @abstractmethod
    def read_text(
        self,
        image_path: "Path | bytes | str",
        case_sensitive: bool = False,
        strategy: "PymordialExtractStrategy | None" = None,
    ) -> list[str]:
        """Read text from an image using OCR.

        Args:
            image_path: Path to image file, raw bytes, or base64 string.
            case_sensitive: Whether OCR should respect case. Defaults to False.
            strategy: Optional extraction strategy to preprocess image.

        Returns:
            A list of strings containing the read text lines.
        """
        pass

    @abstractmethod
    def check_text(
        self,
        text_to_find: str,
        image_path: "Path | bytes | str",
        case_sensitive: bool = False,
        strategy: "PymordialExtractStrategy | None" = None,
    ) -> bool:
        """Check if specific text exists in an image.

        Args:
            text_to_find: The string to search for.
            image_path: Path to image file, raw bytes, or base64 string.
            case_sensitive: Whether to perform case-sensitive matching.
            strategy: Optional extraction strategy.

        Returns:
            True if the text is found, False otherwise.
        """
        pass

    def __repr__(self) -> str:
        """Returns a string representation of the PymordialController."""
        return f"PymordialController(apps={len(self._apps)})"

apps property

Returns the dictionary of registered apps.

Returns:

Type Description
dict[str, PymordialApp]

A dictionary mapping sanitized app names to PymordialApp instances.

__getattr__(name)

Enables dot-notation access to registered apps.

Parameters:

Name Type Description Default
name str

The name of the app to retrieve (sanitized).

required

Returns:

Type Description
PymordialApp

The registered PymordialApp instance.

Raises:

Type Description
AttributeError

If the app name is not registered.

Source code in src/pymordial/core/controller.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def __getattr__(self, name: str) -> PymordialApp:
    """Enables dot-notation access to registered apps.

    Args:
        name: The name of the app to retrieve (sanitized).

    Returns:
        The registered PymordialApp instance.

    Raises:
        AttributeError: If the app name is not registered.
    """
    if name in self._apps:
        return self._apps[name]
    raise AttributeError(
        f"'{type(self).__name__}' object has no attribute '{name}'. "
        f"Available apps: {list(self._apps.keys())}"
    )

__init__(apps=None)

Initializes the PymordialController.

Parameters:

Name Type Description Default
apps list[PymordialApp] | None

Optional list of PymordialApp instances to register immediately.

None
Source code in src/pymordial/core/controller.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(
    self,
    apps: list[PymordialApp] | None = None,
):
    """Initializes the PymordialController.

    Args:
        apps: Optional list of PymordialApp instances to register immediately.
    """
    self._apps: dict[str, PymordialApp] = {}

    if apps:
        for app in apps:
            self.add_app(app)

__repr__()

Returns a string representation of the PymordialController.

Source code in src/pymordial/core/controller.py
299
300
301
def __repr__(self) -> str:
    """Returns a string representation of the PymordialController."""
    return f"PymordialController(apps={len(self._apps)})"

add_app(app)

Registers a PymordialApp instance with this controller.

This method adds the app to the internal registry using a sanitized name. If an app with the same sanitized name already exists, it will be overwritten.

Parameters:

Name Type Description Default
app PymordialApp

The PymordialApp instance to register.

required
Source code in src/pymordial/core/controller.py
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def add_app(self, app: PymordialApp) -> None:
    """Registers a PymordialApp instance with this controller.

    This method adds the app to the internal registry using a sanitized name.
    If an app with the same sanitized name already exists, it will be overwritten.

    Args:
        app: The PymordialApp instance to register.
    """
    # Sanitize app_name for attribute access
    sanitized_name = app.app_name.replace("-", "_").replace(" ", "_")

    # Store in registry
    self._apps[sanitized_name] = app

capture_screen() abstractmethod

Captures the current screen.

Returns:

Type Description
bytes | None

The raw image bytes of the screenshot, or None if capture failed.

Source code in src/pymordial/core/controller.py
115
116
117
118
119
120
121
122
@abstractmethod
def capture_screen(self) -> bytes | None:
    """Captures the current screen.

    Returns:
        The raw image bytes of the screenshot, or None if capture failed.
    """
    pass

check_text(text_to_find, image_path, case_sensitive=False, strategy=None) abstractmethod

Check if specific text exists in an image.

Parameters:

Name Type Description Default
text_to_find str

The string to search for.

required
image_path Path | bytes | str

Path to image file, raw bytes, or base64 string.

required
case_sensitive bool

Whether to perform case-sensitive matching.

False
strategy PymordialExtractStrategy | None

Optional extraction strategy.

None

Returns:

Type Description
bool

True if the text is found, False otherwise.

Source code in src/pymordial/core/controller.py
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
@abstractmethod
def check_text(
    self,
    text_to_find: str,
    image_path: "Path | bytes | str",
    case_sensitive: bool = False,
    strategy: "PymordialExtractStrategy | None" = None,
) -> bool:
    """Check if specific text exists in an image.

    Args:
        text_to_find: The string to search for.
        image_path: Path to image file, raw bytes, or base64 string.
        case_sensitive: Whether to perform case-sensitive matching.
        strategy: Optional extraction strategy.

    Returns:
        True if the text is found, False otherwise.
    """
    pass

click_coord(coords, times=1) abstractmethod

Clicks specific coordinates on the screen.

Parameters:

Name Type Description Default
coords tuple[int, int]

A tuple of (x, y) integer coordinates.

required
times int

The number of times to click. Defaults to 1.

1

Returns:

Type Description
bool

True if the click action was successful, False otherwise.

Source code in src/pymordial/core/controller.py
125
126
127
128
129
130
131
132
133
134
135
136
@abstractmethod
def click_coord(self, coords: tuple[int, int], times: int = 1) -> bool:
    """Clicks specific coordinates on the screen.

    Args:
        coords: A tuple of (x, y) integer coordinates.
        times: The number of times to click. Defaults to 1.

    Returns:
        True if the click action was successful, False otherwise.
    """
    pass

click_element(pymordial_element, times=1, screenshot_img_bytes=None, max_tries=1) abstractmethod

Clicks a UI element on the screen.

Parameters:

Name Type Description Default
pymordial_element PymordialElement

The element blueprint to find and click.

required
times int

The number of times to click. Defaults to 1.

1
screenshot_img_bytes bytes | None

Optional pre-captured screenshot to optimize finding.

None
max_tries int

Maximum number of attempts to find the element. Defaults to 1.

1

Returns:

Type Description
bool

True if the element was found and clicked, False otherwise.

Source code in src/pymordial/core/controller.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
@abstractmethod
def click_element(
    self,
    pymordial_element: "PymordialElement",
    times: int = 1,
    screenshot_img_bytes: bytes | None = None,
    max_tries: int = 1,
) -> bool:
    """Clicks a UI element on the screen.

    Args:
        pymordial_element: The element blueprint to find and click.
        times: The number of times to click. Defaults to 1.
        screenshot_img_bytes: Optional pre-captured screenshot to optimize finding.
        max_tries: Maximum number of attempts to find the element. Defaults to 1.

    Returns:
        True if the element was found and clicked, False otherwise.
    """
    pass

click_elements(pymordial_elements, screenshot_img_bytes=None, max_tries=1) abstractmethod

Clicks any of the elements in the list.

Iterates through the list and clicks the first element found.

Parameters:

Name Type Description Default
pymordial_elements list[PymordialElement]

List of element blueprints to search for.

required
screenshot_img_bytes bytes | None

Optional pre-captured screenshot.

None
max_tries int

Maximum attempts to find any element.

1

Returns:

Type Description
bool

True if any element from the list was clicked, False otherwise.

Source code in src/pymordial/core/controller.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
@abstractmethod
def click_elements(
    self,
    pymordial_elements: list["PymordialElement"],
    screenshot_img_bytes: bytes | None = None,
    max_tries: int = 1,
) -> bool:
    """Clicks any of the elements in the list.

    Iterates through the list and clicks the first element found.

    Args:
        pymordial_elements: List of element blueprints to search for.
        screenshot_img_bytes: Optional pre-captured screenshot.
        max_tries: Maximum attempts to find any element.

    Returns:
        True if any element from the list was clicked, False otherwise.
    """
    pass

close_app(package_name, timeout, wait_time) abstractmethod

Closes an app on the device.

Parameters:

Name Type Description Default
package_name str

The platform-specific package identifier.

required
timeout int

Maximum seconds to wait for closure.

required
wait_time int

Seconds to wait after close command.

required

Returns:

Type Description
bool

True if the app closed successfully, False otherwise.

Source code in src/pymordial/core/controller.py
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
@abstractmethod
def close_app(
    self,
    package_name: str,
    timeout: int,
    wait_time: int,
) -> bool:
    """Closes an app on the device.

    Args:
        package_name: The platform-specific package identifier.
        timeout: Maximum seconds to wait for closure.
        wait_time: Seconds to wait after close command.

    Returns:
        True if the app closed successfully, False otherwise.
    """
    pass

find_element(pymordial_element, pymordial_screenshot=None, max_tries=1) abstractmethod

Finds the coordinates of a UI element on the screen.

Parameters:

Name Type Description Default
pymordial_element PymordialElement

The element blueprint to search for.

required
pymordial_screenshot bytes | None

Optional pre-captured screenshot.

None
max_tries int

Maximum number of search attempts. Defaults to 1.

1

Returns:

Type Description
tuple[int, int] | None

A tuple of (x, y) coordinates if found, None otherwise.

Source code in src/pymordial/core/controller.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
@abstractmethod
def find_element(
    self,
    pymordial_element: "PymordialElement",
    pymordial_screenshot: bytes | None = None,
    max_tries: int = 1,
) -> tuple[int, int] | None:
    """Finds the coordinates of a UI element on the screen.

    Args:
        pymordial_element: The element blueprint to search for.
        pymordial_screenshot: Optional pre-captured screenshot.
        max_tries: Maximum number of search attempts. Defaults to 1.

    Returns:
        A tuple of (x, y) coordinates if found, None otherwise.
    """
    pass

is_element_visible(pymordial_element, pymordial_screenshot=None, max_tries=None) abstractmethod

Checks if a UI element is visible on the screen.

Parameters:

Name Type Description Default
pymordial_element PymordialElement

The element blueprint to check.

required
pymordial_screenshot bytes | None

Optional pre-captured screenshot.

None
max_tries int | None

Maximum number of checks. If None, defaults to 1.

None

Returns:

Type Description
bool

True if the element is visible, False otherwise.

Source code in src/pymordial/core/controller.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
@abstractmethod
def is_element_visible(
    self,
    pymordial_element: "PymordialElement",
    pymordial_screenshot: bytes | None = None,
    max_tries: int | None = None,
) -> bool:
    """Checks if a UI element is visible on the screen.

    Args:
        pymordial_element: The element blueprint to check.
        pymordial_screenshot: Optional pre-captured screenshot.
        max_tries: Maximum number of checks. If None, defaults to 1.

    Returns:
        True if the element is visible, False otherwise.
    """
    pass

list_apps()

Returns a list of registered app names.

Returns:

Type Description
list[str]

A list of strings representing the names of registered apps.

Source code in src/pymordial/core/controller.py
 98
 99
100
101
102
103
104
def list_apps(self) -> list[str]:
    """Returns a list of registered app names.

    Returns:
        A list of strings representing the names of registered apps.
    """
    return list(self._apps.keys())

open_app(app_name, package_name, timeout, wait_time) abstractmethod

Opens an app on the device.

Parameters:

Name Type Description Default
app_name str

The display name of the app.

required
package_name str

The platform-specific package identifier (e.g. bundle ID).

required
timeout int

Maximum seconds to wait for launch.

required
wait_time int

Seconds to wait after launch command.

required

Returns:

Type Description
bool

True if the app launched successfully, False otherwise.

Source code in src/pymordial/core/controller.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
@abstractmethod
def open_app(
    self,
    app_name: str,
    package_name: str,
    timeout: int,
    wait_time: int,
) -> bool:
    """Opens an app on the device.

    Args:
        app_name: The display name of the app.
        package_name: The platform-specific package identifier (e.g. bundle ID).
        timeout: Maximum seconds to wait for launch.
        wait_time: Seconds to wait after launch command.

    Returns:
        True if the app launched successfully, False otherwise.
    """
    pass

read_text(image_path, case_sensitive=False, strategy=None) abstractmethod

Read text from an image using OCR.

Parameters:

Name Type Description Default
image_path Path | bytes | str

Path to image file, raw bytes, or base64 string.

required
case_sensitive bool

Whether OCR should respect case. Defaults to False.

False
strategy PymordialExtractStrategy | None

Optional extraction strategy to preprocess image.

None

Returns:

Type Description
list[str]

A list of strings containing the read text lines.

Source code in src/pymordial/core/controller.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
@abstractmethod
def read_text(
    self,
    image_path: "Path | bytes | str",
    case_sensitive: bool = False,
    strategy: "PymordialExtractStrategy | None" = None,
) -> list[str]:
    """Read text from an image using OCR.

    Args:
        image_path: Path to image file, raw bytes, or base64 string.
        case_sensitive: Whether OCR should respect case. Defaults to False.
        strategy: Optional extraction strategy to preprocess image.

    Returns:
        A list of strings containing the read text lines.
    """
    pass