46 Commits
Author SHA1 Message Date
sankalp 8bfb0dda39 fix: include LICENSE file in release tarball 2026-05-29 17:33:55 +05:45
sankalp ae32ce9c8d fix: always upload tarball to release regardless of ref_type 2026-05-29 17:21:18 +05:45
sankalp 11c3cec99d fix: force-overwrite files with stash instead of merge 2026-05-29 17:13:00 +05:45
sankalp f69b74ab73 fix: stash changes before checking out master in AUR action 2026-05-29 17:06:37 +05:45
sankalp bdf1312dd1 fix: upgrade AUR action to v4.1.3, checkout master before push 2026-05-29 16:59:42 +05:45
sankalp 2544f44309 fix: add contents: write permission for release upload 2026-05-29 16:47:30 +05:45
sankalp 34a57007d1 refactor(src/commands): Cleanup command printing and simplify GPU environment handling
Enhancements:
- Remove unnecessary multiline formatting in GPU, status, and temperature commands
- Consolidate zone type checks into a single condition
- Simplify environment variable tuple definitions
2026-05-29 16:38:24 +05:45
sankalp 3684c5b071 chore(root): bump version to 0.3.5Updated version bump and project metadata enrichment.
New Features
- None

Enhancements
- Updated edition to 2024
- Added license and repository fields
- Refined dependency versions

Bug Fixes
- None
2026-05-29 16:35:31 +05:45
sankalp 8faccc117a feat(src): add temperature, fan, power commands handling
- New Features
  - Added `temperature` subcommand to display system temperatures.
  - Added `fan` subcommand to display fan speeds.
  - Added `power` subcommand under GPU commands to show GPU power state and consumption.
- Enhancements
  - Integrated new subcommands into the command dispatcher.
- Bug Fixes
  - None
2026-05-29 16:35:26 +05:45
sankalp 92806e2843 feat(src/commands): add fan speed monitoring and enhance GPU temperature command
Refactor fan monitoring and improve GPU temperature reporting for better hardware visibility.

- New Features:
  - Add `fan` command to enumerate and display fan speeds via hwmon and thermal cooling devices.
  - Expand temperature detection across hwmon, thermal zones, and NVIDIA GPU sensors.
- Enhancements:
  - Strengthen GPU temperature output with power and clock information.
  - Improve error messages and logging consistency.
- Bug Fixes:
  - Correct error message wording.
  - Gracefully handle missing sensor files.
2026-05-29 16:35:21 +05:45
sankalp bdc95801c3 fix: reorder workflow to create tarball and update sha256 before AUR push 2026-05-29 16:14:29 +05:45
sankalp 2efa7a64f1 feat: auto-create and upload release tarball in workflow 2026-05-29 16:12:09 +05:45
sankalp c8561bf4ff docs(root):update README with new installation methods and project manifest 2026-05-29 16:01:16 +05:45
sankalp f202103a06 fix: exclude unsupported dsa key type from ssh_keyscan 2026-05-29 15:43:46 +05:45
sankalp e0a589d441 fix: update AUR push action inputs for v2.5.0 API 2026-05-29 15:37:41 +05:45
sankalp 151f5ad303 fix: run makepkg as non-root user in docker container 2026-05-29 15:31:51 +05:45
sankalp 92298ddce8 fix: add authors field to Cargo.toml (required by cargo-aur) 2026-05-29 15:25:28 +05:45
sankalp c223fea5f4 fix: replace removed archlinux/setup-pacman action with docker for .SRCINFO 2026-05-29 15:19:22 +05:45
sankalp 80899602e5 fix: make crates.io publish non-blocking, add workflow_dispatch, fix AUR action 2026-05-29 15:12:50 +05:45
sankalp 21defdb70a fix(.github/workflows): correctsetup-pacman action version
- Bug Fixes: Fixed CI workflow by using the correct archlinux/setup-pacman@v1 action instead of archlinux/actions/setup-pacman@v1
2026-05-28 20:36:01 +05:45
sankalp 90965f13b9 feat(root): bump version to 0.3.4New Features
- Upgrade lapctl to 0.3.4, adding new hardware management commands.
- Added clap and ctrlc dependencies to enhance CLI functionality.

Enhancements
- Updated Cargo.toml to edition 2024 and aligned dependency versions.
- Refined project description and repository link.

Bug Fixes
- No bug fixes included in this change.
2026-05-28 20:30:42 +05:45
sankalp 30a89fe2fe chore(root): bump version to 0.3.4 and add repository field
New Features
-

Enhancements
-

Bug Fixes
-
2026-05-28 20:29:37 +05:45
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
18 changed files with 981 additions and 453 deletions

No files matched your search

