Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/reusable-check-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
uses: ./.github/actions/setup-ubuntu-runner

- name: Install Python check tools
run: python -m pip install --upgrade pip build flake8
run: python -m pip install --upgrade pip build flake8 pyright

- name: Lint top-level Python code
run: |
Expand All @@ -45,6 +45,9 @@ jobs:
- name: Install axidev-osk
run: python -m pip install -e .

- name: Type-check application source
run: python -m pyright --pythonpath "$(python -c 'import sys; print(sys.executable)')"

- name: Run app tests
env:
PYTHONPATH: src
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/reusable-check-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
uses: ./.github/actions/setup-windows-runner

- name: Install Python check tools
run: python -m pip install --upgrade pip build flake8
run: python -m pip install --upgrade pip build flake8 pyright

- name: Lint top-level Python code
run: |
Expand All @@ -45,6 +45,9 @@ jobs:
- name: Install axidev-osk
run: python -m pip install -e .

- name: Type-check application source
run: python -m pyright --pythonpath (python -c "import sys; print(sys.executable)")

- name: Run app tests
env:
PYTHONPATH: src
Expand Down
83 changes: 64 additions & 19 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
Written by inayayousfi, typed by gpt-5.6-sol running in OpenCode.
Every call here is inayayousfi's, and no agent acted on its own.

# AGENTS.md

This file defines the architectural guardrails for humans and coding agents working in this repository.
Expand Down Expand Up @@ -26,17 +29,17 @@ The Lua configuration layer is not implemented yet. That does not reduce its imp
4. Keep widget construction separate from process orchestration and backend/input logic.
5. Prefer composition through data and registries over special-case window subclasses.
6. New APIs should be designed so a future Lua config can describe and assemble them.
7. Runtime subsystems should communicate through the central event/command queue, not direct cross-subsystem calls.
7. Runtime subsystems should communicate through the central event/action queue, not direct cross-subsystem calls.
8. Durable application state belongs to the main process/runtime state store, not individual widgets, components, or Lua globals.
9. Services, UI widgets, backend adapters, timers, and platform integrations must not call window managers, widgets, backends, Lua callbacks, or other subsystems directly when a runtime event or command can represent the interaction.
9. Services, UI widgets, backend adapters, timers, and platform integrations must not call window managers, widgets, backends, Lua callbacks, or other subsystems directly when a runtime event or action can represent the interaction.

## Mental Model

- Buttons are components.
- Grids are components that place buttons or other controls.
- Windows are components/surfaces that host one or more grids.
- One main process coordinates windows, services, state, queues, and future config loading.
- UI, backend, Lua, timers, and app controls are event producers/consumers connected through the queue.
- UI, backend, Lua, timers, and app controls are event and action producers/consumers connected through the queue.

This means the current `MainWindow` is an implementation detail, not the final shape of the application.

Expand All @@ -53,7 +56,21 @@ When adding or refactoring code, keep these boundaries clear:
- backend/service concerns:
Keyboard emission, config loading, registries, state synchronization, and future Lua integration.
- runtime/orchestration concerns:
Event queue ownership, command routing, callback scheduling, state store updates, and subsystem boundaries.
Event queue ownership, action routing, callback scheduling, state store updates, and subsystem boundaries.

## Visual And Behavior Configuration

Visual config describes what a component looks like and where it appears. It may contain labels, display variants, geometry, component kinds, and stable IDs. It must not contain backend keys, runtime actions, latch policy, callbacks, or durable interaction state.

Behavior config is separate and belongs to the root application config. Each interactive key or generic button must have exactly one `BehaviorBinding` addressed by its full `SourcePath`. Loading must fail before window construction when a target is missing, duplicated, unresolved, attached to a non-interactive node, or uses an unknown behavior or hook kind.

Components emit only raw interactions such as `component.pressed` and `component.released`. They render complete state snapshots from the central runtime store. Components must not decide keyboard policy, resolve prompt results, call backend services, or keep durable latch state.

Behavior handlers and hooks return queue messages. Blocking before-hooks may cancel or replace default behavior. Later before-hooks still run, and the last cancel or replace decision wins. After-hooks may extend completed behavior but cannot undo it.

