diff --git a/MIGRATION_STATUS.md b/MIGRATION_STATUS.md deleted file mode 100644 index 29e4b48..0000000 --- a/MIGRATION_STATUS.md +++ /dev/null @@ -1,94 +0,0 @@ -# Migration Status - -## Implemented - -- Replaced the default Tauri + Leptos starter UI with a desktop-oriented Razer app shell. -- Added native Linux hidraw discovery for supported devices: - - Razer Basilisk V3 `1532:0099` - - Razer Basilisk V3 Pro wired `1532:00aa` - - Razer Basilisk V3 Pro wireless `1532:00ab` -- Ported the Python report framing used by `qdrazer.protocol.Report` into Rust: - - 90-byte report payload plus report id - - Basilisk transaction id `0x1f` - - command class/id layout - - XOR CRC over bytes 2 through 87 - - feature report send/receive through `HIDIOCSFEATURE` and `HIDIOCGFEATURE` -- Implemented a native connect and snapshot flow: - - serial number - - firmware version - - available onboard profiles - - basic profile settings -- Implemented native writes for the first vertical slice: - - scroll wheel mode - - scroll acceleration - - smart reel - - polling delay - - current DPI X/Y - - DPI stages get/set, stage count, and active stage selection -- Basic tab parity improvements from the Vue app: - - editable DPI stage list - - stage count control - - active stage control - - `Y = X` helper for DPI stages -- Profile tab parity improvements from the Vue app: - - create profile - - delete profile - - direct + white/red/green/blue/cyan slot management UI - - YAML export/import for the selected profile's basic, button, and LED bundle - - backend profile info chunked read/write support (`0x0588` / `0x0508`) carried in exported/imported bundles -- Connect flow parity improvements from the Vue app: - - scan + select device - - auto-connect first supported device after scan - - connect status and error handling -- LED tab parity improvements from the Vue app: - - region selection for wheel/logo/strip - - off/static/spectrum/wave effects - - per-region speed and brightness controls - - apply-to-all regions helper -- Button tab parity improvements from the Vue app: - - per-button assignment editing for normal and hypershift layers - - categories for mouse, keyboard, macro, DPI switch, profile switch, system, consumer, hypershift toggle, scroll mode toggle, and custom payloads - - native get/set button mapping commands through the Rust backend -- Macro tab parity improvements from the Vue app: - - macro list loading - - load/edit/save/delete by macro ID - - YAML export/import for all macro operation lists - - flash reset action - - native macro op encode/decode for keyboard, system, consumer, mouse button, mouse wheel, and delay operations - - preserve loaded `macro_info_hex` when round-tripping an existing macro through the editor -- Sensor tab parity improvements from the Vue app: - - lift mode selection for smart, smart asym, config, and self calibration modes - - calibration start/stop controls - - retrieved calibration data editor - - parameter calculator for symmetric and asymmetric lift settings - - set-params write path for config A / config B blobs -- Debug tooling improvements: - - raw sent/received report logging from the Tauri hidraw backend - - log viewer and clear action in the Info tab - - warning/error entries for exclusive-access and command failures - -## Follow-Up Fixes Applied During Parity Review - -- Removed the user-facing `Use Without Mouse` entry point. -- Added auto-connect after device scan. -- Replaced the polling delay number input with the source-style fixed Hz choices. -- Made short or partially decoded button mappings fall back to `custom/raw` instead of failing the tab. -- Fixed the Sensor tab reactive loop that could freeze the app. -- Restored the stylesheet after the broken import split and re-split it into trunk-linked CSS files under the file-size cap. -- Fixed the button-mapping save payload shape for the Tauri `set_button_mapping` command. -- Fixed LED option/layout regressions caused by over-aggressive wrapping rules. -- Serialized "Apply to all regions" through a single backend command instead of racing multiple hidraw writes from the frontend. - -## Source Features Still To Port - -- Completion audit and parity fixes against the Vue reference still remain. - -## Notes - -This target no longer embeds Pyodide or uses WebHID. The backend is Tauri-native Rust and currently targets Linux hidraw devices. Running the real device workflow requires read/write permission for the matching `/dev/hidraw*` node, typically handled with a udev rule. - -The active bundle target is AppImage. On modern rolling Linux distributions, linuxdeploy's bundled `strip` binary can fail on `.relr.dyn` ELF sections, so build AppImage artifacts with: - -```sh -NO_STRIP=true cargo tauri build -``` diff --git a/PROMPT.md b/PROMPT.md deleted file mode 100644 index 5f07fe4..0000000 --- a/PROMPT.md +++ /dev/null @@ -1,127 +0,0 @@ -Goal: migrate the existing `razerqdhid` app into `razer-linux-desktop` as a native Linux desktop application using Tauri 2 for the shell and Leptos for the UI. - -Context: -- Source app: `./razerqdhid` -- Target app: `./razer-linux-desktop` -- Current target state: the target is still the default Tauri + Leptos template (`src/app.rs`, `src-tauri/src/lib.rs`). -- Current source stack: Vue 3 + Vite + WebHID + Pyodide/Python. -- Source device/protocol logic lives mainly under `razerqdhid/public/py/`. -- This project is Linux-only. The app configures supported Razer mice, currently centered on Basilisk V3 / V3 Pro behavior already implemented in the source project. - -Local test hardware (dev machine): -- Connected device: Razer Basilisk V3 Pro (USB) - - `lsusb`: `1532:00ab` (Razer USA, Ltd) - - USB sysfs node: `/sys/bus/usb/devices/5-2.2.1` - - Exposed HID raw nodes (same VID/PID): `/dev/hidraw3`, `/dev/hidraw5`, `/dev/hidraw6` - - `ID_USB_INTERFACE_NUM`: `00` -> `/dev/hidraw3` (mouse interface) - - `ID_USB_INTERFACE_NUM`: `01` -> `/dev/hidraw5` - - `ID_USB_INTERFACE_NUM`: `02` -> `/dev/hidraw6` - - Device serial string currently reports as `000000000000` (do not rely on it for unique identification). - -What exists in the source app: -- A connect flow in `razerqdhid/src/components/ConnectDevice.vue` -- A main device screen in `razerqdhid/src/components/DeviceMain.vue` -- Configuration sections for: - - basic settings - - button mapping - - LED - - profiles - - macros - - sensor settings - - device info -- The source app currently relies on browser APIs and Pyodide. The desktop app should replace that with native Tauri/Rust integration. - -What I want: -- Port the app into `./razer-linux-desktop`. -- Keep the migrated app grounded in the existing source behavior and feature structure instead of inventing a new product. -- Replace WebHID/Pyodide with a native backend that talks to the device on Linux. -- Reuse or carefully reimplement the existing protocol logic from the Python code in Rust where appropriate. -- Keep the app runnable after each meaningful step. Do not leave the target in a half-broken state. - -Execution requirements: -1. Inspect the source app and identify the real feature surface before changing architecture. -2. Inspect the Python protocol/device code under `razerqdhid/public/py/` and use it as the behavioral reference. -3. Build the migration incrementally, starting with a working vertical slice instead of attempting every feature at once. -4. Preserve the current single-app workflow: - - connect to device - - show main configuration UI - - expose the existing sections progressively -5. Prefer Rust/Tauri-native device access over embedding Python in the final architecture unless there is a concrete blocker. -6. Keep the UI practical and desktop-oriented. Do not ship the default template UI. -7. Do not perform unrelated refactors in the source project unless required for the migration. - -Suggested migration order: -1. Replace the default Tauri + Leptos starter UI with a real app shell. -2. Implement backend device discovery/connection for supported Razer devices on Linux. -3. Port the connect screen and basic device info flow. -4. Port the basic settings/profile selection path end to end. -5. Port the remaining sections in descending order of user value: LED, button mapping, profiles, macros, sensor, info/debug tooling. -6. Remove template/demo code once replaced. - -Definition of done: -- `./razer-linux-desktop` is the active app, not a starter template. -- The app can launch locally as a Tauri desktop app. -- At least one real end-to-end device workflow is implemented natively in the target app. -- The migration status is clear: completed pieces, remaining gaps, and next highest-value steps. -- Any missing source features are called out explicitly instead of being silently dropped. - -Faithful Port Checklist (source parity targets): -- App workflow parity: - - Connect screen: scan/select device, connect, error handling, optional "no hardware" mode for UI-only exploration. - - Main screen: profile selector + tabs matching the Vue app: Basic, Button, LED, Profile, Macro, Sensor, Info, plus log/debug tooling. -- Backend protocol parity (map to Python `razerqdhid/public/py/qdrazer/device.py` + `qdrazer/protocol.py`): - - Device info: - - manufacturer/product strings (USB indexed strings) - - serial number (`0x0082`) - - firmware version (`0x0081`) - - device mode get/set (`0x0084` / `0x0004`) as required for sensor calibration flows - - Basic settings (already partially ported): - - scroll mode get/set (`0x0294` / `0x0214`) - - scroll acceleration get/set (`0x0296` / `0x0216`) - - smart reel get/set (`0x0297` / `0x0217`) - - polling delay get/set (`0x008e` / `0x000e`) - - current DPI X/Y get/set (`0x0485` / `0x0405`) - - DPI stages get/set (`0x0486` / `0x0406`) and active stage selection - - Profile management: - - available profile count/list (`0x0580` / `0x0581`) - - create/delete profile (`0x0502` / `0x0503`) - - profile info read/write chunking (`0x0588` / `0x0508`) for YAML import/export parity (source exports/imports Basic/Button/LED configs) - - flash usage/readiness helpers (`0x068e`, `0x0086`) as needed for safe writes - - LED: - - per-region effect get/set (`0x0f82` / `0x0f02`) including mode/speed/colors - - per-region brightness get/set (`0x0f84` / `0x0f04`) - - optional static LED write path (`0x0f03`) if needed for full parity - - Button mapping + Hypershift: - - button function get/set (`0x028c` / `0x020c`) - - full `ButtonFunction` encoding/decoding parity with `qdrazer.protocol.ButtonFunction` - - categories used by UI: disabled, mouse, keyboard, macro, dpi_switch, profile_switch, system, consumer, hypershift_toggle, scroll_mode_toggle, plus "custom/raw" - - support both Hypershift OFF and ON mappings - - Macros: - - macro list/count (`get_macro_list`/`get_macro_count` paths in Python) and any required query commands - - macro info chunked get/set (`0x068c` / `0x060c`) - - macro size get/set (`0x0688` / `0x0608`) - - macro function chunked get/set (`0x0689` / `0x0609`) - - delete macro (`0x0603`) - - flash reset tooling (`0x060a` + poll `0x068a`) for the source "reset flash" workflow - - YAML import/export for macro functions (source uses YAML to represent macro op lists) - - Sensor / lift-off calibration: - - sensor lift config get/set (`0x0b8b` / `0x0b0b`) - - lift config blobs get/set (`0x0b85` / `0x0b05`, `0x0b8c` / `0x0b0c`, `0x0b8d` / `0x0b0d`) - - sensor state + calibration toggles (`0x0b83` / `0x0b03`, `0x0b09`) - - device mode transitions needed by the calibration workflow (`DeviceMode.DRIVER`/`NORMAL`) - - Logs/debug: - - source-equivalent log console: show sent/received report frames, status, and raw bytes - - safe exclusive-access messaging when device replies with "different command" (indicates competing software) -- UI parity notes from Vue components: - - Basic: includes DPI stages editing (count 1..5), active stage, and a "Y = X" helper. - - LED: regions wheel/logo/strip; effects off/static/spectrum/wave variants; per-region speed + brightness; apply-to-all. - - Button: per-button assignments + per-button Hypershift assignments; category editor; custom/raw inspector. - - Profile: create/delete; YAML export/import for selected profile configs (basic/button/led only in source). - - Macro: list macros; load/edit/save/delete; YAML export/import all. - - Sensor: lift config modes; calibration start/stop; parameter calculator + set params. - -While working: -- Make concrete code changes, not just a plan. -- Explain architecture decisions briefly when they matter. -- Validate builds/tests when possible. -- If the full migration is too large for one pass, complete the highest-value vertical slice and leave the repo in a clean, runnable state with clear next steps. diff --git a/README.md b/README.md index a012086..371de58 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Razer Linux Desktop -A Tauri + Leptos desktop app for configuring supported Razer devices on Linux. +A Tauri + Leptos desktop app for configuring supported Razer devices on Linux. + +Supported are: +- Razer Basilisk V3 +- Razer Basilisk V3 Pro + +This desktop app is a port of the good work from [geezmolycos](https://github.com/geezmolycos/razerqdhid). The port and maintenance of this app is entirely done with Codex GPT 5.5 xhigh. ## CachyOS / Arch Setup diff --git a/index.html b/index.html index 95933a7..60d2abb 100644 --- a/index.html +++ b/index.html @@ -1,14 +1,14 @@ - - - - Razer Basilisk V3 Onboard Memory Tools - - - - - - - + + + + Razer Basilisk V3 + + + + + + + diff --git a/src/app/button_panel.rs b/src/app/button_panel.rs index f9d123a..dc4eb77 100644 --- a/src/app/button_panel.rs +++ b/src/app/button_panel.rs @@ -1,3 +1,19 @@ +const MOUSE_BUTTONS: [&str; 13] = [ + "left", + "right", + "middle", + "wheel_left", + "wheel_right", + "wheel_up", + "wheel_down", + "middle_forward", + "middle_backward", + "forward", + "backward", + "aim", + "bottom", +]; + #[component] fn ButtonPanel( snapshot: DeviceState, @@ -5,21 +21,6 @@ fn ButtonPanel( set_status: WriteSignal, set_busy: WriteSignal, ) -> impl IntoView { - const BUTTONS: [&str; 13] = [ - "aim", - "left", - "middle", - "right", - "forward", - "wheel_up", - "middle_forward", - "wheel_left", - "backward", - "wheel_down", - "middle_backward", - "wheel_right", - "bottom", - ]; const CATEGORIES: [&str; 11] = [ "disabled", "mouse", @@ -93,7 +94,7 @@ fn ButtonPanel( } spawn_local(async move { let mut categories = std::collections::BTreeMap::new(); - for button in BUTTONS { + for button in MOUSE_BUTTONS { if let Ok(mapping) = invoke::( "get_button_mapping", &ButtonMappingQueryArgs { @@ -158,39 +159,70 @@ fn ButtonPanel(

