This file provides guidance for AI agents working on the JoystickGremlin codebase.
JoystickGremlin is a Python application (PySide6/QML) for configuring joystick devices on Windows. It uses vJoy for virtual joystick emulation and supports macros, modes, and Python scripting.
The project is using poetry for dependency management. Thus all calls to python HAVE TO USE poetry.
# Run all tests
poetry run pytest
# Run a single test file
poetry run pytest test/unit/test_profile.py
# Run a specific test
poetry run pytest test/unit/test_profile.py::test_simple_action
# Run tests with verbose output
poetry run pytest -v
# Run tests matching a pattern
poetry run pytest -k "test_simple"
# Run e2e tests only
poetry run pytest test/integration/
# Run action interaction tests only
poetry run pytest test/action_interaction/
# Run unit tests only
poetry run pytest test/unit/# Run ruff linter (ANN = annotations required)
poetry run ruff check .# Run pyright type checker
poetry run pyright# Run in dev mode
poetry run python joystick_gremlin.py- Keep related functionality together
- Follow the existing module organization:
gremlin/- Core application logicgremlin/ui/- UI-related code (PySide6/QML integration)action_plugins/- Action plugins, one directory per actiontest/- Test suitesqml/- QML UI filesvjoy/— vJoy virtual joystick ctypes wrapperdill/— native device input library (DILL.dll) with Python wrapper
joystick_gremlin.py— entry point; initializes devices, Qt engine, plugins
| Class | Module | Purpose |
|---|---|---|
Configuration |
gremlin.config |
Persistent app config |
EventListener |
gremlin.event_handler |
Raw input capture & routing |
ModeManager |
gremlin.mode_manager |
Mode stack & switching when Gremlin is active |
Backend |
gremlin.ui.backend |
Main QML↔Python bridge |
Use metaclass=common.SingletonMetaclass (not the legacy @common.SingletonDecorator).
Profiles are XML files. gremlin.profile.Profile owns load/save and dispatches this via various to_xml and from_xml call chains.
Backend(registered asbackendcontext property) exposes signals and slots to QML- QML-accessible classes use
@QtQml.QmlElementorengine.rootContext().setContextProperty() - Qt signals/slots use
@QtCore.Signal/@QtCore.Slotdecorators - Never call Qt GUI APIs from non-main threads
Every Python file must include:
# -*- coding: utf-8; -*-
# SPDX-License-Identifier: GPL-3.0-only- Use
from __future__ import annotationsfor forward references - Sort imports: stdlib, third-party, local (alphabetically within groups)
- Use absolute imports within the package (e.g.,
from gremlin.types import InputType) - Use
TYPE_CHECKINGguard for imports only needed for type hints to avoid circular imports
Example:
from __future__ import annotations
import logging
from typing import (
cast,
Any,
TYPE_CHECKING
)
from PySide6 import (
QtCore,
QtQml
)
import gremlin.profile
from gremlin.types import InputType
from gremlin.error import GremlinError
if TYPE_CHECKING:
from gremlin.base_classes import AbstractActionData- Required: All function signatures must have type annotations (enforced by ruff ANN rule)
- Use Python 3.13+ syntax:
list[str],dict[str, int](no need forList,Dictfrom typing) - Acceptable exceptions:
PySide6import errors (project-wide Pylance issue)AbstractActionDataattribute unknowns (project-wide)
- Use
X | NoneoverOptional[X]
- Classes:
PascalCase(e.g.,JoystickGremlinApp,Profile) - Functions/methods:
snake_case(e.g.,get_vjoy_device,from_xml) - Constants:
SCREAMING_SNAKE_CASE(e.g.,MAX_CACHE_SIZE) - Private members:
_leading_underscore(e.g.,_cache) - Qt properties/slots: Follow Qt conventions (e.g.,
imageReady,requestImage) - QML variables: Follow Qt convention (e.g.
someVariable) - QML element identifiers: Always start with
_(e.g._label,_actionModel)
- Use
@QtCore.Signaland@QtCore.Slotdecorators for Qt signals/slots - Signals are defined as class attributes:
imageReady = QtCore.Signal(str, str) - Subclass
QtCore.QObjectfor QML-accessible classes if no more specialized class is applicable - Use
@QtQml.QmlElementdecorator for classes to register with QML - Register QML context properties with
engine.rootContext().setContextProperty()to be used in exceptional circumstances only
from PySide6 import QtCore
if TYPE_CHECKING:
import gremlin.ui.type_aliases as ta
class DataProvider(QtCore.QObject):
dataReady = QtCore.Signal(str)
def __init__(self, parent: ta.OQO=None):
self._value = ""
def _get_value(self) -> str:
return self._value
def _set_value(self, val: str) -> None:
self._value = val
self.dataReady.emit(val)
value = QtCore.Property(
str,
fget=_get_value,
fset=_set_value,
notify=dataReady
)- Custom exceptions are defined in
gremlin.errorand inherit fromGremlinError - Use specific exception types (e.g.,
ProfileError,VJoyError,MissingImplementationError) - Provide meaningful error messages
- Exceptions are caught on the highest level and logged there
Example:
from gremlin.error import GremlinError
try:
value = int(text)
except ValueError:
raise GremlinError(f"Invalid device index: {index}")- Use
logging.getLogger("system")for logging
- Never use Qt GUI classes from non-main threads (Qt is not thread-safe)
- Background threads (e.g.,
threading.Thread) are acceptable for non-GUI work (device polling, file monitoring)
The project uses two singleton patterns:
- Only the newer
metaclass=common.SingletonMetaclassshould be used @common.SingletonDecoratoris legacy and is not to be used anymore
- Use
Connectionsfor QML-to-Python signal connections when data binding cannot be used - Use
onSignalNamefor Python-to-QML property changes - QML model classes often inherit from
QtCore.QAbstractListModeland implementrowCount(),data(),roleNames()
Each plugin under action_plugins/<name>/ defines:
AbstractActionDatasubclass — XML serialization, validity, metadata (tag,name,icon,version,input_types,functor)AbstractFunctorsubclass — actual runtime behaviorActionModel— QML UI model, enabling modifying the data via the UI
Required overrides on AbstractActionData: _from_xml(), _to_xml(), is_valid(), _valid_selectors(), _get_container(), _handle_behavior_change().
Example:
from gremlin.base_classes import AbstractActionData, AbstractFunctor
from typing import override
class SpecialActionData(AbstractActionData):
tag = "special-action"
name = "Special Action"
icon = "f123"
version = 1
functor = SpecialActionFunctor
# Implement functions mandated by the base class.- Tests go in
test/unit/,test/integration, ortest/action_interaction/depending on use case- Self-contained unit tests are placed in
test/unit - Simulated action interaction tests are placed in
test/action_interaction - Full end to end system tests are placed in
test/integration
- Self-contained unit tests are placed in
- Function tests for action plugins are placed in
test/action_interaction - Tests in
test/action_interactionhave access to ajgbotfixture (`test/action_interaction/conftest.py:JoystickGremlinBot) which is similar to the Qt pytest fixture - Use pytest fixtures from
test/conftest.pyandtest/unit/conftest.py - Use
pytest.raises()for exception testing
Example:
from test.unit.conftest import get_fake_device_guid
def test_something(xml_dir: pathlib.Path):
p = Profile()
p.from_xml(str(xml_dir / "profile_simple.xml"))
assert len(p.inputs) == 1Use type aliases from gremlin.ui.type_aliases for Qt-specific types:
import gremlin.ui.type_aliases as ta- Use docstrings for classes and complex functions
- Keep docstrings concise; describe purpose and parameters
- Do not add unnecessary inline comments
- Use
dataclassesfor simple data containers - Use
ElementTreefor XML parsing/creation - Use
pathlib.Pathfor file paths
- Pylance may show
Import "PySide6" could not be resolved- this is a project-wide issue, not caused by your changes - Some
AbstractActionDataattribute unknowns may appear - also project-wide
Before considering a task complete, run:
poetry run ruff check .
poetry run pyright
poetry run pytest