Keyboard behavior must declare an explicit output key and interaction mode. The keyboard service owns backend lifecycle, output registration, backend observations, and active press handles. It does not own visual data, latch policy, or durable component state.

A `SourcePath` contains ordered app, profile, window, surface, component, layout, grid, and child-component segments as applicable. Queue messages carry the path as native data. Runtime state uses a collision-free encoding of the full path.

## Preferred Direction For New Work

Expand All @@ -62,8 +79,8 @@ When adding or refactoring code, keep these boundaries clear:
- Prefer registries/factories over `if` ladders tied to one known surface.
- Prefer interfaces that allow multiple instances of the same window/surface type.
- Prefer names that describe reusable concepts like `surface`, `grid`, `panel`, `component`, or `controller` when accurate.
- Prefer event/command messages over direct calls between UI, backend, Lua, and application orchestration.
- Treat runtime events and commands as the default integration boundary between subsystems; direct calls are acceptable only inside one subsystem's own implementation or when adapting an event/command in the main runtime.
- Prefer event/action messages over direct calls between UI, backend, Lua, and application orchestration.
- Treat runtime events and actions as the default integration boundary between subsystems; direct calls are acceptable only inside one subsystem's own implementation or when adapting an event/action in the main runtime.
- Prefer main-owned state updates that can be reset, replayed, logged, and cleaned up during config reloads or profile switches.

## Avoid
Expand All @@ -75,7 +92,7 @@ When adding or refactoring code, keep these boundaries clear:
- writing new code that makes multi-window composition harder
- mixing backend emission logic into button rendering code
- letting UI widgets directly invoke backend services or Lua callbacks when an event can be routed through the queue instead
- letting services directly invoke window managers, windows, widgets, backend adapters, or other services instead of emitting a runtime event or command
- letting services directly invoke window managers, windows, widgets, backend adapters, or other services instead of emitting a runtime event or action
- adding hidden shared runtime state to reusable layouts; reused layouts should instantiate fresh runtime state

## Lua Readiness
Expand All @@ -97,25 +114,53 @@ Bundled layouts such as the default US ISO keyboard should eventually be ordinar

## Queue And State Architecture

The target runtime architecture is queue-driven:
The runtime uses one synchronous first-in, first-out queue for events and actions. Producers add messages to the queue. The dispatcher drains them in order on the calling thread. A handler can return more events or actions, and the dispatcher appends those messages after the handler finishes.

### Message Contract

An event reports something that happened:

RuntimeEvent(event="component.pressed", arguments={...})

An action requests an effect:

RuntimeAction(action="window.show", arguments={...})

The name must be lowercase and dot-separated. The arguments must contain only native data that Lua and Python can exchange without live object references: null, booleans, finite numbers, strings, lists, and string-keyed maps.

Queue messages must not contain Qt objects, backend objects, Python callbacks, Lua functions, dataclass instances, or other process-local values. Use stable IDs and native data. A subsystem can resolve an ID to an object only inside the registered handler that owns that subsystem.

Configured behavior uses the same RuntimeAction shape as queued behavior. Keys, buttons, menu items, hot corners, timers, profile controls, and future Lua callbacks must not introduce parallel action formats.

### Registration And Typing

Every event and action name must be registered before use. A registration supplies an argument decoder and, for an action, its handler. Built-in definitions also provide typed argument records and typed constructors so Pyright checks repository-owned call sites. Lua-defined names remain open-ended and receive runtime checks from their registered decoders.

Duplicate registration fails by default. An explicit override replaces the whole definition, including its decoder and handler. Code that overrides a built-in name is responsible for any incompatibility with existing producers.

Handlers return an ordered list of follow-up RuntimeEvent and RuntimeAction messages. They must not call another subsystem or recursively dispatch messages. The main runtime may adapt a registered action into a concrete call on the window manager, state store, backend, or another runtime-owned service.

### Failures And Ordering

An unknown action, invalid action arguments, or an action-handler exception is logged and produces action.failed. The failure event contains the action name, original arguments, failure stage, exception type, and message.

