Repository navigation
refactor(runtime): run the keyboard as a profile on a pure engine - #36
Open
inayayousfi wants to merge 32 commits into
Open
inayayousfi wants to merge 32 commits into
inayayousfi wants to merge 32 commits into
Conversation
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. Replace concrete runtime command and event DTOs with registered native-data messages, then migrate built-in producers, handlers, and tests. Add full-source Pyright checks and document the queue contract.
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. Run type checking only after PySide6, axidev-io, and Axidev OSK are available to the selected Python interpreter.
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. Move component policy and keyboard output into root SourcePath behavior bindings backed by runtime state snapshots and ordered hooks. Keep controls render-only, isolate keyboard backend I/O, and migrate the bundled config and tests to explicit visual IDs and behavior maps.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Bring main's 0.18.1 features onto the event/action queue: Plasma lock-screen panels, dwell clicking and its toggle key, the pointer locator, display recovery, layer-shell dragging, right-click activation, and observed Caps Lock state. Keep buttons render-only. Keys with keyboard output follow the system's Caps Lock and Num Lock state, the Dwell key is a latchable action button, and key outputs register again whenever keyboard output starts so lock-screen cycles keep working. The lock-screen worker answers its supervisor from runtime events instead of exceptions.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Backend listeners call the dispatcher from their own threads, and the first thread to start a drain used to run every pending handler. Messages from other threads now wait in a locked inbox, and a Qt wake drains them on the Qt thread.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Type the new runtime handler factories, the hot-corner overlay protocol, and Qt application lookups without changing behavior.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Add PURIFICATION_PLAN.md with the settled decisions, the target architecture, and the steps for turning the runtime into an engine configured by profiles, and point AGENTS.md to it.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Decode root configs made of plain data and functions, with errors that name the config path. Keep durable state in one tree that records what each binding reads, so a binding re-runs only when that state changes. Run profile callbacks from the on table and report failures as callback.failed.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Report every keystroke as input.key and record key, lock, keyboard, and window state that profiles can read. Add keyboard.down, keyboard.up, keyboard.tap, keyboard.type_text, process.spawn with process.exited, and log actions.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Build grid, box, stack, button, label, and spacer nodes from profile config with live bindings and node callbacks, and host them in runtime windows alongside the existing keyboard grid.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Add python_defaults with the osk surface a Lua module will mirror, and osk.std keys, windows, and prompts built only from engine primitives. Add the window opacity and input-block actions they use.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Describe the US ISO keyboard, its theme, prompts, and attachments as a profile built on osk.std. Parity tests compare it with measurements taken from the original keyboard widget.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Decode dwell, pointer locator, hot corners, and lock-screen panel attachments, check their targets when a profile starts, and install dwell clicking and the pointer locator on the windows they name.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Move startup, quit, prompts, permission setup, hot corners, and the lock-screen panel to the profile. Delete behaviors, key modes, state tags, the keyboard grid, and the other engine code that held keyboard policy. Rewrite AGENTS.md around the engine, the standard library, and profiles.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. The single-instance service now reports app.activated instead of showing a window itself, but the default profile did not handle it. Show the keyboard on app.activated, and update the Windows-only single-instance test to expect the event.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Fix the bugs a review of this branch found. The lock-screen worker no longer hangs at shutdown, window.close closes the window, a failed binding re-runs, one-shot AltGr holds the real key, left and right modifiers share a latch again, and state follows Lua's rules for nil, empty tables, and true versus 1. Give each fact one owner. The window manager alone tracks live windows, observed state roots are declared by the code that writes them, the dispatcher owns action.failed, and Ghost keeps its own flag, cleared when the engine reports the window unblocked. The palette, font, and hot-corner colors move into the profile. Windows keep Qt's close rule unless they set default_close to false. Quit requests carry their reason, shutdown stops the profile first, and a failing step no longer blocks the exit. Failed keyboard actions report action.failed, and the lock-screen panel answers ERROR when keyboard output cannot start. Ask LayerShellQt for a layer surface per window through Window::get instead of setting QT_WAYLAND_SHELL_INTEGRATION for the whole process, which removes a warning per window. Wrapped labels and windows keep the height their text needs. Merge runtime/events.py and runtime/actions.py into runtime/app_messages.py, register every event in build_engine, and share the test fakes in tests/support.py. Platform changes: minimized windows are restored on every platform, and only overlay windows become layer surfaces.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Quitting becomes one more message in the queue. app.quit waits until nothing else is queued, then shutdown closes the queue first, so later messages are dropped and logged instead of running against a profile that is stopping. Check every profile in the root config when it is decoded, not only the active one. A profile restart reports state.changed for the paths it changed. A stack page cannot set visible, and a style cannot set a property the engine or its node kind sets. A window action naming an unknown window reports action.failed. The engine owns no colors: hot corners and the pointer locator take their colors as required options, given by the default profile. Other fixes: one-shot keys release exactly the keys their press held, None counts as absent in std options and osk.read, a held key whose release fails stays held, labels show a literal &, hot corners only react to the corners configured, a duplicate service name is rejected, the lock-screen panel answers a second prepare, and a failed layer-shell attach is logged. Share the keyboard test context and window, hot-corner, and node test helpers.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. State becomes a registry of declared variables that the engine stores and type-checks without knowing what they mean. Where a variable is declared sets its lifetime: the root config's state survives profile switches, a profile's state resets. Writes and reads of undeclared paths fail, and None resets a variable to its default. Every value option accepts a binding, while identity and structure stay static. Values a binding sets lose their setter actions, so window.set_opacity, window.block_input, window.unblock_input, and dwell.set_enabled go away. Record this as step 8.
inayayousfi
added this pull request to stack #45
October 10, 2026 16:23
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Every state value now lives in a variable declared with osk.var, with a type and a default. The engine declares what it observes, the root config declares variables every profile shares, which survive a restart, and a profile declares its own, which reset when it starts. Reads and writes of undeclared paths fail, values are checked against their type, and None resets a variable or removes a map entry. Every value field of a node, window, attachment, or theme accepts a binding. When state a record read changes, the whole record decodes again and only the changed fields are applied; an invalid result is reported and the last value stays. At load every binding runs against the default state, so a bad one rejects the config with its path. window.set_opacity, window.block_input, window.unblock_input, and dwell.set_enabled are removed. Ghost sets std.ghosted and the window's opacity and input block follow it, with the Ghost button marked passes_input_block. The default profile declares its opacity and dwell settings in the root state and binds the keyboard window and the dwell attachment to them. Window state reports position and size instead of opacity and input blocking. The parity test changes on purpose: Ghost and Dwell now set state instead of sending window and dwell actions. AGENTS.md describes the registry, and step 8 of the plan is done.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Quitting can no longer loop forever. An action registered when_idle, such as app.quit, waits in its own list until the queue and the inbox are empty, and such actions then run one at a time. A key event waiting in the inbox or a second quit used to keep re-queueing it. A failing event handler no longer skips the other handlers of that event. A window's Qt event handlers post their state reports and close requests, so profile callbacks never run inside a Qt event. The config decoder checks more at load, in every profile: an attachment naming an unknown window, more attachments of a kind than its limit allows, and a stack whose current names no page. A binding is registered before its failure is reported, and a fallback that cannot apply raises once and leaves nothing bound. A failed start runs the shutdown steps before raising. Other fixes: setting a value that creates a map entry counts as a change, a theme without a color or font goes back to the app's own, and a bound minimum size applies on a window's first build. Decoded values are compared the way Lua compares them, so true is not 1. The binding tracker alone keeps the last applied values and hands them to each applier. Context is gone: the engine carries the dispatcher, services receive only the dispatcher, the relative-pointer service is looked up by name, and hot corners follow display changes through the queue. ApplicationRuntime requires its root config, so the engine no longer imports python_defaults. Key names get their canonical spelling only in the keyboard service. The parity test changes on purpose: every prompt and notice now hides when closed instead of asking to quit, which looped while the keyboard was hidden. AGENTS.md describes idle actions, posting, handler failures, and the new load checks.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. The helper package that profiles build their config with, and the standard library inside it, now live in python_defaults/axidev_osk, and every profile, test, and doc writes axidev_osk.button(...) and axidev_osk.std instead of osk. The docs name the future Lua table axidev_osk too. Mentions of the Windows osk.exe keep their name.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Each attachment kind now owns its attach step, so installing no longer switches on four kind names and a new kind cannot decode and then do nothing. An attachment whose service is missing, such as hot corners on the lock-screen panel, is logged instead of dropped silently. Button presses and title-bar drags post their events like the other Qt handlers, and the dispatcher takes posted messages one batch at a time, so a second press sees the state the first one set. A binding that fails when it is bound posts its report instead of draining the queue inside bind. Other fixes: a palette binding that returns None leaves the role absent, and a style property changing from true to 1 is applied. One owner per fact: window.move_by registers with the other window actions, the engine's widget property names live in widget_properties.py, every attachment default lives on its settings record, and the test helpers move from runtime/testing.py into tests/support.py with the members only tests used. The hot-corner header comment no longer calls the controller temporary.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. AGENTS.md now maps the codebase: it states that the codebase is the engine, services that talk only through the queue and dispatcher, and that python_defaults/ is temporary until the Lua layer replaces it. What each module does stays in its docstrings, which now also state the behavior AGENTS.md used to describe: event handler failures and the drain warning, booleans never equal to numbers, state kept only in memory, dotted names in state views, quit reasons and SIGTERM, observed variables, and when check_options runs. PURIFICATION_PLAN.md is deleted because its steps are done; the open Lua work is tracked in issue #8. Also remove typed action builders nothing calls: keyboard_up, keyboard_tap, keyboard_type_text, process_spawn, log_info, log_warn, log_error, and linux_open_permission_setup.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. The service takes the dispatcher since services stopped receiving the whole context, but the tests still passed the engine. On Windows the activation then failed inside a Qt slot and never reached the profile, which failed every Windows CI run; the test is skipped elsewhere.
…oks to std inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. The primitives now leave profiles only verbosity as a limit. Timers: timer.start and timer.cancel run timers by ID, and timer.fired reports each one; stopping the profile cancels them, so long-press, auto-hide, and hold-to-confirm are profile code. Hover: buttons take on_enter and on_leave, which always alternate even when Qt repeats an enter. Pointer reports: any node with reports_pointer posts node.pointer_pressed, node.pointer_moved, and node.pointer_released with the position in its own pixels, over its children too, without taking their clicks, so a swipe across a grid of keys reaches the profile. The engine button no longer has active and latched, which only set the stylesheet properties pressed, latched, and interactionState. std.lit sets them through style properties, and std keys and the Dwell button use it, so the theme is unchanged. Node kinds no longer reserve properties of their own. The title bar stays in the engine: on Windows the window keeps the operating system's frame, so the engine does not promise profiles a title bar of their own; windows/chrome.py says why.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. A dwell click on a button runs animateClick, which releases it from a 100 ms timer, and the tests checked once after a fixed 150 ms wait. On a busy CI runner the timer fired after the check and the click was missed. The tests now process events until the click or the end of the fade arrives, failing after 2 s. Waits that check that nothing happens keep their fixed delay.
…se button inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. The dwell indicator's size, dot range, and fade time were constants in the engine, while the hot-corner indicator already took its size as an option. They are now the dwell options indicator_size_px, indicator_min_dot_px, indicator_max_dot_px, and indicator_fade_ms, with the old values as defaults, so a profile can enlarge the dot for a user who cannot see a small one. A stylesheet already sizes every part of the engine-drawn chrome by object name; only the close button's text was fixed. A window's close_label value now sets it, and it may follow state. AGENTS.md now says the engine fixes no color, font, size, timing, or visible text a user may need to change.
…t it means inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Four interactions were decided by the part that saw them. A button turned a right click into a left one and ignored the middle button, dragging the title bar or the resize grip moved or resized the window directly, and dwell clicked the widget under the pointer itself. Now each is reported with what was observed. button.pressed and button.released, and the pointer press and release reports, carry the mouse button. The title bar and grip post window.dragged and window.resize_dragged, and a finished dwell posts dwell.completed with its screen point. The engine adds the effects window.resize_by and window.click (with a mouse button), and std.with_handlers gives the reports their usual meaning: move, resize, and a left click. Standard keys ignore the mouse button, so left and right still do the same. AGENTS.md now states that rule, the promise of the same behavior on every platform, which code may call services, and opt-in reports for frequent observations, and maps the windows, components, services, and platform packages.
…t from inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. AGENTS.md now states how a feature is promoted: from a profile to the standard library when most profiles need it, and to an engine built-in only for speed, native access, or a platform detail. Every built-in can be turned off, the engine still reports the raw interactions it is built from, and its code states why it stays in the engine. Any mouse button now drags the title bar and the resize grip and presses the close button, and the events name it. std moves and resizes only on the left button; the close button still closes on any button unless the window turns off default_close. Back and forward press buttons like right and middle. Nodes can opt in to node.pointer_hovered and node.pointer_left (reports_hover), node.wheel (reports_wheel), and node.geometry_changed (report_geometry), which records nodes.<id>.position and size. Hot corners can be disabled (enabled), can report only (fires_on_rest), and post hot_corner.entered and hot_corner.left. Their indicator sizes and opacity are options, and the theme colors carry their own alpha. The pointer locator glows each button in its locator_color. The palette moved out of the engine into std.locator, which colors keys from their reported geometry; the default keyboard keeps all 89 original colors. Dwell's reversal angle, acceleration, poll interval, and indicator opacities are options. Window placement is an anchor, one of 9 named ones or any x and y, and keep_on_screen can turn off the screen clamp for floating windows.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Fix two bugs: showing a window again no longer shrinks it back to its minimum, and the pointer locator follows each button's shape instead of its bounding rectangle. Make the remaining fixed behavior optional or reported: - windows: fit_content (default on) and observed content_size; window.move_to and window.resize_to actions; observed screens from display.configuration_changed, which moves to engine_messages - chrome: drag start and end are reported on every platform, the grip reports window.resize_drag_started and window.resize_drag_ended, and title-bar padding and spacing come from the stylesheet (qproperty-spacing); the default theme keeps the same geometry - dwell: pause_while_pressed (default on) - grid: bindable row_stretch and column_stretch - buttons: every mouse button presses, unnamed ones report "other" - std.locator: colored, on and palette take opts
…ndows inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. The mask of every mouse button but the left one was ~LeftButton. On the Windows CI runner, with PySide6 6.12, that complement matched no button, so back, forward, and unnamed buttons stopped pressing a Button there. The mask is now AllButtons without LeftButton.
… Wayland refuses inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. A plain window (always_on_top off) ignored window.move_by and window.resize_by, and so move_to and resize_to, without telling the profile. It now moves and resizes as asked. Wayland gives an ordinary window no way to choose its position, so a move there raises and the profile receives action.failed with that reason.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
inayayousfi directed the work and made every decision.
gpt-5.6-sol, running in OpenCode, and Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.
Summary
This branch started as a generic action and event queue. It now also merges main up to 0.18.1 and turns the runtime into an engine that knows nothing about keyboards. The bundled US ISO keyboard is a profile written against that engine, in the shape a Lua config will have.
fn(state) -> valuere-runs only when the state it read changes, and a callbackfn(ctx, event)returns actions. State lives in declared, typed variables (see State registry below), andtrueis not1.input.*,keyboard.*,windows.*), and explicit effects (keyboard.down/up/tap/type_text, showing, hiding, closing, and moving windows,process.spawn, logging).python_defaults/axidev_osk/std, and the bundled keyboard with its colors and font intopython_defaults/default_profile.py, then delete key modes, hooks, state tags, the keyboard grid, and the prompt and permission dialogs from the engine.AGENTS.mdas a map of the engine, the standard library, and profiles, and write each module's behavior in its docstrings. The remaining Lua work is tracked in Define Lua config architecture #8.Architecture
Everything under
src/axidev_osk/exceptpython_defaults/is the engine. It owns windows, the queue, central state, platform integration, keyboard output, and process lifecycle, and it holds no layout, legend, latch, or prompt logic. A Lua loader is expected to replacepython_defaults/by producing the same maps and functions, so the engine does not change when Lua arrives.The default keyboard should behave as before. Quit confirmation, the Linux permission prompts, hot-corner show and hide, Ghost, Dwell, and second-launch activation now come from the profile instead of engine code. SIGTERM still quits without asking, a profile that handles no quit request quits directly, and other quit requests reach the profile with their reason (
signal,stdin_closed, orwindow_closed).Every window keeps Qt's close rule unless it sets
default_closeto false: closing it closes it, and closing the last visible window asks to quit. The default keyboard turns the rule off and asks through its own prompt.Plasma lock-screen panels keep the same supervisor protocol. The profile's
secure_input_panelattachment names the window they show, and the panel now answersERRORwhen keyboard output cannot start.State registry
State is now a registry of declared variables. A profile declares each one with
axidev_osk.var(type, default). Variables in the root config'sstateare shared by every profile and survive a profile switch, while a profile's ownstateresets when that profile starts. Reading or writing a path nobody declared fails, values are checked against their type, andNoneresets a variable to its default or removes a map entry. Every value field of a node, window, attachment, or theme accepts a binding, sowindow.set_opacity,window.block_input,window.unblock_input, anddwell.set_enabledare gone.Profiles also get three generic input primitives.
timer.start(id,delay_ms,repeat) andtimer.cancel(id) scheduletimer.firedevents; restarting an ID replaces its timer, and a profile switch drops every timer. Buttons report hover throughon_enterandon_leave(button.entered,button.left). Any node can setreports_pointer, and the engine then postsnode.pointer_pressed,node.pointer_moved(while pressed) andnode.pointer_releasedwith the node and the pointer'sxandy, for gestures such as swipes. Buttons lose theiractiveandlatchedoptions: how a key looks while down or latched is profile policy, sostd.litsets those style properties from bindings instead. The window chrome stays in the engine, because on Windows a window keeps the operating system's frame and a profile could not replace it there.Every interaction now reaches the profile as an event, and the engine decides nothing about what it means.
button.pressedandbutton.releasedcarry{node, button}, wherebuttonisleft,right,middle,back,forwardorother; every other mouse button presses a button as the left one does, andnode.pointer_pressedandnode.pointer_releasedcarry the button too. Dragging a window's title bar or resize grip postswindow.draggedorwindow.resize_draggedwith the window and the distance, and a finished dwell postsdwell.completedwith the window and the point. The engine no longer moves, resizes or clicks on its own:std.with_handlersmaps these events towindow.move_by, the newwindow.resize_by, and the newwindow.click(left button unless the action names another), so the default profile behaves as before, and a profile can give a right click, a drag or a dwell another meaning, or none.AGENTS.mdstates this rule, along with which code may call services, what the engine promises on every platform, and that frequent observations are opt-in.Built-ins are now optional, and
AGENTS.mdsays when a feature belongs in a profile, the standard library, or the engine. Any mouse button drags the title bar and the resize grip and presses the close button, andwindow.dragged,window.resize_dragged,window.drag_startedandwindow.close_requestedname it;stdmoves and resizes only on the left button. Nodes can opt in tonode.pointer_hoveredandnode.pointer_left(reports_hover),node.wheel(reports_wheel), andnode.geometry_changed(report_geometry), which recordsnodes.<id>.positionandnodes.<id>.size. Hot corners can be turned off (enabled) or only report (fires_on_rest), and posthot_corner.enteredandhot_corner.left. The pointer locator glows each button in itslocator_color, andstd.locatorcomputes the old palette from the keys' reported geometry, so all 89 keys keep their colors. Dwell's thresholds and indicator, the hot-corner indicator, and the locator's poll interval are options. Window placement is an anchor, one of 9 names or anyxandy, andkeep_on_screencan let a floating window leave its screen.The parity tests changed on purpose with this. Ghost used to send block and opacity actions for the keyboard window. It now sets
std.ghosted.keyboard, and the window's opacity and input block follow that flag. Dwell now togglessettings.dwell.enabled, a variable in the root config, instead of the removeddwell.keyboard-dwell.enabled. The layout, key outputs, latches, and the quit, permission, and second-launch flows are checked as before.Platform changes
LayerShellQt::Window::get, called with ctypes, instead of the app settingQT_WAYLAND_SHELL_INTEGRATION=layer-shellfor the whole process. Only overlay windows become layer surfaces, and the "already has a shell integration" warning is gone. The Linux launcher checks that this function exists; without it, the app falls back to the X11 overlay backend.action.failedinstead of being dropped silently.Testing
QT_QPA_PLATFORM=offscreen python -m unittest discover -s tests: 472 passed, 1 skipped on Linux.pyright: 0 errors.flake8 --select=F,E9,W6 src: clean.