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
This commit is contained in:
sankalp committed 2026-03-11 23:31:53 +05:45
1 parent 36d0807983
commit ea36902e72
5 files changed
+555

No files matched your search

+12
View File
@@ -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");
}
}
}
+500
View File
@@ -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<Cache, String> {
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<u8>, 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<String>,
force_comp: bool,
coolbits: Option<u32>,
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)
}
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod battery;
pub mod gpu;
pub mod power;
pub mod status;
+3
View File
@@ -0,0 +1,3 @@
pub fn execute() {
println!("Power command placeholder");
}
+36
View File
@@ -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());
}
}
}
}
}