Skip to main content
← blog

24 April 2026

Controlling Tuxedo Laptop Power Profiles Without the GUI

How I replaced the Tuxedo Control Center desktop app with a small Bash script and a keyboard shortcut.

linuxbashd-bustuxedo

I run a Tuxedo laptop as my daily driver, and Tuxedo ships their own daemon, Tuxedo Control Center (tccd), for managing things like fan curves, power limits, and CPU frequency profiles. The problem: the only way to switch profiles is through a full desktop GUI application. Every time I wanted to drop into power-save mode for a meeting or ramp up to performance mode for a compile job, I had to open a window, click around, and close it.

That bothered me enough that I wrote TCC Bridge: a small Bash wrapper that exposes profile switching on the command line, and more usefully, via a keyboard shortcut in any window manager.

How tccd exposes its API

The daemon registers a D-Bus service at com.tuxedocomputers.tccd on the system bus. D-Bus is Linux's standard IPC mechanism: the same bus your notifications, network manager, and Bluetooth stack talk over. Services on D-Bus expose interfaces with methods you can call, and tccd is no different.

You can introspect the whole interface with gdbus:

bash
gdbus introspect \
  --system \
  --dest com.tuxedocomputers.tccd \
  --object-path /com/tuxedocomputers/tccd

In the output you'll find methods like GetProfilesJSON, GetActiveProfileJSON, and SetTempProfileById — everything you need without touching the GUI.

gdbus introspect output showing the tccd interface
gdbus introspect output showing the tccd interface

The wrapper

Since every D-Bus call follows the same pattern, a small helper avoids repetition and strips the shell-escaping noise from gdbus output:

bash
BUS_ARGS="--system --dest com.tuxedocomputers.tccd --object-path /com/tuxedocomputers/tccd --method com.tuxedocomputers.tccd"

call_tcc() {
    gdbus call $BUS_ARGS."$1" | sed "s/^('//;s/',)$//"
}

Listing profiles is then just a jq query on the JSON GetProfilesJSON returns:

bash
list_profiles() {
    local json=$(call_tcc "GetProfilesJSON")
    echo "ID | Name"
    echo "---|---"
    echo "$json" | jq -r '.[] | "\(.id) | \(.name)"'
}

Switching is done with SetTempProfileById. The function also validates the switch succeeded and fires a desktop notification:

bash
set_profile() {
    local target_id="$1"
    local all_json=$(call_tcc "GetProfilesJSON")
    local target_name=$(echo "$all_json" | jq -r --arg id "$target_id" '.[] | select(.id == $id) | .name')

    gdbus call $BUS_ARGS.SetTempProfileById "$target_id" > /dev/null

    local active_name=$(call_tcc "GetActiveProfileJSON" | jq -r '.name')
    if [[ "$active_name" != "$target_name" ]]; then
        echo "Failed to switch profiles. Current profile is still: $active_name"
        exit 1
    fi

    if command -v notify-send &> /dev/null; then
        notify-send "Tuxedo Control Center" "Switched to profile: $target_name"
    fi
}

The --next command (which I bind to a key) reads the current active profile ID, finds its position in the list, and wraps to the next one:

bash
next_profile() {
    local current_id=$(call_tcc "GetActiveProfileJSON" | jq -r '.id')
    local all_json=$(call_tcc "GetProfilesJSON")

    mapfile -t ids < <(echo "$all_json" | jq -r '.[].id')

    for i in "${!ids[@]}"; do
        if [[ "${ids[$i]}" == "$current_id" ]]; then
            next_idx=$(( (i + 1) % ${#ids[@]} ))
            set_profile "${ids[$next_idx]}"
            return
        fi
    done
}

Profile IDs (not names) are used for the cycling logic — names can contain spaces, IDs don't. mapfile reads them cleanly into an array without any word-splitting headaches.

Binding to a key

In KDE, open System Settings → Shortcuts → Custom Shortcuts, create a new command shortcut, and point it at the script:

bash
~/.local/bin/tuxedo_profile_control.sh --next

One keypress, a notification pops up for two seconds, done. No window, no mouse.

The same approach works in any environment that supports custom keyboard shortcuts bound to shell commands — Hyprland, Sway, i3, GNOME, and so on.

The script also supports --list to see all profiles, --current to show which one is active, and --set <ID> to switch to a specific profile directly.

tuxedo_profile_control.sh --list
tuxedo_profile_control.sh --list
tuxedo_profile_control.sh --next cycling to the next profile
tuxedo_profile_control.sh --next cycling to the next profile
Desktop notification on profile switch
Desktop notification on profile switch

What I learned

I knew D-Bus existed but had never actually used it directly before this project. A few things that surprised me:

  • The gdbus interface is refreshingly straightforward once you know how to introspect the service tree
  • D-Bus type signatures are a bit arcane (you'll see things like (a{sv}) in method signatures) but you only need to handle the specific calls you care about
  • The system bus vs session bus distinction matters: tccd runs as root and exposes itself on the system bus, not the per-user session bus

Sometimes the right answer is a shell script.


Source on GitHub.

// share

// comments