"Button Mapping"

-
- {BUTTONS.into_iter().map(|button| { - let button_name = button.to_string(); - let button_active = button_name.clone(); - let button_click = button_name.clone(); - view! { - - } - }).collect_view()} -
- -

- "Each button can be assigned a function when Hypershift is off, and another function when Hypershift is on." -

-

- "Assign a button to hypershift_toggle to let it switch Hypershift status." -

+
+
+
+ "Selected" + {move || button_label(&selected_button.get()).to_string()} + + {move || { + let button = selected_button.get(); + button_categories + .get() + .get(&button) + .map(|category| button_category_label(category)) + .unwrap_or_else(|| "Loading".to_string()) + }} + +
+ + + +
+ {MOUSE_BUTTONS.into_iter().map(|button| { + let button_name = button.to_string(); + let button_active = button_name.clone(); + let button_click = button_name.clone(); + let button_category = button_name.clone(); + let label = button_label(button).to_string(); + let category_fallback = button_category.clone(); + view! { + + } + }).collect_view()} +
+
+
{move || { let Some(mapping) = mapping_state.get() else { @@ -223,7 +255,7 @@ fn ButtonPanel(

{format!( "Editing {} on profile {}{}.", - mapping.button, + button_label(&mapping.button), mapping.profile, if mapping.hypershift { " with hypershift enabled" } else { "" } )} @@ -238,7 +270,7 @@ fn ButtonPanel( class:active=move || mapping_state.get().map(|item| item.category == category_name).unwrap_or(false) on:click=move |_| reset_category(category) > - {category} + {button_category_label(category)} } }).collect_view()} diff --git a/src/app/helpers/data.rs b/src/app/helpers/data.rs index 613937e..9a459e7 100644 --- a/src/app/helpers/data.rs +++ b/src/app/helpers/data.rs @@ -1,22 +1,40 @@ fn button_label(button: &str) -> &'static str { match button { - "aim" => "Aim", + "aim" => "Aim Trigger", "left" => "Left", "middle" => "Middle", "right" => "Right", "forward" => "Forward", "wheel_up" => "Wheel Up", - "middle_forward" => "Tilt Forward", + "middle_forward" => "Scroll Mode", "wheel_left" => "Wheel Left", "backward" => "Backward", "wheel_down" => "Wheel Down", - "middle_backward" => "Tilt Back", + "middle_backward" => "DPI Cycle", "wheel_right" => "Wheel Right", - "bottom" => "Bottom", + "bottom" => "Profile Button", _ => "Button", } } +fn button_category_label(category: &str) -> String { + match category { + "disabled" => "Disabled", + "mouse" => "Mouse", + "keyboard" => "Keyboard", + "macro" => "Macro", + "dpi_switch" => "DPI Switch", + "profile_switch" => "Profile Switch", + "system" => "System", + "consumer" => "Consumer", + "hypershift_toggle" => "Hypershift", + "scroll_mode_toggle" => "Scroll Mode", + "custom" => "Custom", + _ => category, + } + .to_string() +} + fn default_payload_for_category(category: &str) -> Value { match category { "disabled" => json!({}), @@ -190,4 +208,3 @@ fn calculate_sensor_params( Some((param_a, Vec::new())) } } - diff --git a/styles/panels.css b/styles/panels.css index 3bee979..a45f1b1 100644 --- a/styles/panels.css +++ b/styles/panels.css @@ -214,59 +214,120 @@ font-size: 0.78rem; } -.profile-slot-fixed { - visibility: hidden; -} - -.button-mapper-grid, -.category-grid, -.modifier-grid { - display: grid; - gap: 10px; -} - -.button-mapper-grid { - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - margin-bottom: 16px; -} - -.button-tile, -.category-grid button { - min-height: 40px; - border-radius: 6px; - cursor: pointer; -} - -.button-tile { - display: grid; - gap: 4px; - align-content: start; - padding: 12px; - border: 1px solid var(--color-border-subtle); - text-align: left; - background: var(--color-panel-subtle); -} - -.button-tile span { - color: var(--color-small-text); - font-size: 0.78rem; -} - -.button-tile strong, -.button-tile span, .profile-admin-list strong, .led-region-tabs button, .flash-summary span { overflow-wrap: anywhere; } -.button-tile.active { +.profile-slot-fixed { + visibility: hidden; +} + +.category-grid, +.modifier-grid { + display: grid; + gap: 10px; +} + +.mouse-assignment-layout { + display: grid; + grid-template-columns: minmax(260px, 420px); + gap: 12px; + margin-bottom: 18px; +} + +.mouse-button-picker { + display: grid; + gap: 12px; + align-content: start; +} + +.mouse-selection-summary { + display: grid; + gap: 2px; + border-bottom: 1px solid var(--color-border-subtle); + padding-bottom: 12px; +} + +.mouse-selection-summary span, +.mouse-button-row strong { + color: var(--color-small-text); + font-size: 0.75rem; + font-weight: 800; +} + +.mouse-selection-summary strong { + min-width: 0; + overflow-wrap: anywhere; + font-size: 1.25rem; + line-height: 1.2; +} + +.mouse-selection-summary em { + color: var(--color-metric); + font-style: normal; + font-weight: 800; +} + +.mouse-button-list { + display: grid; + gap: 6px; +} + +.mouse-button-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(76px, max-content); + gap: 10px; + align-items: center; + min-height: 34px; + border: 1px solid transparent; + border-radius: 6px; + padding: 6px 8px; + color: var(--color-secondary-text); + background: transparent; + cursor: pointer; + text-align: left; +} + +.mouse-button-row:hover, +.mouse-button-row:focus-visible { + border-color: var(--color-active); + background: var(--color-panel-subtle); +} + +.mouse-button-row.active { border-color: var(--color-active); background: var(--color-active-surface); } +.mouse-button-row span, +.mouse-button-row strong { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mouse-button-row span { + font-weight: 700; +} + +.mouse-button-row strong { + justify-self: end; + max-width: 132px; +} + .button-hypershift-toggle { - margin-bottom: 18px; + margin: 0; + border-bottom: 1px solid var(--color-border-subtle); + padding-bottom: 12px; +} + +.category-grid button { + min-height: 40px; + border-radius: 6px; + cursor: pointer; } .category-grid { @@ -497,6 +558,12 @@ max-width: 680px; } +@media (max-width: 980px) { + .mouse-assignment-layout { + grid-template-columns: 1fr; + } +} + @media (max-width: 760px) { .app-shell, .panel-grid, @@ -514,4 +581,8 @@ align-items: stretch; flex-direction: column; } + + .mouse-button-list { + grid-template-columns: 1fr; + } }