A small GTK4 / libadwaita app (plus a CLI) for hardware features on System76
laptops and generic Clevo/Tongfang barebones: RGB keyboard backlight control
(with settings that survive a reboot), battery charge threshold control, and
performance mode control (fan behavior plus, optionally, real CPU/GPU power
scaling), on boards whose clevo-acpi driver build supports each feature.
Two keyboard hardware backends are auto-detected:
- System76 laptops — the single-zone
system76_acpi::kbd_backlightLED device (e.g. Darter Pro 7). - Generic Clevo/Tongfang barebones — the multi-zone (left/center/right/
numpad/lightbar)
clevo-acpi::kbd_backlightdevice used by a much wider range of hardware resold under many brands. Some of these boards need a separate driver-enablement package first to get the kernel recognizing them at all — this app itself doesn't touch drivers, only whichever LED device is already present. Battery charge threshold and performance mode control are available only on this backend, and only if the installedclevo-acpibuild includes the corresponding sysfs support (charge_control_start_threshold/charge_control_end_threshold, andperformance_mode, respectively).
On my own System76 laptop, the official Keyboard Configurator app hasn't reliably remembered a color across a reboot for me, and OpenRGB doesn't detect this keyboard at all. I haven't tried every third-party tool that claims to handle this, so I can't say whether one of them already solves it well — but after not finding a solution that worked for me, I built this one instead. It grew out of a set of shell aliases + systemd services I'd put together earlier for doing the same thing from the terminal.
It later grew a second keyboard backend after a separate project (getting the keyboard RGB working on a brand-new, not-yet-recognized Clevo barebone) turned up the same underlying problem in a more general form: good hardware, backlight control that technically exists in the firmware, and no polished way for an ordinary user to reach it from Linux. Rather than build a second app, this one grew a pluggable backend.
Battery charge threshold control followed the same pattern: that Clevo
barebone also has a Windows-only "battery saver" feature (stop charging at
a configurable percentage, resume at another) to reduce long-term battery
wear, with no Linux equivalent. Since the underlying clevo-acpi platform
device was already right there, that driver grew the sysfs attributes for
it, and this app grew a page to control them.
Performance/fan mode control followed the same pattern again: that Clevo
barebone's Windows Control Center software also has a Performance/Balanced/
Quiet mode selector with no Linux equivalent, and no vendor documentation
for how it works. The mapping this app uses was determined empirically
(observing real fan RPM response to sustained CPU load for each candidate
EC command value, with the laptop fully cooled down between isolated
tests) rather than from any spec — see
~/laptopissues/performance-mode/NOTES.md in that project's development
history for the raw data, if you're curious or trying to validate this on
different hardware.
Measuring that mapping turned up something worth knowing if you have this
same EC command on your own board: it only ever affects fan curves. RAPL
power limits, EPP, and measured power draw under load were all identical
across every mode when tested back-to-back. So Balanced/Quiet/Performance
also drive real CPU (and, where possible, GPU) power scaling through
standard tools instead — TLP for the CPU, nvidia-smi clock locking for
the GPU — layered on top of the same fan control. See "CPU/GPU power
scaling" below.
This was built with Claude. I'm a coder (nothing too serious or professional)
going back to BASIC and Visual Basic in the 80s/90s, comfortable with
bash/PHP/JavaScript, but new to Python and to GTK/GNOME app development
specifically — this project has doubled as how I'm learning both. I tested
everything on real hardware (my Darter Pro 7, and a Clevo/Tongfang barebone),
caught and fixed bugs, and made the calls on architecture, licensing, and
distribution. But a meaningful share of the actual code was written with AI
assistance, and I'd rather be upfront about that than have it be a surprise
to anyone looking through the source. The only reason I'm publishing this at
all is because I find it genuinely useful, and if you have a Darter Pro 7
(or a generic Clevo/Tongfang-based laptop with the clevo-acpi backend)
running a recent Ubuntu, you probably will too.
Keyboard
- 32 curated preset colors, grouped by family
- Custom color picker, with a favorites list you can add to / remove from
- Brightness slider
- Reboot persistence: the color and brightness in effect at shutdown are automatically restored on the next boot — whether they were set from this app, a terminal alias, or the keyboard's own hardware shortcut
Battery (clevo-acpi backend only, driver-dependent)
- Set the percentage at which charging resumes and the percentage at which it stops, to keep the battery out of the high-wear near-100% range for laptops that mostly run on AC power
- Thresholds are stored on the embedded controller/firmware side, the same place Windows' Control Center software would write them, so they persist across reboots on their own — no separate persistence service needed
- "Charge to 100% This Time": a one-shot override for the occasional trip or long day away from AC, without having to remember to raise (and later lower) your normal thresholds by hand. Reverts on its own the next time the laptop is running on battery — including across a full shutdown and reboot with AC unplugged in between. See "Charge to 100% this time" below for exactly how and why
Performance (clevo-acpi backend only, driver-dependent)
- Switch between Balanced, Quiet, Performance, and Max Fan modes, from the app window, the tray icon menu, or the CLI
- Balanced/Quiet/Performance also apply real CPU power scaling (via TLP)
and, on laptops with an NVIDIA GPU, GPU clock scaling (via
nvidia-smi) — not just fan curves. See "CPU/GPU power scaling" below; this part is optional and needs a one-time manual setup step - Max Fan pins both fans at high speed regardless of actual temperature until you switch away from it — it's a manual cooling-boost override, not a normal thermal profile, so it's called out separately in the app, and leaves CPU/GPU power settings exactly as they were
- Reboot persistence: the mode you set is saved the moment you set it, and restored once the desktop is fully up at your next login (not via an early-boot systemd service — see "How persistence works" below for why)
- The app window and tray menu both poll the live mode every 5 seconds, so switching from one stays reflected in the other without needing a manual refresh
- Optional automatic switching by power source: turn it on, pick which of Balanced/Quiet/Performance to use on AC and which to use on battery, and the mode follows the AC adapter from then on. Manual mode buttons stay visible but are disabled while this is on (Max Fan stays available either way, since it's an override, not one of the two auto-picked profiles) — turn auto-switch off to take manual control back. See "Automatic profile switching" below
- Optional display refresh rate switching alongside the same AC/battery trigger — pick a rate for AC, a (typically lower) rate for battery, to save power. Part of the same "Automatic Switching" toggle, not a separate feature. See "Automatic profile switching" below
- Optional continuous, temperature-driven custom fan curve, as an alternative to the four fixed presets — a real background daemon with a kernel-level dead-man's-switch, so a crashed or killed control process can never leave the fan stuck at a stale speed. See "Custom fan curve" below
Tray icon
- A small always-present tray icon (started automatically at login, minimized — no window pops up) with a quick menu: open the main window, jump straight to any performance mode with a radio-button indicator showing the current one, or quit
- Closing the main window hides it to the tray instead of quitting; use the tray menu's Quit to actually exit
All
- No root prompts during normal use: a one-time setup grants your user direct write access via a dedicated group + udev rule
clevo-control-panel-clifor scripting: keyboard static colors (whole-keyboard or a single zone on multi-zone hardware), brightness, built-in lighting effects (color cycle, breathe, per-zone rainbow wave), battery threshold status/set, performance mode status/set, automatic-switching status/set (including refresh rate), and fan curve status/enable/disable/release/manual-set
Everything needed (Python 3, PyGObject, GTK4, libadwaita) ships with a
standard GNOME desktop, with two exceptions, both small official-repo
packages that install.sh installs automatically if missing:
python3-gi-cairo, the PyGObject/Cairo bridge used to draw the color swatchesgir1.2-gtk-3.0andgir1.2-ayatanaappindicator3-0.1, used only by the separate tray icon helper process (see "Tray icon architecture" below for why it's a separate GTK3 process rather than part of the main GTK4 app). Optional in the sense that the app runs fine without them — you just won't get a tray icon, and closing the window will quit the app instead of hiding it, since there'd be no way to get it back otherwise.
Also depends on tlp (installed automatically by install.sh) for real
CPU power scaling — see "CPU/GPU power scaling" below. nvidia-smi, if
you have an NVIDIA GPU, is used the same way but isn't a hard dependency
(that part just no-ops without it).
Desktop compatibility: this is a plain GTK4/libadwaita app, so it also runs under KDE Plasma and COSMIC (it will just carry libadwaita's GNOME-style theming rather than matching those desktops natively).
Via the .deb (recommended) — download the .deb from the
latest release
and:
sudo apt install ./clevo-control-panel_<version>_all.deb
This installs the app, desktop launcher, and icon, and its postinst step
automatically does the one-time system setup below for whoever ran apt install. Log out and back in (or reboot) once afterwards — group
membership only applies to new login sessions.
From a repo checkout instead, run once from a terminal:
sudo bash data/install.sh "$USER"
Either path:
- Creates a
clevoctlgroup and adds your user to it - Installs a udev rule so the relevant sysfs files (LED color/brightness, and battery charge thresholds/performance mode if present) are group-writable on every boot
- Installs and enables two systemd services that save the keyboard color/brightness on shutdown and restore them on boot
- Installs a desktop launcher (and, for the
.deb, an icon), an autostart entry that launches the app minimized at login (tray icon + performance mode restore, no window), plus theclevo-control-panel-clicommand
The app also has a "Repair Setup" button (gear icon → Settings) that re-runs
this via pkexec — useful if a second user account on the same machine needs
access, or if something's gotten out of sync.
One more optional, manual step: setup-power-profile-sudoers.sh (see
"CPU/GPU power scaling" below) — not run automatically by either install
path, since it touches sudoers. In a repo checkout that's
data/setup-power-profile-sudoers.sh; for a .deb install it's
/usr/lib/clevo-control-panel/setup-power-profile-sudoers.sh.
Likewise, setup-gpu-mode-sudoers.sh (see "GPU MUX mode switching"
below) is its own separate, optional, manually-run step for the same
reason — same paths, just with gpu-mode in place of power-profile.
Same again for setup-prime-select-sudoers.sh (prime-select in place
of power-profile) — the safety net described in that same section.
And setup-oled-luminance-nudge-sudoers.sh (oled-luminance-nudge in
place of power-profile) — without it, a refresh-rate switch or a
brightness-slider move in dGPU mode still self-corrects via the
oled-luminance daemon's own 1-second safety reassert, just with a
needlessly dim wait instead of an instant one. That reassert interval is
also what actually protects you against a refresh-rate switch triggered
some other way entirely — GNOME Settings' Displays panel, xrandr, a
monitor hotplug — since the DPCD bit reset happens as a side effect of
any Mutter mode-set, not just the ones this app triggers.
./bin/clevo-control-panel # GUI
./bin/clevo-control-panel-cli keyboard status
./bin/clevo-control-panel-cli keyboard color ff00aa
./bin/clevo-control-panel-cli keyboard color 00ff00 --zone left # multi-zone hardware only
./bin/clevo-control-panel-cli keyboard brightness 150
./bin/clevo-control-panel-cli keyboard effect breathe --color 00ffff
./bin/clevo-control-panel-cli battery status
./bin/clevo-control-panel-cli battery set --start 70 --end 80
./bin/clevo-control-panel-cli performance status
./bin/clevo-control-panel-cli performance set quiet
./bin/clevo-control-panel-cli performance auto status
./bin/clevo-control-panel-cli performance auto set --enabled on --ac performance --battery quiet
./bin/clevo-control-panel-cli performance auto set --ac-refresh 165 --battery-refresh 60
./bin/clevo-control-panel-cli fan status
./bin/clevo-control-panel-cli fan curve show
./bin/clevo-control-panel-cli fan enable
or launch "Clevo Control Panel" from your app grid after setup.
The keyboard LED device's sysfs files are virtual and reset on every boot.
save-keyboard-color.service runs clevo-control-panel-cli keyboard save-state just before shutdown/reboot, which writes the current brightness
and per-zone color(s) to /var/lib/clevo-control-panel/state.json;
restore-keyboard-color.service runs clevo-control-panel-cli keyboard restore-state early at boot and writes them back. This captures changes
made any way — this app, the CLI, or the keyboard's own Fn-key color
shortcut — since it reads the live hardware state rather than tracking who
changed it.
Battery charge thresholds work differently: they're stored by the embedded controller/firmware itself (the same mechanism Windows' Control Center software uses), so they already survive a reboot without any systemd service — the sysfs files just reflect whatever's currently stored there.
Performance mode is different again, and deliberately does not use an
early-boot systemd service the way keyboard color does. An earlier version
did exactly that, and while investigating an unrelated issue with it, the
laptop's fans spiked to full speed and it powered off abruptly a few
seconds into boot — details in
~/laptopissues/performance-mode/NOTES.md. The evidence pointed at
applying this specific, reverse-engineered EC command that early (while
ACPI/EC subsystems are still initializing) as the likely cause, something
never actually tested before shipping it that way. The fix: persistence
now happens entirely in the app layer, at a point that's always
fully-booted and interactive, exactly like every real test of this
feature. performance.py's set_mode() writes the new mode to
/var/lib/clevo-control-panel/performance-mode.state the moment it's
set (no shutdown-time hook needed — the read is a plain cached-value
read with no EC interaction); the app's own startup restores it, once,
the first time the app runs after login (via the autostart entry below).
The reverse-engineered EC command behind Balanced/Quiet/Performance turned out to only affect fan curves — confirmed by testing (identical RAPL power limits, EPP, and measured power draw across all three modes under the same load). So real CPU/GPU scaling is layered on top using standard, well- supported tools instead of more EC reverse-engineering:
- CPU, via TLP: each mode writes
/etc/tlp.d/90-clevo-control-panel.conf(CPU governor, EPP, turbo boost, HWP dynamic boost — the same value for AC and battery, since this is a deliberate choice made through the app, not something that should change just because the power source did) and runstlp startto apply it immediately. Quiet also sets a hardscaling_max_freqceiling (1500MHz), applied directly rather than through TLP — disabling turbo alone only limits the CPU to its base clock (3700MHz on the hardware this was built on), nowhere near quiet, and TLP's own equivalent setting turned out to leave a previous cap in place rather than reset it when a mode doesn't want one. Confirmed both directions hold correctly, including that a hybrid CPU's cores can have genuinely different max frequencies (uncapping restores each core's own maximum, not one shared value). - GPU, via
nvidia-smi, if you have an NVIDIA GPU:nvidia-smi -pl(power limit) is attempted but is a no-op on many laptop/Max-Q GPUs — NVIDIA locks that down at the vBIOS level ("not supported in current scope"), confirmed on the hardware this was built on.nvidia-smi -lgc(GPU clock locking) is a different mechanism and isn't restricted the same way — confirmed working, including under real load. Quiet caps the GPU clock low, Balanced caps it moderately, Performance removes the cap entirely. - Intel iGPU (
xedriver): also capped, via/sys/class/drm/cardN/device/tile0/gt0/freq0/max_freq— found on a second, more thorough look; the first search missed it by not traversing through the card'sdevicesymlink correctly. Onlygt0(the render/compute/blitter engine, the one that actually reaches 2.4GHz) is capped — quiet=800MHz, balanced=1500MHz, performance=uncapped.gt1(video decode/encode) is deliberately left alone, since capping media engines wasn't asked for and could hurt video playback for no real benefit. Confirmed holding under real sustained load. Device path resolved dynamically (matches whichever/sys/class/drm/cardNis driven byxe), not hardcoded to one machine's PCI address. - PCIe ASPM, also via TLP, keyed to the real AC/battery state rather
than the selected mode (unlike everything else above) — it's a device
link power-saving setting, not a performance one, so it doesn't need to
track how much CPU/GPU performance was asked for. Left at
default(untouched) on AC;powersupersave(the most aggressive level) on battery, for the NVMe SSD/WiFi card idle power it can save. Confirmed live before shipping: no dmesg errors, network and NVMe access both stayed fully responsive at this setting on the hardware this was built on.
Known gotcha, worth checking if a mode doesn't seem to change anything
while on AC power: /etc/tlp.conf ships with some settings commented
out as illustrative defaults, but on at least one system this project was
built on, CPU_SCALING_GOVERNOR_ON_AC and CPU_ENERGY_PERF_POLICY_ON_AC
were already uncommented (both set to performance) — since
/etc/tlp.conf is meant to be the base config that /etc/tlp.d/*.conf
drop-ins like this app's override, an uncommented value there silently
wins over whatever this app writes, for the AC state only (confirmed live
that the battery-state settings were unaffected). If Quiet/Balanced don't
seem to actually change CPU behavior while plugged in, check
/etc/tlp.conf for uncommented *_ON_AC lines under those two settings
and comment them out.
Both tlp start and nvidia-smi -pl/-lgc/Intel max_freq need root. Rather than a
pkexec prompt on every single mode switch (which would defeat the point
of a quick tray-menu switch), a small script (apply-power-profile.sh,
installed to /usr/lib/clevo-control-panel/ by both install paths) does
the actual work, and a narrowly-scoped sudoers rule lets the clevoctl
group run it — only with the exact arguments quiet, balanced, or
performance, nothing else, no wildcard argument matching. The sudoers
rule itself is a deliberate, security-sensitive file, so it's never
installed automatically: run setup-power-profile-sudoers.sh yourself
when you're ready (it validates with visudo -c before touching
anything real). Without it, these three modes still work exactly as
before — fan control only, CPU/GPU scaling silently skipped.
On boards with a hardware display MUX (the panel wired directly to either the iGPU or dGPU, not the usual Optimus-style always-through-the- iGPU setup), this laptop's BIOS exposes two modes — MSHybrid (both GPUs available, panel muxed to the iGPU) and dGPU (panel muxed directly to the NVIDIA GPU, no copy-through overhead — see the full-range OLED brightness feature below, which only applies in this mode). Normally switching requires rebooting into BIOS Setup.
The GPU page's GPU Mode section shows your current mode (detected from what's actually enumerated on the PCI bus — no privileged access needed for that) and, if the sudoers step below is installed, a button to request the other mode. This works by writing a single byte to a UEFI NVRAM variable — found by diffing every BIOS Setup-related NVRAM variable between a real, BIOS-confirmed boot in each mode; exactly one byte differed in a way that turned out to be both necessary and sufficient, confirmed by writing only that byte (nothing else touched) and getting a real, verified mode change on reboot, in both directions. This only writes a pending request to firmware — it never touches live GPU/display state, and always requires a manual reboot to actually take effect (the button makes this explicit; there's no auto-reboot).
This is deliberately not done via the ACPI \_SB.PC00.GFX0._DSM method
some laptops expose for this (found and investigated first) — that path
has a confirmed live side effect: invoking it with an active display
session hangs the system hard enough to need a forced power-off, and
even a clean reboot afterward never actually persisted the requested
mode. The NVRAM write this feature actually uses has no such risk.
Board-specific: the exact NVRAM variable/offset/values were reverse-
engineered for this exact board's firmware (Insyde H2O, Clevo/Tongfang
L550JNP). switch-gpu-mode.sh refuses to act if the byte it finds
doesn't match one of the two known values, rather than guessing, but
this whole mechanism is not expected to be portable to different
hardware without redoing the same diffing investigation.
Same sudoers pattern as CPU/GPU power scaling above, and its own
separate opt-in: switch-gpu-mode.sh is the narrowly-scoped script (only
the exact arguments dgpu or mshybrid, nothing else), and
setup-gpu-mode-sudoers.sh installs the rule granting clevoctl passwordless
access to it. Without that step, GPU Mode still shows your current mode,
it just can't switch it.
prime-select safety net. Switching to dGPU mode disconnects the Intel
iGPU from the panel entirely — if prime-select is still set to intel
(session bound to Intel-only), the next boot comes up to a black screen,
since nothing is telling the graphics stack to use the GPU actually wired
to the panel anymore. If the setup-prime-select-sudoers.sh step below is
installed, clicking "Reboot Now" in that situation first switches
prime-select to on-demand (showing a "Please Wait" dialog — it runs
update-initramfs/update-grub internally, which can take tens of
seconds) before actually rebooting; "Reboot Later" does the same fix
quietly in the background instead. Without that sudoers step, switching to
dGPU mode while prime-select is intel can leave you with no display
after reboot until you fix it manually (sudo prime-select on-demand or
nvidia, from a TTY or over SSH).
Turn on "Automatic switching" in the Performance page and pick a profile for AC power and a (typically lower-power) profile for battery — from then on, the mode follows the power source with no manual switching needed. Only Balanced/Quiet/Performance are selectable here; Max Fan is a manual cooling override, not a real profile, so it's excluded from the picker and always stays available to click even while auto-switch owns the other three.
The setting lives in /var/lib/clevo-control-panel/auto-profile.json
(system-wide like the other state files, not per-user — the hardware is
shared by whoever's logged in) and is applied in three places, all
funneling through the same performance.py set_mode() a manual click
would use:
- At app startup: if auto-switch is on, the mode is set from the current AC/battery state immediately, rather than restoring whatever was last manually set.
- On every change you make in the app (toggling the switch, changing either dropdown): applied immediately, not left to wait for a notification.
- While the app keeps running:
power_source.pysubscribes to UPower'sOnBatteryD-Bus property (the same system service GNOME/KDE already run for the battery icon and power settings) and switches the instant AC is plugged or unplugged — no polling, no added dependency, since UPower is already present on any standard desktop. Only falls back to polling/sys/class/power_supply/*/type == Mainsevery 5 seconds if UPower isn't running at all, e.g. a minimal install without a full desktop environment. On hardware with no distinguishable AC/battery supply (e.g. a desktop), auto-switch has nothing to key off of and the app falls back to manual mode.
The manual mode buttons (Balanced/Quiet/Performance, not Max Fan) become insensitive while auto-switch is on, both in the app window and the tray menu — otherwise a manual click there would just get silently overridden on the next power-source check, which would be confusing. Turn auto-switch off to take manual control back.
The same toggle also optionally drives display refresh rate: pick a
rate for AC and a (typically lower) rate for battery, left unset by
default ("Don't change") so nothing happens unless you opt in. Applied
through display.py's DisplayRefreshRate, which talks to GNOME
Mutter's own org.gnome.Mutter.DisplayConfig D-Bus interface — the same
one GNOME Settings' Displays panel uses internally, and the Wayland-native
way to do this (there's no xrandr under Wayland). Only works under
GNOME/Mutter, not other Wayland compositors. A failure here (unsupported
rate, no Mutter D-Bus, a non-GNOME session) is swallowed silently, the
same way a CPU/GPU power-profile failure is — this is a comfort/
battery-saving feature, not something that should ever block or crash a
profile switch.
The Battery page's charge thresholds are a hysteresis window, not a
simple ceiling: charging only resumes once capacity drops to/below
the start percentage, and raising the end percentage alone does
nothing if the battery is currently sitting stopped somewhere between
the two (a very plausible moment to reach for this button in the first
place). "Charge to 100% This Time" raises both thresholds — end to
100, start to 99 — so charging resumes immediately regardless of current
capacity, saves your normal thresholds to a small state file
(/var/lib/clevo-control-panel/charge-override.json), and reverts them
automatically the next time the laptop is seen running on battery.
That last part works even across a full shutdown: PowerSourceMonitor
checks the current power source once immediately whenever it's
constructed, not just on live changes, so if you shut down while
charging and unplug at some point before or shortly after your next
login, the app's autostart entry notices it's on battery and reverts
right then — even though nothing was running to see the actual unplug
happen.
Two details worth knowing if you're touching this code: the override uses 99, not 100, for the start threshold, and end has to be raised before start — confirmed live that this EC silently rejects (keeps the previous value, doesn't clamp) any write that would leave start >= end, so start can never actually reach 100 while end is 100, and raising start past its old value requires end to already be higher. Reverting goes in the opposite order (start, then end) for the same reason.
An alternative to the four fixed presets: a continuous, temperature-driven
duty curve you define yourself, built directly on the same clevo-acpi
EC commands performance_mode already uses (a second, independently
discovered command family — see ~/odm-laptop-research/NOTES.md for how
it was found and validated). Turn it on in the Performance page, edit the
temperature/duty points, set a critical-temperature override and a
maximum-duty comfort ceiling, then Apply.
Safety design — this genuinely controls cooling, so it's built with several independent layers, not just one:
- A real background daemon (
clevo-fan-curve.service,Type=notify,Restart=on-failure), not a GUI-process timer — curve-following keeps working even if the window is closed, and systemd restarts it within seconds if it crashes. It's a systemd --user unit, not a system one, so it only ever starts once you've actually logged in, never during early boot — the same ruleperformance_modepersistence already follows, after an earlier incident traced a hard power-off to a different reverse-engineered EC command being issued from a system unit at early boot (see~/laptopissues/performance-mode/NOTES.md). - Graceful release on any normal exit (stop, restart, shutdown, exception) — the daemon always hands control back to firmware auto before it goes away.
- A hard critical-temperature override, re-checked every single tick independent of the curve or the max-duty cap — if temperature crosses the line, both fans go to 100%, full stop.
- A kernel-level watchdog inside
clevo-acpi.citself — a genuine dead-man's-switch: if nothing renews the manual override within a configurable timeout (default 15s), the kernel releases to firmware auto control on its own, with no userspace process involved at all. Confirmed live, including the harshest case: an uncatchably killed (SIGKILL) daemon still gets its fan control released automatically, purely by the kernel timing out. - A prominent manual "Release Fan Control to Firmware" button, always reachable regardless of the toggle state, plus a live daemon-health indicator (service active? status file fresh? critical override engaged?) so a silently stuck daemon is visible, not invisible.
Curve mode and the four discrete presets are mutually exclusive with automatic switching (both make continuous, automated claims on the same EC state) — turning one on turns the other off, with a toast explaining why. All four mode buttons, Max Fan included, become insensitive while a curve is active, since Max Fan and a curve would otherwise race for control of the literal same fan-duty actuator.
clevo-control-panel-cli fan status|curve show|curve set|enable|disable| release|set gives the same functionality from the command line — see
--help on each.
The tray icon runs as a separate process from the main GTK4/libadwaita
app, using the classic GTK3-based AyatanaAppIndicator3 — not a toolkit
choice made lightly. The newer, GTK4-safe AyatanaAppIndicatorGlib
library was tried first, but its set_menu() doesn't actually implement
the com.canonical.dbusmenu D-Bus interface GNOME's ubuntu-appindicators
shell extension needs to show a working menu (confirmed by testing, not
documentation — it's a young library, first packaged for Debian in March
2025). GTK3 and GTK4 can't be loaded in the same process, so the tray
lives in its own small process (clevo_control_panel/tray_helper.py),
talking to the main app over D-Bus for Open/Quit (using Gio.Application's
own built-in remote-activation and remote-actions support — no custom IPC
code needed there), while reading/writing performance mode directly
(backend.py/performance.py are plain Python with no GTK dependency at
all, so there's no reason to round-trip through the main process for
that). The two processes are loosely coupled on purpose: the helper
watches the main app's D-Bus name and quits shortly after it disappears,
however that happens.
clevo_control_panel/ Python package: backends, UI, CLI, tray helper, config, setup helper
bin/clevo-control-panel GUI launcher script
bin/clevo-control-panel-cli CLI launcher script
data/ udev rule, systemd units, autostart/desktop entries, install script,
apply-power-profile.sh + its sudoers setup script
Copyright (c) 2026 Michael Updike. GPLv3 — see LICENSE.