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 | ||
|
|
c7c06f09b8 | ||
|
|
e4c3646fe0 | ||
|
|
9e5b618f19 | ||
|
|
59dabcf6f4 | ||
|
|
c03abaadc5 | ||
|
|
972618a8e3 | ||
|
|
74aa1ff262 | ||
|
|
0219f1c8ce | ||
|
|
191afd2f12 | ||
|
|
02f8ff4c33 | ||
|
|
75c3a39597 | ||
|
|
f5f73a4990 | ||
|
|
c018aeda57 | ||
|
|
73e1edfd97 | ||
|
|
4e46604cfd | ||
|
|
b6c0ca0b9b | ||
|
|
94aed29728 |
No files matched your search
@@ -0,0 +1,110 @@
|
|||||||
|
name: Release Smoke Test
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version to publish (e.g. 0.3.4)'
|
||||||
|
required: true
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
jobs:
|
||||||
|
smoke-test:
|
||||||
|
name: Smoke Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- name: Check formatting
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
- name: Run Clippy Lints
|
||||||
|
run: cargo clippy -- -D warnings
|
||||||
|
- name: Build release
|
||||||
|
run: cargo build --release --verbose
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose
|
||||||
|
- name: Smoke test — binary runs
|
||||||
|
run: ./target/release/lapctl --version
|
||||||
|
- name: Upload release artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lapctl-${{ github.ref_name }}
|
||||||
|
path: ./target/release/lapctl
|
||||||
|
- name: Publish to crates.io
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
|
run: cargo publish
|
||||||
|
publish-aur:
|
||||||
|
name: Publish to AUR
|
||||||
|
needs: smoke-test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Get version
|
||||||
|
id: version
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
cargo install cargo-aur
|
||||||
|
mkdir -p 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
|
||||||
|
run: |
|
||||||
|
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
|
||||||
|
uses: KSXGitHub/github-actions-deploy-aur@v4.1.3
|
||||||
|
with:
|
||||||
|
pkgname: lapctl-bin
|
||||||
|
pkgbuild: aur-pkg/PKGBUILD
|
||||||
|
assets: aur-pkg/.SRCINFO
|
||||||
|
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
|
ssh_keyscan_types: rsa,ecdsa,ed25519
|
||||||
|
commit_username: ${{ github.actor }}
|
||||||
|
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
+102
-3
@@ -216,6 +216,15 @@ version = "2.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block2"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||||
|
dependencies = [
|
||||||
|
"objc2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blocking"
|
name = "blocking"
|
||||||
version = "1.6.2"
|
version = "1.6.2"
|
||||||
@@ -257,6 +266,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.60"
|
version = "4.5.60"
|
||||||
@@ -318,6 +333,29 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctrlc"
|
||||||
|
version = "3.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162"
|
||||||
|
dependencies = [
|
||||||
|
"dispatch2",
|
||||||
|
"nix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dispatch2"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"block2",
|
||||||
|
"libc",
|
||||||
|
"objc2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -429,6 +467,16 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs2"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -596,10 +644,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lapctl"
|
name = "lapctl"
|
||||||
version = "0.3.3"
|
version = "0.3.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
|
"ctrlc",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"fs2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -619,9 +669,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.183"
|
version = "0.2.186"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
@@ -670,6 +720,33 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.31.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||||
|
dependencies = [
|
||||||
|
"objc2-encode",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-encode"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -1319,6 +1396,28 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
+7
-1
@@ -1,7 +1,11 @@
|
|||||||
[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"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/S4NKALP/lapctl"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
@@ -15,3 +19,5 @@ wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
|||||||
zbus = { version = "5.5.0", features = ["tokio"] }
|
zbus = { version = "5.5.0", features = ["tokio"] }
|
||||||
tokio = { version = "1.21", features = ["full"] }
|
tokio = { version = "1.21", features = ["full"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
|
ctrlc = "3.4"
|
||||||
|
fs2 = "0.4"
|
||||||
@@ -12,17 +12,17 @@
|
|||||||
|
|
||||||
### Why lapctl?
|
### Why lapctl?
|
||||||
|
|
||||||
Built with performance and simplicity in mind, it talks directly to your system's hardware interfaces (`sysfs`, `acpi`, `udev`). A high-performance background daemon (**lapctld**) handles all privileged operations via a secure D-Bus interface, allowing you to control your laptop completely **without sudo**.
|
Built for speed and simplicity, it talks directly to your system's hardware interfaces (`sysfs`, `acpi`, `udev`). A background daemon (**lapctld**) handles all privileged operations via a secure D-Bus interface, allowing you to control your laptop completely **without sudo**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Key Features
|
### Key Features
|
||||||
|
|
||||||
- **Graphics Switching**: Effortlessly toggle between Integrated, NVIDIA, and Hybrid modes. Optimize for battery life on the go or raw performance at your desk.
|
- **Graphics Switching**: Effortlessly toggle between Integrated, NVIDIA, and Hybrid modes. Wayland is auto detected; no `--wayland` flag needed. Optimize for battery life on the go or raw performance at your desk.
|
||||||
- **Battery Health**: Modern batteries hate being at 100% all the time. Set custom charge limits (like 80%) to significantly extend your battery's lifespan.
|
- **Battery Health**: Modern batteries hate being at 100% all the time. Set custom charge limits (like 80%) to significantly extend your battery's lifespan.
|
||||||
- **Power Tuning**: Switch through performance profiles or set hard CPU power (TDP) limits in Watts to keep things cool or let them loose.
|
- **Power Tuning**: Switch through performance profiles or set hard CPU power (TDP) limits in Watts to keep things cool or let them loose.
|
||||||
- **Intelligent Cooling**: Force your fans into Performance, Balanced, or Quiet modes (supporting ASUS and Lenovo laptops).
|
- **Intelligent Cooling**: Force your fans into Performance, Balanced, or Quiet modes (supporting ASUS and Lenovo laptops).
|
||||||
- **Display Refresh Rate**: Easily query available refresh rates and change your active display's Hz on-the-fly (100% native Rust Wayland implementation using `zwlr_output_manager_v1` for wlroots compositors like Sway and Hyprland).
|
- **Display Refresh Rate**: Easily query available refresh rates and change your active display's Hz on the fly (100% native Rust Wayland implementation using `zwlr_output_manager_v1` for wlroots compositors like Sway and Hyprland).
|
||||||
- **Touchpad Toggle**: Quickly enable or disable your touchpad from the terminal when using an external mouse.
|
- **Touchpad Toggle**: Quickly enable or disable your touchpad from the terminal when using an external mouse.
|
||||||
- **Instant Status**: Get a bird's eye view of your hardware state, battery health, and current limits with one simple command.
|
- **Instant Status**: Get a bird's eye view of your hardware state, battery health, and current limits with one simple command.
|
||||||
|
|
||||||
@@ -35,30 +35,31 @@ Built with performance and simplicity in mind, it talks directly to your system'
|
|||||||
#### 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).
|
- **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.
|
||||||
- **Wayland Display**: Built entirely natively using `wayland-client` and `wayland-protocols-wlr` (no `wlr-randr` required!)
|
- **Wayland Display**: Built entirely natively using `wayland-client` and `wayland-protocols-wlr` (no `wlr-randr` required!)
|
||||||
|
|
||||||
#### Limitations
|
#### Limitations
|
||||||
@@ -76,10 +77,11 @@ sudo lapctl install-rules
|
|||||||
# From now on, NO sudo is required!
|
# From now on, NO sudo is required!
|
||||||
|
|
||||||
# Manage your GPU
|
# Manage your GPU
|
||||||
lapctl gpu integrated # Max battery
|
lapctl gpu integrated # Max battery
|
||||||
lapctl gpu hybrid # Best of both worlds
|
lapctl gpu hybrid # Best of both worlds
|
||||||
lapctl gpu nvidia # High performance
|
lapctl gpu nvidia # High performance
|
||||||
lapctl gpu run steam # Run 'steam' on dGPU directly while in Hybrid mode
|
lapctl gpu nvidia --no-reboot # Switch without rebooting (Ctrl+C safe)
|
||||||
|
lapctl gpu run steam # Run 'steam' on dGPU directly while in Hybrid mode
|
||||||
|
|
||||||
# Prolong battery life
|
# Prolong battery life
|
||||||
lapctl battery limit 80
|
lapctl battery limit 80
|
||||||
@@ -126,6 +128,11 @@ lapctl
|
|||||||
└── tests/ # Robust integration & unit tests
|
└── tests/ # Robust integration & unit tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Safety features:**
|
||||||
|
- **Concurrency lock** -- Exclusive file lock (`/var/lock/lapctl.lock`) prevents concurrent GPU switch commands from corrupting state
|
||||||
|
- **Interrupt safety** -- If `--no-reboot` GPU switching is interrupted (Ctrl+C, crash), the display manager is automatically restarted via a RAII guard
|
||||||
|
- **Atomic writes** -- Cache file is written atomically (temp file + rename) to prevent partial/corrupt state on crash
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Development & Connection
|
### Development & Connection
|
||||||
|
|||||||
+7
-1
@@ -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
|
||||||
@@ -55,7 +59,7 @@ pub enum Commands {
|
|||||||
Daemon,
|
Daemon,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug, Clone)]
|
||||||
pub enum GpuCommands {
|
pub enum GpuCommands {
|
||||||
/// Query the current graphics mode
|
/// Query the current graphics mode
|
||||||
Query,
|
Query,
|
||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
+328
-67
@@ -7,14 +7,65 @@ use crate::utils::system::{
|
|||||||
assert_root, create_file, get_active_graphical_sessions, get_display_manager,
|
assert_root, create_file, get_active_graphical_sessions, get_display_manager,
|
||||||
is_service_active, manage_service, rebuild_initramfs, terminate_session,
|
is_service_active, manage_service, rebuild_initramfs, terminate_session,
|
||||||
};
|
};
|
||||||
|
use fs2::FileExt;
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use zbus::Connection;
|
use zbus::Connection;
|
||||||
use zbus::proxy;
|
use zbus::proxy;
|
||||||
|
|
||||||
|
static INTERRUPTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
fn setup_signal_handler() {
|
||||||
|
if let Err(e) = ctrlc::set_handler(|| {
|
||||||
|
INTERRUPTED.store(true, Ordering::SeqCst);
|
||||||
|
}) {
|
||||||
|
log::debug!("Could not set Ctrl+C handler (may already be set): {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DmGuard {
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DmGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(ref name) = self.name {
|
||||||
|
info!("Restarting display manager: {}", name);
|
||||||
|
if let Err(e) = manage_service(name, "start") {
|
||||||
|
error!("Failed to restart display manager '{}': {}", name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOCK_PATH: &str = "/var/lock/lapctl.lock";
|
||||||
|
|
||||||
|
struct LockGuard;
|
||||||
|
|
||||||
|
impl LockGuard {
|
||||||
|
fn acquire() -> Result<Self, String> {
|
||||||
|
let file = fs::File::create(LOCK_PATH)
|
||||||
|
.map_err(|e| format!("Failed to create lock file: {}", e))?;
|
||||||
|
file.try_lock_exclusive()
|
||||||
|
.map_err(|e| format!("Another GPU switch operation is already in progress: {}", e))?;
|
||||||
|
info!("Acquired exclusive lock");
|
||||||
|
Ok(LockGuard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for LockGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(e) = fs::remove_file(LOCK_PATH) {
|
||||||
|
log::debug!("Failed to remove lock file: {}", e);
|
||||||
|
}
|
||||||
|
info!("Released lock");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[proxy(
|
#[proxy(
|
||||||
interface = "org.lapctl1",
|
interface = "org.lapctl1",
|
||||||
default_service = "org.lapctl",
|
default_service = "org.lapctl",
|
||||||
@@ -109,6 +160,13 @@ fn try_call_daemon(cmd: &GpuCommands) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_wayland_session() -> bool {
|
||||||
|
std::env::var("WAYLAND_DISPLAY").is_ok()
|
||||||
|
|| std::env::var("XDG_SESSION_TYPE")
|
||||||
|
.map(|v| v == "wayland")
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
const CACHE_FILE_PATH: &str = "/var/cache/lapctl/cache.json";
|
const CACHE_FILE_PATH: &str = "/var/cache/lapctl/cache.json";
|
||||||
const BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf";
|
const BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf";
|
||||||
const BLACKLIST_CONTENT: &str = r#"# Automatically generated by lapctl
|
const BLACKLIST_CONTENT: &str = r#"# Automatically generated by lapctl
|
||||||
@@ -289,6 +347,42 @@ EndSection
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn xorg_modesetting(bus_id: &str) -> String {
|
||||||
|
format!(
|
||||||
|
r#"# Automatically generated by lapctl
|
||||||
|
|
||||||
|
Section "ServerLayout"
|
||||||
|
Identifier "layout"
|
||||||
|
Screen 0 "nvidia"
|
||||||
|
Inactive "modesetting"
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "Device"
|
||||||
|
Identifier "nvidia"
|
||||||
|
Driver "nvidia"
|
||||||
|
BusID "{}"
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "Screen"
|
||||||
|
Identifier "nvidia"
|
||||||
|
Device "nvidia"
|
||||||
|
Option "AllowEmptyInitialConfiguration"
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "Device"
|
||||||
|
Identifier "modesetting"
|
||||||
|
Driver "modesetting"
|
||||||
|
EndSection
|
||||||
|
|
||||||
|
Section "Screen"
|
||||||
|
Identifier "modesetting"
|
||||||
|
Device "modesetting"
|
||||||
|
EndSection
|
||||||
|
"#,
|
||||||
|
bus_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_xrandr_script(igpu_vendor: Option<&String>) -> String {
|
pub fn generate_xrandr_script(igpu_vendor: Option<&String>) -> String {
|
||||||
let source_name = match igpu_vendor.map(|s| s.as_str()) {
|
let source_name = match igpu_vendor.map(|s| s.as_str()) {
|
||||||
Some("intel") => "modesetting".to_string(),
|
Some("intel") => "modesetting".to_string(),
|
||||||
@@ -367,14 +461,24 @@ fn create_cache_file() {
|
|||||||
let bus_id = get_nvidia_gpu_pci_bus();
|
let bus_id = get_nvidia_gpu_pci_bus();
|
||||||
let cache = create_cache_obj(bus_id);
|
let cache = create_cache_obj(bus_id);
|
||||||
|
|
||||||
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() {
|
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent()
|
||||||
let _ = fs::create_dir_all(parent);
|
&& let Err(e) = fs::create_dir_all(parent)
|
||||||
|
{
|
||||||
|
log::debug!("Failed to create cache directory: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(json) = serde_json::to_string_pretty(&cache)
|
if let Ok(json) = serde_json::to_string_pretty(&cache) {
|
||||||
&& fs::write(CACHE_FILE_PATH, json).is_ok()
|
let tmp_path = format!("{}.tmp", CACHE_FILE_PATH);
|
||||||
{
|
if fs::write(&tmp_path, &json).is_ok() {
|
||||||
log::debug!("Created/Updated cache file {}", CACHE_FILE_PATH);
|
if fs::rename(&tmp_path, CACHE_FILE_PATH).is_ok() {
|
||||||
|
log::debug!("Created/Updated cache file {}", CACHE_FILE_PATH);
|
||||||
|
} else {
|
||||||
|
if let Err(e) = fs::remove_file(&tmp_path) {
|
||||||
|
log::debug!("Failed to remove stale temp cache file: {}", e);
|
||||||
|
}
|
||||||
|
log::debug!("Failed to rename temp cache file");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +496,8 @@ fn read_cache_file() -> Result<Cache, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_no_reboot(dm_flag: Option<String>) -> Result<Option<String>, String> {
|
fn prepare_no_reboot(dm_flag: Option<String>) -> Result<DmGuard, String> {
|
||||||
|
setup_signal_handler();
|
||||||
let dm_name = dm_flag.or_else(get_display_manager);
|
let dm_name = dm_flag.or_else(get_display_manager);
|
||||||
|
|
||||||
if let Some(ref name) = dm_name
|
if let Some(ref name) = dm_name
|
||||||
@@ -400,7 +505,9 @@ fn prepare_no_reboot(dm_flag: Option<String>) -> Result<Option<String>, String>
|
|||||||
{
|
{
|
||||||
info!("Stopping display manager: {}", name);
|
info!("Stopping display manager: {}", name);
|
||||||
manage_service(name, "stop")?;
|
manage_service(name, "stop")?;
|
||||||
return Ok(Some(name.clone()));
|
return Ok(DmGuard {
|
||||||
|
name: Some(name.clone()),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no active DM found, check for graphical sessions
|
// If no active DM found, check for graphical sessions
|
||||||
@@ -417,18 +524,18 @@ fn prepare_no_reboot(dm_flag: Option<String>) -> Result<Option<String>, String>
|
|||||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(DmGuard { name: None })
|
||||||
}
|
|
||||||
|
|
||||||
fn finalize_no_reboot(stopped_dm: Option<String>) {
|
|
||||||
if let Some(name) = stopped_dm {
|
|
||||||
info!("Starting display manager: {}", name);
|
|
||||||
let _ = manage_service(&name, "start");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn switch_integrated(no_reboot: bool) {
|
fn switch_integrated(no_reboot: bool) {
|
||||||
assert_root();
|
assert_root();
|
||||||
|
let _lock = match LockGuard::acquire() {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
error!("{}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
println!("Switching to integrated mode");
|
println!("Switching to integrated mode");
|
||||||
|
|
||||||
if !Path::new(CACHE_FILE_PATH).exists() {
|
if !Path::new(CACHE_FILE_PATH).exists() {
|
||||||
@@ -440,15 +547,14 @@ fn switch_integrated(no_reboot: bool) {
|
|||||||
create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false);
|
create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false);
|
||||||
create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false);
|
create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false);
|
||||||
|
|
||||||
let mut stopped_dm = None;
|
let _guard = if no_reboot {
|
||||||
if no_reboot {
|
let guard = match prepare_no_reboot(None) {
|
||||||
match prepare_no_reboot(None) {
|
Ok(g) => g,
|
||||||
Ok(dm) => stopped_dm = dm,
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if let Err(e) = kill_gpu_processes() {
|
if let Err(e) = kill_gpu_processes() {
|
||||||
error!("Failed to kill GPU processes: {}", e);
|
error!("Failed to kill GPU processes: {}", e);
|
||||||
@@ -457,7 +563,9 @@ fn switch_integrated(no_reboot: bool) {
|
|||||||
// Try to unload modules
|
// Try to unload modules
|
||||||
let modules = ["nvidia_uvm", "nvidia_modeset", "nvidia_drm", "nvidia"];
|
let modules = ["nvidia_uvm", "nvidia_modeset", "nvidia_drm", "nvidia"];
|
||||||
for module in modules {
|
for module in modules {
|
||||||
let _ = Command::new("modprobe").args(["-r", module]).status();
|
if let Err(e) = Command::new("modprobe").args(["-r", module]).status() {
|
||||||
|
log::debug!("Failed to run modprobe -r {}: {}", module, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pci_addr) = get_nvidia_gpu_pci_addr() {
|
if let Some(pci_addr) = get_nvidia_gpu_pci_addr() {
|
||||||
@@ -468,7 +576,11 @@ fn switch_integrated(no_reboot: bool) {
|
|||||||
error!("Failed to remove GPU: {}", e);
|
error!("Failed to remove GPU: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
guard
|
||||||
|
} else {
|
||||||
|
DmGuard { name: None }
|
||||||
|
};
|
||||||
|
|
||||||
let is_debug = log::log_enabled!(log::Level::Debug);
|
let is_debug = log::log_enabled!(log::Level::Debug);
|
||||||
let mut dis_cmd = Command::new("systemctl");
|
let mut dis_cmd = Command::new("systemctl");
|
||||||
@@ -479,12 +591,15 @@ fn switch_integrated(no_reboot: bool) {
|
|||||||
}
|
}
|
||||||
match dis_cmd.status() {
|
match dis_cmd.status() {
|
||||||
Ok(s) if s.success() => println!("Successfully disabled nvidia-persistenced.service"),
|
Ok(s) if s.success() => println!("Successfully disabled nvidia-persistenced.service"),
|
||||||
_ => error!("An error ocurred while disabling service"),
|
Ok(s) => error!(
|
||||||
|
"nvidia-persistenced.service disable failed with exit code: {}",
|
||||||
|
s
|
||||||
|
),
|
||||||
|
Err(e) => error!("Failed to run systemctl: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
if no_reboot {
|
if no_reboot {
|
||||||
println!("Operation completed successfully");
|
println!("Operation completed successfully");
|
||||||
finalize_no_reboot(stopped_dm);
|
|
||||||
} else {
|
} else {
|
||||||
rebuild_initramfs();
|
rebuild_initramfs();
|
||||||
println!("Operation completed successfully");
|
println!("Operation completed successfully");
|
||||||
@@ -494,6 +609,13 @@ fn switch_integrated(no_reboot: bool) {
|
|||||||
|
|
||||||
fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
|
fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
|
||||||
assert_root();
|
assert_root();
|
||||||
|
let _lock = match LockGuard::acquire() {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
error!("{}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
println!("Switching to hybrid mode");
|
println!("Switching to hybrid mode");
|
||||||
println!(
|
println!(
|
||||||
"Enable PCI-Express Runtime D3 (RTD3) Power Management: {}",
|
"Enable PCI-Express Runtime D3 (RTD3) Power Management: {}",
|
||||||
@@ -510,20 +632,23 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
|
|||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
let mut stopped_dm = None;
|
let _guard = if no_reboot {
|
||||||
if no_reboot {
|
let guard = match prepare_no_reboot(None) {
|
||||||
match prepare_no_reboot(None) {
|
Ok(g) => g,
|
||||||
Ok(dm) => stopped_dm = dm,
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if let Err(e) = kill_gpu_processes() {
|
if let Err(e) = kill_gpu_processes() {
|
||||||
error!("Failed to kill GPU processes: {}", e);
|
error!("Failed to kill GPU processes: {}", e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
guard
|
||||||
|
} else {
|
||||||
|
DmGuard { name: None }
|
||||||
|
};
|
||||||
|
|
||||||
// persistenced enable
|
// persistenced enable
|
||||||
let is_debug = log::log_enabled!(log::Level::Debug);
|
let is_debug = log::log_enabled!(log::Level::Debug);
|
||||||
@@ -535,7 +660,11 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
|
|||||||
}
|
}
|
||||||
match enable_cmd.status() {
|
match enable_cmd.status() {
|
||||||
Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"),
|
Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"),
|
||||||
_ => error!("An error ocurred while enabling service"),
|
Ok(s) => error!(
|
||||||
|
"nvidia-persistenced.service enable failed with exit code: {}",
|
||||||
|
s
|
||||||
|
),
|
||||||
|
Err(e) => error!("Failed to run systemctl: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(val) = rtd3 {
|
if let Some(val) = rtd3 {
|
||||||
@@ -563,7 +692,6 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
|
|||||||
|
|
||||||
if no_reboot {
|
if no_reboot {
|
||||||
println!("Operation completed successfully");
|
println!("Operation completed successfully");
|
||||||
finalize_no_reboot(stopped_dm);
|
|
||||||
} else {
|
} else {
|
||||||
rebuild_initramfs();
|
rebuild_initramfs();
|
||||||
println!("Operation completed successfully");
|
println!("Operation completed successfully");
|
||||||
@@ -580,6 +708,16 @@ fn switch_nvidia(
|
|||||||
no_reboot: bool,
|
no_reboot: bool,
|
||||||
) {
|
) {
|
||||||
assert_root();
|
assert_root();
|
||||||
|
let _lock = match LockGuard::acquire() {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
error!("{}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let wayland = wayland || is_wayland_session();
|
||||||
|
|
||||||
println!("Switching to nvidia mode");
|
println!("Switching to nvidia mode");
|
||||||
println!("Enable ForceCompositionPipeline: {}", force_comp);
|
println!("Enable ForceCompositionPipeline: {}", force_comp);
|
||||||
println!("Enable Coolbits: {}", coolbits.is_some());
|
println!("Enable Coolbits: {}", coolbits.is_some());
|
||||||
@@ -628,6 +766,8 @@ fn switch_nvidia(
|
|||||||
} else if vendor == "amd" {
|
} else if vendor == "amd" {
|
||||||
create_file(XORG_PATH, &xorg_amd(&pci_bus), false);
|
create_file(XORG_PATH, &xorg_amd(&pci_bus), false);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
create_file(XORG_PATH, &xorg_modesetting(&pci_bus), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut extra_xorg = EXTRA_XORG_CONTENT.to_string();
|
let mut extra_xorg = EXTRA_XORG_CONTENT.to_string();
|
||||||
@@ -668,20 +808,23 @@ fn switch_nvidia(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut stopped_dm = None;
|
let _guard = if no_reboot {
|
||||||
if no_reboot {
|
let guard = match prepare_no_reboot(dm.clone()) {
|
||||||
match prepare_no_reboot(dm.clone()) {
|
Ok(g) => g,
|
||||||
Ok(dm_name) => stopped_dm = dm_name,
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if let Err(e) = kill_gpu_processes() {
|
if let Err(e) = kill_gpu_processes() {
|
||||||
error!("Failed to kill GPU processes: {}", e);
|
error!("Failed to kill GPU processes: {}", e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
guard
|
||||||
|
} else {
|
||||||
|
DmGuard { name: None }
|
||||||
|
};
|
||||||
|
|
||||||
// persistenced enable
|
// persistenced enable
|
||||||
let is_debug = log::log_enabled!(log::Level::Debug);
|
let is_debug = log::log_enabled!(log::Level::Debug);
|
||||||
@@ -693,12 +836,15 @@ fn switch_nvidia(
|
|||||||
}
|
}
|
||||||
match enable_cmd.status() {
|
match enable_cmd.status() {
|
||||||
Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"),
|
Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"),
|
||||||
_ => error!("An error ocurred while enabling service"),
|
Ok(s) => error!(
|
||||||
|
"nvidia-persistenced.service enable failed with exit code: {}",
|
||||||
|
s
|
||||||
|
),
|
||||||
|
Err(e) => error!("Failed to run systemctl: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
if no_reboot {
|
if no_reboot {
|
||||||
println!("Operation completed successfully");
|
println!("Operation completed successfully");
|
||||||
finalize_no_reboot(stopped_dm);
|
|
||||||
} else {
|
} else {
|
||||||
rebuild_initramfs();
|
rebuild_initramfs();
|
||||||
println!("Operation completed successfully");
|
println!("Operation completed successfully");
|
||||||
@@ -708,54 +854,160 @@ 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)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(cmd: &GpuCommands) {
|
pub fn execute(cmd: &GpuCommands) {
|
||||||
match cmd {
|
let cmd = match cmd {
|
||||||
|
GpuCommands::Nvidia {
|
||||||
|
wayland: false,
|
||||||
|
dm,
|
||||||
|
force_comp,
|
||||||
|
coolbits,
|
||||||
|
use_nvidia_current,
|
||||||
|
no_reboot,
|
||||||
|
} if is_wayland_session() => {
|
||||||
|
println!("Auto-detected Wayland session");
|
||||||
|
GpuCommands::Nvidia {
|
||||||
|
wayland: true,
|
||||||
|
dm: dm.clone(),
|
||||||
|
force_comp: *force_comp,
|
||||||
|
coolbits: *coolbits,
|
||||||
|
use_nvidia_current: *use_nvidia_current,
|
||||||
|
no_reboot: *no_reboot,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => cmd.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match &cmd {
|
||||||
GpuCommands::Integrated { .. } => println!("Switching to Integrated GPU mode..."),
|
GpuCommands::Integrated { .. } => println!("Switching to Integrated GPU mode..."),
|
||||||
GpuCommands::Hybrid { .. } => println!("Switching to Hybrid GPU mode..."),
|
GpuCommands::Hybrid { .. } => println!("Switching to Hybrid GPU mode..."),
|
||||||
GpuCommands::Nvidia { .. } => println!("Switching to NVIDIA-only GPU mode..."),
|
GpuCommands::Nvidia { .. } => println!("Switching to NVIDIA-only GPU mode..."),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if try_call_daemon(cmd) {
|
if try_call_daemon(&cmd) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
execute_local(cmd);
|
execute_local(&cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_local(cmd: &GpuCommands) {
|
pub fn execute_local(cmd: &GpuCommands) {
|
||||||
@@ -771,9 +1023,13 @@ pub fn execute_local(cmd: &GpuCommands) {
|
|||||||
GpuCommands::CacheDelete => {
|
GpuCommands::CacheDelete => {
|
||||||
assert_root();
|
assert_root();
|
||||||
if Path::new(CACHE_FILE_PATH).exists() {
|
if Path::new(CACHE_FILE_PATH).exists() {
|
||||||
let _ = fs::remove_file(CACHE_FILE_PATH);
|
if let Err(e) = fs::remove_file(CACHE_FILE_PATH) {
|
||||||
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() {
|
error!("Failed to remove cache file: {}", e);
|
||||||
let _ = fs::remove_dir(parent);
|
}
|
||||||
|
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent()
|
||||||
|
&& let Err(e) = fs::remove_dir(parent)
|
||||||
|
{
|
||||||
|
log::debug!("Failed to remove cache directory: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -793,9 +1049,13 @@ pub fn execute_local(cmd: &GpuCommands) {
|
|||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
if Path::new(CACHE_FILE_PATH).exists() {
|
if Path::new(CACHE_FILE_PATH).exists() {
|
||||||
let _ = fs::remove_file(CACHE_FILE_PATH);
|
if let Err(e) = fs::remove_file(CACHE_FILE_PATH) {
|
||||||
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() {
|
error!("Failed to remove cache file: {}", e);
|
||||||
let _ = fs::remove_dir(parent);
|
}
|
||||||
|
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent()
|
||||||
|
&& let Err(e) = fs::remove_dir(parent)
|
||||||
|
{
|
||||||
|
log::debug!("Failed to remove cache directory: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rebuild_initramfs();
|
rebuild_initramfs();
|
||||||
@@ -828,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,5 +1,5 @@
|
|||||||
use crate::utils::system::{assert_root, create_file};
|
use crate::utils::system::{assert_root, create_file};
|
||||||
use log::info;
|
use log::{error, info};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
// No longer needed: all privileged operations are now handled by the lapctld daemon via D-Bus.
|
// No longer needed: all privileged operations are now handled by the lapctld daemon via D-Bus.
|
||||||
@@ -76,8 +76,20 @@ WantedBy=multi-user.target
|
|||||||
pub fn execute() {
|
pub fn execute() {
|
||||||
assert_root();
|
assert_root();
|
||||||
|
|
||||||
let current_exe = std::env::current_exe().expect("Failed to get current executable path");
|
let current_exe = match std::env::current_exe() {
|
||||||
let exe_path = current_exe.to_str().expect("Invalid executable path");
|
Ok(exe) => exe,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to get current executable path: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let exe_path = match current_exe.to_str() {
|
||||||
|
Some(p) => p,
|
||||||
|
None => {
|
||||||
|
error!("Executable path contains invalid UTF-8");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
println!("Found lapctl at: {}", exe_path);
|
println!("Found lapctl at: {}", exe_path);
|
||||||
println!("Installing lapctl configuration with dynamic paths...");
|
println!("Installing lapctl configuration with dynamic paths...");
|
||||||
@@ -105,21 +117,39 @@ pub fn execute() {
|
|||||||
println!("Activating background daemon (lapctld)...");
|
println!("Activating background daemon (lapctld)...");
|
||||||
|
|
||||||
// Reload systemd to recognize the new service
|
// Reload systemd to recognize the new service
|
||||||
let _ = Command::new("systemctl").arg("daemon-reload").status();
|
if let Err(e) = Command::new("systemctl").arg("daemon-reload").status() {
|
||||||
|
error!("Failed to run systemctl daemon-reload: {}", e);
|
||||||
|
println!("You may need to run 'systemctl daemon-reload' manually.");
|
||||||
|
}
|
||||||
|
|
||||||
// Enable and start the daemon
|
// Enable and start the daemon
|
||||||
let status = Command::new("systemctl")
|
let status = Command::new("systemctl")
|
||||||
.args(["enable", "--now", "lapctld"])
|
.args(["enable", "--now", "lapctld"])
|
||||||
.status();
|
.status();
|
||||||
|
|
||||||
if let Ok(s) = status {
|
let daemon_ok = match status {
|
||||||
if s.success() {
|
Ok(s) if s.success() => {
|
||||||
info!("lapctld daemon enabled and started.");
|
info!("lapctld daemon enabled and started.");
|
||||||
} else {
|
true
|
||||||
log::error!("Failed to enable/start lapctld daemon.");
|
|
||||||
}
|
}
|
||||||
}
|
Ok(s) => {
|
||||||
|
log::error!(
|
||||||
|
"systemctl enable --now lapctld failed with exit code: {}",
|
||||||
|
s
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to run systemctl: {}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
println!("Installation successful! The lapctld daemon is now running.");
|
if daemon_ok {
|
||||||
|
println!("Installation successful! The lapctld daemon is now running.");
|
||||||
|
} else {
|
||||||
|
println!("Installation completed (but daemon may not be running).");
|
||||||
|
println!("Try starting it manually with: systemctl start lapctld");
|
||||||
|
}
|
||||||
println!("You can now use all lapctl commands from any location.");
|
println!("You can now use all lapctl commands from any location.");
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
+25
-3
@@ -209,6 +209,10 @@ fn set_amd_hwmon_limit(watts: u32) -> bool {
|
|||||||
}
|
}
|
||||||
success
|
success
|
||||||
}
|
}
|
||||||
|
fn is_ideapad() -> bool {
|
||||||
|
Path::new("/sys/bus/platform/drivers/ideapad_acpi").exists()
|
||||||
|
}
|
||||||
|
|
||||||
fn set_platform_profile(profile: &str) {
|
fn set_platform_profile(profile: &str) {
|
||||||
let platform_profile_path = Path::new("/sys/firmware/acpi/platform_profile");
|
let platform_profile_path = Path::new("/sys/firmware/acpi/platform_profile");
|
||||||
if platform_profile_path.exists() {
|
if platform_profile_path.exists() {
|
||||||
@@ -242,21 +246,39 @@ pub fn execute_local(command: &PowerCommands) {
|
|||||||
match command {
|
match command {
|
||||||
PowerCommands::Performance => {
|
PowerCommands::Performance => {
|
||||||
println!("Setting power profile to Performance");
|
println!("Setting power profile to Performance");
|
||||||
set_platform_profile("performance");
|
if !is_ideapad() {
|
||||||
|
set_platform_profile("performance");
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"Skipping platform_profile (decoupled from fan control on Ideapad). Use 'lapctl cooling' for fan speed."
|
||||||
|
);
|
||||||
|
}
|
||||||
set_cpu_governor("performance");
|
set_cpu_governor("performance");
|
||||||
set_energy_performance_preference("performance");
|
set_energy_performance_preference("performance");
|
||||||
println!("Operation completed successfully.");
|
println!("Operation completed successfully.");
|
||||||
}
|
}
|
||||||
PowerCommands::Balanced => {
|
PowerCommands::Balanced => {
|
||||||
println!("Setting power profile to Balanced");
|
println!("Setting power profile to Balanced");
|
||||||
set_platform_profile("balanced");
|
if !is_ideapad() {
|
||||||
|
set_platform_profile("balanced");
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"Skipping platform_profile (decoupled from fan control on Ideapad). Use 'lapctl cooling' for fan speed."
|
||||||
|
);
|
||||||
|
}
|
||||||
set_cpu_governor("schedutil");
|
set_cpu_governor("schedutil");
|
||||||
set_energy_performance_preference("balance_performance");
|
set_energy_performance_preference("balance_performance");
|
||||||
println!("Operation completed successfully.");
|
println!("Operation completed successfully.");
|
||||||
}
|
}
|
||||||
PowerCommands::BatterySave => {
|
PowerCommands::BatterySave => {
|
||||||
println!("Setting power profile to Battery Saver");
|
println!("Setting power profile to Battery Saver");
|
||||||
set_platform_profile("low-power");
|
if !is_ideapad() {
|
||||||
|
set_platform_profile("low-power");
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"Skipping platform_profile (decoupled from fan control on Ideapad). Use 'lapctl cooling' for fan speed."
|
||||||
|
);
|
||||||
|
}
|
||||||
set_cpu_governor("powersave");
|
set_cpu_governor("powersave");
|
||||||
set_energy_performance_preference("power");
|
set_energy_performance_preference("power");
|
||||||
println!("Operation completed successfully.");
|
println!("Operation completed successfully.");
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,12 @@ impl LapctlInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_battery_limit(&self, percent: u32) -> zbus::fdo::Result<()> {
|
pub async fn set_battery_limit(&self, percent: u32) -> zbus::fdo::Result<()> {
|
||||||
|
if percent == 0 || percent > 100 {
|
||||||
|
return Err(zbus::fdo::Error::InvalidArgs(format!(
|
||||||
|
"Invalid battery limit: {}. Must be between 1 and 100.",
|
||||||
|
percent
|
||||||
|
)));
|
||||||
|
}
|
||||||
crate::commands::battery::execute_local(&crate::cli::BatteryCommands::Limit {
|
crate::commands::battery::execute_local(&crate::cli::BatteryCommands::Limit {
|
||||||
percent: percent as u8,
|
percent: percent as u8,
|
||||||
});
|
});
|
||||||
|
|||||||
+32
-17
@@ -21,30 +21,42 @@ pub fn get_current_mode() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_nvidia_gpu_pci_bus() -> String {
|
pub fn get_nvidia_gpu_pci_bus() -> String {
|
||||||
let output = Command::new("lspci")
|
let output = match Command::new("lspci").output() {
|
||||||
.output()
|
Ok(o) => o,
|
||||||
.expect("Failed to execute lspci");
|
Err(e) => {
|
||||||
|
error!("Failed to execute lspci: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
let lspci_output = String::from_utf8_lossy(&output.stdout);
|
let lspci_output = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
for line in lspci_output.lines() {
|
for line in lspci_output.lines() {
|
||||||
if line.contains("NVIDIA")
|
if line.contains("NVIDIA")
|
||||||
&& (line.contains("VGA compatible controller") || line.contains("3D controller"))
|
&& (line.contains("VGA compatible controller") || line.contains("3D controller"))
|
||||||
{
|
{
|
||||||
let pci_bus_id_raw = line.split_whitespace().next().unwrap();
|
let pci_bus_id_raw = match line.split_whitespace().next() {
|
||||||
|
Some(id) => id,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
let pci_bus_id = pci_bus_id_raw.replace("0000:", "");
|
let pci_bus_id = pci_bus_id_raw.replace("0000:", "");
|
||||||
info!("Found Nvidia GPU at {}", pci_bus_id);
|
info!("Found Nvidia GPU at {}", pci_bus_id);
|
||||||
|
|
||||||
let parts: Vec<&str> = pci_bus_id.split(':').collect();
|
let parts: Vec<&str> = pci_bus_id.split(':').collect();
|
||||||
let bus = parts[0];
|
if parts.len() < 2 {
|
||||||
|
warn!("Unexpected PCI bus ID format: {}", pci_bus_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let device_fun: Vec<&str> = parts[1].split('.').collect();
|
let device_fun: Vec<&str> = parts[1].split('.').collect();
|
||||||
let device = device_fun[0];
|
if device_fun.len() < 2 {
|
||||||
let function = device_fun[1];
|
warn!("Unexpected PCI device.function format: {}", parts[1]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return format!(
|
return format!(
|
||||||
"PCI:{}:{}:{}",
|
"PCI:{}:{}:{}",
|
||||||
u32::from_str_radix(bus, 16).unwrap_or(0),
|
u32::from_str_radix(parts[0], 16).unwrap_or(0),
|
||||||
u32::from_str_radix(device, 16).unwrap_or(0),
|
u32::from_str_radix(device_fun[0], 16).unwrap_or(0),
|
||||||
u32::from_str_radix(function, 16).unwrap_or(0)
|
u32::from_str_radix(device_fun[1], 16).unwrap_or(0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,16 +67,14 @@ pub fn get_nvidia_gpu_pci_bus() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_nvidia_gpu_pci_addr() -> Option<String> {
|
pub fn get_nvidia_gpu_pci_addr() -> Option<String> {
|
||||||
let output = Command::new("lspci")
|
let output = Command::new("lspci").output().ok()?;
|
||||||
.output()
|
|
||||||
.expect("Failed to execute lspci");
|
|
||||||
let lspci_output = String::from_utf8_lossy(&output.stdout);
|
let lspci_output = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
for line in lspci_output.lines() {
|
for line in lspci_output.lines() {
|
||||||
if line.contains("NVIDIA")
|
if line.contains("NVIDIA")
|
||||||
&& (line.contains("VGA compatible controller") || line.contains("3D controller"))
|
&& (line.contains("VGA compatible controller") || line.contains("3D controller"))
|
||||||
{
|
{
|
||||||
let pci_addr = line.split_whitespace().next().unwrap();
|
let pci_addr = line.split_whitespace().next()?;
|
||||||
if pci_addr.contains(':') {
|
if pci_addr.contains(':') {
|
||||||
if pci_addr.chars().filter(|&c| c == ':').count() == 1 {
|
if pci_addr.chars().filter(|&c| c == ':').count() == 1 {
|
||||||
return Some(format!("0000:{}", pci_addr));
|
return Some(format!("0000:{}", pci_addr));
|
||||||
@@ -109,12 +119,17 @@ pub fn rescan_pci() -> Result<(), String> {
|
|||||||
pub fn kill_gpu_processes() -> Result<(), String> {
|
pub fn kill_gpu_processes() -> Result<(), String> {
|
||||||
info!("Killing processes using NVIDIA GPU...");
|
info!("Killing processes using NVIDIA GPU...");
|
||||||
// Try using fuser first as it is more likely to be available and easy to use
|
// Try using fuser first as it is more likely to be available and easy to use
|
||||||
let _ = Command::new("fuser").args(["-k", "/dev/nvidia*"]).output();
|
if let Err(e) = Command::new("fuser").args(["-k", "/dev/nvidia*"]).output() {
|
||||||
|
log::warn!("fuser failed (may not be installed): {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
// Also kill specific nvidia services if they are running
|
// Also kill specific nvidia services if they are running
|
||||||
let _ = Command::new("systemctl")
|
if let Err(e) = Command::new("systemctl")
|
||||||
.args(["stop", "nvidia-persistenced.service"])
|
.args(["stop", "nvidia-persistenced.service"])
|
||||||
.output();
|
.output()
|
||||||
|
{
|
||||||
|
log::debug!("Failed to stop nvidia-persistenced: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -28,10 +28,18 @@ 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 => {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = match tokio::runtime::Runtime::new() {
|
||||||
|
Ok(rt) => rt,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to create tokio runtime: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
if let Err(e) = lapctl::daemon::run().await {
|
if let Err(e) = lapctl::daemon::run().await {
|
||||||
eprintln!("Daemon error: {}", e);
|
eprintln!("Daemon error: {}", e);
|
||||||
|
|||||||
+33
-8
@@ -17,8 +17,12 @@ pub fn assert_root() {
|
|||||||
|
|
||||||
match status {
|
match status {
|
||||||
Ok(s) if s.success() => std::process::exit(0),
|
Ok(s) if s.success() => std::process::exit(0),
|
||||||
_ => {
|
Ok(s) => {
|
||||||
error!("Privilege escalation failed or was cancelled.");
|
error!("Privilege escalation failed with exit code: {}", s);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Privilege escalation failed: {}", e);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,8 +53,11 @@ pub fn create_file(path: &str, content: &str, executable: bool) {
|
|||||||
|
|
||||||
if executable && let Ok(mut perms) = fs::metadata(path_obj).map(|m| m.permissions()) {
|
if executable && let Ok(mut perms) = fs::metadata(path_obj).map(|m| m.permissions()) {
|
||||||
perms.set_mode(perms.mode() | 0o111);
|
perms.set_mode(perms.mode() | 0o111);
|
||||||
let _ = fs::set_permissions(path_obj, perms);
|
if let Err(e) = fs::set_permissions(path_obj, perms) {
|
||||||
info!("Added execution privilege to file {}", path);
|
error!("Failed to set executable permission on '{}': {}", path, e);
|
||||||
|
} else {
|
||||||
|
info!("Added execution privilege to file {}", path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -164,10 +171,17 @@ pub fn rebuild_initramfs() {
|
|||||||
Ok(status) if status.success() => {
|
Ok(status) if status.success() => {
|
||||||
println!("Successfully rebuilt the initramfs!");
|
println!("Successfully rebuilt the initramfs!");
|
||||||
}
|
}
|
||||||
_ => {
|
Ok(status) => {
|
||||||
error!("An error occurred while rebuilding the initramfs");
|
error!("initramfs rebuild failed with exit code: {}", status);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to run initramfs rebuild command: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"Unknown distribution: initramfs was not rebuilt. You may need to rebuild it manually before rebooting."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +191,14 @@ pub fn manage_service(name: &str, action: &str) -> Result<(), String> {
|
|||||||
cmd.args([action, name]);
|
cmd.args([action, name]);
|
||||||
match cmd.status() {
|
match cmd.status() {
|
||||||
Ok(s) if s.success() => Ok(()),
|
Ok(s) if s.success() => Ok(()),
|
||||||
_ => Err(format!("Failed to {} service {}", action, name)),
|
Ok(s) => Err(format!(
|
||||||
|
"systemctl {} {} failed with exit code: {}",
|
||||||
|
action, name, s
|
||||||
|
)),
|
||||||
|
Err(e) => Err(format!(
|
||||||
|
"Failed to run systemctl {} {}: {}",
|
||||||
|
action, name, e
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +208,10 @@ pub fn terminate_session(id: &str) -> Result<(), String> {
|
|||||||
cmd.args(["terminate-session", id]);
|
cmd.args(["terminate-session", id]);
|
||||||
match cmd.status() {
|
match cmd.status() {
|
||||||
Ok(s) if s.success() => Ok(()),
|
Ok(s) if s.success() => Ok(()),
|
||||||
_ => Err(format!("Failed to terminate session {}", id)),
|
Ok(s) => Err(format!(
|
||||||
|
"loginctl terminate-session {} failed with exit code: {}",
|
||||||
|
id, s
|
||||||
|
)),
|
||||||
|
Err(e) => Err(format!("Failed to run loginctl: {}", e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user