Repository navigation
feat(defaults): add a wheel profile, board layouts, and a canvas node - #46
Open
inayayousfi wants to merge 16 commits into
Open
inayayousfi wants to merge 16 commits into
inayayousfi wants to merge 16 commits into
Conversation
inayayousfi
added this pull request to stack #45
October 12, 2026 18:04
inayayousfi
force-pushed
the
feat/wheel-profile
branch
from
October 13, 2026 11:15
953ff47 to
7ea2339
Compare
This was referenced Oct 13, 2026
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. A new wheel node kind places its children as the slices of a disc, slice 0 on top and numbered clockwise, or as its center, and clips each child to its shape. A button child stays an ordinary button: clicks, hover, and dwell targeting follow its wedge. Node kinds can now check their children's placements together at load, which the wheel uses to allow one child per slice. Keys are now described in two layers. std.board names the positions of an 80% board the way XKB does, and a std.layouts table says what each position is: its legend, its Shift legend, and the literal key it sends. keys.at builds a key for a position that follows a layout variable, and the default keyboard is rebuilt this way on settings.layout, a shared root variable set to "us". Its key IDs are now positions, such as key:AC01 for A; the parity data for legends and outputs is unchanged. std.wheel walks a hand-written map of the board on 6 slices: numbers with the function row inside them, three letter slices by board column, navigation, and modifiers. Picking a key sends it and returns to the top, a modifier waits for the next key, and the center goes back to the top. The new wheel profile shows it with the same strip, dwell, pointer glow, hot corners, and lock-screen panel as the default keyboard.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. The engine no longer knows about wheels. The wheel node kind and the check_children hook are gone, replaced by a canvas node that places each child by its shape, a polygon in pixels, centers its box on an optional anchor point, and clips it to the shape. Both are ordinary values, so bindings can move them. Any node can set report_size, and the engine then writes its size to nodes.<id>.size through a new node.resized event. std.wheel now computes every wedge and the center itself from the canvas's reported size, so the wheel stretches into an ellipse with its window. It has 10 slices: digits, the other symbols with Tab and Enter, navigation, Escape with the function and rare keys, Backspace, Space, the modifiers, and the three letter rows. A slice behaves exactly like the key button for the same entry through keys.behavior, which replaces keys.tap: holding repeats and modifiers latch. A menu slice previews its keys spread over the wedge. Buttons take a list of label parts, which they spread in rows over their shape, and draw their border along a shape a canvas gives them. Every label shrinks to fit its button and never grows past the size its style gives it. A Dvorak table joins the US layout, and the settings strip gains a Layout menu that switches both keyboards. The default keyboard's key unit drops from 48 to 32 px with a smaller theme, so the minimum sizes in the parity data are rescaled; cells, legends, and outputs are unchanged. A window with the strip now gives its spare height to its content again, so keys grow with the window.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. A node kind whose children's placement may follow state now declares decode_live_placement and place_child, and builds its children with builder.build(child, into=its widget). Setting only one of the two hooks fails at registration, and building a child without into fails at build, so a placement can no longer be ignored silently. The canvas is the only code that knows shape and anchor, and either one under another parent is an unknown key. The canvas gives the outline to any child with a set_shape method instead of checking for Button. A single button label is drawn by the style again, scaled down when it must shrink, so state rules such as a :pressed text color apply. A label that changes to an empty list now shows nothing. The border color of a shaped button is sampled once per state, and the docstring says what the sample assumes about the theme. keys.on_layout builds each position's behavior in every layout up front; keys.at and std.wheel both use it, which removes the wheel's lazy cache. The default profile shares one status label between its keyboards, and the osk docstring and the purification plan are corrected.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. axidev_io's stop_listener waits for its listener thread while holding the GIL, and that thread needs the GIL to deliver an event. Releasing held keys right before the stop could put such an event in flight, so the app hung on quit until systemd killed it. Shutdown now stops listening first. A physical key event during the stop can still hit the race; axide-dev/axidev-io-python#3 tracks the binding fix.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. A wheel whose nested menu has another slice count than its top level, or an item that is not a position, a menu, or None, now fails when the config is built instead of breaking slices when the menu opens. menu and row take the slice count, so each tree's top level is the one source. A layout table missing a position the keys need fails the same way; an unknown layout name set at runtime is left to the config check. Label parts land only in the widest stretch a shape covers on every line of their row, so a dented shape no longer puts them in its gap, and they are drawn by the style like a single label, so state rules such as a pressed text color reach them. When no row layout fits, the parts are drawn as one label instead of disappearing. Smaller cleanups: the unused wheel.column is gone, names no longer shadow the layouts module or the middle function, and AGENTS.md and the plan describe the strip's Layout menu and list labels.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. The node kind recipe names decode_live_placement, and a new recipe says where a layout goes. The canvas, board positions, layouts, and wheel are described in their own modules' docstrings.
inayayousfi
force-pushed
the
feat/wheel-profile
branch
from
October 13, 2026 13:47
7ea2339 to
d6bd7b7
Compare
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Brings in timers, hover, pointer reports, std.lit, and the strip's lit controls. std.keys.behavior still names active and latched, and the places that turn it into a widget (from_entry, at, and the wheel slice) now light it through std.lit instead of the removed button options. A node may set both report_size and reports_pointer, and a canvas child's live placement decodes beside the engine-wide style check.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. The pressed-color test compared every text pixel, so the colored edge pixels of subpixel font smoothing on the Ubuntu runner failed it. It now compares the text's average color, which still tells white from blue. The offscreen platform finds no fonts on Windows by itself, so label sizes read as -1 there. The Windows check now points it at the system font folder with QT_QPA_FONTDIR.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Brings in the dwell tests' wait for clicks.
inayayousfi
marked this pull request as ready for review
October 13, 2026 15:16
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Brings in the dwell indicator options and the close_label window value.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Brings in the interaction reports: mouse buttons, title-bar and grip drags, and finished dwells, with std giving them their usual meaning.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Brings in optional built-ins and their raw reports: any-button drags, hover, wheel, and geometry reports, hot-corner and dwell options, std.locator, and window anchors. report_geometry replaces report_size and node.resized: it records nodes.<id>.size as before, plus nodes.<id>.position, and std.wheel turns it on. The keyboard and the wheel color their buttons through std.locator. Five navigation keys change locator color, matching what the old engine palette gives the 32-pixel keys.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Brings the shape audit fixes and keep_position; nothing needed adapting.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. The button option label_min_px (default 1) is the smallest pixel size a label shrinks to when it does not fit; below it the label is cut instead of shrinking further.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Brings the Windows mouse-button fix.
inayayousfi directed the work and made every decision. Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out. Brings plain-window moves and resizes.
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.
Claude Opus 5.5, running in Claude Code, carried inayayousfi's decisions out.
This sits on top of #44 and adds a third bundled profile, wheel, which types through a ring of slices instead of a key grid. The idea comes from a web prototype where the pointer picks a slice around a center, and each slice either opens a smaller menu or sends its key.
The engine stays keyboard-agnostic and gains two generic pieces. A new canvas node places each child by its shape, a polygon in pixels, centers the child's box on an optional anchor point, and clips the child to the shape, so a button child is still an ordinary button whose clicks, hover and dwell targeting follow its outline. Shape and anchor are values like any other and can be bindings. They are read through a new pair of node kind hooks, decode_live_placement and place_child, so the canvas owns its children's placement the way a grid owns its cells, and a kind that sets only one of the two is refused at registration. std.wheel turns on report_geometry, from #36, so the engine writes the canvas's size to nodes..size, which is how a profile fits shapes to the space it gets. Buttons take a list of label parts, which they spread in rows over their shape, draw their border along a shape, and shrink any label to fit while never growing it past the size their style gives. A single label is still drawn by the Qt style, scaled down when it must shrink, so stylesheet text rules such as a :pressed color keep working.
Everything keyboard-specific lives in axidev_osk.std. std.board names the positions of an 80% board the way XKB does (AD01 is where US Q sits), and a table in std.layouts says what each position is: its legend, its Shift legend and the literal key it sends. US and Dvorak ship. keys.behavior turns an entry into how it acts as a key (press, hold-to-repeat, release, latching modifiers, lock lighting), so a grid key and a wheel slice holding the same entry behave the same, and keys.on_layout resolves those behaviors for whichever layout a binding names. The default keyboard is rebuilt on this and reads a shared variable, settings.layout, which the settings strip now switches through a new Layout menu.
std.wheel draws the whole wheel itself. It reads the canvas's reported size and computes every wedge and the center as plain lists of points, so the wheel stretches into an ellipse with its window. It has 10 slices: digits, the other symbols with Tab and Enter, navigation, Escape with the function and rare keys, Backspace, Space, the modifiers, and the three letter rows. A menu slice previews the keys inside it spread over its wedge. Picking a key sends it and goes back to the top, modifiers latch like on the grid, and the center returns to the top from any menu.
The default keyboard changes on purpose in two ways. Its key unit drops from 48 to 32 px and the theme gets smaller paddings and fonts, so the minimum sizes in tests/test_default_profile.py are rescaled; cells, legends and outputs keep their original measurements. Its node IDs are now positions, so key:A is key:AC01. Separately, a window with the settings strip now gives its spare height to its content again: since #44 the strip's spacer was taking it, so keys stopped growing with the window.
One fix outside the profiles: on quit, KeyboardService now stops listening before it releases held keys. axidev_io's stop_listener waits for its listener thread while holding the GIL, and a release arriving mid-stop could hang the app until systemd killed it, which happened twice here with latched Ctrl keys. A physical key event during the stop can still hit the race; axide-dev/axidev-io-python#3 tracks the binding fix.
The full suite (516 tests) and flake8 pass. Pyright reports one error, the unresolved axidev_io import in services/keyboard/io.py, because that package is missing from the local virtual environment. The branch has run as the installed app on KDE Plasma Wayland, including the lock-screen supervisor reload, but a real lock and unlock cycle and the shutdown fix with held keys are not verified yet. No platform-specific code changed besides the shutdown order.
CI: the Windows check sets
QT_QPA_FONTDIRso offscreen Qt finds the system fonts, and the label color test compares average colors, so subpixel font smoothing on Ubuntu no longer fails it.