diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5143d11..8c8b139 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,10 @@ jobs: - name: Install target for Windows run: rustup target add x86_64-pc-windows-gnu + - name: Run fmt + run: cargo fmt --all --check - name: Run clippy - run: cargo clippy --all-targets --all-features + run: cargo clippy --all-targets --all-features -- -Dwarnings - name: Build for Windows run: cargo build --verbose --release --target x86_64-pc-windows-gnu - name: Build for Linux diff --git a/Cargo.lock b/Cargo.lock index 645915b..1fa378e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,6 +357,32 @@ dependencies = [ "toml", ] +[[package]] +name = "const-str" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" + +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1220,6 +1246,8 @@ dependencies = [ "anyhow", "bincode", "clap", + "const-str", + "const_format", "hidapi", "rand", "serde", @@ -1227,6 +1255,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", + "winreg", ] [[package]] @@ -2217,6 +2246,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unsafe-any-ors" version = "1.0.0" @@ -2729,6 +2764,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "x11" version = "2.21.0" diff --git a/librazer/Cargo.toml b/librazer/Cargo.toml index ce174b7..38eada4 100644 --- a/librazer/Cargo.toml +++ b/librazer/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" anyhow = "1.0.80" bincode = "1.3.3" clap = { version = "4.5.1", features = ["derive"] } +const-str = "0.5.7" +const_format = "0.2.32" hidapi = {version = "2.6.1", features = ["windows-native"]} rand = "0.8.5" serde = { version = "1.0.197", features = ["derive"] } @@ -14,3 +16,6 @@ serde-big-array = "0.5.1" serde_json = "1.0.114" strum = "0.26.1" strum_macros = "0.26.1" + +[target.'cfg(windows)'.dependencies] +winreg = { version = "0.52", features = ["transactions"] } diff --git a/librazer/src/descriptor.rs b/librazer/src/descriptor.rs new file mode 100644 index 0000000..a38b6da --- /dev/null +++ b/librazer/src/descriptor.rs @@ -0,0 +1,44 @@ +use crate::feature; + +// model_number_prefix shall conform to https://mysupport.razer.com/app/answers/detail/a_id/5481 +#[derive(Debug, Clone)] +pub struct Descriptor { + pub model_number_prefix: &'static str, + pub name: &'static str, + pub pid: u16, + pub features: &'static [&'static str], +} + +pub const SUPPORTED: &[Descriptor] = &[ + Descriptor { + model_number_prefix: "RZ09-0483T", + name: "Razer Blade 16” (2023) Black", + pid: 0x029f, + features: &[ + "battery-care", + "fan", + "kbd-backlight", + "lid-logo", + "lights-always-on", + "perf", + ], + }, + Descriptor { + model_number_prefix: "RZ09-0482X", + name: "Razer Blade 14” (2023) Mercury", + pid: 0x029d, + features: &[ + "battery-care", + "fan", + "kbd-backlight", + "lights-always-on", + "perf", + ], + }, +]; + +const _VALIDATE_FEATURES: () = { + crate::const_for! { device in SUPPORTED => { + feature::validate_features(device.features); + }} +}; diff --git a/librazer/src/device.rs b/librazer/src/device.rs index 6812179..995b7a5 100644 --- a/librazer/src/device.rs +++ b/librazer/src/device.rs @@ -1,64 +1,51 @@ +use crate::descriptor::{Descriptor, SUPPORTED}; use crate::packet::Packet; use anyhow::{anyhow, Context, Result}; use std::{thread, time}; -pub struct DeviceInfo { - pub name: &'static str, - pub pid: u16, - pub path: Option, -} - -pub const SUPPORTED: &[DeviceInfo] = &[ - DeviceInfo { - name: "Razer Blade 16 2023", - pid: 0x029f, - path: None, - }, - DeviceInfo { - name: "Razer Blade 14 2023", - pid: 0x029d, - path: None, - }, -]; - pub struct Device { device: hidapi::HidDevice, - info: DeviceInfo, + pub info: Descriptor, +} + +// Read the model id and clip to conform with https://mysupport.razer.com/app/answers/detail/a_id/5481 +fn read_device_model() -> Result { + #[cfg(target_os = "windows")] + { + let hklm = winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE); + let bios = hklm.open_subkey("HARDWARE\\DESCRIPTION\\System\\BIOS")?; + let system_sku: String = bios.get_value("SystemSKU")?; + Ok(system_sku.chars().take(10).collect()) + } + #[cfg(not(target_os = "windows"))] + anyhow::bail!("Automatic model detection is not implemented for this platform") } impl Device { const RAZER_VID: u16 = 0x1532; - pub fn info(&self) -> &DeviceInfo { + pub fn info(&self) -> &Descriptor { &self.info } - pub fn new(pid: u16, name: &'static str) -> Result { + pub fn new(descriptor: Descriptor) -> Result { let api = hidapi::HidApi::new().context("Failed to create hid api")?; // there are multiple devices with the same pid, pick first that support feature report - for info in api - .device_list() - .filter(|info| (info.vendor_id(), info.product_id()) == (Device::RAZER_VID, pid)) - { + for info in api.device_list().filter(|info| { + (info.vendor_id(), info.product_id()) == (Device::RAZER_VID, descriptor.pid) + }) { let path = info.path(); let device = api.open_path(path)?; if device.send_feature_report(&[0, 0]).is_ok() { return Ok(Device { device, - info: DeviceInfo { - name, - pid, - path: Some(path.to_str().unwrap().to_string()), - }, + info: descriptor.clone(), }); } } - anyhow::bail!( - "No device with pid 0x{:04x} and feature report support found", - pid - ) + anyhow::bail!("Failed to open device {:?}", descriptor) } pub fn send(&self, report: Packet) -> Result { @@ -89,27 +76,39 @@ impl Device { response.ensure_matches_report(&report) } - pub fn enumerate() -> Result> { - let api = hidapi::HidApi::new().context("Failed to create hid api")?; - Ok(api + pub fn enumerate() -> Result<(Vec, String)> { + let razer_pid_list: Vec<_> = hidapi::HidApi::new()? .device_list() .filter(|info| info.vendor_id() == Device::RAZER_VID) - .map(|info| DeviceInfo { - name: "", - pid: info.product_id(), - path: Some(info.path().to_str().unwrap().to_string()), - }) - .collect()) + .map(|info| info.product_id()) + .collect::>() + .into_iter() + .collect(); + + if razer_pid_list.is_empty() { + anyhow::bail!("No Razer devices found") + } + + match read_device_model() { + Ok(model) if model.starts_with("RZ09-") => Ok((razer_pid_list, model)), + Ok(model) => anyhow::bail!("Detected model but it's not a Razer laptop: {}", model), + Err(e) => anyhow::bail!("Failed to detect model: {}", e), + } } pub fn detect() -> Result { - for discovered in Device::enumerate()? { - for supported in SUPPORTED { - if supported.pid == discovered.pid { - return Device::new(supported.pid, supported.name); - } - } + let (pid_list, model_number_prefix) = Device::enumerate()?; + + match SUPPORTED + .iter() + .find(|supported| model_number_prefix == supported.model_number_prefix) + { + Some(supported) => Device::new(supported.clone()), + None => anyhow::bail!( + "Model {} with PIDs {:0>4x?} is not supported", + model_number_prefix, + pid_list + ), } - anyhow::bail!("Device is not supported") } } diff --git a/librazer/src/feature.rs b/librazer/src/feature.rs new file mode 100644 index 0000000..cf68da8 --- /dev/null +++ b/librazer/src/feature.rs @@ -0,0 +1,73 @@ +use const_format::{map_ascii_case, Case}; + +pub trait Feature { + fn name(&self) -> &'static str; +} + +macro_rules! feature_list { + ($($type:ident,)*) => { + $( + #[derive(Default)] + pub struct $type {} + + impl Feature for $type { + fn name(&self) -> &'static str { + map_ascii_case!(Case::Kebab, stringify!($type)) + } + } + )* + + pub const ALL_FEATURES: &[&'static str] = &[ + $(map_ascii_case!(Case::Kebab, stringify!($type)),)* + ]; + + #[macro_export] + macro_rules! iter_features { + ($apply:expr) => { + { + let mut v = Vec::new(); + $( + let entry = $type::default(); + v.push($apply(entry.name(), entry)); + )* + v + } + } + } + } +} + +#[macro_export] +macro_rules! const_for { + ($var:ident in $iter:expr => $block:block) => { + let mut iter = $iter; + while let [$var, tail @ ..] = iter { + iter = tail; + $block + } + }; +} + +const fn contains(array: &[&str], value: &str) -> bool { + const_for! { it in array => { + if const_str::equal!(*it, value) { + return true; + } + }} + false +} + +pub const fn validate_features(features: &[&str]) { + const_for! { f in features => { + assert!(contains(ALL_FEATURES, f), "Feature is not in supported list"); + }} +} + +feature_list![ + BatteryCare, + LidLogo, + LightsAlwaysOn, + KbdBacklight, + Fan, + Perf, +]; diff --git a/librazer/src/lib.rs b/librazer/src/lib.rs index 3659b2a..d6a9c97 100644 --- a/librazer/src/lib.rs +++ b/librazer/src/lib.rs @@ -1,5 +1,7 @@ pub mod command; pub mod device; +pub mod feature; pub mod types; +pub mod descriptor; mod packet; diff --git a/razer-cli/Cargo.toml b/razer-cli/Cargo.toml index a9d1eac..9ef1940 100644 --- a/razer-cli/Cargo.toml +++ b/razer-cli/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" [dependencies] librazer = { path = "../librazer" } -clap = { version = "4.5.1", features = ["derive"] } +clap = { version = "4.5.1", features = ["cargo"] } clap-num = "1.1.1" anyhow = "1.0.80" diff --git a/razer-cli/src/main.rs b/razer-cli/src/main.rs index c21d35f..c05279c 100644 --- a/razer-cli/src/main.rs +++ b/razer-cli/src/main.rs @@ -1,189 +1,309 @@ use librazer::command; use librazer::device; +use librazer::feature; use librazer::types::{ BatteryCare, CpuBoost, FanMode, FanZone, GpuBoost, LightsAlwaysOn, LogoMode, MaxFanSpeedMode, PerfMode, }; +use librazer::feature::Feature; + use anyhow::Result; -use clap::{Args, Parser, Subcommand}; -use clap_num::maybe_hex; - -pub fn get_info(device: &device::Device) -> Result { - use std::fmt::Write; - let mut info = String::new(); - - let (perf_mode, fan_mode) = command::get_perf_mode(device)?; - writeln!(&mut info, "Performance: {:?}", perf_mode)?; - - if perf_mode == PerfMode::Balanced { - match fan_mode { - FanMode::Auto => writeln!(&mut info, "Fan: {:?}", fan_mode)?, - FanMode::Manual => writeln!( - &mut info, - "Fan: {} RPM", - command::get_fan_rpm(device, FanZone::Zone1)? - )?, - } +use clap::{arg, Command}; + +trait Cli: feature::Feature { + fn cmd(&self) -> Option { + None + } + fn handle(&self, _device: &device::Device, _matches: &clap::ArgMatches) -> Result<()> { + Ok(()) } +} + +macro_rules! impl_unary_cmd_cli { + ($parser:block, $name:literal, $arg_name:literal, $desc:literal,$arg_desc:literal) => { + clap::Command::new($name) + .about($desc) + .arg(arg!(<$arg_name> $arg_desc).value_parser($parser)) + .arg_required_else_help(true) + + } +} + +macro_rules! impl_unary_handle_cli { + (<$arg_type:ty>($matches:ident, $device:ident, $name:literal, $arg_name:literal, $setter:path)) => { + match $matches.subcommand() { + Some(($name, matches)) => { + $setter($device, *matches.get_one::<$arg_type>($arg_name).unwrap())? + } + _ => (), + } + }; +} - if perf_mode == PerfMode::Custom { - let cpu_boost = command::get_cpu_boost(device)?; - let gpu_boost = command::get_gpu_boost(device)?; - writeln!(&mut info, "CPU: {:?}", cpu_boost)?; - writeln!(&mut info, "GPU: {:?}", gpu_boost)?; - - if (cpu_boost == CpuBoost::Boost || cpu_boost == CpuBoost::Overclock) - && (gpu_boost == GpuBoost::High) - { - writeln!( - &mut info, - "Max Fan Speed: {:?}", - command::get_max_fan_speed_mode(device)? - )?; +macro_rules! impl_unary_cli { + (<$feature_type:ty><$arg_type:ty>($desc:literal,$arg_desc:literal,$setter:path,$getter:path)) => { + impl Cli for $feature_type { + fn cmd(&self) -> Option { + Some( + clap::Command::new(self.name()) + .about($desc) + .arg(arg!( $arg_desc).value_parser(clap::value_parser!($arg_type))) + .arg_required_else_help(true), + ) + } + fn handle(&self, device: &device::Device, matches: &clap::ArgMatches) -> Result<()> { + match matches.subcommand() { + Some((ident, matches)) if ident == self.name() => { + let arg = matches.get_one::<$arg_type>("ARG").unwrap(); + $setter(device, *arg) + } + Some(("info", _)) => Ok(println!("{}: {:?}", self.name(), $getter(device))), + _ => Ok(()), + } + } } + } +} - writeln!(&mut info, "Logo: {:?}", command::get_logo_mode(device)?)?; - writeln!( - &mut info, - "Brightness: {}", - command::get_keyboard_brightness(device)? - )?; - writeln!( - &mut info, - "Lights always on: {:?}", - command::get_lights_always_on(device)? - )?; - write!( - &mut info, - "Battery care: {:?}", - command::get_battery_care(device)? - )?; - - Ok(info) +impl_unary_cli! {("Set keyboard backlight brightness", "Number in range [0, 255]", command::set_keyboard_brightness, command::get_keyboard_brightness)} +impl_unary_cli! {("Enable or disable battery care", "", command::set_battery_care, command::get_battery_care)} +impl_unary_cli! {("Set lid logo mode", "", command::set_logo_mode, command::get_logo_mode)} +impl_unary_cli! {("Set lights always on", "", command::set_lights_always_on, command::get_lights_always_on)} + +struct CustomCommand; + +impl Feature for CustomCommand { + fn name(&self) -> &'static str { + "cmd" + } } -#[derive(Parser)] -#[command(name = "razerctl", version, about)] -struct Razerctl { - #[command(subcommand)] - pub command: RazerCtlCommand, +impl Cli for CustomCommand { + fn cmd(&self) -> Option { + Some( + clap::Command::new(self.name()) + .about("Run custom command [WARNING: Use at your own risk]") + .arg( + arg!( "Command in hex format, e.g. 0x0d82") + .required(true) + .value_parser(clap_num::maybe_hex::), + ) + .arg( + arg!(... "Arguments to the command, e.g. 0 1 3 5") + .required(false) + .trailing_var_arg(true) + .value_parser(clap_num::maybe_hex::), + ) + .arg_required_else_help(true), + ) + } + fn handle(&self, device: &device::Device, matches: &clap::ArgMatches) -> Result<()> { + match matches.subcommand() { + Some((ident, matches)) if ident == self.name() => { + let cmd = *matches.get_one::("COMMAND").unwrap(); + let args: Vec = matches.get_many::("ARGS").unwrap().copied().collect(); + println!("Running custom command: {:x?} {:?}", cmd, args); + command::custom_command(device, cmd, &args) + } + _ => Ok(()), + } + } +} - /// PID of the Razer device to use - #[clap(short, long, value_parser=maybe_hex::)] - pub pid: Option, +impl Cli for feature::Fan { + fn cmd(&self) -> Option { + Some( + clap::Command::new(self.name()) + .about("Control fan") + .subcommand(clap::Command::new("auto").about("Set fan mode to auto")) + .subcommand(clap::Command::new("manual").about("Set fan mode to manual")) + .subcommand(impl_unary_cmd_cli!{{clap::value_parser!(u16).range(2000..=5000)}, "rpm", "RPM", "Set fan rpm", "Fan RPM in range [2000, 5000]"}) + .subcommand(impl_unary_cmd_cli!{{clap::value_parser!(MaxFanSpeedMode)}, "max", "MAX", "Control Max Fan Speed Mode", "Max Fan Speed Mode"}) + .arg_required_else_help(true), + ) + } + + fn handle(&self, device: &device::Device, matches: &clap::ArgMatches) -> Result<()> { + match matches.subcommand() { + Some((ident, matches)) if ident == self.name() => { + impl_unary_handle_cli! {(matches, device, "rpm", "RPM", command::set_fan_rpm)} + impl_unary_handle_cli! {(matches, device, "max", "MAX", command::set_max_fan_speed_mode)} + + match matches.subcommand() { + Some(("auto", _)) => command::set_fan_mode(device, FanMode::Auto), + Some(("manual", _)) => command::set_fan_mode(device, FanMode::Manual), + _ => Ok(()), + } + } + Some(("info", _)) => { + match command::get_perf_mode(device) { + Ok((_, fan_mode @ FanMode::Auto)) => println!("Fan: {:?}", fan_mode), + Ok((_, fan_mode @ FanMode::Manual)) => { + println!( + "Fan: {:?}@{:?} RPM", + fan_mode, + command::get_fan_rpm(device, FanZone::Zone1) + ) + } + Err(e) => println!("{}", e), + } + Ok(()) + } + _ => Ok(()), + } + } } -#[derive(Subcommand)] -enum RazerCtlCommand { - /// List discovered Razer devices - Enumerate, - /// Get device info - Info, - /// Control performance modes - Perf(PerfModeCommand), - /// Control fan - Fan(FanCommand), - /// Run Custom Command - Cmd { - #[clap(value_parser=maybe_hex::)] - command: u16, - #[clap(value_parser=maybe_hex::)] - args: Vec, - }, - /// Control Logo - Logo { logo_mode: LogoMode }, - /// Keyboard backlight - Backlight { brightness: u8 }, - /// Lights always on - LightOn { always_on: LightsAlwaysOn }, - /// Battery Care - BatteryCare { battery_care: BatteryCare }, +impl Cli for feature::Perf { + fn cmd(&self) -> Option { + Some( + clap::Command::new(self.name()) + .about("Control performance modes") + .subcommand(impl_unary_cmd_cli!{{clap::value_parser!(PerfMode)}, "mode", "MODE", "Set performance mode", "Performance mode"}) + .subcommand(impl_unary_cmd_cli!{{clap::value_parser!(CpuBoost)}, "cpu", "CPU", "Set CPU boost", "CPU boost"}) + .subcommand( impl_unary_cmd_cli!{{clap::value_parser!(GpuBoost)}, "gpu", "GPU", "Set GPU boost", "GPU boost"}) + .arg_required_else_help(true), + ) + } + + fn handle(&self, device: &device::Device, matches: &clap::ArgMatches) -> Result<()> { + match matches.subcommand() { + Some((ident, matches)) if ident == self.name() => { + impl_unary_handle_cli! {(matches, device, "mode", "MODE", command::set_perf_mode)} + impl_unary_handle_cli! {(matches, device, "cpu", "CPU", command::set_cpu_boost)} + impl_unary_handle_cli! {(matches, device, "gpu", "GPU", command::set_gpu_boost)} + Ok(()) + } + Some(("info", _)) => { + let perf_mode = command::get_perf_mode(device); + println!("Performance: {:?}", perf_mode); + if let Ok((PerfMode::Custom, _)) = perf_mode { + let cpu_boost = command::get_cpu_boost(device); + let gpu_boost = command::get_gpu_boost(device); + println!("CPU: {:?}", cpu_boost); + println!("GPU: {:?}", gpu_boost); + + if let (Ok(CpuBoost::Boost) | Ok(CpuBoost::Overclock), Ok(GpuBoost::High)) = + (cpu_boost, gpu_boost) + { + println!( + "Max Fan Speed: {:?}", + command::get_max_fan_speed_mode(device) + ) + } + } + Ok(()) + } + _ => Ok(()), + } + } } -#[derive(Args)] -struct PerfModeCommand { - #[command(subcommand)] - pub action: PerfModeActionCommand, +fn enumerate() -> Result<()> { + let (pid_list, model_number_prefix) = device::Device::enumerate()?; + + println!("Model: {}", model_number_prefix); + println!( + "Supported: {}", + librazer::descriptor::SUPPORTED + .iter() + .any(|supported| model_number_prefix == supported.model_number_prefix) + ); + println!("PID: {:#06x?}", pid_list); + Ok(()) } -#[derive(Subcommand)] -enum PerfModeActionCommand { - /// Set performance mode - Mode { perf_mode: PerfMode }, - /// Set CPU boost - Cpu { cpu_boost: CpuBoost }, - /// Set GPU boost - Gpu { gpu_boost: GpuBoost }, +fn update_cmd(cmd: Command, features: &[Box]) -> Command { + features + .iter() + .filter_map(|f| f.cmd()) + .fold(cmd, |cmd, f| cmd.subcommand(f)) } -#[derive(Args)] -struct FanCommand { - #[command(subcommand)] - pub subcommand: FanSubcommand, +fn handle( + device: &device::Device, + matches: &clap::ArgMatches, + features: &Vec>, +) -> Result<()> { + if let Some(("info", _)) = matches.subcommand() { + println!("Device: {:?}", device.info); + } + + for f in features { + f.handle(device, matches)?; + } + Ok(()) } -#[derive(Subcommand)] -enum FanSubcommand { - /// Set fan mode to auto - Auto, - /// Set fan mode to manual - Manual, - /// Set fan rpm - Rpm { - #[arg(value_parser = clap::value_parser!(u16).range(2000..=5000))] - rpm: u16, - }, - /// Control Max Fan Speed Mode - Max { max_fan_speed_mode: MaxFanSpeedMode }, +fn gen_cli_features(feature_list: &[&str]) -> Vec> { + use feature::*; + librazer::iter_features!(|_, feature| -> Box { Box::new(feature) }) + .into_iter() + .filter(|f| feature_list.contains(&f.name())) + .collect() } fn main() -> Result<()> { - let parser = Razerctl::parse(); - - if let RazerCtlCommand::Enumerate = parser.command { - device::Device::enumerate()?.iter().for_each(|info| { - println!( - "RazerDevice {{ pid: 0x{:04x}, path: {} }}", - info.pid, - info.path.as_ref().unwrap() + let info_cmd = clap::Command::new("info").about("Get device info"); + let auto_cmd = clap::Command::new("auto") + .about("Automatically detect supported Razer device and enable device specific features") + .subcommand(info_cmd.clone()) + .subcommand_required(true); + + let manual_cmd =clap::Command::new("manual").about("Manually specify PID of the Razer device and enable all features (many might not work)") + .arg( + arg!(-p --pid "PID of the Razer device to use") + .required(true) + .value_parser(clap_num::maybe_hex::) ) - }); - return Ok(()); - } + .arg_required_else_help(true) + .subcommand(info_cmd) + .subcommand_required(true); - let device = match parser.pid { - Some(pid) => device::Device::new(pid, ""), - _ => device::Device::detect(), - }?; + // TODO: find a better way to detect auto mode in advance + let is_auto_mode = std::env::args_os().nth(1) == Some("auto".into()); + let device = match is_auto_mode { + true => Some(device::Device::detect()?), + _ => None, + }; + let feature_list = match device { + Some(ref device) => device.info.features, + _ => feature::ALL_FEATURES, + }; - match parser.command { - RazerCtlCommand::Enumerate => { - unreachable!("Enumerate handled above") + let mut cli_features: Vec> = gen_cli_features(feature_list); + cli_features.push(Box::new(CustomCommand)); + + let cmd = clap::command!() + .color(clap::ColorChoice::Always) + .subcommand_required(true) + .subcommand(update_cmd(auto_cmd, &cli_features)) + .subcommand(update_cmd(manual_cmd, &cli_features)) + .subcommand(clap::Command::new("enumerate").about("List discovered Razer devices")); + + let matches = cmd.get_matches(); + + match matches.subcommand() { + Some(("enumerate", _)) => { + enumerate()?; } - RazerCtlCommand::Info => Ok(println!("{}", get_info(&device)?)), - RazerCtlCommand::Cmd { command, args } => command::custom_command(&device, command, &args), - RazerCtlCommand::Perf(command) => match command.action { - PerfModeActionCommand::Mode { perf_mode } => command::set_perf_mode(&device, perf_mode), - PerfModeActionCommand::Cpu { cpu_boost } => command::set_cpu_boost(&device, cpu_boost), - PerfModeActionCommand::Gpu { gpu_boost } => command::set_gpu_boost(&device, gpu_boost), - }, - RazerCtlCommand::Fan(command) => match command.subcommand { - FanSubcommand::Auto => command::set_fan_mode(&device, FanMode::Auto), - FanSubcommand::Manual => command::set_fan_mode(&device, FanMode::Manual), - FanSubcommand::Rpm { rpm } => command::set_fan_rpm(&device, rpm), - FanSubcommand::Max { max_fan_speed_mode } => { - command::set_max_fan_speed_mode(&device, max_fan_speed_mode) - } - }, - RazerCtlCommand::Logo { logo_mode } => command::set_logo_mode(&device, logo_mode), - RazerCtlCommand::Backlight { brightness } => { - command::set_keyboard_brightness(&device, brightness) + Some(("auto", submatches)) => { + handle(&device.unwrap(), submatches, &cli_features)?; } - RazerCtlCommand::LightOn { always_on } => command::set_lights_always_on(&device, always_on), - RazerCtlCommand::BatteryCare { battery_care } => { - command::set_battery_care(&device, battery_care) + Some(("manual", submatches)) => { + let device = device::Device::new(librazer::descriptor::Descriptor { + model_number_prefix: "Unknown", + name: "Unknown", + pid: *submatches.get_one::("pid").unwrap(), + features: feature::ALL_FEATURES, + })?; + handle(&device, submatches, &cli_features)?; } - } + Some((cmd, _)) => unimplemented!("Subcommand not implemented: {}", cmd), + None => unreachable!(), + }; + + Ok(()) } diff --git a/razer-tray/src/main.rs b/razer-tray/src/main.rs index 628939f..2d187c4 100644 --- a/razer-tray/src/main.rs +++ b/razer-tray/src/main.rs @@ -15,7 +15,11 @@ use tray_icon::{ #[cfg(target_os = "windows")] use windows::Win32::Foundation::HANDLE; #[cfg(target_os = "windows")] -use windows::Win32::System::Threading::{GetCurrentProcess, SetPriorityClass, SetProcessInformation, ProcessPowerThrottling, IDLE_PRIORITY_CLASS, PROCESS_POWER_THROTTLING_CURRENT_VERSION, PROCESS_POWER_THROTTLING_EXECUTION_SPEED, PROCESS_POWER_THROTTLING_STATE}; +use windows::Win32::System::Threading::{ + GetCurrentProcess, ProcessPowerThrottling, SetPriorityClass, SetProcessInformation, + IDLE_PRIORITY_CLASS, PROCESS_POWER_THROTTLING_CURRENT_VERSION, + PROCESS_POWER_THROTTLING_EXECUTION_SPEED, PROCESS_POWER_THROTTLING_STATE, +}; const PKG_NAME: &str = env!("CARGO_PKG_NAME");