├── .gitignore
├── gui
├── gui.rc
├── build.rs
├── gui.exe.manifest
├── Cargo.toml
└── src
│ ├── color_chooser.rs
│ └── gui.rs
├── screenshot.png
├── cli
├── Cargo.toml
└── src
│ └── cli.rs
├── lib
├── Cargo.toml
└── src
│ ├── cfg.rs
│ ├── error.rs
│ ├── device.rs
│ └── lib.rs
├── Cargo.toml
├── README.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | /Cargo.lock
2 | /target
3 |
--------------------------------------------------------------------------------
/gui/gui.rc:
--------------------------------------------------------------------------------
1 | #define RT_MANIFEST 24
2 | 1 RT_MANIFEST "gui.exe.manifest"
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gpoulios/deathadderv2/HEAD/screenshot.png
--------------------------------------------------------------------------------
/gui/build.rs:
--------------------------------------------------------------------------------
1 | use embed_resource;
2 | fn main() {
3 | embed_resource::compile("gui.rc", embed_resource::NONE);
4 | }
--------------------------------------------------------------------------------
/cli/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "deathadder-rgb-cli"
3 | edition = { workspace = true }
4 | version = { workspace = true }
5 | authors = { workspace = true }
6 | description = { workspace = true }
7 | repository = { workspace = true }
8 | license = { workspace = true }
9 |
10 | [[bin]]
11 | name = "deathadder-rgb-cli"
12 | path = "src/cli.rs"
13 |
14 | [dependencies]
15 | librazer = { path = "../lib" }
16 | rgb = { workspace = true }
--------------------------------------------------------------------------------
/lib/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "librazer"
3 | description = "A partial port of openrazer's driver for DeathAdder v2"
4 | edition = { workspace = true }
5 | version = { workspace = true }
6 | authors = { workspace = true }
7 | repository = { workspace = true }
8 |
9 | [dependencies]
10 | rusb = { workspace = true }
11 | serde = { version = "1.0.152", features = ["derive"] }
12 | rgb = { workspace = true, features = ["serde"] }
13 | confy = "0.5.1"
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["lib", "cli", "gui"]
3 | default-members = ["gui"]
4 |
5 | [workspace.package]
6 | edition = "2021"
7 | version = "0.3.2"
8 | authors = ["George Poulios"]
9 | description = "A utility to control Razer DeathAdder v2 on Windows"
10 | readme = "./README.md"
11 | repository = "https://github.com/gpoulios/deathadderv2"
12 | license = "GPLv3"
13 |
14 | [workspace.dependencies]
15 | rgb = { version = "0.8.36" }
16 | rusb = { version = "0.9" }
17 |
--------------------------------------------------------------------------------
/gui/gui.exe.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/gui/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "deathadder-rgb-gui"
3 | edition = { workspace = true }
4 | version = { workspace = true }
5 | authors = { workspace = true }
6 | description = { workspace = true }
7 | repository = { workspace = true }
8 | license = { workspace = true }
9 |
10 | [[bin]]
11 | name = "deathadder-rgb-gui"
12 | path = "src/gui.rs"
13 |
14 | [dependencies]
15 | librazer = { path = "../lib" }
16 | rgb = { workspace = true }
17 | native-windows-gui = "1.0.13"
18 | native-windows-derive = "1.0.5"
19 | rusb = { workspace = true }
20 | hidapi-rusb = "1.3.2"
21 |
22 | [dependencies.windows]
23 | version = "0.46.0"
24 | features = [
25 | "Win32_Foundation",
26 | "Win32_UI_Controls_Dialogs",
27 | "Win32_UI_WindowsAndMessaging",
28 | "Win32_System_Diagnostics_Debug"
29 | ]
30 |
31 | [build-dependencies]
32 | embed-resource = "2.0.0"
--------------------------------------------------------------------------------
/lib/src/cfg.rs:
--------------------------------------------------------------------------------
1 | use std::default::Default;
2 | use serde::{Serialize, Deserialize};
3 | use confy::ConfyError;
4 | use rgb::RGB8;
5 |
6 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
7 | pub struct Config {
8 | pub same_color: bool,
9 | pub same_brightness: bool,
10 | pub logo_color: RGB8,
11 | pub scroll_color: RGB8,
12 | }
13 |
14 | impl Config {
15 | pub fn save(&self) -> Result<(), ConfyError> {
16 | confy::store("deathadder_v2", None, self)
17 | }
18 |
19 | pub fn load() -> Option {
20 | match confy::load("deathadder_v2", None) {
21 | Ok(cfg) => Some(cfg),
22 | Err(_) => None
23 | }
24 | }
25 | }
26 |
27 | impl Default for Config {
28 | fn default() -> Self {
29 | Self {
30 | same_color: true,
31 | same_brightness: true,
32 | logo_color: RGB8::new(0xAA, 0xAA, 0xAA),
33 | scroll_color: RGB8::new(0xAA, 0xAA, 0xAA),
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/cli/src/cli.rs:
--------------------------------------------------------------------------------
1 | use rgb::RGB8;
2 | use librazer::cfg::Config;
3 | use librazer::common::rgb_from_hex;
4 | use librazer::device::{DeathAdderV2, RazerMouse};
5 |
6 | fn main() {
7 | let args: Vec = std::env::args().collect();
8 |
9 | let parse_arg = |input: &str| -> RGB8 {
10 | match rgb_from_hex(input) {
11 | Ok(rgb) => rgb,
12 | Err(e) => panic!("argument '{}' should be in the \
13 | form [0x/#]RGB[h] or [0x/#]RRGGBB[h] where R, G, and B are hex \
14 | digits: {}", input, e)
15 | }
16 | };
17 |
18 | let cfgopt = Config::load();
19 |
20 | let (logo_color, scroll_color) = match args.len() {
21 | ..=1 => {
22 | match cfgopt {
23 | Some(cfg) => (cfg.logo_color, cfg.scroll_color),
24 | None => panic!("failed to load configuration; please specify \
25 | arguments manually")
26 | }
27 | },
28 | 2..=3 => {
29 | let color = parse_arg(args[1].as_ref());
30 | (color, if args.len() == 3 {
31 | parse_arg(args[2].as_ref())
32 | } else {
33 | color
34 | })
35 | },
36 | _ => panic!("usage: {} [(body) color] [wheel color]", args[0])
37 | };
38 |
39 | let dav2 = DeathAdderV2::new().expect("failed to open device");
40 |
41 | _= dav2.set_logo_color(logo_color)
42 | .map_err(|e| panic!("failed to set logo color: {}", e))
43 | .and_then(|_| dav2.set_scroll_color(scroll_color))
44 | .map_err(|e| panic!("failed to set scroll color: {}", e));
45 |
46 | _ = Config {
47 | logo_color: logo_color,
48 | scroll_color: scroll_color,
49 | ..cfgopt.unwrap_or(Default::default())
50 | }.save().map_err(|e| panic!("failed to save config: {}", e));
51 | }
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # deathadderv2
2 |
3 | A tool to configure the Razer DeathAdder v2 and save the settings in the on-board memory.
4 |
5 | I just wanted static color without having to run in the background 2-3 apps and 6 services that come with Synapse. Although the device supports it, for some reason Razer's driver does not save the color in the on-board memory. As a result, you need to keep running Synapse and co. or the mouse goes back to those wave effects that I don't like as they keep catching my eye when typing or reading.
6 |
7 | Device protocol has been largely ported from [openrazer](https://github.com/openrazer/openrazer) (except for DPI stages which I didn't find in openrazer). GUI mostly built using [native-windows-gui](https://github.com/gabdube/native-windows-gui).
8 |
9 | So far, it supports the following (all saved on the device, including the color):
10 |
11 | - DPI and DPI stages
12 | - Polling rate
13 | - Static logo and scroll wheel color
14 | - Logo and scroll wheel brightness
15 |
16 | It doesn't support:
17 |
18 | - Wave/breath/spectrum effects
19 | - Profiles
20 | - I believe they're emulated by Synapse and not really supported by the hardware, otherwise I'd be glad to implement them
21 |
22 | - Other devices
23 |
24 | ## Requirements
25 |
26 | This is not supposed to be for Linux hosts. If you are on Linux, see [openrazer](https://github.com/openrazer/openrazer), it's a great project, and supports many more features, as well as almost all devices.
27 |
28 | For Windows users, the only requirement is to be using the [libusb driver](https://github.com/libusb/libusb/wiki/Windows) (either WinUSB or libusb-win32). One way to install it is using [Zadig](https://zadig.akeo.ie/). You only need to do this once. Change the entry "Razer DeathAdder V2 (Interface 3)" by using the spinner to select either "WinUSB (vXXX)" (recommended) or "libusb-win32 (vX.Y.Z)" and hit "Replace driver". In my case (Win11) it seemed to time out while creating a restore point but it actually installed it.
29 |
30 | ## Usage
31 |
32 | The UI should be self-explanatory. No need to keep it running in the background.
33 |
34 | 
35 |
36 | Contrary to all other settings, I have not found a way to retrieve the current color from the device so the app will save the last applied color to a file under %APPDATA%/deathadder/config/default-config.toml, just so it doesn't reset every time it opens.
37 |
38 | ---
39 | This project is licensed under the GPL.
--------------------------------------------------------------------------------
/lib/src/error.rs:
--------------------------------------------------------------------------------
1 | use std::{num::ParseIntError, fmt, result, error};
2 |
3 | #[derive(Debug)]
4 | pub enum ParseRGBError {
5 | WrongLength(usize),
6 | ParseHex(ParseIntError),
7 | }
8 |
9 | impl fmt::Display for ParseRGBError {
10 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
11 | match *self {
12 | ParseRGBError::WrongLength(len) =>
13 | write!(f, "excluding pre/suffixes, \
14 | string can only be of length 3 or 6 ({} given)", len),
15 | ParseRGBError::ParseHex(ref pie) =>
16 | write!(f, "{}", pie),
17 | }
18 | }
19 | }
20 |
21 | impl error::Error for ParseRGBError {
22 | fn source(&self) -> Option<&(dyn error::Error + 'static)> {
23 | match *self {
24 | ParseRGBError::WrongLength(_) => None,
25 | ParseRGBError::ParseHex(ref pie) => Some(pie),
26 | }
27 | }
28 | }
29 |
30 | impl From for ParseRGBError {
31 | fn from(err: ParseIntError) -> ParseRGBError {
32 | ParseRGBError::ParseHex(err)
33 | }
34 | }
35 |
36 | /// A result of a function that may return a `Error`.
37 | pub type USBResult = result::Result;
38 |
39 | #[derive(Debug)]
40 | pub enum USBError {
41 | NonCompatibleDevice,
42 | DeviceNotFound,
43 | /// (total, written) An incomplete write
44 | IncompleteWrite(usize, usize),
45 | /// (total, read) An incomplete read
46 | IncompleteRead(usize, usize),
47 | ResponseMismatch,
48 | DeviceBusy,
49 | CommandFailed,
50 | CommandNotSupported,
51 | CommandTimeout,
52 | ResponseUnknownStatus(u8),
53 | ResponseUnknownValue(u8),
54 | /// Wrapper for rusb::Error
55 | RUSBError(rusb::Error),
56 | }
57 |
58 | impl fmt::Display for USBError {
59 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60 | match *self {
61 | USBError::NonCompatibleDevice => write!(f, "device is incompatible"),
62 | USBError::DeviceNotFound => write!(f, "device not found"),
63 | USBError::IncompleteWrite(total, written) =>
64 | write!(f, "failed to write full control message \
65 | (written {} out of {} bytes)", written, total),
66 | USBError::IncompleteRead(total, read) =>
67 | write!(f, "failed to read full control message \
68 | (read {} out of {} bytes)", read, total),
69 | USBError::ResponseMismatch => write!(f, "wrong response type"),
70 | USBError::DeviceBusy => write!(f, "device is busy"),
71 | USBError::CommandFailed => write!(f, "command failed"),
72 | USBError::CommandNotSupported => write!(f, "command not supported"),
73 | USBError::CommandTimeout => write!(f, "command timed out"),
74 | USBError::ResponseUnknownStatus(status) =>
75 | write!(f, "unrecognized status in response: {:#02X}", status),
76 | USBError::ResponseUnknownValue(value) =>
77 | write!(f, "unrecognized value in response: {:#02X}", value),
78 | USBError::RUSBError(ref e) => write!(f, "{}", e),
79 | }
80 | }
81 | }
82 |
83 | impl error::Error for USBError {
84 | fn source(&self) -> Option<&(dyn error::Error + 'static)> {
85 | match *self {
86 | USBError::RUSBError(ref e) => Some(e),
87 | _ => None
88 | }
89 | }
90 | }
91 |
92 | impl From for USBError {
93 | fn from(err: rusb::Error) -> USBError {
94 | USBError::RUSBError(err)
95 | }
96 | }
--------------------------------------------------------------------------------
/gui/src/color_chooser.rs:
--------------------------------------------------------------------------------
1 | use std::{mem::{size_of, MaybeUninit}, thread::{self, JoinHandle}, ffi::CStr};
2 | use windows::{
3 | core::{PCSTR},
4 | Win32::{
5 | Foundation::{HWND, WPARAM, LRESULT, LPARAM, RECT, COLORREF},
6 | UI::{
7 | WindowsAndMessaging::{
8 | WM_INITDIALOG, WM_COMMAND, WM_PAINT, EN_UPDATE, GetWindowTextA,
9 | SWP_NOSIZE, SWP_NOZORDER, GetWindowRect, GetDesktopWindow,
10 | GetClientRect, SetWindowPos,
11 | SetWindowLongPtrA, GetWindowLongPtrA, DWLP_MSGRESULT, WINDOW_LONG_PTR_INDEX,
12 | },
13 | Controls::Dialogs::*
14 | },
15 | },
16 | };
17 | use rgb::RGB8;
18 |
19 | /*
20 | * trying hard to follow:
21 | * https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongptra
22 | * We're gonna need DWLP_USER to set a pointer to our Color Dialog struct
23 | * on the winapi Dialog window (GWLP_USERDATA only works for window class)
24 | */
25 | const DWLP_DLGPROC: u32 = DWLP_MSGRESULT + size_of::() as u32;
26 | const DWLP_USER: WINDOW_LONG_PTR_INDEX = WINDOW_LONG_PTR_INDEX(
27 | (DWLP_DLGPROC + size_of::() as u32) as i32);
28 |
29 | /*
30 | * std::ffi::CStr::from_bytes_until_nul() is atm nightly experimental API so
31 | * we need this to convert a byte array with one or more null terminators in it
32 | */
33 | unsafe fn u8sz_to_u8(s: &[u8]) -> u8 {
34 | let str = CStr::from_ptr(s.as_ptr() as *const _).to_str().unwrap();
35 | str.parse::().unwrap()
36 | }
37 |
38 | pub type ColorChangeCallback<'a> = dyn Fn(&ColorDialog, &RGB8) + Send + Sync + 'a;
39 |
40 | pub struct ColorDialog<'a> {
41 | current: [u8; 3],
42 | last_notified: RGB8,
43 | change_cb: Option>>,
44 | }
45 |
46 | impl Default for ColorDialog<'_> {
47 | fn default() -> Self {
48 | unsafe {
49 | // safe as 0 a valid bit-pattern for all fields
50 | MaybeUninit::::zeroed().assume_init()
51 | }
52 | }
53 | }
54 |
55 | impl<'a> ColorDialog<'a> {
56 | pub fn new() -> Self {
57 | Self { ..Default::default() }
58 | }
59 |
60 | pub fn show_async(
61 | &'static mut self,
62 | parent: HWND,
63 | initial: Option,
64 | change_cb: Option
65 | ) -> JoinHandle