cctv 2.4.1

Terminal interface for CoolerControl
<div align="center">
  <h1>
  <img alt="cctv_logo" src="https://gitlab.com/coolercontrol/cctv/-/raw/master/assets/cctv.png" width="200">
  <br>
  cctv
  <br>
  </h1>

A terminal client for [CoolerControl](https://gitlab.com/coolercontrol/coolercontrol).

![](assets/demo.webm)
:warning: Requires the CoolerControl service daemon to be running either locally or remotely, [see Configuration](#configuration).

</div>

## Features

- **TUI**: Realtime charts of temperatures and fan speeds, cooling profile
editor, mode switcher, alert viewer, and task manager.
- **CLI**: Commands for use in scripts and automation.
- **Task automation**: Event-driven rules that pair triggers with actions (e.g.
run a script). See [Tasks]#tasks.
- **IPC**: A leader/follower model using Linux abstract Unix sockets. The
leader (typically a background `daemonize` instance or TUI) maintains a
persistent, authenticated connection to the `coolercontrold` daemon and serves
CLI stubs over IPC.
- **Remote daemon support**: Connect to a `coolercontrold` instance on another
machine via `daemon_address`/`port` config or multiple config files with
`CCTV_CONFIG_FILEPATH`.

## Usage Tips

- **Run a background instance** with via the [systemd
service](#systemd-unit) or as a [CoolerControl plugin]#coolercontrol-plugin to keep a persistent
connection to `coolercontrold`.
- **Pipe `dump` to `jq`** for scripting. For example, to get the name of the
currently active mode: 

```bash
cctv dump | jq -r '.state.current_mode_uid as $uid | .modes[] | select(.uid == $uid) | .name'
```

Get alert UIDs:

```bash
cctv dump | jq -r '.state.alerts[] | {uid: .uid, name: .name}'
```
- **Hide channels** (e.g. per-core temperatures) from `cctv` by disabling them
in the CoolerControl UI.
- **Multiple daemon targets**: Use `CCTV_CONFIG_FILEPATH` with different config
files to run several `cctv` instances on one machine, each connected to a
different `coolercontrold`.

[[_TOC_]]

## Building

Clone the git repo and build with cargo. Requires a working [Rust
installation](https://www.rust-lang.org/tools/install).

```bash
git clone https://gitlab.com/coolercontrol/cctv.git
cd cctv
cargo build --release
cargo run --release
```

## Installing/Upgrading

There are no packages for distributions yet but you can download and install the
latest binary artifact built from master or build from source yourself.

<details>
<summary>

### AUR (Arch Linux) — *Not yet published*

</summary>

Install [`cctv-bin`](https://aur.archlinux.org/packages/cctv-bin) with your preferred AUR helper:

```bash
yay -S cctv-bin
```

To run cctv tasks in the background, enable the systemd service:

```bash
sudo systemctl enable --now "cctv@$USER.service"
```

</details>

### Latest release

```bash
TAG=$(curl -sL https://gitlab.com/api/v4/projects/69657520/releases/permalink/latest | jq -r .tag_name)
curl -Lo /tmp/cctv \
    "https://gitlab.com/api/v4/projects/69657520/packages/generic/releases/${TAG#v}/cctv"
sudo install -Dm 755 /tmp/cctv -t /usr/bin
```

### From source

Install with cargo. Requires a working [Rust installation](https://www.rust-lang.org/tools/install).

```bash
cargo install --git https://gitlab.com/coolercontrol/cctv.git
```

### From crates.io

`cctv` is also published to crates.io. Note that the version there may be slightly outdated.

```bash
cargo install cctv
```

Make sure `$HOME/.cargo/bin` is in your path:

```bash
# Bash users
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# zsh users
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
```

## Usage

### Running a persistent daemon

`cctv` instances can offload work to each other. Running a persistent instance
will help with ratelimits and CoolerControl daemon load if you're calling
oneshot commands in a loop.

#### Systemd Unit

To run cctv tasks in the background, enable the systemd service:

```bash
TAG=$(curl -sL https://gitlab.com/api/v4/projects/69657520/releases/permalink/latest | jq -r .tag_name)
curl -L "https://gitlab.com/coolercontrol/cctv/-/archive/${TAG}/cctv-${TAG}.tar.gz" | \
    tar -xzf - --strip-components=2 -C /tmp "cctv-${TAG}/dist/cctv@.service"
sudo install -Dm 644 /tmp/cctv@.service -t /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now "cctv@$USER.service"
```

#### CoolerControl Plugin

CoolerControl can manage a cctv daemon. See the
[documentation](https://docs.coolercontrol.org/automation/plugins.html#cctv).

### CLI

See `cctv --help` for a list of available commands.

The `dump` command is meant to be used with [`jq`](https://jqlang.org).

For example, to get the name of the currently active mode: 

```Bash
cctv dump | jq -r '.state.current_mode_uid as $uid | .modes[] | select(.uid == $uid) | .name'
```

Feel free to reach out to request dedicated subcommands for commonly used operations.

### TUI

#### Keybindings

##### Shared

| Keys | Action |
|---|---|
| Down, e, j | Select next item |
| Up, i, k | Select previous item |
| Right, o, l | Go to the next page |
| Left, n, h | Go to the previous page |
| R | Refresh app data |
| s | Toggle legend visibility |
| q | Quit |
| - | Decrease chart time range by 30 seconds (min 30s) |
| +/= | Increase chart time range by 30 seconds |
| Ctrl+b | Toggle dock visibility. You can still use relevant keybindings when the dock is hidden. |

##### Devices

| Keys | Action |
|---|---|
| a | Toggle between the default and the alternative chart |

cctv shows all channels enabled in the CoolerControl daemon. To hide a channel (e.g. per-core temperatures), disable it in the CoolerControl UI.

##### Modes

| Keys | Action |
|---|---|
| Enter | Activate currently highlighted mode |

##### Profiles

| Keys | Action |
|---|---|
| E | Enter edit mode |
| Enter | Save working changes |
| Esc | Leave edit mode and keep a draft |
| A | Apply saved changes to the daemon |
| D | Reset edit buffer to current profile values |

##### Tasks

| Keys | Action |
|---|---|
| N | Create a new task and open editor |
| E | Edit the selected task |
| Enter | Validate and save changes |
| Esc | Close editor and save a draft |
| X | Toggle deletion mark on the selected task |
| A | Validate and apply all tasks to the config file |
| D | Discard in-memory changes (reload from disk) |

#### Editing profiles

There are two input modes: normal and edit. Normal mode is the default for
navigation. Edit mode opens a popup prefilled with the current settings.

Edits are in one of three states: draft, saved, and applied.

| State | Description |
|---|---|
| Draft | Local edits kept in memory, not yet validated. |
| Saved | Validated edits used for graph drawing, ineffective until applied. |
| Applied | Final changes sent to the daemon. |

Changes are validated when moving between stages.

| From | To | Validation |
|---|---|---|
| Draft | Saved | All syntactically correct changes are accepted. |
| Saved | Applied | All changes deemed valid by the daemon are accepted. |

There are no guard rails beyond these. You will be notified of errors; no
notification means success.

:light_bulb: Refreshing the page syncs `cctv` with the daemon and drops saved changes.

:warning: Changes made in `cctv` require a page refresh to be reflected in the GUI.
The reverse is also true.

#### Editing tasks

Tasks that have been modified but not yet persisted are marked in the dock:

| Suffix | Meaning |
|---|---|
| *(none)* | Applied — matches what's on disk. |
| (Draft) | Local edits, not yet validated. |
| (Unapplied) | Validated, but not yet written to disk. |
| (Deleted) | Marked for deletion — will be removed on apply. |

### Tasks

Tasks are event-driven automation rules that pair a **trigger** with an
**action**. They run only on the leader `cctv` instance to prevent duplicate
execution.

#### Triggers

| Trigger | Fields | Description |
|---|---|---|
| Alert | `alert_uid`, `state` (`Active`, `Inactive`, `Error`) | Fires when an alert transitions to the given state. |
| Mode change | `mode_uid` | Fires when the specified mode is activated. |

:light_bulb: Get alert UIDs with `cctv dump | jq -r '.state.alerts[] | {uid: .uid, name: .name}'`
and mode UIDs with `cctv dump | jq -r '.modes[] | {uid: .uid, name: .name}'`.

#### Actions

| Type | Fields | Description |
|---|---|---|
| `Script` | `path` | Executes a script. The shebang is parsed to determine the interpreter; the script does not have to be executable but must contain a valid shebang or be runnable via `/usr/bin/env bash`. |
| `ChangeMode` | `mode_uid` | Activates the specified mode via the daemon. |
| `ActivatePreviousMode` || Reverts to the previously active mode. |

#### Example

```json
{
	"tasks": [
		{
			"trigger": { "alert_uid": "<uid>", "state": "Active" },
			"action": { "type": "Script", "path": "/path/to/script" }
		},
		{
			"trigger": { "alert_uid": "<uid>", "state": "Inactive" },
			"action": { "type": "ActivatePreviousMode" }
		},
		{
			"trigger": { "mode_uid": "<uid>" },
			"action": { "type": "ChangeMode", "mode_uid": "<target-mode-uid>" }
		}
	]
}
```

## Configuration

Configuration is optional. `cctv` reads its from _one of_ these locations, in
order of precedence:
- `CCTV_CONFIG_FILEPATH` environment variable
- `$XDG_CONFIG_HOME/coolercontrol/cctv.json`
- `$HOME/.config/coolercontrol/cctv.json`

If a file is not found, `cctv` will use default values and create one for you.

If `daemon_address` or `port` are undefined, `cctv` will pull them from the
local `coolercontrold` config file located at `/etc/coolercontrol/config.toml`.

These are useful for connecting to remote daemons. Combined with
`CCTV_CONFIG_FILEPATH`, you can have multiple `cctv` instances on one machine
connecting to different `coolercontrold` instances.

Finally, for security, the password is read from the `CCTV_DAEMON_PASSWORD`
environment variable.

### Configuration Options

| Option | Description | Default |
|---|---|---|
| `daemon_address` | Address CoolerControl is listening on. | Value in the `coolercontrold` configuration file or internal default. |
| `port` | Port CoolerControl is listening on. | Value in the `coolercontrold` configuration file or internal default. |
| `time_range_s` | History in seconds for timeseries charts. | 60 |
| `username` | CoolerControl username. | *Internal default* |
| `skip_splash` | Skips the splash screen. | false |
| `tasks` | List of tasks to execute.| [] |

### Environment variables

| Environment Variable | Description | Default |
|---|---|---|
| `CCTV_CONFIG_FILEPATH` | Absolute path to the CCTV configuration file. | See [above]#configuration. |
| `CCTV_DAEMON_PASSWORD` | CoolerControl password. | *Internal default* |

Note that there's currently no SSL or IPv6 support.

### Obtaining cctv logs

TUI logs are written to `cctv-<timestamp>-<pid>.log` in
`XDG_STATE_HOME/coolercontrol` or `HOME/.local/state/coolercontrol`.
A `cctv-latest.log` symlink always points to the most recently started
instance's log. Other modes write to stdout. Daemon logs from a systemd unit
can be retrieved with `journalctl -u "cctv@$USER"`.