36 Commits
Author SHA1 Message Date
sankalp c7c06f09b8 fix(.github/workflows): correct package name in release-smoke.yml
- Bug Fixes: Update pkgname from lapctl to lapctl-bin to match AUR package naming
2026-05-28 20:24:55 +05:45
sankalp e4c3646fe0 chore(.github/workflows): add release smoke test workflowAdds CI pipeline that runs on tag and release events, checks formatting, lints, builds, tests, verifies binary execution, uploads artifact, and publishes to crates.io and AUR.
- New Features: - Implements automated release smoke testing pipeline.
- Enhancements: - Adds formatting linting and binary verification steps.
- Bug Fixes: -
2026-05-26 10:40:28 +05:45
sankalp 9e5b618f19 chore(root): add project metadata to Cargo.toml
- New Features: Added description, license, and repository fields to clarify project identity.
- Enhancements: Improved Cargo.toml metadata completeness.  - Bug Fixes: None
2026-05-26 10:36:25 +05:45
sankalp 59dabcf6f4 feat(.opencode/plans):add release smoke test workflow and update Cargo.toml metadata 2026-05-26 10:33:40 +05:45
sankalp c03abaadc5 feat(src/commands): Add Ideapad detection and conditional platform profile handling
Enables proper handling of Ideapad laptops by skipping `set_platform_profile` when it is decoupled from fan control, improving compatibility and user feedback.

New Features
- Added `is_ideapad()` helper function.

Enhancements
- Conditional execution of `set_platform_profile` based on Ideapad detection.
- Added informative messages when platform profile is skipped on Ideapad.

Bug Fixes
- None.
2026-05-25 19:02:19 +05:45
sankalp 972618a8e3 fix(src/hardware): improveerror handling for GPU process killing
New Features
- Added explicit error handling and logging for `fuser` command failures.

Enhancements
- Refined process termination logic with richer diagnostics and graceful degradation when tools are unavailable.

Bug Fixes
- Prevented silent failures when stopping `nvidia-persistenced.service` by logging errors instead of ignoring them.
2026-05-25 14:50:17 +05:45
sankalp 74aa1ff262 fix(src): handle tokio runtime creation errors gracefullyBug Fixes:
- Added proper error handling for tokio runtime creation, exiting with a clear error message instead of panicking.
2026-05-25 14:50:06 +05:45
sankalp 0219f1c8ce refactor(src/utils): improve error handling and messaging in system utilitiesNew Features
- None

Enhancements- Added detailed error reporting with exit codes for privilege escalation, initramfs rebuild, service management, and session termination
- Implemented graceful handling of permission‑setting failures with proper logging
- Enhanced error messages for systemctl and loginctl commands, providing clearer diagnostics

Bug Fixes- Fixed incomplete error propagation in status matching, now reports actual exit codes or underlying errors
2026-05-25 14:50:00 +05:45
sankalp 191afd2f12 feat(src/commands): enhance error handling, logging, and robustness in GPU command implementations
- **New Features**: Added detailed error logging for signal handling, directory creation, cache file operations, and service management; improved cleanup with explicit error propagation.
- **Enhancements**: Refactored Drop implementations to handle failures gracefully; upgraded status handling for systemctl commands with precise exit code reporting; strengthened directory and file removal with debug logging.
- **Bug Fixes**: Fixed missing error propagation in Ctrl+C handler; corrected service enable/disable status reporting; ensured proper removal of cache files and directories with appropriate logging of failures.
2026-05-25 14:49:48 +05:45
sankalp 02f8ff4c33 docs(root): Clarify Wayland auto‑detection and add safety features documentation
- New Features: Add `--no-reboot` flag usage example for GPU switching
- Enhancements: Document Wayland auto‑detection and new safety mechanisms (concurrency lock, interrupt‑safe restart, atomic writes)
- Bug Fixes: Minor phrasing corrections and typo fixes in README sections
2026-05-25 13:38:26 +05:45
sankalp 75c3a39597 feat(src/daemon): add battery limit validation to set_battery_limit
The change ensures that only valid battery percentages (1‑100) are processed, preventing out‑of‑range values from reaching the underlying command.

- New Features
  - Validation of battery limit percentage added.
- Enhancements  - Better error reporting for invalid arguments.
- Bug Fixes
  - Prevents processing of invalid battery limit values.
2026-05-25 13:36:40 +05:45
sankalp f5f73a4990 fix(src/hardware): improvePCI address parsing and error handling
- Improves robustness of NVIDIA GPU PCI bus detection.
- Adds validation for unexpected PCI format strings.
- Prevents panic by handling missing command output and malformed IDs gracefully.
2026-05-25 13:36:34 +05:45
sankalp c018aeda57 chore(root): Update dependencies and add new crates
Add several new crates (block2, cfg_aliases, ctrlc, dispatch2, fs2, nix, objc2, objc2-encode, winapi and their sub‑crates) to support new functionality and improve compatibility. Bump versions of core crates (libc, futures-core, etc.) to latest releases.

- New Features:
  - Introduce `block2`, `cfg_aliases`, `ctrlc`, `dispatch2`, `fs2`, `nix`, `objc2`, `objc2-encode`, and `winapi` (including target‑specific crates) to enable additional system interaction and async capabilities.
- Enhancements:
  - Upgrade `libc`, `futures-core`, `futures-util`, `tokio`, `clap`, `serde`, and other dependencies to latest stable versions for performance and security improvements.
- Bug Fixes:
  - Updated checksums and dependency resolution to reflect newer releases, reducing potential runtime issues.
2026-05-25 13:36:29 +05:45
sankalp 73e1edfd97 fix(src/utils): add warning for unknowndistribution when rebuilding initramfs
Log a warning when the initramfs rebuild is skipped because the distribution is unknown, ensuring the user is informed.

New Features
- None

Enhancements
- Added explicit warning for unknown distributions to prevent silent skips
- Improved logging clarity in rebuild_initramfs

Bug Fixes
- Fixed missing warning path that could hide build issues on unsupported distros
2026-05-25 13:36:23 +05:45
sankalp 4e46604cfd feat(src/commands): implement atomic interrupt handling, display‑manager guard, lock file for operation serialization, safe cache writes, and improve daemon enable messaging
The changes add robust interruption support, exclusive‑lock enforcement, and safer file handling to the GPU switching commands, while clarifying installation output.

- New Features  - Atomic `INTERRUPTED` flag with Ctrl‑C signal handler
  - `DmGuard` struct to manage display‑manager start/stop lifecycle
  - `LockGuard` with exclusive lock file (`/var/lock/lapctl.lock`) to serialize GPU switches
  - Safe temporary‑file write + rename for cache updates
- Enhancements
  - Unified `prepare_no_reboot` returns `DmGuard` instead of `Option<String>`
  - Consistent early‑exit handling with lock acquisition checks
  - Improved debug/info logging for DM actions
  - Restructured `finalize_no_reboot` to return a guard value
  - Clarified installation success/failure messages for `lapctld`
- Bug Fixes
  - Prevented race conditions on concurrent GPU switches by locking before operation
  - Ensured lock is released on early return paths
  - Fixed incomplete `finalize_no_reboot` handling
  - Corrected daemon enable detection and user feedback logic
2026-05-25 13:36:16 +05:45
sankalp b6c0ca0b9b feat(src): add Clone derive to GpuCommandsThe GpuCommands enum now derives Clone, enabling it to be cloned and used in contexts such as message passing or state storage.
New Features:
- Enables cloning of GpuCommands variants for greater flexibility.

