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

My daily driver is a Tuxedo laptop, and Tuxedo ships their own daemon for it: Tuxedo Control Center (tccd). It manages fan curves, power limits, CPU frequency profiles, all the hardware knobs. The catch is that the only official way to switch profiles is a full desktop GUI application. Every time I wanted to drop into power-save mode for a meeting, or crank up to performance mode for a big compile, I had to open a window, click through it, and close it again.

That annoyed me enough to write TCC Bridge, a small Bash wrapper that puts profile switching on the command line. More usefully, it puts profile switching on 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. If you haven't poked at D-Bus before, it's Linux's standard IPC mechanism, the same bus your notifications, network manager, and Bluetooth stack all talk over. Services expose interfaces with methods you can call, and tccd plays along nicely.

You can introspect the whole interface with gdbus:

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

Buried in the output are methods like GetProfilesJSON, GetActiveProfileJSON, and SetTempProfileById. In other words, everything the GUI does, sitting right there on the bus waiting to be called.

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

The wrapper

Every D-Bus call follows the same pattern, so a small helper keeps things tidy and strips the shell-escaping noise that gdbus wraps around its 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/',)$//"
}

With that in place, listing profiles is just a jq query over the JSON that GetProfilesJSON returns:

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

Switching goes through SetTempProfileById. I also made the function check that the switch actually took, and fire a desktop notification so I get visual feedback:

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 piece I actually use every day is --next, which I bind to a key. It reads the current active profile ID, finds where it sits in the list, and wraps around 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
}

Note that the cycling logic works on profile IDs, not names. Names can contain spaces, IDs can't, and mapfile reads them into an array without any word-splitting headaches.

Binding to a key

In KDE it's 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 trick works anywhere you can bind a shell command to a key: Hyprland, Sway, i3, GNOME, take your pick.

Beyond --next, the script also supports --list to see all profiles, --current to show the active one, and --set <ID> to jump straight to a specific profile.

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 this was the first project where I actually used it directly, and a few things surprised me:

  • Once you know how to introspect the service tree, gdbus is refreshingly straightforward to work with
  • D-Bus type signatures look arcane (you'll run into things like (a{sv}) in method signatures), but in practice you only ever need to understand the handful of calls you care about
  • The system bus versus session bus distinction matters. tccd runs as root and lives on the system bus, not the per-user session bus, and nothing works until you pass --system

Sometimes the right answer is a shell script.


Source on GitHub.

// related project

TCC Bridge

// share

// comments