From ea36902e72be436f416fd159cea56c0f58594e2e Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 11 Mar 2026 23:31:53 +0545 Subject: [PATCH] feat(commands): add battery, gpu, power, and status command implementations Added full command implementations for battery limit/status, GPU management (cache operations, mode switching, NVIDIA handling), and a status reporter that prints GPU mode and battery details. Introduced module registration and placeholder power command. New Features - Battery command support (`limit`, `status`) - GPU command suite (`cache-delete`, `cache-query`, `reset`, `reset-sddm`, `integrated`, `hybrid`, `nvidia`) - Status command output showing GPU mode and battery information - Power command placeholder scaffold Enhancements - Unified command registration via `mod.rs` - Added root checks and file utilities for cache and config operations - Integrated logging infrastructure Bug Fixes: none --- src/commands/battery.rs | 12 + src/commands/gpu.rs | 500 ++++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 4 + src/commands/power.rs | 3 + src/commands/status.rs | 36 +++ 5 files changed, 555 insertions(+) diff --git a/src/commands/battery.rs b/src/commands/battery.rs index e69de29..e9485b6 100644 --- a/src/commands/battery.rs +++ b/src/commands/battery.rs @@ -0,0 +1,12 @@ +use crate::cli::BatteryCommands; + +pub fn execute(command: &BatteryCommands) { + match command { + BatteryCommands::Limit { percent } => { + println!("Setting battery limit to {}%", percent); + } + BatteryCommands::Status => { + println!("Battery status placeholder"); + } + } +} diff --git a/src/commands/gpu.rs b/src/commands/gpu.rs index e69de29..07b1e2c 100644 --- a/src/commands/gpu.rs +++ b/src/commands/gpu.rs @@ -0,0 +1,500 @@ +use crate::cli::GpuCommands; +use crate::hardware::gpu::{get_amd_igpu_name, get_current_mode, get_igpu_vendor, get_nvidia_gpu_pci_bus}; +use crate::utils::system::{assert_root, create_file, get_display_manager, rebuild_initramfs}; +use log::{error, info}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; +use std::process::Command; + +const CACHE_FILE_PATH: &str = "/var/cache/lapctl/cache.json"; +const BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf"; +const BLACKLIST_CONTENT: &str = r#"# Automatically generated by lapctl +blacklist nouveau +blacklist nova_core +blacklist nova_drm +blacklist nvidia +blacklist nvidia_drm +blacklist nvidia_uvm +blacklist nvidia_modeset +blacklist nvidia_current +blacklist nvidia_current_drm +blacklist nvidia_current_uvm +blacklist nvidia_current_modeset +blacklist i2c_nvidia_gpu +alias nouveau off +alias nova_core off +alias nova_drm off +alias nvidia off +alias nvidia_drm off +alias nvidia_uvm off +alias nvidia_modeset off +alias nvidia_current off +alias nvidia_current_drm off +alias nvidia_current_uvm off +alias nvidia_current_modeset off +alias i2c_nvidia_gpu off +"#; + +const UDEV_INTEGRATED_PATH: &str = "/etc/udev/rules.d/50-remove-nvidia.rules"; +const UDEV_INTEGRATED: &str = r#"# Automatically generated by lapctl + +# Remove NVIDIA USB xHCI Host Controller devices, if present +ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{power/control}="auto", ATTR{remove}="1" + +# Remove NVIDIA USB Type-C UCSI devices, if present +ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c8000", ATTR{power/control}="auto", ATTR{remove}="1" + +# Remove NVIDIA Audio devices, if present +ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x040300", ATTR{power/control}="auto", ATTR{remove}="1" + +# Remove NVIDIA VGA/3D controller devices +ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x03[0-9]*", ATTR{power/control}="auto", ATTR{remove}="1" +"#; + +const UDEV_PM_PATH: &str = "/etc/udev/rules.d/80-nvidia-pm.rules"; +const UDEV_PM_CONTENT: &str = r#"# Automatically generated by lapctl + +# Remove NVIDIA USB xHCI Host Controller devices, if present +ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{remove}="1" + +# Remove NVIDIA USB Type-C UCSI devices, if present +ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c8000", ATTR{remove}="1" + +# Remove NVIDIA Audio devices, if present +ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x040300", ATTR{remove}="1" + +# Enable runtime PM for NVIDIA VGA/3D controller devices on driver bind +ACTION=="bind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="auto" +ACTION=="bind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="auto" + +# Disable runtime PM for NVIDIA VGA/3D controller devices on driver unbind +ACTION=="unbind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030000", TEST=="power/control", ATTR{power/control}="on" +ACTION=="unbind", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x030200", TEST=="power/control", ATTR{power/control}="on" +"#; + +const XORG_PATH: &str = "/etc/X11/xorg.conf"; +const EXTRA_XORG_PATH: &str = "/etc/X11/xorg.conf.d/10-nvidia.conf"; +const MODESET_PATH: &str = "/etc/modprobe.d/nvidia.conf"; +const SDDM_XSETUP_PATH: &str = "/usr/share/sddm/scripts/Xsetup"; +const LIGHTDM_SCRIPT_PATH: &str = "/etc/lightdm/nvidia.sh"; +const LIGHTDM_CONFIG_PATH: &str = "/etc/lightdm/lightdm.conf.d/20-nvidia.conf"; + +const SDDM_XSETUP_CONTENT: &str = r#"#!/bin/sh +# Xsetup - run as root before the login dialog appears + +"#; + +const EXTRA_XORG_CONTENT: &str = r#"# Automatically generated by lapctl + +Section "OutputClass" + Identifier "nvidia" + MatchDriver "nvidia-drm" + Driver "nvidia" +"#; + +const LIGHTDM_CONFIG_CONTENT: &str = r#"# Automatically generated by lapctl + +[Seat:*] +display-setup-script=/etc/lightdm/nvidia.sh +"#; + +const NVIDIA_XRANDR_SCRIPT: &str = r#"#!/bin/sh +# Automatically generated by lapctl + +current="" + +xrandr --setprovideroutputsource "{}" NVIDIA-0 +xrandr --auto + +for next in $(xrandr --listmonitors | grep -E " *[0-9]+:.*" | cut -d" " -f6); do + [ -z "$current" ] && current=$next && continue + xrandr --output "$current" --auto --output "$next" --auto --right-of "$current" + current=$next +done +"#; + +// Helpers +fn xorg_intel(bus_id: &str) -> String { + format!(r#"# Automatically generated by lapctl + +Section "ServerLayout" + Identifier "layout" + Screen 0 "nvidia" + Inactive "intel" +EndSection + +Section "Device" + Identifier "nvidia" + Driver "nvidia" + BusID "{}" +EndSection + +Section "Screen" + Identifier "nvidia" + Device "nvidia" + Option "AllowEmptyInitialConfiguration" +EndSection + +Section "Device" + Identifier "intel" + Driver "modesetting" +EndSection + +Section "Screen" + Identifier "intel" + Device "intel" +EndSection +"#, bus_id) +} + +fn xorg_amd(bus_id: &str) -> String { + format!(r#"# Automatically generated by lapctl + +Section "ServerLayout" + Identifier "layout" + Screen 0 "nvidia" + Inactive "amdgpu" +EndSection + +Section "Device" + Identifier "nvidia" + Driver "nvidia" + BusID "{}" +EndSection + +Section "Screen" + Identifier "nvidia" + Device "nvidia" + Option "AllowEmptyInitialConfiguration" +EndSection + +Section "Device" + Identifier "amdgpu" + Driver "amdgpu" +EndSection + +Section "Screen" + Identifier "amd" + Device "amdgpu" +EndSection +"#, bus_id) +} + +fn generate_xrandr_script(igpu_vendor: Option<&String>) -> String { + let source_name = match igpu_vendor.map(|s| s.as_str()) { + Some("intel") => "modesetting".to_string(), + Some("amd") => { + if let Some(amd_name) = get_amd_igpu_name() { + amd_name + } else { + "modesetting".to_string() + } + } + _ => "modesetting".to_string(), + }; + NVIDIA_XRANDR_SCRIPT.replace("{}", &source_name) +} + +fn cleanup() { + let to_remove = vec![ + BLACKLIST_PATH, + UDEV_INTEGRATED_PATH, + UDEV_PM_PATH, + XORG_PATH, + EXTRA_XORG_PATH, + MODESET_PATH, + LIGHTDM_SCRIPT_PATH, + LIGHTDM_CONFIG_PATH, + "/etc/lapctl-wayland-nvidia", + // legacy paths + "/etc/X11/xorg.conf.d/90-nvidia.conf", + "/lib/udev/rules.d/50-remove-nvidia.rules", + "/lib/udev/rules.d/80-nvidia-pm.rules", + ]; + + for file_path in to_remove { + if Path::new(file_path).exists() { + if let Err(e) = fs::remove_file(file_path) { + error!("Failed to remove file '{}': {}", file_path, e); + } else { + info!("Removed file {}", file_path); + } + } + } + + let backup_path = format!("{}.bak", SDDM_XSETUP_PATH); + if Path::new(&backup_path).exists() { + info!("Restoring Xsetup backup"); + if let Ok(content) = fs::read_to_string(&backup_path) { + create_file(SDDM_XSETUP_PATH, &content, true); + } + if let Err(e) = fs::remove_file(&backup_path) { + error!("Failed to remove backup: {}", e); + } else { + info!("Removed file {}", backup_path); + } + } +} + +// CACHING LOGIC +#[derive(Serialize, Deserialize, Debug)] +struct Cache { + nvidia_gpu_pci_bus: String, +} + +fn create_cache_obj(bus_id: String) -> Cache { + Cache { nvidia_gpu_pci_bus: bus_id } +} + +fn create_cache_file() { + if get_current_mode() != "hybrid" { + error!("--cache-create requires that the system be in the hybrid Optimus mode"); + return; + } + let bus_id = get_nvidia_gpu_pci_bus(); + let cache = create_cache_obj(bus_id); + + if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() { + let _ = fs::create_dir_all(parent); + } + + if let Ok(json) = serde_json::to_string_pretty(&cache) { + if fs::write(CACHE_FILE_PATH, json).is_ok() { + log::debug!("Created file {}", CACHE_FILE_PATH); + } + } +} + +fn read_cache_file() -> Result { + if Path::new(CACHE_FILE_PATH).exists() { + let content = fs::read_to_string(CACHE_FILE_PATH).map_err(|e| e.to_string())?; + serde_json::from_str(&content).map_err(|e| e.to_string()) + } else if get_current_mode() == "hybrid" { + Ok(create_cache_obj(get_nvidia_gpu_pci_bus())) + } else { + Err("No cache present. Operation requires that the system be in the hybrid Optimus mode".into()) + } +} + +fn switch_integrated() { + assert_root(); + println!("Switching to integrated mode"); + + let is_debug = log::log_enabled!(log::Level::Debug); + let mut dis_cmd = Command::new("systemctl"); + dis_cmd.args(&["disable", "nvidia-persistenced.service"]); + if !is_debug { + dis_cmd.stdout(std::process::Stdio::null()); + dis_cmd.stderr(std::process::Stdio::null()); + } + match dis_cmd.status() { + Ok(s) if s.success() => println!("Successfully disabled nvidia-persistenced.service"), + _ => error!("An error ocurred while disabling service"), + } + + if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() { + create_cache_file(); + } + + cleanup(); + + create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false); + create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false); + + rebuild_initramfs(); + println!("Operation completed successfully"); + println!("Please reboot your computer for changes to take effect!"); +} + +fn switch_hybrid(rtd3: Option, use_nvidia_current: bool) { + assert_root(); + println!("Switching to hybrid mode"); + println!("Enable PCI-Express Runtime D3 (RTD3) Power Management: {}", rtd3.is_some()); + + if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() { + create_cache_file(); + } + + cleanup(); + + // persistenced enable + let is_debug = log::log_enabled!(log::Level::Debug); + let mut enable_cmd = Command::new("systemctl"); + enable_cmd.args(&["enable", "nvidia-persistenced.service"]); + if !is_debug { + enable_cmd.stdout(std::process::Stdio::null()); + enable_cmd.stderr(std::process::Stdio::null()); + } + match enable_cmd.status() { + Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"), + _ => error!("An error ocurred while enabling service"), + } + + if let Some(val) = rtd3 { + let modeset_content = if use_nvidia_current { + format!("# Automatically generated by lapctl\n\noptions nvidia-current-drm modeset=1\noptions nvidia-current \"NVreg_DynamicPowerManagement=0x0{}\"\noptions nvidia-current NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n", val) + } else { + format!("# Automatically generated by lapctl\n\noptions nvidia-drm modeset=1\noptions nvidia \"NVreg_DynamicPowerManagement=0x0{}\"\noptions nvidia NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n", val) + }; + create_file(MODESET_PATH, &modeset_content, false); + create_file(UDEV_PM_PATH, UDEV_PM_CONTENT, false); + } else { + let modeset_content = if use_nvidia_current { + "# Automatically generated by lapctl\n\noptions nvidia-current-drm modeset=1\noptions nvidia-current NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n".to_string() + } else { + "# Automatically generated by lapctl\n\noptions nvidia-drm modeset=1\noptions nvidia NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n".to_string() + }; + create_file(MODESET_PATH, &modeset_content, false); + } + + rebuild_initramfs(); + println!("Operation completed successfully"); + println!("Please reboot your computer for changes to take effect!"); +} + +fn switch_nvidia( + dm: Option, + force_comp: bool, + coolbits: Option, + use_nvidia_current: bool, + wayland: bool, +) { + assert_root(); + println!("Switching to nvidia mode"); + println!("Enable ForceCompositionPipeline: {}", force_comp); + println!("Enable Coolbits: {}", coolbits.is_some()); + + // persistenced enable + let is_debug = log::log_enabled!(log::Level::Debug); + let mut enable_cmd = Command::new("systemctl"); + enable_cmd.args(&["enable", "nvidia-persistenced.service"]); + if !is_debug { + enable_cmd.stdout(std::process::Stdio::null()); + enable_cmd.stderr(std::process::Stdio::null()); + } + match enable_cmd.status() { + Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"), + _ => error!("An error ocurred while enabling service"), + } + + if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() { + create_cache_file(); + } + + let cache = read_cache_file(); + let pci_bus = match cache { + Ok(c) => c.nvidia_gpu_pci_bus, + Err(_) => get_nvidia_gpu_pci_bus() + }; + + cleanup(); + + if use_nvidia_current { + create_file(MODESET_PATH, "# Automatically generated by lapctl\n\noptions nvidia-current-drm modeset=1\noptions nvidia-current NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n", false); + } else { + create_file(MODESET_PATH, "# Automatically generated by lapctl\n\noptions nvidia-drm modeset=1\noptions nvidia NVreg_UsePageAttributeTable=1 NVreg_InitializeSystemMemoryAllocations=0\n", false); + } + + if wayland { + create_file("/etc/lapctl-wayland-nvidia", "# Wayland mode marker for lapctl\n", false); + } else { + let igpu_vendor = get_igpu_vendor(); + if let Some(ref vendor) = igpu_vendor { + if vendor == "intel" { + create_file(XORG_PATH, &xorg_intel(&pci_bus), false); + } else if vendor == "amd" { + create_file(XORG_PATH, &xorg_amd(&pci_bus), false); + } + } + + let mut extra_xorg = EXTRA_XORG_CONTENT.to_string(); + if force_comp { + extra_xorg.push_str(" Option \"ForceCompositionPipeline\" \"true\"\n"); + } + if let Some(cb) = coolbits { + extra_xorg.push_str(&format!(" Option \"Coolbits\" \"{}\"\n", cb)); + } + if force_comp || coolbits.is_some() { + extra_xorg.push_str("EndSection\n"); + create_file(EXTRA_XORG_PATH, &extra_xorg, false); + } + + let display_manager = dm.or_else(|| get_display_manager()); + + if let Some(cdm) = display_manager { + if cdm == "sddm" { + if Path::new(SDDM_XSETUP_PATH).exists() { + info!("Creating Xsetup backup"); + if let Ok(content) = fs::read_to_string(SDDM_XSETUP_PATH) { + create_file(&format!("{}.bak", SDDM_XSETUP_PATH), &content, false); + } + } + create_file(SDDM_XSETUP_PATH, &generate_xrandr_script(igpu_vendor.as_ref()), true); + } else if cdm == "lightdm" { + create_file(LIGHTDM_SCRIPT_PATH, &generate_xrandr_script(igpu_vendor.as_ref()), true); + create_file(LIGHTDM_CONFIG_PATH, LIGHTDM_CONFIG_CONTENT, false); + } + } + } + + rebuild_initramfs(); + println!("Operation completed successfully"); + println!("Please reboot your computer for changes to take effect!"); +} + +pub fn execute(cmd: &GpuCommands) { + match cmd { + GpuCommands::Query => { + let mode = get_current_mode(); + println!("{}", mode); + } + GpuCommands::CacheCreate => { + assert_root(); + create_cache_file(); + } + GpuCommands::CacheDelete => { + assert_root(); + if Path::new(CACHE_FILE_PATH).exists() { + let _ = fs::remove_file(CACHE_FILE_PATH); + if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() { + let _ = fs::remove_dir(parent); + } + } + } + GpuCommands::CacheQuery => { + if let Ok(content) = fs::read_to_string(CACHE_FILE_PATH) { + println!("{}", content); + } else { + println!("ERROR: Could not read {}", CACHE_FILE_PATH); + } + } + GpuCommands::Reset => { + assert_root(); + + if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() { + create_cache_file(); + } + + cleanup(); + if Path::new(CACHE_FILE_PATH).exists() { + let _ = fs::remove_file(CACHE_FILE_PATH); + if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() { + let _ = fs::remove_dir(parent); + } + } + rebuild_initramfs(); + println!("Operation completed successfully"); + } + GpuCommands::ResetSddm => { + assert_root(); + create_file(SDDM_XSETUP_PATH, SDDM_XSETUP_CONTENT, true); + println!("Operation completed successfully"); + } + GpuCommands::Integrated => switch_integrated(), + GpuCommands::Hybrid { rtd3, use_nvidia_current } => switch_hybrid(rtd3.clone(), *use_nvidia_current), + GpuCommands::Nvidia { dm, force_comp, coolbits, use_nvidia_current, wayland } => { + switch_nvidia(dm.clone(), *force_comp, coolbits.clone(), *use_nvidia_current, *wayland) + } + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e69de29..a2de3c3 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -0,0 +1,4 @@ +pub mod battery; +pub mod gpu; +pub mod power; +pub mod status; diff --git a/src/commands/power.rs b/src/commands/power.rs index e69de29..1685c34 100644 --- a/src/commands/power.rs +++ b/src/commands/power.rs @@ -0,0 +1,3 @@ +pub fn execute() { + println!("Power command placeholder"); +} diff --git a/src/commands/status.rs b/src/commands/status.rs index e69de29..d04bbb6 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -0,0 +1,36 @@ +use crate::hardware::gpu; +use std::fs; + +pub fn execute() { + println!("--- lapctl status ---"); + + // GPU Mode + let current_mode = gpu::get_current_mode(); + println!("GPU Mode: {}", current_mode); + + // Battery Limit / Status checks (we can peek into typical battery paths) + // Note: this assumes a typical sysfs structure. A complete implementation + // would parse /sys/class/power_supply/BAT0 or similar. + let sys_class_power = "/sys/class/power_supply"; + if let Ok(entries) = fs::read_dir(sys_class_power) { + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if name_str.starts_with("BAT") { + let bat_path = entry.path(); + + let capacity = fs::read_to_string(bat_path.join("capacity")).unwrap_or_else(|_| "Unknown".into()); + let status = fs::read_to_string(bat_path.join("status")).unwrap_or_else(|_| "Unknown".into()); + + println!("{}:", name_str); + println!(" Capacity: {}%", capacity.trim()); + println!(" Status: {}", status.trim()); + + // Print charing limit if exists (e.g., charge_control_end_threshold) + if let Ok(limit) = fs::read_to_string(bat_path.join("charge_control_end_threshold")) { + println!(" Charge Limit: {}%", limit.trim()); + } + } + } + } +}