Enhancements:
- Improves CLI ergonomics by allowing command enum to be transmitted across async boundaries.
2026-05-25 13:27:23 +05:45
sankalp 94aed29728 feat(src/commands): add Wayland detection and Xorg modesetting support
Enable automatic handling of NVIDIA commands in Wayland sessions and generate proper Xorg config snippets.

- New Features
  - Adds `is_wayland_session()` that detects a Wayland environment via `WAYLAND_DISPLAY` or `XDG_SESSION_TYPE`.
  - Adds `xorg_modesetting(bus_id)` that builds a full Xorg configuration snippet for the modesetting driver.

- Enhancements
  - `switch_nvidia()` now automatically treats NVIDIA commands as Wayland‑aware when a Wayland session is detected.
  - Refactored command matching in `execute()` to preserve original fields while injecting Wayland awareness.
  - Fixed daemon invocation to pass a reference to the (potentially transformed) command.

- Bug Fixes
  - Corrected argument passing in `try_call_daemon` and `execute_local` to avoid move errors.
  - Prevented duplicate command handling when a Wayland session is detected.
2026-05-25 13:27:17 +05:45
sankalp d12d0a0916 bumped to 0.3.3 2026-05-08 22:14:36 +05:45
sankalp 30dadec637 fixed clippy:derivable-imps warning 2026-05-08 22:12:31 +05:45
sankalp dba3995608 docs(root): Remove sleep inhibitordocumentation and inhibit daemon CLI examples
Clean up obsolete documentation and CLI usage examples from README.
2026-05-08 22:08:03 +05:45
sankalp c8a688f44f refactor(tests): drop inhibit test file 2026-05-08 22:07:55 +05:45
sankalp 7de76d0365 refactor(src/daemon): drop set_system_inhibition method and simplify Default impl
Simplify daemon interface by removing obsolete system inhibition logic.

New Features
-
Enhancements
- Cleaned up dead code path.
-
Bug Fixes
- Prevented potential file descriptor leak by removing inhibition handling.
2026-05-08 22:07:53 +05:45
sankalp 055fdcd500 refactor(src/utils): remove systemd-inhibit handling from rebuild_initramfsSimplify command construction by dropping unused inhibition logic.
- New Features:
- Enhancements: Simplified command construction in rebuild_initramfs.
- Bug Fixes: Resolved potential error when systemd-inhibit is unavailable.
2026-05-08 22:07:49 +05:45
sankalp 934aeda6aa refactor(src): remove inhibit command
New Features
-

Enhancements
- Dropped inhibit subcommand and related modules.
- Cleaned up command handling logic in main.rs.

Bug Fixes- No bug fixes introduced.
2026-05-08 22:07:37 +05:45
sankalp 8947bd643a feat(commands):simplify battery limit messaging for Ideapad laptopsThe command now detects Ideapad hardware and provides clearer messages about Conservation Mode, removing outdated warnings. 2026-03-23 18:30:37 +05:45
sankalp 5ac2aed516 chore(root): bump lapctl version to 0.3.2
Updated Cargo.lock and Cargo.toml to version 0.3.2 of the `lapctl` package.

New Features
- None

Enhancements
- Updated dependency version to 0.3.2Bug Fixes
- None
2026-03-23 17:28:36 +05:45
sankalp 4020f65e28 bumped to 0.3.2 2026-03-23 17:10:37 +05:45
sankalp d52b3992e3 feat(src/commands):add Ideapad conservation mode fallback and improve charge limit messaging 2026-03-23 17:08:36 +05:45
sankalp 01d2e148a0 refactor(src/daemon): replace execute calls with execute_local for dbus commands
Why: Adopt local execution path to streamline command handling and reduce RPC overhead.

New Features
-

Enhancements
- Replace `gpu::execute` with `gpu::execute_local` in integrated, hybrid, and nvidia command invocations.
- Replace `crate::commands::power`, `battery`, `cooling`, and `touchpad` executions with `execute_local`.
- Remove the unused `LAPCTL_DAEMON_INTERNAL` environment variable setting.

Bug Fixes
- None
2026-03-23 16:16:31 +05:45
sankalp 0dc00ec5c9 refactor(src/commands): Refactor battery and touchpad command execution
Refactored command flow to separate daemon invocation, added status handling, and improved error handling.

- New Features: Added `execute_local` helper and `BatteryCommands::Status` handling.
- Enhancements: Simplified daemon checks, removed redundant condition, improved readability and error messages.
- Bug Fixes: Fixed missed daemon return path for status command and ensured proper touchpad inhibition logic.
2026-03-23 16:16:25 +05:45
sankalp 319ca54300 bumped to 0.3.1 2026-03-23 15:40:19 +05:45
sankalp f12134a9c8 feat(src/daemon): set LAPCTL_DAEMON_INTERNAL env var internally
Set an internal environment variable when the daemon starts to allow components to detect internal mode and prepare for future runtime configuration.

- New Features
  - Introduce `LAPCTL_DAEMON_INTERNAL` set to "1" inside the daemon.
- Enhancements
  - Enable downstream components to detect internal daemon execution.
2026-03-23 15:39:08 +05:45
sankalp 86ac9b556c chore(root): remove PKGBUILD and update .gitignore
Removed legacy packaging script and cleaned .gitignore entries.

New Features
-

Enhancements
-

Bug Fixes
-
2026-03-23 15:39:04 +05:45
sankalp c564259a19 refactor(src/commands): add timeouthandling and internal daemon guard
Adds 2‑second timeouts around D‑Bus interactions and guards against internal daemon self‑calls; refactors command handling to use a consistent async timeout pattern and improves error reporting.

New Features
- Wrap D‑Bus calls with a 2‑second timeout to prevent hangs
- Early‑exit when $LAPCTL_DAEMON_INTERNAL is set to skip self‑calls- Apply consistent timeout usage across all commands

Enhancements
- Unified error handling using the `matches!` macro
- Reduced race conditions in connection establishment

Bug Fixes
- Prevent silent failures when a connection timeout occurs
2026-03-23 15:38:57 +05:45
sankalp 46cbcb5b4b update 2026-03-23 15:28:18 +05:45
sankalp 899d4c831f docs(root): Simplify Arch Linux installation instructions
Remove outdated Arch AUR install recommendation and clean up documentation wording

New Features
-

Enhancements
- Updated `yay -S` command to install `lapctl` instead of `lapctl-git`
- Removed redundant dependency and udev rules note
- Adjusted emphasis formatting in `install-rules` description