+110
View File
@@ -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
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.3.2"
version = "0.3.5"
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"
+7 -1
View File
@@ -1,7 +1,11 @@
[package]
name = "lapctl"
version = "0.3.2"
version = "0.3.5"
edition = "2024"
authors = ["S4NKALP"]
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 +19,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"
+26 -26
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.
---
@@ -36,31 +35,31 @@ Built with performance and simplicity in mind, it talks directly to your system'
#### Option 1: Arch Linux (AUR)
```bash
yay -S lapctl
yay -S lapctl-bin
sudo lapctl install-rules
```
#### Option 2: Using `just` (Recommended for other distros)
If you have [just](https://github.com/casey/just) installed:
#### Option 2: crates.io
```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
sudo lapctl install-rules
```
_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
- **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
@@ -78,10 +77,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
@@ -104,11 +104,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
```
@@ -133,6 +128,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
+7 -18
View File
@@ -44,6 +44,10 @@ pub enum Commands {
},
/// Hardware status
Status,
/// Show system temperatures
Temperature,
/// Show fan speeds
Fan,
/// Install udev rules for rootless operation
InstallRules,
/// Touchpad management
@@ -51,28 +55,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,
@@ -131,6 +118,8 @@ pub enum GpuCommands {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
/// Show GPU power state and consumption
Power,
}
#[derive(Subcommand, Debug)]
+100
View File
@@ -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
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",
@@ -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 BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf";
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 {
let source_name = match igpu_vendor.map(|s| s.as_str()) {
Some("intel") => "modesetting".to_string(),
@@ -367,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");
}
}
}
}
@@ -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);
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);
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
@@ -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));
}
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() {
@@ -440,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);
@@ -457,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() {
@@ -468,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");
@@ -479,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");
@@ -494,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: {}",
@@ -510,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);
@@ -535,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 {
@@ -563,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");
@@ -580,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());
@@ -628,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();
@@ -668,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);
@@ -693,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");
@@ -708,54 +854,160 @@ fn switch_nvidia(
fn run_on_dgpu(command: &[String]) {
if command.is_empty() {
error!("No command provided to run.");
error!("No command specified.");
return;
}
if get_current_mode() != "hybrid" {
log::warn!(
"Running on dGPU is typically only effective in Hybrid mode. \
Your current mode is '{}'. The application may fail to start or \
not use the NVIDIA GPU.",
get_current_mode()
);
error!("GPU run is only supported in Hybrid mode.");
return;
}
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 {
child.args(&command[1..]);
cmd.args(&command[1..]);
}
// Set PRIME offload variables natively
child.env("__NV_PRIME_RENDER_OFFLOAD", "1");
child.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
child.env("__VK_LAYER_NV_optimus", "NVIDIA_only");
for (key, value) in &env_vars {
cmd.env(key, value);
}
match child.status() {
info!("Running '{}' on discrete GPU", command.join(" "));
match cmd.status() {
Ok(status) => {
log::info!("Application exited with: {}", status);
if !status.success() {
error!("Command exited with status: {}", status);
}
}
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(&current_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) {
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::Hybrid { .. } => println!("Switching to Hybrid GPU mode..."),
GpuCommands::Nvidia { .. } => println!("Switching to NVIDIA-only GPU mode..."),
_ => {}
}
if try_call_daemon(cmd) {
if try_call_daemon(&cmd) {
return;
}
execute_local(cmd);
execute_local(&cmd);
}
pub fn execute_local(cmd: &GpuCommands) {
@@ -771,9 +1023,13 @@ pub fn execute_local(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);
}
}
}
@@ -793,9 +1049,13 @@ pub fn execute_local(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();
@@ -828,5 +1088,6 @@ pub fn execute_local(cmd: &GpuCommands) {
*no_reboot,
),
GpuCommands::Run { command } => run_on_dgpu(command),
GpuCommands::Power => show_gpu_power_state(),
}
}
-175
View File
@@ -1,175 +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 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,
};
let res = tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_system_inhibition(active, why, who),
)
.await;
matches!(res, Ok(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() {
println!(
"Attempting to activate system inhibition via lapctld: '{}' by '{}'",
why, who
);
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"
]
);
}
}
+40 -10
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.
@@ -76,8 +76,20 @@ WantedBy=multi-user.target
pub fn execute() {
assert_root();
let current_exe = std::env::current_exe().expect("Failed to get current executable path");
let exe_path = current_exe.to_str().expect("Invalid executable path");
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...");
@@ -105,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.");
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.");
}
+2 -1
View File
@@ -1,9 +1,10 @@
pub mod battery;
pub mod cooling;
pub mod display;
pub mod fan;
pub mod gpu;
pub mod inhibit;
pub mod install_rules;
pub mod power;
pub mod status;
pub mod temperature;
pub mod touchpad;
+25 -3
View File
@@ -209,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() {
@@ -242,21 +246,39 @@ 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");
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.");
+32
View File
@@ -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
let displays = display::get_active_display_info();
if displays.is_empty() {
+120
View File
@@ -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.");
}
}
+8 -57
View File
@@ -1,21 +1,9 @@
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 {
@@ -66,6 +54,12 @@ impl LapctlInterface {
}
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 {
percent: percent as u8,
});
@@ -118,47 +112,4 @@ impl LapctlInterface {
crate::commands::touchpad::execute_local(&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.");
}
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(())
}
+9 -8
View File
@@ -28,17 +28,18 @@ fn main() {
Commands::Cooling { command } => commands::cooling::execute(command),
Commands::Display { command } => commands::display::execute(command),
Commands::Status => commands::status::execute(),
Commands::Temperature => commands::temperature::execute(),
Commands::Fan => commands::fan::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");
}
}