thermalcanary 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- thermalcanary-1.1.0/.claude/settings.local.json +5 -0
- thermalcanary-1.1.0/.githooks/pre-commit +75 -0
- thermalcanary-1.1.0/.github/workflows/ci.yml +112 -0
- thermalcanary-1.1.0/.gitignore +18 -0
- thermalcanary-1.1.0/LICENSE +21 -0
- thermalcanary-1.1.0/PKG-INFO +15 -0
- thermalcanary-1.1.0/README.md +181 -0
- thermalcanary-1.1.0/assets/icon.png +0 -0
- thermalcanary-1.1.0/assets/logo.png +0 -0
- thermalcanary-1.1.0/bin/build_logo.py +62 -0
- thermalcanary-1.1.0/bin/fonts/Exo2-SemiBold.ttf +0 -0
- thermalcanary-1.1.0/config.example.yaml +23 -0
- thermalcanary-1.1.0/coverage-badge.json +1 -0
- thermalcanary-1.1.0/install.sh +280 -0
- thermalcanary-1.1.0/mutation-badge.json +1 -0
- thermalcanary-1.1.0/packaging/aur/.SRCINFO +26 -0
- thermalcanary-1.1.0/packaging/aur/PKGBUILD +75 -0
- thermalcanary-1.1.0/packaging/flatpak/io.github.ibasaw.ThermalCanary.desktop +12 -0
- thermalcanary-1.1.0/packaging/flatpak/io.github.ibasaw.ThermalCanary.metainfo.xml +89 -0
- thermalcanary-1.1.0/packaging/flatpak/io.github.ibasaw.ThermalCanary.yml +84 -0
- thermalcanary-1.1.0/pyproject.toml +53 -0
- thermalcanary-1.1.0/screenshot-settings.png +0 -0
- thermalcanary-1.1.0/screenshot.png +0 -0
- thermalcanary-1.1.0/tests/__init__.py +0 -0
- thermalcanary-1.1.0/tests/conftest.py +57 -0
- thermalcanary-1.1.0/tests/test_app.py +254 -0
- thermalcanary-1.1.0/tests/test_config.py +459 -0
- thermalcanary-1.1.0/tests/test_first_run.py +106 -0
- thermalcanary-1.1.0/tests/test_gauge.py +439 -0
- thermalcanary-1.1.0/tests/test_gpu_sysfs.py +146 -0
- thermalcanary-1.1.0/tests/test_integration.py +64 -0
- thermalcanary-1.1.0/tests/test_screens.py +106 -0
- thermalcanary-1.1.0/tests/test_sensor.py +356 -0
- thermalcanary-1.1.0/tests/test_sensor_amd.py +129 -0
- thermalcanary-1.1.0/tests/test_sensor_intel.py +48 -0
- thermalcanary-1.1.0/tests/test_sensor_nvidia.py +137 -0
- thermalcanary-1.1.0/tests/test_sensor_select.py +121 -0
- thermalcanary-1.1.0/tests/test_settings_dpms.py +193 -0
- thermalcanary-1.1.0/tests/test_tray.py +202 -0
- thermalcanary-1.1.0/thermalcanary/__init__.py +7 -0
- thermalcanary-1.1.0/thermalcanary/__main__.py +4 -0
- thermalcanary-1.1.0/thermalcanary/_gpu_sysfs.py +61 -0
- thermalcanary-1.1.0/thermalcanary/amd.py +24 -0
- thermalcanary-1.1.0/thermalcanary/app.py +662 -0
- thermalcanary-1.1.0/thermalcanary/config.py +141 -0
- thermalcanary-1.1.0/thermalcanary/first_run.py +139 -0
- thermalcanary-1.1.0/thermalcanary/gauge.py +244 -0
- thermalcanary-1.1.0/thermalcanary/intel.py +11 -0
- thermalcanary-1.1.0/thermalcanary/nvidia.py +26 -0
- thermalcanary-1.1.0/thermalcanary/screens.py +47 -0
- thermalcanary-1.1.0/thermalcanary/sensor.py +116 -0
- thermalcanary-1.1.0/thermalcanary/sensor_select.py +82 -0
- thermalcanary-1.1.0/thermalcanary/settings.py +527 -0
- thermalcanary-1.1.0/thermalcanary/tray.py +89 -0
- thermalcanary-1.1.0/todos.md +1 -0
- thermalcanary-1.1.0/tools/fake_gpu.py +91 -0
- thermalcanary-1.1.0/uninstall.sh +46 -0
- thermalcanary-1.1.0/uv.lock +846 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# Pre-commit hook — fast checks on staged files only.
|
|
4
|
+
# Install: git config core.hooksPath .githooks
|
|
5
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
10
|
+
|
|
11
|
+
STAGED_PY=$(git diff --cached --name-only --diff-filter=AM | grep '\.py$' | tr '\n' ' ' || true)
|
|
12
|
+
STAGED_ALL=$(git diff --cached --name-only --diff-filter=AM | tr '\n' ' ' || true)
|
|
13
|
+
|
|
14
|
+
ERRORS=0
|
|
15
|
+
|
|
16
|
+
# ── Python: syntax check on staged files ─────────────────────────────────────
|
|
17
|
+
if [ -n "$STAGED_PY" ]; then
|
|
18
|
+
echo "── Python syntax check (staged files) ──"
|
|
19
|
+
for f in $STAGED_PY; do
|
|
20
|
+
if ! uv run --project "$ROOT" python -m py_compile "$f" 2>&1; then
|
|
21
|
+
echo " Syntax error in: $f"
|
|
22
|
+
ERRORS=1
|
|
23
|
+
fi
|
|
24
|
+
done
|
|
25
|
+
[ $ERRORS -eq 0 ] && echo " OK"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# ── Bandit: security lint on staged Python files ──────────────────────────────
|
|
29
|
+
# Test files are excluded: pytest legitimately requires `assert` (B101), and the
|
|
30
|
+
# whole point of test fixtures is to use hardcoded paths (B108). Lint app code.
|
|
31
|
+
if [ -n "$STAGED_PY" ]; then
|
|
32
|
+
echo "── Bandit security lint (staged files) ──"
|
|
33
|
+
BANDIT_TARGETS=""
|
|
34
|
+
for f in $STAGED_PY; do
|
|
35
|
+
case "$f" in
|
|
36
|
+
tests/*|*/tests/*) ;;
|
|
37
|
+
*) BANDIT_TARGETS="$BANDIT_TARGETS $f" ;;
|
|
38
|
+
esac
|
|
39
|
+
done
|
|
40
|
+
if [ -n "$BANDIT_TARGETS" ]; then
|
|
41
|
+
BANDIT_EXIT=0
|
|
42
|
+
uv run --project "$ROOT" bandit -q $BANDIT_TARGETS || BANDIT_EXIT=$?
|
|
43
|
+
if [ $BANDIT_EXIT -ne 0 ]; then
|
|
44
|
+
ERRORS=1
|
|
45
|
+
fi
|
|
46
|
+
else
|
|
47
|
+
echo " (only test files staged — skipped)"
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# ── Secrets scan: block commits containing secrets/keys ──────────────────────
|
|
52
|
+
if [ -n "$STAGED_ALL" ]; then
|
|
53
|
+
echo "── Secrets scan (staged files) ──"
|
|
54
|
+
SECRET_PATTERNS='(PRIVATE KEY-----)'
|
|
55
|
+
SECRET_PATTERNS="$SECRET_PATTERNS|(sk_live_[a-zA-Z0-9]{20,})"
|
|
56
|
+
SECRET_PATTERNS="$SECRET_PATTERNS|(ls_live_[a-zA-Z0-9]{20,})"
|
|
57
|
+
SECRET_PATTERNS="$SECRET_PATTERNS|(password\s*[:=]\s*['\"][^'\"]{8,}['\"])"
|
|
58
|
+
SECRET_PATTERNS="$SECRET_PATTERNS|(_HMAC_SECRET\s*=\s*['\"][^'\"]{8,}['\"])"
|
|
59
|
+
|
|
60
|
+
if git diff --cached --diff-filter=AM -U0 -- ':!.githooks/pre-commit' | grep -iEq "$SECRET_PATTERNS"; then
|
|
61
|
+
echo " Potential secret detected in staged changes!"
|
|
62
|
+
echo " Review the diff and remove secrets before committing."
|
|
63
|
+
ERRORS=1
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# ── Result ────────────────────────────────────────────────────────────────────
|
|
68
|
+
if [ $ERRORS -ne 0 ]; then
|
|
69
|
+
echo ""
|
|
70
|
+
echo "Pre-commit checks failed. Fix the errors above, then try again."
|
|
71
|
+
echo "To bypass (emergency only): git commit --no-verify"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
echo "Pre-commit checks passed."
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-24.04
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
|
|
15
|
+
- name: Install Qt system dependencies
|
|
16
|
+
run: |
|
|
17
|
+
sudo apt-get update -qq
|
|
18
|
+
sudo apt-get install -y --no-install-recommends \
|
|
19
|
+
libxcb-cursor0 libxcb-xinerama0 libegl1 libgl1 \
|
|
20
|
+
libglib2.0-0 libdbus-1-3
|
|
21
|
+
|
|
22
|
+
- uses: astral-sh/setup-uv@v6
|
|
23
|
+
|
|
24
|
+
- name: Install Python dependencies
|
|
25
|
+
run: uv sync --extra dev
|
|
26
|
+
|
|
27
|
+
- name: Run tests with coverage
|
|
28
|
+
run: uv run pytest tests/ --cov=thermalcanary --cov-report=term-missing --cov-report=json
|
|
29
|
+
env:
|
|
30
|
+
QT_QPA_PLATFORM: offscreen
|
|
31
|
+
|
|
32
|
+
- name: Run mutation tests (changed files only)
|
|
33
|
+
id: mutation
|
|
34
|
+
run: |
|
|
35
|
+
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
36
|
+
BASE="${{ github.event.pull_request.base.sha }}"
|
|
37
|
+
else
|
|
38
|
+
BASE="$(git rev-parse HEAD~1 2>/dev/null || echo '')"
|
|
39
|
+
fi
|
|
40
|
+
if [ -n "$BASE" ]; then
|
|
41
|
+
PATHS=$(git diff --name-only "$BASE" HEAD -- 'thermalcanary/*.py' | tr '\n' ' ')
|
|
42
|
+
else
|
|
43
|
+
PATHS=""
|
|
44
|
+
fi
|
|
45
|
+
if [ -z "$PATHS" ]; then
|
|
46
|
+
echo "No thermalcanary/ files changed — skipping mutation tests."
|
|
47
|
+
echo "score=-1" >> "$GITHUB_OUTPUT"
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
echo "Mutating: $PATHS"
|
|
51
|
+
# mutmut 3.x accepts module-name patterns as positional args, NOT
|
|
52
|
+
# --paths-to-mutate (which was a 2.x flag). Convert path → module.
|
|
53
|
+
MODS=""
|
|
54
|
+
for f in $PATHS; do
|
|
55
|
+
MODS="$MODS $(echo "$f" | sed 's|/|.|g; s|\.py$||').*"
|
|
56
|
+
done
|
|
57
|
+
# Fresh cache each CI run so `mutmut results` only lists what we ran.
|
|
58
|
+
rm -rf mutants/.mutmut-cache mutants/_*.json .mutmut-cache 2>/dev/null || true
|
|
59
|
+
uv run mutmut run $MODS || true
|
|
60
|
+
# mutmut 3.x results: one line per non-killed mutant ending in
|
|
61
|
+
# ": survived" / ": segfault" / ": timeout" / ": not checked".
|
|
62
|
+
# Killed mutants are hidden — derive from totals.
|
|
63
|
+
RESULTS=$(uv run mutmut results 2>&1)
|
|
64
|
+
TOTAL_LINES=$(echo "$RESULTS" | grep -cE "^\s+thermalcanary\." || true)
|
|
65
|
+
SURVIVED=$(echo "$RESULTS" | grep -cE ": survived$" || true)
|
|
66
|
+
SEGFAULT=$(echo "$RESULTS" | grep -cE ": segfault$" || true)
|
|
67
|
+
TIMEOUT=$(echo "$RESULTS" | grep -cE ": timeout$" || true)
|
|
68
|
+
NOTCHECK=$(echo "$RESULTS" | grep -cE ": not checked$" || true)
|
|
69
|
+
KILLED=$((TOTAL_LINES - SURVIVED - SEGFAULT - TIMEOUT - NOTCHECK))
|
|
70
|
+
[ "$KILLED" -lt 0 ] && KILLED=0
|
|
71
|
+
TOTAL=$((KILLED + SURVIVED))
|
|
72
|
+
SCORE=$(python3 -c "print(f'{$KILLED/$TOTAL*100:.0f}' if $TOTAL else '0')")
|
|
73
|
+
echo "Mutation score: ${SCORE}% (${KILLED}/${TOTAL} killed)"
|
|
74
|
+
echo "score=$SCORE" >> "$GITHUB_OUTPUT"
|
|
75
|
+
echo "killed=$KILLED" >> "$GITHUB_OUTPUT"
|
|
76
|
+
echo "total=$TOTAL" >> "$GITHUB_OUTPUT"
|
|
77
|
+
python3 -c "import sys; sys.exit(0 if $TOTAL == 0 or $KILLED/$TOTAL >= 0.50 else 1)" \
|
|
78
|
+
|| { echo "FAIL: mutation kill rate below 50%"; exit 1; }
|
|
79
|
+
env:
|
|
80
|
+
QT_QPA_PLATFORM: offscreen
|
|
81
|
+
|
|
82
|
+
- name: Update badges
|
|
83
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.mutation.outputs.score != '-1'
|
|
84
|
+
run: |
|
|
85
|
+
# coverage badge
|
|
86
|
+
COV=$(python3 -c "import json; d=json.load(open('coverage.json')); print(int(d['totals']['percent_covered_display']))")
|
|
87
|
+
if [ "$COV" -ge 90 ]; then COV_COLOR=brightgreen
|
|
88
|
+
elif [ "$COV" -ge 80 ]; then COV_COLOR=green
|
|
89
|
+
elif [ "$COV" -ge 70 ]; then COV_COLOR=yellowgreen
|
|
90
|
+
elif [ "$COV" -ge 60 ]; then COV_COLOR=yellow
|
|
91
|
+
else COV_COLOR=orange
|
|
92
|
+
fi
|
|
93
|
+
printf '{"schemaVersion":1,"label":"coverage","message":"%s%%","color":"%s"}\n' "$COV" "$COV_COLOR" \
|
|
94
|
+
> coverage-badge.json
|
|
95
|
+
# mutation badge
|
|
96
|
+
S=${{ steps.mutation.outputs.score }}
|
|
97
|
+
if [ "$S" -ge 80 ]; then COLOR=brightgreen
|
|
98
|
+
elif [ "$S" -ge 70 ]; then COLOR=green
|
|
99
|
+
elif [ "$S" -ge 60 ]; then COLOR=yellowgreen
|
|
100
|
+
elif [ "$S" -ge 50 ]; then COLOR=yellow
|
|
101
|
+
else COLOR=orange
|
|
102
|
+
fi
|
|
103
|
+
printf '{"schemaVersion":1,"label":"mutation","message":"%s%% (%s/%s)","color":"%s"}\n' \
|
|
104
|
+
"$S" "${{ steps.mutation.outputs.killed }}" "${{ steps.mutation.outputs.total }}" "$COLOR" \
|
|
105
|
+
> mutation-badge.json
|
|
106
|
+
git config user.name "github-actions[bot]"
|
|
107
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
108
|
+
git add coverage-badge.json mutation-badge.json
|
|
109
|
+
git diff --cached --quiet || git commit -m "chore: update badges [skip ci]"
|
|
110
|
+
git push
|
|
111
|
+
env:
|
|
112
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ibasaw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thermalcanary
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
License-File: LICENSE
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: nvidia-ml-py>=12.0.0
|
|
7
|
+
Requires-Dist: psutil>=5.9.0
|
|
8
|
+
Requires-Dist: pyqt6>=6.5.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: bandit>=1.7; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-mock>=3.14; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-qt>=4.4; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.png" alt="Thermal Canary" width="600">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://github.com/ibasaw/thermalcanary/actions/workflows/ci.yml"><img src="https://github.com/ibasaw/thermalcanary/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
|
|
7
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibasaw/thermalcanary/main/coverage-badge.json&cacheSeconds=300" alt="Coverage">
|
|
8
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibasaw/thermalcanary/main/mutation-badge.json&cacheSeconds=300" alt="Mutation score">
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+">
|
|
11
|
+
<img src="https://img.shields.io/badge/platform-Linux-lightgrey.svg" alt="Platform: Linux">
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
The Linux software for dedicated hardware monitoring screens — like AIDA64's sensor panel, but native to your desktop.
|
|
15
|
+
|
|
16
|
+
6 circular arc gauges (CPU temp, usage, RAM · GPU temp, fan, VRAM) built with PyQt6 and pynvml.
|
|
17
|
+
Smooth 60fps animation, rolling average stabilisation, dynamic heat colors. Auto-starts on login.
|
|
18
|
+
|
|
19
|
+
**Built for dedicated monitoring screens** — works out of the box on stretched panels (1920×480), small IPS monitors, or any secondary display you use as a permanent hardware panel.
|
|
20
|
+
|
|
21
|
+
> ⚠ **Important** — this is NOT an overlay. Thermal Canary is a fullscreen app intended for a **dedicated monitor**. Don't try to use it as an in-game overlay (use MangoHud for that).
|
|
22
|
+
|
|
23
|
+
**Fully responsive** — gauges scale to any resolution and orientation: ultrawide, portrait, rotated, compact.
|
|
24
|
+
|
|
25
|
+
**One-command install** — a single `bash install.sh` sets up everything automatically.
|
|
26
|
+
|
|
27
|
+
### Dedicated monitoring screen setup
|
|
28
|
+
|
|
29
|
+
Plug in a secondary screen (a stretched panel, a small IPS monitor, anything), select it in the Settings sidebar under **Monitor**, and click **Set as default**. Thermal Canary will always open on that screen at login — no configuration files to edit.
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
## Gauges
|
|
36
|
+
|
|
37
|
+
| Row | Gauge | Source |
|
|
38
|
+
|-----|-------|--------|
|
|
39
|
+
| CPU | CPU Temperature | `psutil.sensors_temperatures()` — coretemp/k10temp (configurable) |
|
|
40
|
+
| CPU | CPU Usage % | psutil |
|
|
41
|
+
| CPU | RAM Usage % | psutil |
|
|
42
|
+
| GPU | GPU Temperature | NVIDIA: pynvml · AMD: sysfs hwmon · Intel: xe/i915 sysfs |
|
|
43
|
+
| GPU | GPU Fan Speed % | NVIDIA: pynvml · AMD: sysfs hwmon (0% = fans stopped or AMD integrated GPU with no fan) |
|
|
44
|
+
| GPU | GPU VRAM Usage % | NVIDIA: pynvml · AMD: sysfs hwmon · Intel: always 0 (not exposed by driver) |
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
> **Tested and supported on Ubuntu 24.04 LTS.** Other distros (Fedora, Arch, openSUSE) are supported by the installer but have not been formally tested yet. Ubuntu versions below 24.04 may work but are not guaranteed.
|
|
49
|
+
|
|
50
|
+
The installer checks and installs everything automatically. Here is the full dependency list:
|
|
51
|
+
|
|
52
|
+
**System packages** (installed automatically via your distro's package manager):
|
|
53
|
+
|
|
54
|
+
| Dependency | Purpose |
|
|
55
|
+
|------------|---------|
|
|
56
|
+
| Python 3.10+ | Runtime |
|
|
57
|
+
| `python3-venv` | Isolated Python environment |
|
|
58
|
+
| `lm-sensors` | Populates `/sys/class/hwmon` for CPU temperature |
|
|
59
|
+
| XCB cursor libs (`libxcb-cursor0` / `xcb-util-cursor`) | Required by PyQt6 on X11 |
|
|
60
|
+
|
|
61
|
+
**NVIDIA driver** — checked separately. The installer prints distro-specific install instructions if the driver is missing. GPU gauges (temperature, fan, VRAM) require the driver for NVIDIA; AMD and Intel GPUs are read via sysfs without any driver installation.
|
|
62
|
+
|
|
63
|
+
> **GPU backends**: NVIDIA (`pynvml`, direct NVML — no `nvidia-smi` subprocess), AMD (`amdgpu` sysfs hwmon — kernel driver, no extra install), Intel (`xe`/`i915` sysfs hwmon). The backend is auto-detected at startup or can be forced in the Settings sidebar. If no GPU is detected, all three GPU gauges show `—`.
|
|
64
|
+
|
|
65
|
+
**Python libraries** (installed automatically into a venv):
|
|
66
|
+
|
|
67
|
+
| Library | Purpose |
|
|
68
|
+
|---------|---------|
|
|
69
|
+
| `PyQt6` | GUI framework |
|
|
70
|
+
| `psutil` | CPU temperature, CPU usage, RAM usage |
|
|
71
|
+
| `nvidia-ml-py` (`pynvml`) | GPU metrics via direct NVML calls — no `nvidia-smi` subprocess |
|
|
72
|
+
| `PyYAML` | Config file read/write |
|
|
73
|
+
|
|
74
|
+
Supported package managers: **apt** (Debian/Ubuntu), **dnf** (Fedora/RHEL), **pacman** (Arch), **zypper** (openSUSE).
|
|
75
|
+
|
|
76
|
+
## Install
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git clone https://github.com/ibasaw/thermalcanary.git
|
|
80
|
+
cd thermalcanary
|
|
81
|
+
bash install.sh
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Thermal Canary runs entirely as your user — no root needed at runtime.**
|
|
85
|
+
|
|
86
|
+
The installer separates system-package installation from everything else:
|
|
87
|
+
|
|
88
|
+
- `bash install.sh` — checks system packages and prints any missing ones with the exact `sudo` command to install them, then proceeds with the rootless steps (venv, pip, desktop files, launch)
|
|
89
|
+
- `bash install.sh --install-deps` — same, but also invokes `sudo` automatically to install missing packages
|
|
90
|
+
|
|
91
|
+
The installer will:
|
|
92
|
+
1. Check for NVIDIA driver — prints distro-specific install instructions if missing (GPU gauges need it; CPU/RAM gauges work without)
|
|
93
|
+
2. Check system packages — prints missing ones or installs them if `--install-deps` was passed
|
|
94
|
+
3. Copy the app package and icon to `~/.local/share/thermalcanary/`
|
|
95
|
+
4. Create a Python venv and install all Python dependencies (PyQt6, psutil, nvidia-ml-py, PyYAML)
|
|
96
|
+
5. Verify all dependencies are working
|
|
97
|
+
6. Copy the default config to `~/.config/thermalcanary/config.yaml` (only on first install — never overwrites existing config)
|
|
98
|
+
7. Register the app in `~/.local/share/applications/` for taskbar icon support
|
|
99
|
+
8. Register an autostart entry so the app launches 8 seconds after login
|
|
100
|
+
9. Kill any running instance and launch the app immediately
|
|
101
|
+
|
|
102
|
+
The clone folder is only needed to run the installer. You can delete it afterwards.
|
|
103
|
+
|
|
104
|
+
## Installed file layout
|
|
105
|
+
|
|
106
|
+
| Path | What |
|
|
107
|
+
|------|------|
|
|
108
|
+
| `~/.local/share/thermalcanary/thermalcanary/` | App package |
|
|
109
|
+
| `~/.local/share/thermalcanary/assets/` | App icon |
|
|
110
|
+
| `~/.local/share/thermalcanary/venv/` | Python virtual environment |
|
|
111
|
+
| `~/.local/share/icons/hicolor/*/apps/thermalcanary.png` | System icon (taskbar) |
|
|
112
|
+
| `~/.local/share/applications/thermalcanary.desktop` | App entry (taskbar icon) |
|
|
113
|
+
| `~/.config/thermalcanary/config.yaml` | User configuration (auto-saved by the app) |
|
|
114
|
+
| `~/.config/autostart/thermalcanary.desktop` | Autostart on login |
|
|
115
|
+
|
|
116
|
+
## Launch manually
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
cd ~/.local/share/thermalcanary && venv/bin/python3 -m thermalcanary
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Settings sidebar
|
|
123
|
+
|
|
124
|
+
Two buttons sit in the top-right corner: **⚙** opens the settings sidebar, **✕** closes the app (with confirmation). Press **Ctrl+,** to toggle the sidebar from the keyboard. All changes apply live without restarting.
|
|
125
|
+
|
|
126
|
+
| Section | Setting | Description |
|
|
127
|
+
|---------|---------|-------------|
|
|
128
|
+
| Display | Monitor | Which monitor to display on — works with any number of monitors and any orientation |
|
|
129
|
+
| Display | Set as default | Save the current monitor as the startup monitor — the app always opens here on launch |
|
|
130
|
+
| Sampling | Poll rate | Sensor poll interval (100ms – 10s) |
|
|
131
|
+
| Sampling | Smoothing | Rolling average window size (1–60 samples) |
|
|
132
|
+
| Colors | Background | Window background color |
|
|
133
|
+
| Colors | Inner circle | Gauge center fill color |
|
|
134
|
+
| Colors | Arc track | Unfilled arc track color |
|
|
135
|
+
| Colors | Tick marks | Tick mark color |
|
|
136
|
+
|
|
137
|
+
The **Reset to defaults** button restores all colors and sampling values to factory settings while keeping your saved default monitor.
|
|
138
|
+
|
|
139
|
+
## Configuration file
|
|
140
|
+
|
|
141
|
+
`~/.config/thermalcanary/config.yaml` is written automatically by the settings sidebar. You can also edit it by hand; changes take effect on next launch.
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
poll_ms: 1000 # sensor poll interval in milliseconds
|
|
145
|
+
smooth_n: 5 # rolling average window (number of samples)
|
|
146
|
+
|
|
147
|
+
bg_color: "#252040" # window background
|
|
148
|
+
inner_color: "#1e1a35" # gauge inner circle
|
|
149
|
+
track_color: "#332e55" # arc track (unfilled)
|
|
150
|
+
tick_color: "#3d3860" # tick marks
|
|
151
|
+
|
|
152
|
+
# screen_index and default_screen_index are set via the Settings sidebar
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Autostart caveat
|
|
156
|
+
|
|
157
|
+
The autostart entry inherits `$DISPLAY` from the login session, falling back to `:0`. If the app doesn't start on login, check your display number with `echo $DISPLAY` and edit `~/.config/autostart/thermalcanary.desktop` accordingly.
|
|
158
|
+
|
|
159
|
+
## Uninstall
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
bash uninstall.sh
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Removes the app, venv, icon, config, and all desktop entries after confirmation.
|
|
166
|
+
|
|
167
|
+
## Architecture
|
|
168
|
+
|
|
169
|
+
- **SensorWorker** runs in a `QThread` — all sensor reads are off the main thread
|
|
170
|
+
- GPU metrics use **pynvml** (direct NVML calls, <1ms) — never `subprocess nvidia-smi` which causes observer-effect CPU temperature spikes of 10–15°C
|
|
171
|
+
- CPU temp and usage are stabilised with a `deque`-based rolling average
|
|
172
|
+
- **Config** is reactive: `Config(QObject)` emits a signal on every change, all settings apply live
|
|
173
|
+
- Window runs **fullscreen** (`showFullScreen`) — no title bar, fills the entire monitor. This bypasses Mutter's `WM_NORMAL_HINTS` enforcement which otherwise clamps window geometry to `minimumSizeHint`, making maximize unreliable on short or rotated monitors
|
|
174
|
+
- Monitor switching uses an event-driven state machine: `showNormal()` → wait for `WindowStateChange` (Mutter ack) → `windowHandle().setScreen()` + `setGeometry()` → 50ms → `showFullScreen()`. `wmctrl` is called once after first show to set `_NET_WM_STATE_SKIP_TASKBAR/SKIP_PAGER` (hides the window from GNOME Dash without using `Qt.WindowType.Tool`, which would break cross-monitor placement on Mutter)
|
|
175
|
+
- Monitor indices are validated against actual connected screens at startup — safe on any number of monitors
|
|
176
|
+
- Single-instance lock via `fcntl.flock` on `$XDG_RUNTIME_DIR/thermalcanary.lock`
|
|
177
|
+
- Runs entirely as the logged-in user — no root required at runtime
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT — see [LICENSE](LICENSE)
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate assets/logo.png — horizontal lockup: canary icon + "Thermal Canary" wordmark."""
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
|
5
|
+
|
|
6
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
7
|
+
ICON = ROOT / "assets" / "icon.png"
|
|
8
|
+
FONT_PATH = ROOT / "bin" / "fonts" / "Exo2-SemiBold.ttf"
|
|
9
|
+
OUT = ROOT / "assets" / "logo.png"
|
|
10
|
+
|
|
11
|
+
CANVAS = (1400, 400)
|
|
12
|
+
BIRD_HEIGHT = 320
|
|
13
|
+
BIRD_X = 80
|
|
14
|
+
BIRD_Y = (CANVAS[1] - BIRD_HEIGHT) // 2 # 40px top/bottom margin
|
|
15
|
+
GAP = 60
|
|
16
|
+
FONT_SIZE = 140
|
|
17
|
+
COLOR_THERMAL = (242, 242, 242, 255) # warm off-white
|
|
18
|
+
COLOR_CANARY = (245, 197, 24, 255) # matches bird yellow
|
|
19
|
+
SHADOW = (0, 0, 0, 80)
|
|
20
|
+
OPTICAL_NUDGE = -20 # raise text to align with bird body centroid
|
|
21
|
+
|
|
22
|
+
canvas = Image.new("RGBA", CANVAS, (0, 0, 0, 0))
|
|
23
|
+
|
|
24
|
+
# Bird
|
|
25
|
+
bird = Image.open(ICON).convert("RGBA")
|
|
26
|
+
bw = int(bird.width * BIRD_HEIGHT / bird.height)
|
|
27
|
+
bird = bird.resize((bw, BIRD_HEIGHT), Image.LANCZOS)
|
|
28
|
+
bird_layer = Image.new("RGBA", CANVAS, (0, 0, 0, 0))
|
|
29
|
+
bird_layer.paste(bird, (BIRD_X, BIRD_Y), bird)
|
|
30
|
+
canvas = Image.alpha_composite(canvas, bird_layer)
|
|
31
|
+
|
|
32
|
+
# Font with fallback chain
|
|
33
|
+
for font_path in [FONT_PATH, "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"]:
|
|
34
|
+
try:
|
|
35
|
+
font = ImageFont.truetype(str(font_path), FONT_SIZE)
|
|
36
|
+
break
|
|
37
|
+
except (OSError, IOError):
|
|
38
|
+
continue
|
|
39
|
+
else:
|
|
40
|
+
font = ImageFont.load_default()
|
|
41
|
+
|
|
42
|
+
text_x = BIRD_X + bw + GAP
|
|
43
|
+
text_y = (CANVAS[1] - FONT_SIZE) // 2 + OPTICAL_NUDGE
|
|
44
|
+
w_thermal = font.getbbox("Thermal ")[2]
|
|
45
|
+
|
|
46
|
+
# Shadow layer
|
|
47
|
+
shadow_layer = Image.new("RGBA", CANVAS, (0, 0, 0, 0))
|
|
48
|
+
sd = ImageDraw.Draw(shadow_layer)
|
|
49
|
+
sd.text((text_x, text_y + 2), "Thermal ", font=font, fill=SHADOW)
|
|
50
|
+
sd.text((text_x + w_thermal, text_y + 2), "Canary", font=font, fill=SHADOW)
|
|
51
|
+
shadow_layer = shadow_layer.filter(ImageFilter.GaussianBlur(radius=2))
|
|
52
|
+
canvas = Image.alpha_composite(canvas, shadow_layer)
|
|
53
|
+
|
|
54
|
+
# Text layer
|
|
55
|
+
text_layer = Image.new("RGBA", CANVAS, (0, 0, 0, 0))
|
|
56
|
+
td = ImageDraw.Draw(text_layer)
|
|
57
|
+
td.text((text_x, text_y), "Thermal ", font=font, fill=COLOR_THERMAL)
|
|
58
|
+
td.text((text_x + w_thermal, text_y), "Canary", font=font, fill=COLOR_CANARY)
|
|
59
|
+
canvas = Image.alpha_composite(canvas, text_layer)
|
|
60
|
+
|
|
61
|
+
canvas.save(OUT, "PNG", optimize=True)
|
|
62
|
+
print(f"Saved {OUT} ({OUT.stat().st_size // 1024} KB)")
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Thermal Canary — configuration
|
|
2
|
+
# Installed to: ~/.config/thermalcanary/config.yaml
|
|
3
|
+
# Edited live by the Settings sidebar — you can also edit this file by hand.
|
|
4
|
+
|
|
5
|
+
# Monitor to display on (0 = primary, 1 = second, 2 = third...).
|
|
6
|
+
# Defaults to your primary monitor. Set via the Settings sidebar.
|
|
7
|
+
# screen_index: 0
|
|
8
|
+
|
|
9
|
+
# Monitor to return to when "Reset to defaults" is pressed.
|
|
10
|
+
# Set via the "Set as default" button in the Settings sidebar.
|
|
11
|
+
# default_screen_index: 0
|
|
12
|
+
|
|
13
|
+
# Sensor poll interval in milliseconds (lower = more responsive, higher = less CPU)
|
|
14
|
+
poll_ms: 1000
|
|
15
|
+
|
|
16
|
+
# Rolling average window: number of samples averaged to smooth noisy sensors
|
|
17
|
+
smooth_n: 5
|
|
18
|
+
|
|
19
|
+
# Colors — hex strings
|
|
20
|
+
bg_color: "#252040" # window background
|
|
21
|
+
inner_color: "#1e1a35" # gauge inner circle
|
|
22
|
+
track_color: "#332e55" # arc track (unfilled portion)
|
|
23
|
+
tick_color: "#3d3860" # tick marks
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"schemaVersion":1,"label":"coverage","message":"72%","color":"yellowgreen"}
|