Imported from ninbura/wooting_profile_automator (
AGENTS.md). Install upstream withnpx skills add ninbura/wooting_profile_automator. Copyright stays with the author.
Wooting Profile Automator
A Rust/Dioxus (desktop) port of Wooting-Profile-Switcher
(local copy at ~/repos/github/Wooting-Profile-Switcher, a Tauri 2 + egui app using wooting-rgb-sys).
Read these before changing anything in their area. They are the source of truth and this file deliberately does not duplicate them:
docs/protocol.md— the reverse engineered Wooting USB HID protocol: framing, paging, the stage-and-commit write sequence, and the command/report tables.docs/development.md— build and test workflow, architecture, and the macOS window chrome notes.docs/cli.md— every headless subcommand andprobesubcommand.README.md— the user-facing feature set and vocabulary.
Workflow rules
- Run the Dioxus CLI as
~/.cargo/bin/dxon this machine. Baredxresolves to Deno's shim. - Kill the running app before any
cargo build,clippyortest. Rewriting the binary under a live process gets itSIGKILLed withCode Signature Invalid, which reads as a crash in your change. Never leave two instances running either; they fight over the single HID handle. - Verify hardware behaviour with a differing payload. An identical-payload write-back passes even when the write was ignored.
Code layout (vertical slices)
src/
main.rs routing only: CLI subcommand or GUI launch
app.rs window/tray/dock lifecycle, app shell, the worker event pump
state.rs AppState: a Copy bundle of signals provided at the root
ui.rs presentation helpers (see slot numbering below)
core/ keyboard.rs (driver), hardware.rs (worker), config.rs, atomic_file.rs
devices/ keyboard list, profile switching, per-keyboard connect settings
connect/ engine.rs — what to apply when a keyboard appears
library/ store.rs, backups.rs, snapshot.rs, portable.rs — the on-disk library
activity/ the activity log dialog
settings/ autostart.rs, backup.rs
cli/ headless subcommands
components/ vendored `dx components` registry output (plain CSS modules)
Invariants
These are the ones that are expensive to rediscover. docs/development.md
explains the reasoning; this is the short list of what not to break.
- All HID access lives on the worker thread (
core/hardware.rs). SubmitRequests, readEvents.app.rs's pump is the only writer of UI device/op/activity state. A blocking HID call on the UI thread freezes the window, and a second HID client in the process misbehaves. - Every flash write goes through
hardware::write_slot. It refuses stale schemas, validates, backs the slot up first (a backup failure aborts the write), and restores the previously active profile. Add new write callers there, never straight tokeyboard::load_profile_for_serial.write_slot_rawis the unwrapped primitive for multi-slot writes that should share one activate-and-restore. config::update(|c| …)is the only sanctioned way to change settings. The globalMutex<Config>is the source of truth because the worker thread reads it and cannot use signals.- Reading that mutex inside a
use_memois not reactive and produces stale UI. Lock and clone once eagerly outside the hooks, seed ause_signal, derive withuse_memoover signals only. - Slot numbering: the protocol and everything under
core/use 0-3. The GUI shows 1-4 viaui::slot_number. The CLI deliberately stays on 0-3. - Tray/menu event handlers must be installed in
main()beforedioxus::launch.tray-iconandmudakeep the handler in a process-globalOnceLockand the first registration wins, so doing this from a component hook is a silent no-op. - The autostart entry passes
--hiddenand points at the executable, never at the.appbundle: launchd, the Run registry key and a desktop entry all exec the path directly, and a bundle directory is not executable. macOS therefore uses a launch agent rather than an AppleScript login item, because it is the only mode that forwards arguments.mainreads the flag as "this is a login launch" and starts with the window hidden and no Dock icon. - Two styling systems on purpose: Tailwind for layout, the registry components' theme CSS for widgets. Accepted for the accessibility the primitives bring.
- No em-dashes in user-facing copy, anywhere in the app or the docs.
Storage semantics
- Profile dumps:
<config>/wooting_profile_automator/profiles/*.json, all per-profile components (PROFILE_COMPONENTS) hex-encoded with provenance. - Automatic backups:
profiles/backups/<serial>__slot<N>__<unix secs>.json. One global newest-first ring capped byconfig.backup_keep(default 20). The serial is the identity; the keyboard's name is resolved from settings at display time so renames show through, falling back tosource_labelstamped at capture, then to the serial. - Whole-keyboard backups:
full-backups/<serial>__<unix secs>.json, a sibling ofprofiles/backups/, not a child. That is deliberate:backups::prunedoes a non-recursive read of its own directory, so a sibling is structurally unreachable from it and full backups can never be pruned bybackup_keepor by Settings' "Delete all automatic backups". Only an explicit per-row delete removes one. A backup that carries automations must also carry the library dumps those rules name (RuleSource::Dump), or a restore produces rules pointing at nothing. On restore, a dependency whose name already exists with different contents is saved under a free name and the rule retargeted, never overwriting what you had. - App settings backups are file only by design. A copy inside the installation would be destroyed by the events it exists to survive.
Vocabulary
Three of these are easy to confuse. Capture reads a profile off the keyboard into the library. Load writes one from the library onto the keyboard. Export/Import move an app artefact to or from a file. The device-card button was called "Export…" until file export existed.
Goals
- Port the profile-switching functionality to a Dioxus desktop app.
- Key new feature: set a default/fallback profile automatically when the keyboard connects to the host (on boot, replug, or KVM switch).
Dioxus 0.7 reference
Scaffolded by dx init, kept because 0.7 renamed most of the API and the
training-data version of it is wrong.
You are an expert 0.7 Dioxus assistant. Dioxus 0.7 changes every api in dioxus. Only use this up to date documentation. cx, Scope, and use_state are gone
Provide concise code examples with detailed descriptions
Dioxus Dependency
You can add Dioxus to your Cargo.toml like this:
[dependencies]
dioxus = { version = "0.7.1" }
[features]
default = ["web", "webview", "server"]
web = ["dioxus/web"]
webview = ["dioxus/desktop"]
server = ["dioxus/server"]
Launching your application
You need to create a main function that sets up the Dioxus runtime and mounts your root component.
use dioxus::prelude::*;
fn main() {
dioxus::launch(App);
}
#[component]
fn App() -> Element {
rsx! { "Hello, Dioxus!" }
}
Then serve with dx serve:
curl -sSL http://dioxus.dev/install.sh | sh
dx serve
UI with RSX
rsx! {
div {
class: "container", // Attribute
color: "red", // Inline styles
width: if condition { "100%" }, // Conditional attributes
"Hello, Dioxus!"
}
// Prefer loops over iterators
for i in 0..5 {
div { "{i}" } // use elements or components directly in loops
}
if condition {
div { "Condition is true!" } // use elements or components directly in conditionals
}
{children} // Expressions are wrapped in brace
{(0..5).map(|i| rsx! { span { "Item {i}" } })} // Iterators must be wrapped in braces
}
Assets
The asset macro can be used to link to local files to use in your project. All links start with / and are relative to the root of your project.
rsx! {
img {
src: asset!("/assets/image.png"),
alt: "An image",
}
}
Styles
The document::Stylesheet component will inject the stylesheet into the <head> of the document
rsx! {
document::Stylesheet {
href: asset!("/assets/styles.css"),
}
}
Components
Components are the building blocks of apps
- Component are functions annotated with the
#[component]macro. - The function name must start with a capital letter or contain an underscore.
- A component re-renders only under two conditions:
- Its props change (as determined by
PartialEq). - An internal reactive state it depends on is updated.
- Its props change (as determined by
#[component]
fn Input(mut value: Signal<String>) -> Element {
rsx! {
input {
value,
oninput: move |e| {
*value.write() = e.value();
},
onkeydown: move |e| {
if e.key() == Key::Enter {
value.write().clear();
}
},
}
}
}
Each component accepts function arguments (props)
- Props must be owned values, not references. Use
StringandVec<T>instead of&stror&[T]. - Props must implement
PartialEqandClone. - To make props reactive and copy, you can wrap the type in
ReadOnlySignal. Any reactive state like memos and resources that readReadOnlySignalprops will automatically re-run when the prop changes.
State
A signal is a wrapper around a value that automatically tracks where it's read and written. Changing a signal's value causes code that relies on the signal to rerun.
Local State
The use_signal hook creates state that is local to a single component. You can call the signal like a function (e.g. my_signal()) to clone the value, or use .read() to get a reference. .write() gets a mutable reference to the value.
Use use_memo to create a memoized value that recalculates when its dependencies change. Memos are useful for expensive calculations that you don't want to repeat unnecessarily.
#[component]
fn Counter() -> Element {
let mut count = use_signal(|| 0);
let mut doubled = use_memo(move || count() * 2); // doubled will re-run when count changes because it reads the signal
rsx! {
h1 { "Count: {count}" } // Counter will re-render when count changes because it reads the signal
h2 { "Doubled: {doubled}" }
button {
onclick: move |_| *count.write() += 1, // Writing to the signal rerenders Counter
"Increment"
}
button {
onclick: move |_| count.with_mut(|count| *count += 1), // use with_mut to mutate the signal
"Increment with with_mut"
}
}
}
Context API
The Context API allows you to share state down the component tree. A parent provides the state using use_context_provider, and any child can access it with use_context
#[component]
fn App() -> Element {
let mut theme = use_signal(|| "light".to_string());
use_context_provider(|| theme); // Provide a type to children
rsx! { Child {} }
}
#[component]
fn Child() -> Element {
let theme = use_context::<Signal<String>>(); // Consume the same type
rsx! {
div {
"Current theme: {theme}"
}
}
}
Async
For state that depends on an asynchronous operation (like a network request), Dioxus provides a hook called use_resource. This hook manages the lifecycle of the async task and provides the result to your component.
- The
use_resourcehook takes anasyncclosure. It re-runs this closure whenever any signals it depends on (reads) are updated - The
Resourceobject returned can be in several states when read:
Noneif the resource is still loadingSome(value)if the resource has successfully loaded
let mut dog = use_resource(move || async move {
// api request
});
match dog() {
Some(dog_info) => rsx! { Dog { dog_info } },
None => rsx! { "Loading..." },
}
Routing
All possible routes are defined in a single Rust enum that derives Routable. Each variant represents a route and is annotated with #[route("/path")]. Dynamic Segments can capture parts of the URL path as parameters by using :name in the route string. These become fields in the enum variant.
The Router<Route> {} component is the entry point that manages rendering the correct component for the current URL.
You can use the #[layout(NavBar)] to create a layout shared between pages and place an Outlet<Route> {} inside your layout component. The child routes will be rendered in the outlet.
#[derive(Routable, Clone, PartialEq)]
enum Route {
#[layout(NavBar)] // This will use NavBar as the layout for all routes
#[route("/")]
Home {},
#[route("/blog/:id")] // Dynamic segment
BlogPost { id: i32 },
}
#[component]
fn NavBar() -> Element {
rsx! {
a { href: "/", "Home" }
Outlet<Route> {} // Renders Home or BlogPost
}
}
#[component]
fn App() -> Element {
rsx! { Router::<Route> {} }
}
dioxus = { version = "0.7.1", features = ["router"] }
Fullstack
Fullstack enables server rendering and ipc calls. It uses Cargo features (server and a client feature like web) to split the code into a server and client binaries.
dioxus = { version = "0.7.1", features = ["fullstack"] }
Server Functions
Use the #[post] / #[get] macros to define an async function that will only run on the server. On the server, this macro generates an API endpoint. On the client, it generates a function that makes an HTTP request to that endpoint.
#[post("/api/double/:path/&query")]
async fn double_server(number: i32, path: String, query: i32) -> Result<i32, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
Ok(number * 2)
}
Hydration
Hydration is the process of making a server-rendered HTML page interactive on the client. The server sends the initial HTML, and then the client-side runs, attaches event listeners, and takes control of future rendering.
Errors
The initial UI rendered by the component on the client must be identical to the UI rendered on the server.
- Use the
use_server_futurehook instead ofuse_resource. It runs the future on the server, serializes the result, and sends it to the client, ensuring the client has the data immediately for its first render. - Any code that relies on browser-specific APIs (like accessing
localStorage) must be run after hydration. Place this code inside ause_effecthook.