Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bfb0dda39 | ||
|
|
ae32ce9c8d | ||
|
|
11c3cec99d | ||
|
|
f69b74ab73 | ||
|
|
bdf1312dd1 | ||
|
|
2544f44309 | ||
|
|
34a57007d1 | ||
|
|
3684c5b071 | ||
|
|
8faccc117a | ||
|
|
92806e2843 | ||
|
|
bdc95801c3 | ||
|
|
2efa7a64f1 | ||
|
|
c8561bf4ff | ||
|
|
f202103a06 | ||
|
|
e0a589d441 | ||
|
|
151f5ad303 | ||
|
|
92298ddce8 | ||
|
|
c223fea5f4 | ||
|
|
80899602e5 | ||
|
|
21defdb70a | ||
|
|
90965f13b9 | ||
|
|
30a89fe2fe |
No files matched your search
@@ -6,6 +6,13 @@ on:
|
|||||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version to publish (e.g. 0.3.4)'
|
||||||
|
required: true
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
jobs:
|
jobs:
|
||||||
@@ -35,6 +42,7 @@ jobs:
|
|||||||
name: lapctl-${{ github.ref_name }}
|
name: lapctl-${{ github.ref_name }}
|
||||||
path: ./target/release/lapctl
|
path: ./target/release/lapctl
|
||||||
- name: Publish to crates.io
|
- name: Publish to crates.io
|
||||||
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
run: cargo publish
|
run: cargo publish
|
||||||
@@ -47,31 +55,56 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Set up Rust
|
- name: Set up Rust
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Setup pacman
|
|
||||||
uses: archlinux/actions/setup-pacman@v1
|
|
||||||
with:
|
|
||||||
update: true
|
|
||||||
- name: Install base-devel (for makepkg)
|
|
||||||
run: sudo pacman -S --noconfirm --needed base-devel
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
id: version
|
id: version
|
||||||
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
run: echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
env:
|
||||||
|
VERSION: ${{ github.event.inputs.version || github.ref_name }}
|
||||||
|
- name: Download release binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lapctl-${{ github.ref_name }}
|
||||||
|
path: .
|
||||||
|
- name: Create tarball with binary
|
||||||
|
run: |
|
||||||
|
tar czf "lapctl-${{ steps.version.outputs.version }}-x86_64.tar.gz" \
|
||||||
|
lapctl LICENSE
|
||||||
- name: Generate PKGBUILD with cargo-aur
|
- name: Generate PKGBUILD with cargo-aur
|
||||||
run: |
|
run: |
|
||||||
cargo install cargo-aur
|
cargo install cargo-aur
|
||||||
mkdir -p aur-pkg
|
mkdir -p aur-pkg
|
||||||
cargo aur --output aur-pkg
|
cargo aur --output aur-pkg
|
||||||
|
- name: Update PKGBUILD checksums
|
||||||
|
run: |
|
||||||
|
SHA=$(sha256sum "lapctl-${{ steps.version.outputs.version }}-x86_64.tar.gz" | cut -d' ' -f1)
|
||||||
|
sed -i "s/sha256sums=([^)]*)/sha256sums=('$SHA')/" aur-pkg/PKGBUILD
|
||||||
- name: Generate .SRCINFO
|
- name: Generate .SRCINFO
|
||||||
working-directory: aur-pkg
|
run: |
|
||||||
run: makepkg --printsrcinfo > .SRCINFO
|
docker run --rm -v "$(pwd)/aur-pkg:/build" archlinux:latest \
|
||||||
|
bash -c "
|
||||||
|
useradd -m builder
|
||||||
|
chown -R builder:builder /build
|
||||||
|
cd /build && su builder -c 'makepkg --printsrcinfo > .SRCINFO'
|
||||||
|
"
|
||||||
- name: Push to AUR
|
- name: Push to AUR
|
||||||
uses: KSXGitHub/github-actions-deploy-aur@v2.5.0
|
uses: KSXGitHub/github-actions-deploy-aur@v4.1.3
|
||||||
with:
|
with:
|
||||||
pkgname: lapctl-bin
|
pkgname: lapctl-bin
|
||||||
pkgver: ${{ steps.version.outputs.version }}
|
pkgbuild: aur-pkg/PKGBUILD
|
||||||
assets: |
|
assets: aur-pkg/.SRCINFO
|
||||||
aur-pkg/PKGBUILD
|
|
||||||
aur-pkg/.SRCINFO
|
|
||||||
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
|
ssh_keyscan_types: rsa,ecdsa,ed25519
|
||||||
commit_username: ${{ github.actor }}
|
commit_username: ${{ github.actor }}
|
||||||
commit_email: ${{ github.actor }}@users.noreply.github.com
|
commit_email: ${{ github.actor }}@users.noreply.github.com
|
||||||
|
allow_empty_commits: true
|
||||||
|
force_push: true
|
||||||
|
post_process: |
|
||||||
|
git add -A && git stash && git checkout master && git checkout stash -- . && git stash drop
|
||||||
|
- name: Upload tarball to release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
TAG="v${{ steps.version.outputs.version }}"
|
||||||
|
gh release create "$TAG" --title "$TAG" --notes "" 2>/dev/null || true
|
||||||
|
gh release upload "$TAG" \
|
||||||
|
"lapctl-${{ steps.version.outputs.version }}-x86_64.tar.gz" --clobber
|
||||||
Generated
+1
-1
@@ -644,7 +644,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lapctl"
|
name = "lapctl"
|
||||||
version = "0.3.3"
|
version = "0.3.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lapctl"
|
name = "lapctl"
|
||||||
version = "0.3.3"
|
version = "0.3.5"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
authors = ["S4NKALP"]
|
||||||
description = "CLI tool for managing laptop hardware features on Linux"
|
description = "CLI tool for managing laptop hardware features on Linux"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/S4NKALP/lapctl"
|
repository = "https://github.com/S4NKALP/lapctl"
|
||||||
|
|||||||
@@ -35,27 +35,28 @@ Built for speed and simplicity, it talks directly to your system's hardware inte
|
|||||||
#### Option 1: Arch Linux (AUR)
|
#### Option 1: Arch Linux (AUR)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yay -S lapctl
|
yay -S lapctl-bin
|
||||||
|
sudo lapctl install-rules
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Option 2: Using `just` (Recommended for other distros)
|
#### Option 2: crates.io
|
||||||
|
|
||||||
If you have [just](https://github.com/casey/just) installed:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cargo install lapctl
|
||||||
|
sudo lapctl install-rules
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option 3: From source (using `just`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/S4NKALP/lapctl.git
|
||||||
|
cd lapctl
|
||||||
just install
|
just install
|
||||||
sudo lapctl install-rules
|
sudo lapctl install-rules
|
||||||
```
|
```
|
||||||
|
|
||||||
_The `install-rules` command automatically sets up the D-Bus policy and starts the `lapctld` background service._
|
_The `install-rules` command automatically sets up the D-Bus policy and starts the `lapctld` background service._
|
||||||
|
|
||||||
#### Option 3: Using `cargo`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install --path .
|
|
||||||
sudo lapctl install-rules
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Requirements
|
#### Requirements
|
||||||
|
|
||||||
- **GPU Switching (Optional)**: `xrandr` and `nvidia-settings` are strictly required **ONLY** when using the `lapctl gpu` command on X11 (to route proprietary NVIDIA Optimus drivers). Wayland is auto detected so no extra flags are needed.
|
- **GPU Switching (Optional)**: `xrandr` and `nvidia-settings` are strictly required **ONLY** when using the `lapctl gpu` command on X11 (to route proprietary NVIDIA Optimus drivers). Wayland is auto detected so no extra flags are needed.
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ pub enum Commands {
|
|||||||
},
|
},
|
||||||
/// Hardware status
|
/// Hardware status
|
||||||
Status,
|
Status,
|
||||||
|
/// Show system temperatures
|
||||||
|
Temperature,
|
||||||
|
/// Show fan speeds
|
||||||
|
Fan,
|
||||||
/// Install udev rules for rootless operation
|
/// Install udev rules for rootless operation
|
||||||
InstallRules,
|
InstallRules,
|
||||||
/// Touchpad management
|
/// Touchpad management
|
||||||
@@ -114,6 +118,8 @@ pub enum GpuCommands {
|
|||||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
command: Vec<String>,
|
command: Vec<String>,
|
||||||
},
|
},
|
||||||
|
/// Show GPU power state and consumption
|
||||||
|
Power,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn execute() {
|
||||||
|
println!("--- Fan Speed ---");
|
||||||
|
let mut found = false;
|
||||||
|
|
||||||
|
// Read fan speeds from hwmon
|
||||||
|
let hwmon_dir = Path::new("/sys/class/hwmon");
|
||||||
|
if hwmon_dir.exists()
|
||||||
|
&& let Ok(entries) = fs::read_dir(hwmon_dir)
|
||||||
|
{
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let name_path = path.join("name");
|
||||||
|
let Ok(name) = fs::read_to_string(&name_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let hwmon_name = name.trim().to_string();
|
||||||
|
|
||||||
|
let mut idx = 1;
|
||||||
|
loop {
|
||||||
|
let input_path = path.join(format!("fan{}_input", idx));
|
||||||
|
if !input_path.exists() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(raw) = fs::read_to_string(&input_path) {
|
||||||
|
let rpm: u64 = raw.trim().parse().unwrap_or(0);
|
||||||
|
|
||||||
|
let label = path.join(format!("fan{}_label", idx));
|
||||||
|
let label_str = fs::read_to_string(&label)
|
||||||
|
.map(|l| l.trim().to_string())
|
||||||
|
.unwrap_or_else(|_| format!("Fan {}", idx));
|
||||||
|
|
||||||
|
let display_name = match hwmon_name.as_str() {
|
||||||
|
"acpi_fan" => "CPU Fan",
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
|
||||||
|
if rpm == 0 {
|
||||||
|
println!(" {} ({}): Stopped", display_name, label_str);
|
||||||
|
} else {
|
||||||
|
println!(" {} ({}): {} RPM", display_name, label_str, rpm);
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show fan policy from thermal cooling devices
|
||||||
|
let thermal_dir = Path::new("/sys/class/thermal");
|
||||||
|
if thermal_dir.exists()
|
||||||
|
&& let Ok(entries) = fs::read_dir(thermal_dir)
|
||||||
|
{
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name_str = name.to_string_lossy();
|
||||||
|
if !name_str.starts_with("cooling_device") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let type_path = entry.path().join("type");
|
||||||
|
let state_path = entry.path().join("cur_state");
|
||||||
|
let max_state_path = entry.path().join("max_state");
|
||||||
|
|
||||||
|
if let (Ok(type_content), Ok(state), Ok(max_state)) = (
|
||||||
|
fs::read_to_string(&type_path),
|
||||||
|
fs::read_to_string(&state_path),
|
||||||
|
fs::read_to_string(&max_state_path),
|
||||||
|
) {
|
||||||
|
let zone_type = type_content.trim();
|
||||||
|
let cur_state: u64 = state.trim().parse().unwrap_or(0);
|
||||||
|
let max: u64 = max_state.trim().parse().unwrap_or(0);
|
||||||
|
|
||||||
|
if zone_type == "Fan" {
|
||||||
|
let policy = if cur_state == 0 {
|
||||||
|
"Off"
|
||||||
|
} else if cur_state >= max {
|
||||||
|
"Full Speed"
|
||||||
|
} else {
|
||||||
|
"Variable"
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
" {} ({}): Level {}/{} ({})",
|
||||||
|
name_str, zone_type, cur_state, max, policy
|
||||||
|
);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
println!(" No fan sensors found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+102
-17
@@ -854,37 +854,121 @@ fn switch_nvidia(
|
|||||||
|
|
||||||
fn run_on_dgpu(command: &[String]) {
|
fn run_on_dgpu(command: &[String]) {
|
||||||
if command.is_empty() {
|
if command.is_empty() {
|
||||||
error!("No command provided to run.");
|
error!("No command specified.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if get_current_mode() != "hybrid" {
|
if get_current_mode() != "hybrid" {
|
||||||
log::warn!(
|
error!("GPU run is only supported in Hybrid mode.");
|
||||||
"Running on dGPU is typically only effective in Hybrid mode. \
|
return;
|
||||||
Your current mode is '{}'. The application may fail to start or \
|
|
||||||
not use the NVIDIA GPU.",
|
|
||||||
get_current_mode()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("Launching '{}' on the discrete GPU...", command.join(" "));
|
let env_vars = vec![
|
||||||
|
("__NV_PRIME_RENDER_OFFLOAD", "1".to_string()),
|
||||||
|
("__GLX_VENDOR_LIBRARY_NAME", "nvidia".to_string()),
|
||||||
|
("__VK_LAYER_NV_optimus", "NVIDIA_only".to_string()),
|
||||||
|
];
|
||||||
|
|
||||||
let mut child = Command::new(&command[0]);
|
let mut cmd = Command::new(&command[0]);
|
||||||
if command.len() > 1 {
|
if command.len() > 1 {
|
||||||
child.args(&command[1..]);
|
cmd.args(&command[1..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set PRIME offload variables natively
|
for (key, value) in &env_vars {
|
||||||
child.env("__NV_PRIME_RENDER_OFFLOAD", "1");
|
cmd.env(key, value);
|
||||||
child.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
|
}
|
||||||
child.env("__VK_LAYER_NV_optimus", "NVIDIA_only");
|
|
||||||
|
|
||||||
match child.status() {
|
info!("Running '{}' on discrete GPU", command.join(" "));
|
||||||
|
|
||||||
|
match cmd.status() {
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
log::info!("Application exited with: {}", status);
|
if !status.success() {
|
||||||
|
error!("Command exited with status: {}", status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to launch application: {}", e);
|
error!("Failed to execute command: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_gpu_power_state() {
|
||||||
|
println!("--- GPU Power State ---");
|
||||||
|
|
||||||
|
// Read PCI power state from sysfs
|
||||||
|
let pci_base = Path::new("/sys/bus/pci/devices");
|
||||||
|
if let Ok(entries) = fs::read_dir(pci_base) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let vendor_path = entry.path().join("vendor");
|
||||||
|
let class_path = entry.path().join("class");
|
||||||
|
|
||||||
|
let Ok(vendor) = fs::read_to_string(&vendor_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(class) = fs::read_to_string(&class_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// NVIDIA vendor = 0x10de, VGA class starts with 0x0300
|
||||||
|
if vendor.trim() == "0x10de" && class.trim().starts_with("0x0300") {
|
||||||
|
let pci_addr = entry.file_name().to_string_lossy().to_string();
|
||||||
|
println!(" Device: NVIDIA ({})", pci_addr);
|
||||||
|
|
||||||
|
// Power state
|
||||||
|
let runtime_status = entry.path().join("power/runtime_status");
|
||||||
|
if let Ok(status) = fs::read_to_string(&runtime_status) {
|
||||||
|
println!(" Runtime Status: {}", status.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power control
|
||||||
|
let power_control = entry.path().join("power/control");
|
||||||
|
if let Ok(control) = fs::read_to_string(&power_control) {
|
||||||
|
println!(" Power Control: {}", control.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current power state (D0 = fully on, D3 = off)
|
||||||
|
let current_state = entry.path().join("power_state");
|
||||||
|
if let Ok(state) = fs::read_to_string(¤t_state) {
|
||||||
|
println!(" PCI Power State: {}", state.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max power state
|
||||||
|
let max_state = entry.path().join("max_power_state");
|
||||||
|
if let Ok(state) = fs::read_to_string(&max_state) {
|
||||||
|
println!(" Max Power State: {}", state.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NVIDIA power info via nvidia-smi
|
||||||
|
match std::process::Command::new("nvidia-smi")
|
||||||
|
.args([
|
||||||
|
"--query-gpu=power.draw,power.limit,power.default_limit,power.max_limit,clocks.current.graphics,clocks.max.graphics",
|
||||||
|
"--format=csv,noheader",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in stdout.lines() {
|
||||||
|
let parts: Vec<&str> = line.split(", ").collect();
|
||||||
|
if parts.len() >= 5 {
|
||||||
|
println!();
|
||||||
|
println!(" Power Draw: {}", parts[0].trim());
|
||||||
|
println!(" Power Limit: {}", parts[1].trim());
|
||||||
|
println!(" Default Limit: {}", parts[2].trim());
|
||||||
|
println!(" Max Limit: {}", parts[3].trim());
|
||||||
|
println!(" Current Clock: {}", parts[4].trim());
|
||||||
|
if parts.len() > 5 {
|
||||||
|
println!(" Max Clock: {}", parts[5].trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::debug!("nvidia-smi not available or GPU not powered on");
|
||||||
|
println!(" (nvidia-smi not available - GPU may be powered off)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1004,5 +1088,6 @@ pub fn execute_local(cmd: &GpuCommands) {
|
|||||||
*no_reboot,
|
*no_reboot,
|
||||||
),
|
),
|
||||||
GpuCommands::Run { command } => run_on_dgpu(command),
|
GpuCommands::Run { command } => run_on_dgpu(command),
|
||||||
|
GpuCommands::Power => show_gpu_power_state(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
pub mod battery;
|
pub mod battery;
|
||||||
pub mod cooling;
|
pub mod cooling;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
|
pub mod fan;
|
||||||
pub mod gpu;
|
pub mod gpu;
|
||||||
pub mod install_rules;
|
pub mod install_rules;
|
||||||
pub mod power;
|
pub mod power;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
pub mod temperature;
|
||||||
pub mod touchpad;
|
pub mod touchpad;
|
||||||
@@ -181,6 +181,38 @@ pub fn execute() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Temperature
|
||||||
|
if let Ok(temp_output) = fs::read_to_string("/sys/class/hwmon/hwmon5/temp1_input")
|
||||||
|
&& let Ok(millideg) = temp_output.trim().parse::<i64>()
|
||||||
|
{
|
||||||
|
println!("CPU Temp: {:.1}°C", millideg as f64 / 1000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fan Speed
|
||||||
|
if let Ok(fan_output) = fs::read_to_string("/sys/class/hwmon/hwmon1/fan1_input")
|
||||||
|
&& let Ok(rpm) = fan_output.trim().parse::<u64>()
|
||||||
|
{
|
||||||
|
println!("Fan Speed: {} RPM", rpm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPU Power State
|
||||||
|
if let Ok(output) = std::process::Command::new("nvidia-smi")
|
||||||
|
.args([
|
||||||
|
"--query-gpu=power.draw,temperature.gpu",
|
||||||
|
"--format=csv,noheader",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
&& output.status.success()
|
||||||
|
{
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if let Some(line) = stdout.lines().next() {
|
||||||
|
let parts: Vec<&str> = line.split(", ").collect();
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
println!("GPU: {} @ {}°C (NVIDIA)", parts[0].trim(), parts[1].trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Display Status
|
// Display Status
|
||||||
let displays = display::get_active_display_info();
|
let displays = display::get_active_display_info();
|
||||||
if displays.is_empty() {
|
if displays.is_empty() {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
use log::debug;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub fn execute() {
|
||||||
|
println!("--- Temperature ---");
|
||||||
|
let mut found = false;
|
||||||
|
|
||||||
|
// Read all hwmon temperature sensors
|
||||||
|
let hwmon_dir = Path::new("/sys/class/hwmon");
|
||||||
|
if hwmon_dir.exists()
|
||||||
|
&& let Ok(entries) = fs::read_dir(hwmon_dir)
|
||||||
|
{
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let name_path = path.join("name");
|
||||||
|
let Ok(name) = fs::read_to_string(&name_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let hwmon_name = name.trim().to_string();
|
||||||
|
|
||||||
|
let mut idx = 1;
|
||||||
|
loop {
|
||||||
|
let input_path = path.join(format!("temp{}_input", idx));
|
||||||
|
if !input_path.exists() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(raw) = fs::read_to_string(&input_path) {
|
||||||
|
let millideg: i64 = raw.trim().parse().unwrap_or(0);
|
||||||
|
let celsius = millideg as f64 / 1000.0;
|
||||||
|
|
||||||
|
let label = path.join(format!("temp{}_label", idx));
|
||||||
|
let label_str = fs::read_to_string(&label)
|
||||||
|
.map(|l| l.trim().to_string())
|
||||||
|
.unwrap_or_else(|_| format!("Sensor {}", idx));
|
||||||
|
|
||||||
|
let display_name = match hwmon_name.as_str() {
|
||||||
|
"k10temp" => "CPU",
|
||||||
|
"acpitz" => "ACPI Thermal Zone",
|
||||||
|
"nvme" => "NVMe SSD",
|
||||||
|
"mt7921_phy0" => "WiFi",
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(" {} ({}): {:.1}°C", display_name, label_str, celsius);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NVIDIA GPU temperature via nvidia-smi
|
||||||
|
if let Ok(output) = std::process::Command::new("nvidia-smi")
|
||||||
|
.args([
|
||||||
|
"--query-gpu=temperature.gpu,power.draw,clocks.current.graphics",
|
||||||
|
"--format=csv,noheader",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
&& output.status.success()
|
||||||
|
{
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in stdout.lines() {
|
||||||
|
let parts: Vec<&str> = line.split(", ").collect();
|
||||||
|
if let Some(temp) = parts.first()
|
||||||
|
&& let Ok(temp_c) = temp.trim().parse::<f64>()
|
||||||
|
{
|
||||||
|
let info = if parts.len() >= 3 {
|
||||||
|
format!(" [{} @ {}]", parts[1].trim(), parts[2].trim())
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
println!(" GPU (NVIDIA): {:.0}°C{}", temp_c, info);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug!("nvidia-smi not available, skipping GPU temperature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read thermal zone temperatures
|
||||||
|
let thermal_dir = Path::new("/sys/class/thermal");
|
||||||
|
if thermal_dir.exists()
|
||||||
|
&& let Ok(entries) = fs::read_dir(thermal_dir)
|
||||||
|
{
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name_str = name.to_string_lossy();
|
||||||
|
if !name_str.starts_with("thermal_zone") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let type_path = entry.path().join("type");
|
||||||
|
let temp_path = entry.path().join("temp");
|
||||||
|
|
||||||
|
if let (Ok(type_content), Ok(temp_content)) = (
|
||||||
|
fs::read_to_string(&type_path),
|
||||||
|
fs::read_to_string(&temp_path),
|
||||||
|
) {
|
||||||
|
let zone_type = type_content.trim();
|
||||||
|
let millideg: i64 = temp_content.trim().parse().unwrap_or(0);
|
||||||
|
let celsius = millideg as f64 / 1000.0;
|
||||||
|
|
||||||
|
// Skip if we already showed this from hwmon
|
||||||
|
if zone_type == "x86_pkg_temp" || zone_type == "k10temp" || zone_type == "acpitz" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(" {} ({}): {:.1}°C", name_str, zone_type, celsius);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
println!(" No temperature sensors found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ fn main() {
|
|||||||
Commands::Cooling { command } => commands::cooling::execute(command),
|
Commands::Cooling { command } => commands::cooling::execute(command),
|
||||||
Commands::Display { command } => commands::display::execute(command),
|
Commands::Display { command } => commands::display::execute(command),
|
||||||
Commands::Status => commands::status::execute(),
|
Commands::Status => commands::status::execute(),
|
||||||
|
Commands::Temperature => commands::temperature::execute(),
|
||||||
|
Commands::Fan => commands::fan::execute(),
|
||||||
Commands::InstallRules => commands::install_rules::execute(),
|
Commands::InstallRules => commands::install_rules::execute(),
|
||||||
Commands::Touchpad { command } => commands::touchpad::execute(command),
|
Commands::Touchpad { command } => commands::touchpad::execute(command),
|
||||||
Commands::Daemon => {
|
Commands::Daemon => {
|
||||||
|
|||||||
Reference in new issue
Block a user