Bug Fixes
- none
2026-03-22 18:05:36 +05:45
19 changed files with 895 additions and 601 deletions

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
name: Release Smoke Test
on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+'
release:
types: [published]
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
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: 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
id: version
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Generate PKGBUILD with cargo-aur
run: |
cargo install cargo-aur
mkdir -p aur-pkg
cargo aur --output aur-pkg
- name: Generate .SRCINFO
working-directory: aur-pkg
run: makepkg --printsrcinfo > .SRCINFO
- name: Push to AUR
uses: KSXGitHub/github-actions-deploy-aur@v2.5.0
with:
pkgname: lapctl-bin
pkgver: ${{ steps.version.outputs.version }}
assets: |
aur-pkg/PKGBUILD
aur-pkg/.SRCINFO
ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
commit_username: ${{ github.actor }}
commit_email: ${{ github.actor }}@users.noreply.github.com
+1 -3
View File
@@ -23,6 +23,4 @@ target
# Added by cargo
/target
./lapctl.py
lapctl.py
/target
Generated
+102 -3
View File
@@ -216,6 +216,15 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]]
name = "blocking"
version = "1.6.2"
@@ -257,6 +266,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.60"
@@ -318,6 +333,29 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "downcast-rs"
version = "1.2.1"
@@ -429,6 +467,16 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "futures-core"
version = "0.3.32"
@@ -596,10 +644,12 @@ dependencies = [
[[package]]
name = "lapctl"
version = "0.2.0"
version = "0.3.3"
dependencies = [
"clap",
"ctrlc",
"env_logger",
"fs2",
"futures-util",
"log",
"regex",
@@ -619,9 +669,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.183"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "linux-raw-sys"
@@ -670,6 +720,33 @@ dependencies = [
"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]]
name = "once_cell"
version = "1.21.4"
@@ -1319,6 +1396,28 @@ dependencies = [
"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]]
name = "windows-link"
version = "0.2.1"
+6 -1
View File
@@ -1,7 +1,10 @@
[package]
name = "lapctl"
version = "0.2.0"
version = "0.3.3"
edition = "2024"
description = "CLI tool for managing laptop hardware features on Linux"
license = "MIT"
repository = "https://github.com/S4NKALP/lapctl"
[dependencies]
clap = { version = "4.5", features = ["derive"] }
@@ -15,3 +18,5 @@ wayland-protocols-wlr = { version = "0.3", features = ["client"] }
zbus = { version = "5.5.0", features = ["tokio"] }
tokio = { version = "1.21", features = ["full"] }
futures-util = "0.3"
ctrlc = "3.4"
fs2 = "0.4"
+16 -19
View File
@@ -12,19 +12,18 @@
### 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
- **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.
- **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).
- **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.
- **Sleep Inhibitor**: Running a long compile or a critical download? Use the inhibitor to stop your laptop from falling asleep mid task.
- **Instant Status**: Get a bird's eye view of your hardware state, battery health, and current limits with one simple command.
---
@@ -35,10 +34,8 @@ Built with performance and simplicity in mind, it talks directly to your system'
#### Option 1: Arch Linux (AUR)
If you are on Arch Linux, you can install **lapctl-git** from the AUR. This is the recommended method for Arch users as it automatically handles dependencies and udev rules.
```bash
yay -S lapctl-git
yay -S lapctl
```
#### Option 2: Using `just` (Recommended for other distros)
@@ -50,7 +47,7 @@ just install
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`
@@ -61,8 +58,7 @@ sudo lapctl install-rules
#### Requirements
- **systemd**: For sleep inhibitor (`systemd-inhibit`)
- **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!)
#### Limitations
@@ -80,10 +76,11 @@ sudo lapctl install-rules
# From now on, NO sudo is required!
# Manage your GPU
lapctl gpu integrated # Max battery
lapctl gpu hybrid # Best of both worlds
lapctl gpu nvidia # High performance
lapctl gpu run steam # Run 'steam' on dGPU directly while in Hybrid mode
lapctl gpu integrated # Max battery
lapctl gpu hybrid # Best of both worlds
lapctl gpu nvidia # High performance
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
lapctl battery limit 80
@@ -106,11 +103,6 @@ lapctl touchpad enable
lapctl display rates
lapctl display set-rate 144
# Keep it awake (requires lapctld)
lapctl inhibit --daemon # Stay awake persistently (via daemon)
lapctl inhibit --stop # Allow sleep again
lapctl inhibit --why "Keeping awake" -- sleep 60 # One-shot (local)
# Check everything
lapctl status
```
@@ -135,6 +127,11 @@ lapctl
└── 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
+1 -18
View File
@@ -51,28 +51,11 @@ pub enum Commands {
#[command(subcommand)]
command: TouchpadCommands,
},
/// Inhibit system sleep/suspend
Inhibit {
/// The command to run while inhibiting (optional)
command: Vec<String>,
/// Why the system is being inhibited
#[arg(long, default_value = "lapctl inhibiting sleep")]
why: String,
/// Who is inhibiting the system
#[arg(long, default_value = "lapctl")]
who: String,
/// Run the inhibitor in the background (daemon mode)
#[arg(long)]
daemon: bool,
/// Stop any active persistent inhibition managed by the daemon
#[arg(long)]
stop: bool,
},
/// Start the lapctl background daemon (requires root)
Daemon,
}
#[derive(Subcommand, Debug)]
#[derive(Subcommand, Debug, Clone)]
pub enum GpuCommands {
/// Query the current graphics mode
Query,
+99 -74
View File
@@ -14,6 +14,10 @@ trait Lapctl {
async fn set_battery_limit(&self, percent: u32) -> zbus::Result<()>;
}
fn is_ideapad() -> bool {
Path::new("/sys/bus/platform/drivers/ideapad_acpi").exists()
}
fn try_call_daemon(command: &BatteryCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
@@ -21,30 +25,35 @@ fn try_call_daemon(command: &BatteryCommands) -> bool {
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let connection =
match tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
let res = match command {
BatteryCommands::Limit { percent } => {
proxy.set_battery_limit(*percent as u32).await.is_ok()
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_battery_limit(*percent as u32),
)
.await
}
_ => false,
}
_ => return false,
};
matches!(res, Ok(Ok(_)))
})
}
pub fn execute(command: &BatteryCommands) {
if matches!(command, BatteryCommands::Limit { .. }) && try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
match command {
BatteryCommands::Limit { percent } => {
if !(&1..=&100).contains(&percent) {
@@ -52,9 +61,51 @@ pub fn execute(command: &BatteryCommands) {
std::process::exit(1);
}
println!("Setting battery charge limit to {}%", percent);
if is_ideapad() {
if *percent < 100 {
println!("Setting battery charge limit (Lenovo Conservation Mode ~60%)...");
} else {
println!("Disabling battery charge limit (charging to 100%)...");
}
} else {
println!("Setting battery charge limit to {}%...", percent);
}
}
BatteryCommands::Status => {}
}
if matches!(command, BatteryCommands::Limit { .. }) && try_call_daemon(command) {
return;
}
execute_local(command);
}
pub fn execute_local(command: &BatteryCommands) {
match command {
BatteryCommands::Limit { percent } => {
let mut success_count = 0;
// Ideapad fallback - search for conservation_mode in ideapad_acpi driver directories
if let Ok(drivers) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi") {
for entry in drivers.flatten() {
let conservation_path = entry.path().join("conservation_mode");
if conservation_path.exists() {
let val = if *percent < 100 { "1" } else { "0" };
match fs::write(&conservation_path, val) {
Ok(_) => {
success_count += 1;
}
Err(e) => {
debug!("Failed to write to {}: {}", conservation_path.display(), e)
}
}
}
}
}
// Only try standard thresholds if we haven't already succeeded with Ideapad fallback
// or if we want to ensure all bases are covered. For now, let's do both but prioritize the message.
let sys_class_power = Path::new("/sys/class/power_supply");
if let Ok(entries) = fs::read_dir(sys_class_power) {
@@ -65,6 +116,10 @@ pub fn execute(command: &BatteryCommands) {
if name_str.starts_with("BAT") {
let threshold_path = entry.path().join("charge_control_end_threshold");
if threshold_path.exists() {
println!(
"Setting battery charge limit for {} to {}%",
name_str, percent
);
match fs::write(&threshold_path, percent.to_string()) {
Ok(_) => {
info!("Successfully set limit for {}", name_str);
@@ -74,49 +129,6 @@ pub fn execute(command: &BatteryCommands) {
debug!("Failed to write to {}: {}", threshold_path.display(), e)
}
}
} else {
// Ideapad fallback
let mut ideapad_found = false;
if let Ok(ideapad_entries) =
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
{
for ideapad_entry in ideapad_entries.flatten() {
let ideapad_path = ideapad_entry.path();
let conservation_path = ideapad_path.join("conservation_mode");
if conservation_path.exists() {
ideapad_found = true;
let val = if *percent < 100 { "1" } else { "0" };
match fs::write(&conservation_path, val) {
Ok(_) => {
if *percent < 100 {
println!(
"WARNING: Your laptop (Lenovo Ideapad) does NOT support custom charge limits (like {}%).",
percent
);
println!(
"Instead, lapctl has enabled Lenovo's built-in 'Conservation Mode' via the Ideapad ACPI."
);
println!(
"This mode is hard-coded into your laptop's firmware to stop charging at ~60%."
);
} else {
println!(
"Disabled Conservation Mode (charging to 100%)."
);
}
success_count += 1;
}
Err(e) => debug!(
"Failed to write to conservation_mode: {}",
e
),
}
}
}
}
if !ideapad_found {
debug!("limit not supported for {} (missing thresholds)", name_str);
}
}
}
}
@@ -131,7 +143,6 @@ pub fn execute(command: &BatteryCommands) {
}
}
BatteryCommands::Status => {
// Re-use logic from `lapctl status` to show battery-specific details:
let sys_class_power = Path::new("/sys/class/power_supply");
if let Ok(entries) = fs::read_dir(sys_class_power) {
for entry in entries.flatten() {
@@ -148,25 +159,39 @@ pub fn execute(command: &BatteryCommands) {
println!(" Capacity: {}%", capacity.trim());
println!(" Status: {}", status.trim());
if let Ok(limit) =
fs::read_to_string(bat_path.join("charge_control_end_threshold"))
// Check for Ideapad conservation mode first using robust detection
let mut limit_shown = false;
if let Ok(drivers) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
{
println!(" Charge Limit: {}%", limit.trim());
} else {
// Ideapad fallback display
if let Ok(ideapad_entries) =
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
for entry in drivers.flatten() {
let conservation_path = entry.path().join("conservation_mode");
if conservation_path.exists()
&& let Ok(mode) = fs::read_to_string(&conservation_path)
{
if mode.trim() == "1" {
println!(" Charge Limit: Conservation Mode (~60%)");
limit_shown = true;
} else if mode.trim() == "0" {
// On Ideapads 0 means 100% or off.
}
}
}
}
if !limit_shown {
if let Ok(limit) =
fs::read_to_string(bat_path.join("charge_control_end_threshold"))
{
for ideapad_entry in ideapad_entries.flatten() {
let conservation_path =
ideapad_entry.path().join("conservation_mode");
if conservation_path.exists()
&& let Ok(mode) = fs::read_to_string(&conservation_path)
{
if mode.trim() == "1" {
println!(" Charge Limit: Conservation Mode (~60%)");
} else {
println!(" Charge Limit: {}%", limit.trim());
} else {
// Re-check if it's an ideapad and we haven't shown a limit (likely 100%)
if let Ok(drivers) =
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
{
for entry in drivers.flatten() {
if entry.path().join("conservation_mode").exists() {
println!(" Charge Limit: 100%");
break;
}
}
}
+42 -14
View File
@@ -21,26 +21,44 @@ fn try_call_daemon(command: &CoolingCommands) -> bool {
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let connection =
match tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
CoolingCommands::Performance => proxy
.set_cooling_profile("performance".to_string())
let res = match command {
CoolingCommands::Performance => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_cooling_profile("performance".to_string()),
)
.await
.is_ok(),
CoolingCommands::Balanced => proxy
.set_cooling_profile("balanced".to_string())
}
CoolingCommands::Balanced => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_cooling_profile("balanced".to_string()),
)
.await
.is_ok(),
CoolingCommands::Quiet => proxy.set_cooling_profile("quiet".to_string()).await.is_ok(),
}
}
CoolingCommands::Quiet => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_cooling_profile("quiet".to_string()),
)
.await
}
};
matches!(res, Ok(Ok(_)))
})
}
@@ -79,11 +97,21 @@ fn set_asus_throttle_policy(mode: &str) -> bool {
}
pub fn execute(command: &CoolingCommands) {
let desc = match command {
CoolingCommands::Performance => "Performance",
CoolingCommands::Balanced => "Balanced",
CoolingCommands::Quiet => "Quiet",
};
println!("Setting thermal/cooling profile to {}", desc);
if try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
execute_local(command);
}
pub fn execute_local(command: &CoolingCommands) {
let (ideapad_mode, asus_mode, desc) = match command {
CoolingCommands::Performance => ("1", "1", "Performance"),
CoolingCommands::Balanced => ("0", "0", "Balanced"),
+264 -66
View File
@@ -7,14 +7,65 @@ use crate::utils::system::{
assert_root, create_file, get_active_graphical_sessions, get_display_manager,
is_service_active, manage_service, rebuild_initramfs, terminate_session,
};
use fs2::FileExt;
use log::{error, info};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use zbus::Connection;
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(
interface = "org.lapctl1",
default_service = "org.lapctl",
@@ -46,18 +97,26 @@ fn try_call_daemon(cmd: &GpuCommands) -> bool {
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let connection =
match tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match cmd {
let res = match cmd {
GpuCommands::Integrated { no_reboot } => {
proxy.switch_gpu_integrated(*no_reboot).await.is_ok()
tokio::time::timeout(
std::time::Duration::from_secs(5),
proxy.switch_gpu_integrated(*no_reboot),
)
.await
}
GpuCommands::Hybrid {
rtd3,
@@ -65,10 +124,11 @@ fn try_call_daemon(cmd: &GpuCommands) -> bool {
no_reboot,
} => {
let rtd3_val = rtd3.map(|v| v as i32).unwrap_or(-1);
proxy
.switch_gpu_hybrid(rtd3_val, *use_nvidia_current, *no_reboot)
.await
.is_ok()
tokio::time::timeout(
std::time::Duration::from_secs(5),
proxy.switch_gpu_hybrid(rtd3_val, *use_nvidia_current, *no_reboot),
)
.await
}
GpuCommands::Nvidia {
dm,
@@ -80,23 +140,33 @@ fn try_call_daemon(cmd: &GpuCommands) -> bool {
} => {
let dm_val = dm.clone().unwrap_or_default();
let coolbits_val = coolbits.map(|v| v as i32).unwrap_or(-1);
proxy
.switch_gpu_nvidia(
tokio::time::timeout(
std::time::Duration::from_secs(5),
proxy.switch_gpu_nvidia(
dm_val,
*force_comp,
coolbits_val,
*use_nvidia_current,
*wayland,
*no_reboot,
)
.await
.is_ok()
),
)
.await
}
_ => false,
}
_ => return false,
};
matches!(res, Ok(Ok(_)))
})
}
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 BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf";
const BLACKLIST_CONTENT: &str = r#"# Automatically generated by lapctl
@@ -277,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 {
let source_name = match igpu_vendor.map(|s| s.as_str()) {
Some("intel") => "modesetting".to_string(),
@@ -355,14 +461,24 @@ fn create_cache_file() {
let bus_id = get_nvidia_gpu_pci_bus();
let cache = create_cache_obj(bus_id);
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() {
let _ = fs::create_dir_all(parent);
if let Some(parent) = Path::new(CACHE_FILE_PATH).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)
&& fs::write(CACHE_FILE_PATH, json).is_ok()
{
log::debug!("Created/Updated cache file {}", CACHE_FILE_PATH);
if let Ok(json) = serde_json::to_string_pretty(&cache) {
let tmp_path = format!("{}.tmp", CACHE_FILE_PATH);
if fs::write(&tmp_path, &json).is_ok() {
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");
}
}
}
}
@@ -380,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);
if let Some(ref name) = dm_name
@@ -388,7 +505,9 @@ fn prepare_no_reboot(dm_flag: Option<String>) -> Result<Option<String>, String>
{
info!("Stopping display manager: {}", name);
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
@@ -405,18 +524,18 @@ fn prepare_no_reboot(dm_flag: Option<String>) -> Result<Option<String>, String>
std::thread::sleep(std::time::Duration::from_secs(2));
}
Ok(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");
}
Ok(DmGuard { name: None })
}
fn switch_integrated(no_reboot: bool) {
assert_root();
let _lock = match LockGuard::acquire() {
Ok(l) => l,
Err(e) => {
error!("{}", e);
return;
}
};
println!("Switching to integrated mode");
if !Path::new(CACHE_FILE_PATH).exists() {
@@ -428,15 +547,14 @@ fn switch_integrated(no_reboot: bool) {
create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false);
create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false);
let mut stopped_dm = None;
if no_reboot {
match prepare_no_reboot(None) {
Ok(dm) => stopped_dm = dm,
let _guard = if no_reboot {
let guard = match prepare_no_reboot(None) {
Ok(g) => g,
Err(e) => {
error!("{}", e);
return;
}
}
};
if let Err(e) = kill_gpu_processes() {
error!("Failed to kill GPU processes: {}", e);
@@ -445,7 +563,9 @@ fn switch_integrated(no_reboot: bool) {
// Try to unload modules
let modules = ["nvidia_uvm", "nvidia_modeset", "nvidia_drm", "nvidia"];
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() {
@@ -456,7 +576,11 @@ fn switch_integrated(no_reboot: bool) {
error!("Failed to remove GPU: {}", e);
}
}
}
guard
} else {
DmGuard { name: None }
};
let is_debug = log::log_enabled!(log::Level::Debug);
let mut dis_cmd = Command::new("systemctl");
@@ -467,12 +591,15 @@ fn switch_integrated(no_reboot: bool) {
}
match dis_cmd.status() {
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 {
println!("Operation completed successfully");
finalize_no_reboot(stopped_dm);
} else {
rebuild_initramfs();
println!("Operation completed successfully");
@@ -482,6 +609,13 @@ fn switch_integrated(no_reboot: bool) {
fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
assert_root();
let _lock = match LockGuard::acquire() {
Ok(l) => l,
Err(e) => {
error!("{}", e);
return;
}
};
println!("Switching to hybrid mode");
println!(
"Enable PCI-Express Runtime D3 (RTD3) Power Management: {}",
@@ -498,20 +632,23 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
cleanup();
let mut stopped_dm = None;
if no_reboot {
match prepare_no_reboot(None) {
Ok(dm) => stopped_dm = dm,
let _guard = if no_reboot {
let guard = match prepare_no_reboot(None) {
Ok(g) => g,
Err(e) => {
error!("{}", e);
return;
}
}
};
if let Err(e) = kill_gpu_processes() {
error!("Failed to kill GPU processes: {}", e);
}
}
guard
} else {
DmGuard { name: None }
};
// persistenced enable
let is_debug = log::log_enabled!(log::Level::Debug);
@@ -523,7 +660,11 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
}
match enable_cmd.status() {
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 {
@@ -551,7 +692,6 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
if no_reboot {
println!("Operation completed successfully");
finalize_no_reboot(stopped_dm);
} else {
rebuild_initramfs();
println!("Operation completed successfully");
@@ -568,6 +708,16 @@ fn switch_nvidia(
no_reboot: bool,
) {
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!("Enable ForceCompositionPipeline: {}", force_comp);
println!("Enable Coolbits: {}", coolbits.is_some());
@@ -616,6 +766,8 @@ fn switch_nvidia(
} else if vendor == "amd" {
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();
@@ -656,20 +808,23 @@ fn switch_nvidia(
}
}
let mut stopped_dm = None;
if no_reboot {
match prepare_no_reboot(dm.clone()) {
Ok(dm_name) => stopped_dm = dm_name,
let _guard = if no_reboot {
let guard = match prepare_no_reboot(dm.clone()) {
Ok(g) => g,
Err(e) => {
error!("{}", e);
return;
}
}
};
if let Err(e) = kill_gpu_processes() {
error!("Failed to kill GPU processes: {}", e);
}
}
guard
} else {
DmGuard { name: None }
};
// persistenced enable
let is_debug = log::log_enabled!(log::Level::Debug);
@@ -681,12 +836,15 @@ fn switch_nvidia(
}
match enable_cmd.status() {
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 {
println!("Operation completed successfully");
finalize_no_reboot(stopped_dm);
} else {
rebuild_initramfs();
println!("Operation completed successfully");
@@ -732,11 +890,43 @@ fn run_on_dgpu(command: &[String]) {
}
pub fn execute(cmd: &GpuCommands) {
if try_call_daemon(cmd) {
println!("Request handled by lapctld daemon.");
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::Hybrid { .. } => println!("Switching to Hybrid GPU mode..."),
GpuCommands::Nvidia { .. } => println!("Switching to NVIDIA-only GPU mode..."),
_ => {}
}
if try_call_daemon(&cmd) {
return;
}
execute_local(&cmd);
}
pub fn execute_local(cmd: &GpuCommands) {
match cmd {
GpuCommands::Query => {
let mode = get_current_mode();
@@ -749,9 +939,13 @@ pub fn execute(cmd: &GpuCommands) {
GpuCommands::CacheDelete => {
assert_root();
if Path::new(CACHE_FILE_PATH).exists() {
let _ = fs::remove_file(CACHE_FILE_PATH);
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() {
let _ = fs::remove_dir(parent);
if let Err(e) = fs::remove_file(CACHE_FILE_PATH) {
error!("Failed to remove cache file: {}", e);
}
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);
}
}
}
@@ -771,9 +965,13 @@ pub fn execute(cmd: &GpuCommands) {
cleanup();
if Path::new(CACHE_FILE_PATH).exists() {
let _ = fs::remove_file(CACHE_FILE_PATH);
if let Some(parent) = Path::new(CACHE_FILE_PATH).parent() {
let _ = fs::remove_dir(parent);
if let Err(e) = fs::remove_file(CACHE_FILE_PATH) {
error!("Failed to remove cache file: {}", e);
}
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();
-161
View File
@@ -1,161 +0,0 @@
use log::{error, info};
use std::process::Command;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_system_inhibition(
&self,
active: bool,
why: String,
who: String,
) -> zbus::Result<()>;
}
fn try_call_daemon_inhibit(active: bool, why: String, who: String) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
proxy.set_system_inhibition(active, why, who).await.is_ok()
})
}
pub fn execute(command_args: &[String], why: &str, who: &str, daemon: bool, stop: bool) {
if stop {
if try_call_daemon_inhibit(false, why.to_string(), who.to_string()) {
println!("Persistent system inhibition deactivated via lapctld.");
} else {
error!("Failed to deactivate inhibition via daemon. Is lapctld running?");
}
return;
}
if daemon && command_args.is_empty() {
if try_call_daemon_inhibit(true, why.to_string(), who.to_string()) {
println!("Persistent system inhibition activated via lapctld.");
println!("The system will stay awake until you run: lapctl inhibit --stop");
return;
} else {
error!("Failed to activate inhibition via daemon. Is lapctld running?");
// Fallback to local systemd-inhibit if daemon call fails
}
}
let args = build_args(command_args, why, who);
if command_args.is_empty() {
if !daemon {
info!("No command provided. Inhibiting sleep indefinitely. Press Ctrl+C to stop.");
}
} else {
info!(
"Running command while inhibiting sleep: {}",
command_args.join(" ")
);
}
if daemon {
match Command::new("systemd-inhibit").args(&args).spawn() {
Ok(child) => {
info!("Inhibitor started in background with PID: {}", child.id());
info!("To stop it, use: pkill -P {} systemd-inhibit", child.id());
// In background mode, we just let the child run and exit lapctl
}
Err(e) => {
error!("Failed to spawn systemd-inhibit: {}", e);
}
}
} else {
match Command::new("systemd-inhibit").args(&args).status() {
Ok(status) => {
if !status.success() {
error!("systemd-inhibit exited with status: {}", status);
}
}
Err(e) => {
error!("Failed to execute systemd-inhibit: {}", e);
eprintln!(
"Error: make sure 'systemd-inhibit' is installed and available in your PATH."
);
}
}
}
}
pub(crate) fn build_args(command_args: &[String], why: &str, who: &str) -> Vec<String> {
let mut args = vec![
"--why".to_string(),
why.to_string(),
"--who".to_string(),
who.to_string(),
"--what".to_string(),
"sleep:idle".to_string(),
];
if command_args.is_empty() {
args.push("sleep".to_string());
args.push("infinity".to_string());
} else {
args.extend(command_args.iter().cloned());
}
args
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_args_no_command() {
let args = build_args(&[], "testing", "lapctl");
assert_eq!(
args,
vec![
"--why",
"testing",
"--who",
"lapctl",
"--what",
"sleep:idle",
"sleep",
"infinity"
]
);
}
#[test]
fn test_build_args_with_command() {
let command_args = vec!["ls".to_string(), "-l".to_string()];
let args = build_args(&command_args, "testing", "lapctl");
assert_eq!(
args,
vec![
"--why",
"testing",
"--who",
"lapctl",
"--what",
"sleep:idle",
"ls",
"-l"
]
);
}
}
+52 -16
View File
@@ -1,5 +1,5 @@
use crate::utils::system::{assert_root, create_file};
use log::info;
use log::{error, info};
use std::process::Command;
// No longer needed: all privileged operations are now handled by the lapctld daemon via D-Bus.
@@ -12,7 +12,7 @@ const POLKIT_POLICY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<policyconfig>
<vendor>lapctl</vendor>
<vendor_url>https://github.com/S4NKALP/lapctl</vendor_url>
<action id="org.s4nkalp.lapctl.execute">
<action id="org.lapctl.execute">
<description>Elevated execution for lapctl</description>
<message>Authentication is required to perform privileged lapctl operations</message>
<defaults>
@@ -20,7 +20,7 @@ const POLKIT_POLICY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/lapctl</annotate>
<annotate key="org.freedesktop.policykit.exec.path">{EXE_PATH}</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">false</annotate>
</action>
</policyconfig>
@@ -66,7 +66,7 @@ After=dbus.service
[Service]
Type=simple
ExecStart=/usr/bin/lapctl daemon
ExecStart={EXE_PATH} daemon
Restart=on-failure
[Install]
@@ -76,21 +76,39 @@ WantedBy=multi-user.target
pub fn execute() {
assert_root();
println!("Installing lapctl configuration for daemon communication...");
let current_exe = match std::env::current_exe() {
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!("Installing lapctl configuration with dynamic paths...");
let dbus_policy_path = "/etc/dbus-1/system.d/org.lapctl.conf";
create_file(dbus_policy_path, DBUS_POLICY, false);
let systemd_content = SYSTEMD_SERVICE.replace("{EXE_PATH}", exe_path);
let service_path = "/etc/systemd/system/lapctld.service";
create_file(service_path, SYSTEMD_SERVICE, false);
create_file(service_path, &systemd_content, false);
// Also install the udev rules file (even if empty now) to avoid errors if it was previously there
let rules_path = "/etc/udev/rules.d/99-lapctl.rules";
create_file(rules_path, LAPCTL_UDEV_RULES, false);
// Polkit fallbacks for rootless execution without the daemon
let polkit_policy_path = "/usr/share/polkit-1/actions/org.s4nkalp.lapctl.policy";
create_file(polkit_policy_path, POLKIT_POLICY, false);
let polkit_content = POLKIT_POLICY.replace("{EXE_PATH}", exe_path);
let polkit_policy_path = "/usr/share/polkit-1/actions/org.lapctl.policy";
create_file(polkit_policy_path, &polkit_content, false);
let polkit_rules_path = "/etc/polkit-1/rules.d/10-lapctl.rules";
create_file(polkit_rules_path, POLKIT_RULES, false);
@@ -99,21 +117,39 @@ pub fn execute() {
println!("Activating background daemon (lapctld)...");
// 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
let status = Command::new("systemctl")
.args(["enable", "--now", "lapctld"])
.status();
if let Ok(s) = status {
if s.success() {
let daemon_ok = match status {
Ok(s) if s.success() => {
info!("lapctld daemon enabled and started.");
} else {
log::error!("Failed to enable/start lapctld daemon.");
true
}
}
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.");
println!("You can now use all lapctl commands without sudo.");
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.");
}
-1
View File
@@ -2,7 +2,6 @@ pub mod battery;
pub mod cooling;
pub mod display;
pub mod gpu;
pub mod inhibit;
pub mod install_rules;
pub mod power;
pub mod status;
+75 -22
View File
@@ -22,30 +22,51 @@ fn try_call_daemon(command: &PowerCommands) -> bool {
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let connection =
match tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
PowerCommands::Performance => proxy
.set_power_profile("performance".to_string())
let res = match command {
PowerCommands::Performance => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_power_profile("performance".to_string()),
)
.await
.is_ok(),
PowerCommands::Balanced => proxy
.set_power_profile("balanced".to_string())
}
PowerCommands::Balanced => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_power_profile("balanced".to_string()),
)
.await
.is_ok(),
PowerCommands::BatterySave => proxy
.set_power_profile("battery-save".to_string())
}
PowerCommands::BatterySave => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_power_profile("battery-save".to_string()),
)
.await
.is_ok(),
PowerCommands::LimitTdp { watts } => proxy.set_tdp_limit(*watts).await.is_ok(),
}
}
PowerCommands::LimitTdp { watts } => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_tdp_limit(*watts),
)
.await
}
};
matches!(res, Ok(Ok(_)))
})
}
@@ -188,6 +209,10 @@ fn set_amd_hwmon_limit(watts: u32) -> bool {
}
success
}
fn is_ideapad() -> bool {
Path::new("/sys/bus/platform/drivers/ideapad_acpi").exists()
}
fn set_platform_profile(profile: &str) {
let platform_profile_path = Path::new("/sys/firmware/acpi/platform_profile");
if platform_profile_path.exists() {
@@ -201,31 +226,59 @@ fn set_platform_profile(profile: &str) {
}
pub fn execute(command: &PowerCommands) {
match command {
PowerCommands::Performance => println!("Setting power profile to Performance"),
PowerCommands::Balanced => println!("Setting power profile to Balanced"),
PowerCommands::BatterySave => println!("Setting power profile to Battery Saver"),
PowerCommands::LimitTdp { watts } => {
println!("Setting CPU Package TDP Limit to {} Watts...", watts)
}
}
if try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
execute_local(command);
}
pub fn execute_local(command: &PowerCommands) {
match command {
PowerCommands::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_energy_performance_preference("performance");
println!("Operation completed successfully.");
}
PowerCommands::Balanced => {
println!("Setting power profile to Balanced");
set_platform_profile("balanced");
// Schedutil is the modern balanced default. Fallback isn't critical since cpufreq
// usually rejects invalid profiles, and standard Intel/AMD systems use EPP now anyway.
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_energy_performance_preference("balance_performance");
println!("Operation completed successfully.");
}
PowerCommands::BatterySave => {
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_energy_performance_preference("power");
println!("Operation completed successfully.");
+72 -46
View File
@@ -1,5 +1,5 @@
use crate::cli::TouchpadCommands;
use log::{error, info};
use log::{debug, error, info};
use std::fs;
use std::path::Path;
use zbus::Connection;
@@ -21,73 +21,99 @@ fn try_call_daemon(command: &TouchpadCommands) -> bool {
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let connection =
match tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
TouchpadCommands::Enable => proxy.set_touchpad_inhibition(false).await.is_ok(),
TouchpadCommands::Disable => proxy.set_touchpad_inhibition(true).await.is_ok(),
}
let res = match command {
TouchpadCommands::Enable => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_touchpad_inhibition(false),
)
.await
}
TouchpadCommands::Disable => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_touchpad_inhibition(true),
)
.await
}
};
matches!(res, Ok(Ok(_)))
})
}
pub fn execute(command: &TouchpadCommands) {
match command {
TouchpadCommands::Enable => println!("Enabling touchpad..."),
TouchpadCommands::Disable => println!("Disabling touchpad..."),
}
if try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
let sys_class_input = Path::new("/sys/class/input");
if !sys_class_input.exists() {
execute_local(command);
}
pub fn execute_local(command: &TouchpadCommands) {
match command {
TouchpadCommands::Enable => set_touchpad_inhibition(false),
TouchpadCommands::Disable => set_touchpad_inhibition(true),
}
}
fn set_touchpad_inhibition(inhibited: bool) {
let dev_input = Path::new("/sys/class/input");
if !dev_input.exists() {
error!("Could not find /sys/class/input. Is this a Linux system?");
return;
}
let mut found_touchpads = Vec::new();
if let Ok(entries) = fs::read_dir(sys_class_input) {
if let Ok(entries) = fs::read_dir(dev_input) {
let mut found = false;
for entry in entries.flatten() {
let path = entry.path();
let name_path = path.join("name");
let inhibited_path = path.join("inhibited");
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_path.exists()
&& inhibited_path.exists()
&& let Ok(name) = fs::read_to_string(&name_path)
&& name.to_lowercase().contains("touchpad")
{
found_touchpads.push((name.trim().to_string(), inhibited_path));
}
}
}
if found_touchpads.is_empty() {
error!("No touchpad devices found with 'inhibited' support in /sys/class/input.");
return;
}
match command {
TouchpadCommands::Enable => {
for (name, path) in found_touchpads {
match fs::write(&path, "0") {
Ok(_) => info!("Enabled touchpad: {}", name),
Err(e) => error!("Failed to enable touchpad {}: {}", name, e),
if name_str.starts_with("input") {
let name_file = entry.path().join("name");
if let Ok(content) = fs::read_to_string(name_file) {
let content_lower = content.to_lowercase();
if content_lower.contains("touchpad") || content_lower.contains("trackpad") {
found = true;
let inhibited_file = entry.path().join("inhibited");
if inhibited_file.exists() {
let val = if inhibited { "1" } else { "0" };
match fs::write(&inhibited_file, val) {
Ok(_) => info!("Set inhibit={} for {}", inhibited, name_str),
Err(e) => {
debug!("Failed to write to {}: {}", inhibited_file.display(), e)
}
}
}
}
}
}
}
TouchpadCommands::Disable => {
for (name, path) in found_touchpads {
match fs::write(&path, "1") {
Ok(_) => info!("Disabled touchpad: {}", name),
Err(e) => error!("Failed to disable touchpad {}: {}", name, e),
}
}
if found {
println!("Operation completed successfully.");
} else {
error!("No touchpad device found in /sys/class/input.");
}
} else {
error!("Failed to read directory /sys/class/input.");
}
}
+16 -65
View File
@@ -1,26 +1,14 @@
use crate::cli::GpuCommands;
use crate::commands::gpu;
use std::sync::Arc;
use tokio::sync::Mutex;
use zbus::interface;
use zbus::zvariant::OwnedFd;
pub struct LapctlInterface {
inhibit_fd: Arc<Mutex<Option<OwnedFd>>>,
}
impl Default for LapctlInterface {
fn default() -> Self {
Self {
inhibit_fd: Arc::new(Mutex::new(None)),
}
}
}
#[derive(Default)]
pub struct LapctlInterface {}
#[interface(name = "org.lapctl1")]
impl LapctlInterface {
pub async fn switch_gpu_integrated(&self, no_reboot: bool) -> zbus::fdo::Result<()> {
gpu::execute(&GpuCommands::Integrated { no_reboot });
gpu::execute_local(&GpuCommands::Integrated { no_reboot });
Ok(())
}
@@ -31,7 +19,7 @@ impl LapctlInterface {
no_reboot: bool,
) -> zbus::fdo::Result<()> {
let rtd3_opt = if rtd3 < 0 { None } else { Some(rtd3 as u8) };
gpu::execute(&GpuCommands::Hybrid {
gpu::execute_local(&GpuCommands::Hybrid {
rtd3: rtd3_opt,
use_nvidia_current,
no_reboot,
@@ -54,7 +42,7 @@ impl LapctlInterface {
} else {
Some(coolbits as u32)
};
gpu::execute(&GpuCommands::Nvidia {
gpu::execute_local(&GpuCommands::Nvidia {
dm: dm_opt,
force_comp,
coolbits: coolbits_opt,
@@ -66,7 +54,13 @@ impl LapctlInterface {
}
pub async fn set_battery_limit(&self, percent: u32) -> zbus::fdo::Result<()> {
crate::commands::battery::execute(&crate::cli::BatteryCommands::Limit {
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 {
percent: percent as u8,
});
Ok(())
@@ -84,12 +78,12 @@ impl LapctlInterface {
)));
}
};
crate::commands::power::execute(&cmd);
crate::commands::power::execute_local(&cmd);
Ok(())
}
pub async fn set_tdp_limit(&self, watts: u32) -> zbus::fdo::Result<()> {
crate::commands::power::execute(&crate::cli::PowerCommands::LimitTdp { watts });
crate::commands::power::execute_local(&crate::cli::PowerCommands::LimitTdp { watts });
Ok(())
}
@@ -105,7 +99,7 @@ impl LapctlInterface {
)));
}
};
crate::commands::cooling::execute(&cmd);
crate::commands::cooling::execute_local(&cmd);
Ok(())
}
@@ -115,50 +109,7 @@ impl LapctlInterface {
} else {
crate::cli::TouchpadCommands::Enable
};
crate::commands::touchpad::execute(&cmd);
Ok(())
}
pub async fn set_system_inhibition(
&self,
active: bool,
why: String,
who: String,
) -> zbus::fdo::Result<()> {
let mut fd_lock = self.inhibit_fd.lock().await;
if active {
if fd_lock.is_some() {
return Ok(()); // Already inhibiting
}
let conn = zbus::Connection::system().await.map_err(|e| {
zbus::fdo::Error::Failed(format!("Failed to connect to system bus: {}", e))
})?;
let msg = conn
.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.login1.Manager"),
"Inhibit",
&("sleep:idle", who, why, "block"),
)
.await
.map_err(|e| zbus::fdo::Error::Failed(format!("Failed to inhibit: {}", e)))?;
let fd: OwnedFd = msg
.body()
.deserialize()
.map_err(|e| zbus::fdo::Error::Failed(format!("Failed to parse FD: {}", e)))?;
*fd_lock = Some(fd);
log::info!("System inhibition activated via lapctld.");
} else {
*fd_lock = None;
log::info!("System inhibition deactivated via lapctld.");
}
crate::commands::touchpad::execute_local(&cmd);
Ok(())
}
}
+32 -17
View File
@@ -21,30 +21,42 @@ pub fn get_current_mode() -> String {
}
pub fn get_nvidia_gpu_pci_bus() -> String {
let output = Command::new("lspci")
.output()
.expect("Failed to execute lspci");
let output = match Command::new("lspci").output() {
Ok(o) => o,
Err(e) => {
error!("Failed to execute lspci: {}", e);
std::process::exit(1);
}
};
let lspci_output = String::from_utf8_lossy(&output.stdout);
for line in lspci_output.lines() {
if line.contains("NVIDIA")
&& (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:", "");
info!("Found Nvidia GPU at {}", pci_bus_id);
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 = device_fun[0];
let function = device_fun[1];
if device_fun.len() < 2 {
warn!("Unexpected PCI device.function format: {}", parts[1]);
continue;
}
return format!(
"PCI:{}:{}:{}",
u32::from_str_radix(bus, 16).unwrap_or(0),
u32::from_str_radix(device, 16).unwrap_or(0),
u32::from_str_radix(function, 16).unwrap_or(0)
u32::from_str_radix(parts[0], 16).unwrap_or(0),
u32::from_str_radix(device_fun[0], 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> {
let output = Command::new("lspci")
.output()
.expect("Failed to execute lspci");
let output = Command::new("lspci").output().ok()?;
let lspci_output = String::from_utf8_lossy(&output.stdout);
for line in lspci_output.lines() {
if line.contains("NVIDIA")
&& (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.chars().filter(|&c| c == ':').count() == 1 {
return Some(format!("0000:{}", pci_addr));
@@ -109,12 +119,17 @@ pub fn rescan_pci() -> Result<(), String> {
pub fn kill_gpu_processes() -> Result<(), String> {
info!("Killing processes using NVIDIA GPU...");
// 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
let _ = Command::new("systemctl")
if let Err(e) = Command::new("systemctl")
.args(["stop", "nvidia-persistenced.service"])
.output();
.output()
{
log::debug!("Failed to stop nvidia-persistenced: {}", e);
}
Ok(())
}
+7 -8
View File
@@ -30,15 +30,14 @@ fn main() {
Commands::Status => commands::status::execute(),
Commands::InstallRules => commands::install_rules::execute(),
Commands::Touchpad { command } => commands::touchpad::execute(command),
Commands::Inhibit {
command,
why,
who,
daemon,
stop,
} => commands::inhibit::execute(command, why, who, *daemon, *stop),
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 {
if let Err(e) = lapctl::daemon::run().await {
eprintln!("Daemon error: {}", e);
+33 -22
View File
@@ -17,8 +17,12 @@ pub fn assert_root() {
match status {
Ok(s) if s.success() => std::process::exit(0),
_ => {
error!("Privilege escalation failed or was cancelled.");
Ok(s) => {
error!("Privilege escalation failed with exit code: {}", s);
std::process::exit(1);
}
Err(e) => {
error!("Privilege escalation failed: {}", e);
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()) {
perms.set_mode(perms.mode() | 0o111);
let _ = fs::set_permissions(path_obj, perms);
info!("Added execution privilege to file {}", path);
if let Err(e) = fs::set_permissions(path_obj, perms) {
error!("Failed to set executable permission on '{}': {}", path, e);
} else {
info!("Added execution privilege to file {}", path);
}
}
}
Err(e) => {
@@ -148,20 +155,6 @@ pub fn rebuild_initramfs() {
command = vec!["mkinitcpio", "-P"];
}
if let Ok(which) = Command::new("which").arg("systemd-inhibit").output()
&& which.status.success()
{
let mut new_cmd = vec![
"systemd-inhibit",
"--who=lapctl",
"--why",
"Rebuilding initramfs",
"--",
];
new_cmd.extend(command.iter());
command = new_cmd;
}
if !command.is_empty() {
println!("Rebuilding the initramfs...");
let is_debug = log::log_enabled!(log::Level::Debug);
@@ -178,10 +171,17 @@ pub fn rebuild_initramfs() {
Ok(status) if status.success() => {
println!("Successfully rebuilt the initramfs!");
}
_ => {
error!("An error occurred while rebuilding the initramfs");
Ok(status) => {
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."
);
}
}
@@ -191,7 +191,14 @@ pub fn manage_service(name: &str, action: &str) -> Result<(), String> {
cmd.args([action, name]);
match cmd.status() {
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
)),
}
}
@@ -201,6 +208,10 @@ pub fn terminate_session(id: &str) -> Result<(), String> {
cmd.args(["terminate-session", id]);
match cmd.status() {
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)),
}
}
-45
View File
@@ -1,45 +0,0 @@
use clap::Parser;
use lapctl::cli::{Cli, Commands};
#[test]
fn test_inhibit_cli_parsing() {
let args = vec!["lapctl", "inhibit", "sleep", "10", "--why", "testing"];
let cli = Cli::parse_from(args);
if let Commands::Inhibit {
command,
why,
who,
daemon,
stop: _,
} = cli.command
{
assert_eq!(command, vec!["sleep".to_string(), "10".to_string()]);
assert_eq!(why, "testing");
assert_eq!(who, "lapctl"); // default value
assert!(!daemon);
} else {
panic!("Command should be Inhibit");
}
}
#[test]
fn test_inhibit_daemon_parsing() {
let args = vec!["lapctl", "inhibit", "--daemon"];
let cli = Cli::parse_from(args);
if let Commands::Inhibit {
command,
why,
who: _,
daemon,
stop: _,
} = cli.command
{
assert!(command.is_empty());
assert_eq!(why, "lapctl inhibiting sleep"); // default value
assert!(daemon);
} else {
panic!("Command should be Inhibit");
}
}