An unknown event, invalid event arguments, or an event-handler exception is logged. The dispatcher skips the remaining handlers for that event and continues with the queue. It does not emit a second failure event, which avoids recursive failure handling.

The dispatcher warns after every 10,000 messages processed without returning. It does not stop the drain. Custom actions are allowed to produce unbounded work, so a cyclic action can keep the UI thread busy and produce unlimited logs.

- UI widgets emit interaction events into the queue.
- Backend/input services emit observed input or status events into the queue.
- Timers, app controls, profile switching, and config reloads emit events into the queue.
- The main runtime consumes ordered events, updates the main-owned state store, routes Lua callback work to the Lua actor, and applies returned commands through the queue.
- Lua callbacks do not directly mutate widgets, backend objects, or durable state. They receive context and event objects, then return or enqueue commands.
### Lua Boundary

Use this model even when the current implementation is still simpler. New work should move the app toward explicit events, commands, and state-store updates rather than direct object-to-object coupling.
Lua tables convert recursively to the native argument map. JSON text is not the queue format. Lua-defined actions register names, decoders, and callback references through the future Lua actor. The queue stores the reference and native arguments, never the Lua function itself.

When a subsystem observes something, it should emit an event DTO. When a subsystem wants something to happen, it should dispatch a command DTO. The main runtime/orchestration layer owns translating those events and commands into concrete calls on window managers, services, state stores, and platform adapters. Do not wire services directly to windows or window managers, and do not wire UI components directly to backend services, unless the call remains entirely inside the same subsystem and cannot reasonably cross the runtime event/command boundary.
Lua callbacks are deferred by default. They receive event or action context and return events or actions to the queue. They do not mutate widgets, backend objects, or durable state directly. Cancellation, replacement, and extension of default behavior must be represented by explicit queue data rather than direct cross-subsystem calls.

Durable state should be namespaced by app/profile/window/layout/component identity, but owned centrally by the main process. Components render state snapshots and emit events; they should not be the source of truth for application state. Profile switches, config reloads, and runtime resets should be able to cleanly discard all non-preserved state from this central store.
### State Ownership

Callbacks should be treated as deferred/asynchronous by default. Callback ordering must remain deterministic through the queue, and callbacks should be able to cancel or replace default behavior through explicit event/command APIs.
Durable state remains owned by the main runtime state store and is namespaced by app, profile, window, layout, and component identity. Components render state snapshots and emit events. Profile switches, config reloads, and runtime resets must be able to discard all non-preserved state without depending on widget or Lua closure lifetime.

User configs should be loaded from standard locations such as `XDG_CONFIG_HOME` or `~/.config` on Unix-like systems, and the usual per-user config location on Windows. Bundled configs should be used as fallback defaults and examples when user config is missing or invalid.
User configs should be loaded from standard locations such as XDG_CONFIG_HOME or ~/.config on Unix-like systems, and the usual per-user config location on Windows. Bundled configs should be fallback defaults and examples when user config is missing or invalid.

For more detailed Lua config and runtime architecture direction, refer to GitHub issue #8: `Define Lua config architecture`.
For more detailed Lua config direction, refer to GitHub issue #8: Define Lua config architecture.

## Practical Rule For Contributors

Expand Down
42 changes: 0 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,48 +150,6 @@ What's planned:
- config-driven composition of grids and layouts
- more reusable grid and container primitives

## Contributing

Changes should land through pull requests rather than direct pushes to `main`.

Clone the repository with submodules:

```bash
git clone --recurse-submodules https://github.com/axide-dev/axidev-osk.git
cd axidev-osk
```

For normal development, install the vendored input backend and this project into a local virtual environment:

```bash
python -m venv .venv
.venv/bin/python -m pip install -e ./vendor/axidev-io-python -e .
```

Start the app from the checkout:

```bash
PYTHONPATH=src .venv/bin/python -m axidev_osk
```

Read [`AGENTS.md`](./AGENTS.md) before structural changes. It documents the modular architecture rules.

PR guidance:

- keep each PR focused on one concern
- call out architectural impact when changing windows, grids, layouts, or orchestration
- note platform-specific behavior when Windows, X11, or Wayland changes

### Commit Style

Commits use this subject format:

```text
type(scope): short imperative summary
```

Use lowercase `type` and `scope`. Keep the summary short and imperative.

## License

Axidev OSK is licensed under GPLv3. See [`LICENSE`](./LICENSE).
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ where = ["src"]

[tool.setuptools.package-data]
"axidev_osk.assets" = ["*.ico", "*.svg"]

[tool.pyright]
include = ["src/axidev_osk"]
typeCheckingMode = "standard"
pythonVersion = "3.10"
pythonPlatform = "All"
4 changes: 2 additions & 2 deletions src/axidev_osk/application/linux_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ..runtime.prompt import PromptResolutionWaiter

if TYPE_CHECKING:
from ..config.models import AppConfig, WindowConfig
from ..config.models import AppConfig, PromptConfig, WindowConfig
from ..runtime.dispatcher import Dispatcher
from ..runtime.window_manager import WindowManager
from ..services.keyboard import KeyboardService
Expand All @@ -32,7 +32,7 @@ def __init__(
dispatcher: "Dispatcher",
keyboard: "KeyboardService",
window_manager: "WindowManager",
build_prompt_window_config: Callable[[object], "WindowConfig"],
build_prompt_window_config: Callable[["PromptConfig"], "WindowConfig"],
) -> None:
self._config = config
self._dispatcher = dispatcher
Expand Down
24 changes: 17 additions & 7 deletions src/axidev_osk/cli/linux_greeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,12 @@ def _select_manager(
key_reader: Callable[[], str] | None = None,
output: TextIO | None = None,
) -> str:
output = output or sys.stdout
output = output if output is not None else sys.stdout
if output is None:
raise linux.LinuxSetupError("interactive selection requires an output stream")
if key_reader is None:
if not sys.stdin.isatty() or not output.isatty():
input_stream = sys.stdin
if input_stream is None or not input_stream.isatty() or not output.isatty():
raise linux.LinuxSetupError("--manager is required without an interactive terminal")
key_reader = _terminal_key_reader

Expand Down Expand Up @@ -287,10 +290,13 @@ def _select_manager(
def _terminal_key_reader() -> str:
if termios is None or tty is None:
raise linux.LinuxSetupError("interactive selection is unavailable in this terminal")
descriptor = sys.stdin.fileno()
previous = termios.tcgetattr(descriptor)
input_stream = sys.stdin
if input_stream is None:
raise linux.LinuxSetupError("interactive selection requires an input stream")
descriptor = input_stream.fileno()
previous = termios.tcgetattr(descriptor) # type: ignore[attr-defined]
try:
tty.setraw(descriptor)
tty.setraw(descriptor) # type: ignore[attr-defined]
first = os.read(descriptor, 1)
if first in {b"\r", b"\n"}:
return "enter"
Expand All @@ -304,7 +310,7 @@ def _terminal_key_reader() -> str:
third = os.read(descriptor, 1)
return {b"A": "up", b"B": "down"}.get(third, "unknown")
finally:
termios.tcsetattr(descriptor, termios.TCSADRAIN, previous)
termios.tcsetattr(descriptor, termios.TCSADRAIN, previous) # type: ignore[attr-defined]


def _installed_launcher() -> Path:
Expand Down Expand Up @@ -974,7 +980,11 @@ def _read_process_environment(pid: int) -> dict[str, str] | None:

def _install_signal_handlers(handler: Callable[[int, Any], None]) -> dict[int, Any]:
previous = {}
for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
signals = [signal.SIGTERM, signal.SIGINT]
sighup = getattr(signal, "SIGHUP", None)
if sighup is not None:
signals.append(sighup)
for signum in signals:
previous[signum] = signal.signal(signum, handler)
return previous

Expand Down
7 changes: 1 addition & 6 deletions src/axidev_osk/components/button/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
"""Button component registration and primitives."""

from .builder import build_button_component, register
from .key import KeyButton, create_key_button
from .state import KeyInteractionState, KeyStateChange, KeyStateMachine
from .key import create_key_button

__all__ = [
"KeyButton",
"KeyInteractionState",
"KeyStateChange",
"KeyStateMachine",
"build_button_component",
"create_key_button",
"register",
Expand Down
Loading