Ship AppImage with bundled GTK runtime and automated releases
AppImage / build (push) Has been cancelled
AppImage / release (push) Has been cancelled
Tests / unit (de, 3.10) (push) Has been cancelled
Tests / unit (de, 3.14) (push) Has been cancelled
Tests / unit (en, 3.10) (push) Has been cancelled
Tests / unit (en, 3.14) (push) Has been cancelled
AppImage / build (push) Has been cancelled
AppImage / release (push) Has been cancelled
Tests / unit (de, 3.10) (push) Has been cancelled
Tests / unit (de, 3.14) (push) Has been cancelled
Tests / unit (en, 3.10) (push) Has been cancelled
Tests / unit (en, 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# The Containerfile only installs Ubuntu packages; source is mounted at runtime.
|
||||||
|
**
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: AppImage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 35
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Check release version
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
env:
|
||||||
|
RELEASE_TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
python3 - <<'PY'
|
||||||
|
import os, tomllib
|
||||||
|
version = tomllib.load(open('pyproject.toml', 'rb'))['project']['version']
|
||||||
|
assert os.environ['RELEASE_TAG'] == 'v' + version, 'Tag must match pyproject.toml'
|
||||||
|
PY
|
||||||
|
- name: Build isolated Ubuntu runtime and source archives
|
||||||
|
run: |
|
||||||
|
docker build -t btd700-appimage-builder -f packaging/appimage/Containerfile .
|
||||||
|
docker run --rm -v "$PWD:/src" btd700-appimage-builder
|
||||||
|
- name: Verify unit tests
|
||||||
|
run: |
|
||||||
|
docker run --rm -v "$PWD:/src" btd700-appimage-builder bash -c '
|
||||||
|
BTD700_LANGUAGE=en python3 -m unittest discover -s tests -v &&
|
||||||
|
BTD700_LANGUAGE=de python3 -m unittest discover -s tests -v'
|
||||||
|
- name: Verify packaged English/German GUI and tray without host Python or GTK
|
||||||
|
run: |
|
||||||
|
docker build -t btd700-appimage-test -f packaging/appimage/TestContainerfile .
|
||||||
|
docker run --rm -v "$PWD:/src" btd700-appimage-test
|
||||||
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
||||||
|
with:
|
||||||
|
name: appimage-x86_64
|
||||||
|
path: dist/*
|
||||||
|
compression-level: 0
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
release:
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||||
|
with:
|
||||||
|
name: appimage-x86_64
|
||||||
|
path: dist
|
||||||
|
- name: Publish verified release assets
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
RELEASE_TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
(cd dist && sha256sum --check SHA256SUMS)
|
||||||
|
gh release create "$RELEASE_TAG" dist/* --draft --verify-tag \
|
||||||
|
--title "BTD 700 Control $RELEASE_TAG" \
|
||||||
|
--notes-file packaging/appimage/RELEASE_NOTES.md
|
||||||
|
gh release edit "$RELEASE_TAG" --draft=false --latest
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.3.0 — 2026-09-05
|
||||||
|
|
||||||
|
- Downloadable x86-64 AppImage for glibc 2.39+ desktops, including Python, GTK,
|
||||||
|
libadwaita and the tray library; no Python package installation needed.
|
||||||
|
- AppImage-aware application-menu installation and autostart using its permanent
|
||||||
|
file path, including a persistent icon and support for extracted AppDirs.
|
||||||
|
- Container build and GitHub release workflow, checksums, bundled license notices
|
||||||
|
and corresponding dependency source archives.
|
||||||
|
- English/German packaged GUI and D-Bus tray verification with simulated hardware.
|
||||||
|
|
||||||
## 0.2.0 — 2026-09-05
|
## 0.2.0 — 2026-09-05
|
||||||
|
|
||||||
- English and German app, tray, CLI help and error messages, selected by system
|
- English and German app, tray, CLI help and error messages, selected by system
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
include README.md README.de.md LICENSE THIRD_PARTY_NOTICES.md CONTRIBUTING.md CHANGELOG.md
|
include README.md README.de.md LICENSE THIRD_PARTY_NOTICES.md CONTRIBUTING.md CHANGELOG.md
|
||||||
include run.sh install.py
|
include run.sh install.py
|
||||||
recursive-include packaging *.svg *.rules
|
recursive-include packaging *.svg *.rules
|
||||||
|
recursive-include packaging/appimage *.sh *.py *.desktop *.md Containerfile TestContainerfile AppRun
|
||||||
recursive-include docs *.md *.png
|
recursive-include docs *.md *.png
|
||||||
recursive-include tools *.py
|
recursive-include tools *.py
|
||||||
recursive-include tests *.py
|
recursive-include tests *.py
|
||||||
|
|||||||
+27
-6
@@ -1,6 +1,6 @@
|
|||||||
# BTD 700 Control für Linux
|
# BTD 700 Control für Linux
|
||||||
|
|
||||||
[English](README.md) · [Quellcode v0.2.0 herunterladen](https://github.com/justin-eckenweber/btd700linux/archive/refs/tags/v0.2.0.zip) · [Fehler melden](https://github.com/justin-eckenweber/btd700linux/issues)
|
[English](README.md) · [AppImage herunterladen](https://github.com/justin-eckenweber/btd700linux/releases/latest) · [Fehler melden](https://github.com/justin-eckenweber/btd700linux/issues)
|
||||||
|
|
||||||
Native GTK-4-App zur Steuerung des **Sennheiser BTD 700**, mit einem Menü im
|
Native GTK-4-App zur Steuerung des **Sennheiser BTD 700**, mit einem Menü im
|
||||||
Infobereich von GNOME/KDE. Eigenständige Implementierung des HID-Steuerprotokolls,
|
Infobereich von GNOME/KDE. Eigenständige Implementierung des HID-Steuerprotokolls,
|
||||||
@@ -29,16 +29,37 @@ Die Bilder zeigen die englische Oberfläche; Deutsch ist ebenfalls enthalten.
|
|||||||
<img src="docs/screenshots/auracast-settings.png" width="46%" alt="Auracast-Einstellungen und Infobereich-Optionen">
|
<img src="docs/screenshots/auracast-settings.png" width="46%" alt="Auracast-Einstellungen und Infobereich-Optionen">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Herunterladen
|
## AppImage herunterladen
|
||||||
|
|
||||||
|
[Das AppImage findest du unter GitHub Releases](https://github.com/justin-eckenweber/btd700linux/releases/latest).
|
||||||
|
Python, GTK, libadwaita und die Tray-Bibliothek sind enthalten. Das Paket ist für
|
||||||
|
**x86-64 mit glibc 2.39 oder neuer**, etwa Ubuntu 24.04+ oder das getestete Bazzite 44.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x BTD_700_Control-0.3.0-x86_64.AppImage
|
||||||
|
./BTD_700_Control-0.3.0-x86_64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
Datei zuerst an ihren dauerhaften Platz legen. Mit `--install-desktop` legst du
|
||||||
|
einen Anwendungsmenü-Eintrag an; „Beim Anmelden starten“ in der App aktiviert den
|
||||||
|
Autostart im Tray. `--remove-desktop` entfernt beide Einträge. Nach Verschieben
|
||||||
|
oder Umbenennen der Datei den Menüeintrag neu anlegen und Autostart aus-/einschalten.
|
||||||
|
|
||||||
|
Ohne FUSE funktioniert der Start mit
|
||||||
|
`APPIMAGE_EXTRACT_AND_RUN=1 ./BTD_700_Control-0.3.0-x86_64.AppImage`.
|
||||||
|
Die [AppImage-Anleitung](docs/APPIMAGE.md) erklärt dauerhaftes Entpacken und eigene
|
||||||
|
Builds. Prüfsummen und Bibliotheksquellen liegen beim Release. Die USB-Berechtigung
|
||||||
|
und ein Tray-Host bleiben nötig; GNOME benötigt eine StatusNotifier/AppIndicator-Erweiterung.
|
||||||
|
|
||||||
|
## Alternativ: Quellcode herunterladen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/justin-eckenweber/btd700linux.git
|
git clone https://github.com/justin-eckenweber/btd700linux.git
|
||||||
cd btd700linux
|
cd btd700linux
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternativ die oben verlinkte ZIP-Datei entpacken und `bash run.sh` im Projektordner
|
Alternativ [Quellcode v0.3.0 als ZIP](https://github.com/justin-eckenweber/btd700linux/archive/refs/tags/v0.3.0.zip)
|
||||||
ausführen. Die aktuelle Version wird als Quellcode verteilt; es gibt noch kein
|
entpacken und `bash run.sh` im Projektordner ausführen.
|
||||||
AppImage, Flatpak oder eigenständiges Binärpaket.
|
|
||||||
|
|
||||||
## Starten
|
## Starten
|
||||||
|
|
||||||
@@ -101,7 +122,7 @@ Die angezeigten Bit-/kHz-Werte stammen vom Dongle. Die USB-Abtastrate wird vom
|
|||||||
Audiosystem (z. B. PipeWire) bestimmt und ist kein hier nachgewiesener Stellbefehl.
|
Audiosystem (z. B. PipeWire) bestimmt und ist kein hier nachgewiesener Stellbefehl.
|
||||||
Koppeln erfolgt über die Taste am Dongle; „Verbinden“ versucht die bestehende Kopplung.
|
Koppeln erfolgt über die Taste am Dongle; „Verbinden“ versucht die bestehende Kopplung.
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen für den Start aus dem Quellcode
|
||||||
|
|
||||||
Python ≥ 3.10, PyGObject, GTK ≥ 4.10, libadwaita ≥ 1.5 und libdbusmenu mit
|
Python ≥ 3.10, PyGObject, GTK ≥ 4.10, libadwaita ≥ 1.5 und libdbusmenu mit
|
||||||
GObject-Introspection. Auf dem hier verwendeten Bazzite sind die benötigten
|
GObject-Introspection. Auf dem hier verwendeten Bazzite sind die benötigten
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="#install">Install</a> ·
|
<a href="#install">Install</a> ·
|
||||||
<a href="https://github.com/justin-eckenweber/btd700linux/archive/refs/tags/v0.2.0.zip">Download source v0.2.0</a> ·
|
<a href="https://github.com/justin-eckenweber/btd700linux/releases/latest">Download AppImage</a> ·
|
||||||
<a href="README.de.md">Deutsche Anleitung</a> ·
|
<a href="README.de.md">Deutsche Anleitung</a> ·
|
||||||
<a href="https://github.com/justin-eckenweber/btd700linux/issues">Report an issue</a>
|
<a href="https://github.com/justin-eckenweber/btd700linux/issues">Report an issue</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -65,11 +65,33 @@ window quits the app instead.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Version **0.2.0** is distributed as source. You do not need to compile the app or
|
### AppImage (x86-64)
|
||||||
install Python packages with pip. There is currently no AppImage, Flatpak or
|
|
||||||
self-contained binary download.
|
|
||||||
|
|
||||||
### 1. Install the system libraries
|
[Download the AppImage from GitHub Releases](https://github.com/justin-eckenweber/btd700linux/releases/latest).
|
||||||
|
It includes Python, GTK, libadwaita and the tray library. Requires **glibc 2.39+**
|
||||||
|
(for example Ubuntu 24.04 or newer, or the tested Bazzite 44 desktop).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x BTD_700_Control-0.3.0-x86_64.AppImage
|
||||||
|
./BTD_700_Control-0.3.0-x86_64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
Put it in a permanent folder, then optionally run it with `--install-desktop` to
|
||||||
|
add an application-menu entry. Enable **Start at login** inside the app for tray
|
||||||
|
autostart. Use `--remove-desktop` to remove both entries. Keep the AppImage at the
|
||||||
|
same path; after moving it, recreate the menu entry and toggle autostart off/on.
|
||||||
|
|
||||||
|
If FUSE is unavailable, start with
|
||||||
|
`APPIMAGE_EXTRACT_AND_RUN=1 ./BTD_700_Control-0.3.0-x86_64.AppImage`.
|
||||||
|
See the [AppImage guide](docs/APPIMAGE.md) for permanent extraction, build instructions,
|
||||||
|
checksums and dependency sources. USB permissions and the desktop's tray host are
|
||||||
|
still required; GNOME needs a StatusNotifier/AppIndicator extension.
|
||||||
|
|
||||||
|
### Run from source
|
||||||
|
|
||||||
|
You can also run the Python source directly, without compiling or using pip.
|
||||||
|
|
||||||
|
#### 1. Install the system libraries
|
||||||
|
|
||||||
Requirements: **Python 3.10+**, PyGObject, **GTK 4.10+**, **libadwaita 1.5+** and
|
Requirements: **Python 3.10+**, PyGObject, **GTK 4.10+**, **libadwaita 1.5+** and
|
||||||
libdbusmenu with GObject introspection. A graphical desktop session is needed for
|
libdbusmenu with GObject introspection. A graphical desktop session is needed for
|
||||||
@@ -99,7 +121,7 @@ KDE Plasma provides a tray host. Other desktops may work but have not been teste
|
|||||||
Package references: [Fedora libdbusmenu](https://packages.fedoraproject.org/pkgs/libdbusmenu/libdbusmenu/),
|
Package references: [Fedora libdbusmenu](https://packages.fedoraproject.org/pkgs/libdbusmenu/libdbusmenu/),
|
||||||
[Ubuntu introspection package](https://packages.ubuntu.com/noble/gir1.2-dbusmenu-glib-0.4).
|
[Ubuntu introspection package](https://packages.ubuntu.com/noble/gir1.2-dbusmenu-glib-0.4).
|
||||||
|
|
||||||
### 2. Download and run
|
#### 2. Download and run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/justin-eckenweber/btd700linux.git
|
git clone https://github.com/justin-eckenweber/btd700linux.git
|
||||||
@@ -107,7 +129,7 @@ cd btd700linux
|
|||||||
./run.sh
|
./run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Or [download the v0.2.0 source ZIP](https://github.com/justin-eckenweber/btd700linux/archive/refs/tags/v0.2.0.zip),
|
Or [download the v0.3.0 source ZIP](https://github.com/justin-eckenweber/btd700linux/archive/refs/tags/v0.3.0.zip),
|
||||||
extract it, and run `bash run.sh` inside the extracted folder.
|
extract it, and run `bash run.sh` inside the extracted folder.
|
||||||
|
|
||||||
Want to explore without touching any hardware?
|
Want to explore without touching any hardware?
|
||||||
@@ -116,7 +138,7 @@ Want to explore without touching any hardware?
|
|||||||
./run.sh --demo
|
./run.sh --demo
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Add it to your application menu
|
#### 3. Add it to your application menu
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 install.py
|
python3 install.py
|
||||||
@@ -133,7 +155,8 @@ python3 install.py --uninstall # Remove launcher and autostart entry
|
|||||||
|
|
||||||
### USB permissions
|
### USB permissions
|
||||||
|
|
||||||
If the app reports that USB access is denied:
|
If the app reports that USB access is denied, use the rule from the source checkout
|
||||||
|
or download `70-btd700-control.rules` from the same AppImage release (adjust its path below):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo install -m 0644 packaging/70-btd700-control.rules /etc/udev/rules.d/70-btd700-control.rules
|
sudo install -m 0644 packaging/70-btd700-control.rules /etc/udev/rules.d/70-btd700-control.rules
|
||||||
|
|||||||
+15
-2
@@ -3,8 +3,21 @@
|
|||||||
The MIT license in this repository covers this project's own implementation,
|
The MIT license in this repository covers this project's own implementation,
|
||||||
documentation and artwork. It does not relicense third-party software or trademarks.
|
documentation and artwork. It does not relicense third-party software or trademarks.
|
||||||
|
|
||||||
- Python, PyGObject, GTK, libadwaita and libdbusmenu are provided by your system,
|
- When running from source, Python, PyGObject, GTK, libadwaita and libdbusmenu are
|
||||||
not bundled here. Their respective licenses continue to apply.
|
provided by your system. The AppImage bundles these libraries and their runtime
|
||||||
|
dependencies from Ubuntu 24.04. Their respective licenses continue to apply.
|
||||||
|
- AppImage license/copyright texts and the package manifest are included under
|
||||||
|
`usr/share/doc/btd700-bundled/`; shared license texts are under
|
||||||
|
`usr/share/common-licenses/`. Extract with `--appimage-extract` to inspect them.
|
||||||
|
Exact corresponding Ubuntu source archives, package versions and build
|
||||||
|
instructions are downloadable beside each AppImage as
|
||||||
|
`*-dependency-sources.tar.gz`. These dependencies are not relicensed under MIT.
|
||||||
|
- The AppImage type2 runtime is MIT-licensed and statically includes musl (MIT),
|
||||||
|
libfuse (LGPL 2.1), squashfuse (BSD), zstd (BSD) and zlib (zlib license).
|
||||||
|
Runtime notices are included in the image; runtime sources, build scripts,
|
||||||
|
libfuse sources/patches and squashfuse sources accompany the dependency archive.
|
||||||
|
See [AppImage build documentation](docs/APPIMAGE.md) for replacing libraries and
|
||||||
|
rebuilding the runtime or app.
|
||||||
- The control protocol was reconstructed by examining the official Windows
|
- The control protocol was reconstructed by examining the official Windows
|
||||||
Sennheiser Dongle Control 1.0.5 application. The vendor application, decompiled
|
Sennheiser Dongle Control 1.0.5 application. The vendor application, decompiled
|
||||||
sources, firmware and vendor graphics are not included. Provenance and references
|
sources, firmware and vendor graphics are not included. Provenance and references
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ def tr(message):
|
|||||||
# German source messages are retained for compatibility with the first release.
|
# German source messages are retained for compatibility with the first release.
|
||||||
# Keep placeholders identical in both languages; never translate protocol values.
|
# Keep placeholders identical in both languages; never translate protocol values.
|
||||||
ENGLISH = {
|
ENGLISH = {
|
||||||
|
'Menüeintrag und Autostart entfernt. Die Programmdateien bleiben erhalten.':
|
||||||
|
'Removed the launcher and start-at-login entry. The application files are unchanged.',
|
||||||
|
'Im Anwendungsmenü „BTD 700 Control“ öffnen. Die AppImage-Datei muss an diesem Ort bleiben.':
|
||||||
|
'Open BTD 700 Control from the application menu. Keep the AppImage file at this location.',
|
||||||
'Menüeintrag und Autostart entfernt. Der Projektordner bleibt erhalten.': 'Removed the launcher and start-at-login entry. The project folder is unchanged.',
|
'Menüeintrag und Autostart entfernt. Der Projektordner bleibt erhalten.': 'Removed the launcher and start-at-login entry. The project folder is unchanged.',
|
||||||
'Status aktualisieren': 'Refresh status',
|
'Status aktualisieren': 'Refresh status',
|
||||||
'App vollständig beenden': 'Quit application',
|
'App vollständig beenden': 'Quit application',
|
||||||
|
|||||||
+24
-3
@@ -1,6 +1,7 @@
|
|||||||
"""User-scoped desktop integration."""
|
"""User-scoped desktop integration."""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
@@ -8,10 +9,17 @@ APP_ID = 'io.github.btd700linux.Control'
|
|||||||
|
|
||||||
|
|
||||||
def desktop_entry(*, background=False):
|
def desktop_entry(*, background=False):
|
||||||
|
appdir = os.environ.get('APPDIR')
|
||||||
|
appimage = os.environ.get('APPIMAGE') if appdir else None
|
||||||
checkout = (ROOT / 'run.sh').is_file()
|
checkout = (ROOT / 'run.sh').is_file()
|
||||||
path = str(ROOT / 'run.sh') if checkout else sys.executable
|
path = appimage or (str(Path(appdir) / 'AppRun') if appdir else None) or (str(ROOT / 'run.sh') if checkout else sys.executable)
|
||||||
module_args = '' if checkout else ' -m btd700'
|
module_args = '' if appdir or checkout else ' -m btd700'
|
||||||
icon = str(ROOT / 'packaging/btd700-control.svg') if checkout else 'audio-headphones'
|
# The pinned type2 runtime removes its CLI flag before launching AppRun;
|
||||||
|
# its extraction directory also identifies subsequent launches in this mode.
|
||||||
|
if appimage and ('APPIMAGE_EXTRACT_AND_RUN' in os.environ or Path(appdir).name.startswith('appimage_extracted_')):
|
||||||
|
module_args = ' --appimage-extract-and-run'
|
||||||
|
icon = (str(installed_icon()) if appimage else str(Path(appdir) / 'btd700-control.svg') if appdir
|
||||||
|
else str(ROOT / 'packaging/btd700-control.svg') if checkout else 'audio-headphones')
|
||||||
# Desktop Entry Exec quoting is not shell quoting.
|
# Desktop Entry Exec quoting is not shell quoting.
|
||||||
escaped = path.replace('\\', '\\\\').replace('"', '\\"').replace('`', '\\`').replace('$', '\\$').replace('%', '%%')
|
escaped = path.replace('\\', '\\\\').replace('"', '\\"').replace('`', '\\`').replace('$', '\\$').replace('%', '%%')
|
||||||
return ('[Desktop Entry]\nType=Application\nName=BTD 700 Control\n'
|
return ('[Desktop Entry]\nType=Application\nName=BTD 700 Control\n'
|
||||||
@@ -29,9 +37,22 @@ def autostart_path():
|
|||||||
return base / 'autostart' / f'{APP_ID}.desktop'
|
return base / 'autostart' / f'{APP_ID}.desktop'
|
||||||
|
|
||||||
|
|
||||||
|
def installed_icon():
|
||||||
|
base = Path(os.environ.get('XDG_DATA_HOME', Path.home() / '.local/share'))
|
||||||
|
return base / 'icons/hicolor/scalable/apps' / f'{APP_ID}.svg'
|
||||||
|
|
||||||
|
|
||||||
|
def install_appimage_icon():
|
||||||
|
if os.environ.get('APPIMAGE') and os.environ.get('APPDIR'):
|
||||||
|
path = installed_icon()
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copyfile(Path(os.environ['APPDIR']) / 'btd700-control.svg', path)
|
||||||
|
|
||||||
|
|
||||||
def set_autostart(enabled):
|
def set_autostart(enabled):
|
||||||
path = autostart_path()
|
path = autostart_path()
|
||||||
if enabled:
|
if enabled:
|
||||||
|
install_appimage_icon()
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(desktop_entry(background=True))
|
path.write_text(desktop_entry(background=True))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# AppImage packaging
|
||||||
|
|
||||||
|
The x86-64 AppImage bundles Python 3.12, PyGObject, GTK 4, libadwaita,
|
||||||
|
libdbusmenu, icons and fallback fonts from Ubuntu 24.04. It requires a Linux
|
||||||
|
host with **glibc 2.39 or newer** and a graphical Wayland or X11 session.
|
||||||
|
It does not bundle glibc, audio drivers or firmware. ARM and older glibc
|
||||||
|
distributions are not supported by this binary; running from source is another option.
|
||||||
|
|
||||||
|
## Download and use
|
||||||
|
|
||||||
|
Download the `.AppImage` from [GitHub Releases](https://github.com/justin-eckenweber/btd700linux/releases/latest),
|
||||||
|
make it executable, and open it. All regular CLI arguments also work.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x BTD_700_Control-*-x86_64.AppImage
|
||||||
|
./BTD_700_Control-0.3.0-x86_64.AppImage
|
||||||
|
./BTD_700_Control-0.3.0-x86_64.AppImage --demo
|
||||||
|
```
|
||||||
|
|
||||||
|
Put the file at its permanent location before adding a menu entry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./BTD_700_Control-0.3.0-x86_64.AppImage --install-desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
The menu entry and **Start at login** use the original AppImage path, not its
|
||||||
|
temporary mount. Moving or renaming the file requires installing the menu entry
|
||||||
|
again and toggling Start at login off/on. `--remove-desktop` removes this app's
|
||||||
|
menu entry, autostart entry and installed icon. It leaves the AppImage in place.
|
||||||
|
|
||||||
|
If FUSE is unavailable, use the runtime's extraction mode:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APPIMAGE_EXTRACT_AND_RUN=1 ./BTD_700_Control-0.3.0-x86_64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
When installing a menu entry or enabling autostart from this mode, the launcher
|
||||||
|
preserves it using the runtime's `--appimage-extract-and-run` argument.
|
||||||
|
|
||||||
|
For a permanent launcher on such a system, extract once with `--appimage-extract`,
|
||||||
|
keep the resulting `squashfs-root` directory, and run its `AppRun`. The launcher
|
||||||
|
created from that extracted copy uses its `AppRun` wrapper. Remove its
|
||||||
|
desktop/autostart entries before deleting the directory.
|
||||||
|
|
||||||
|
GNOME still needs a working StatusNotifier/AppIndicator extension for a tray.
|
||||||
|
USB access uses the host's `/dev/hidraw` permissions. If access is denied, download
|
||||||
|
`70-btd700-control.rules` from the same release and install it as described in the
|
||||||
|
[README](../README.md#usb-permissions). The app never changes system permissions itself.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Run from the repository root using rootless Podman (Docker can also build the
|
||||||
|
same Containerfile):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
podman build -t btd700-appimage-builder -f packaging/appimage/Containerfile .
|
||||||
|
podman run --rm -v "$PWD:/src:z" btd700-appimage-builder
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the resulting file on a minimal test host without Python, GTK, libadwaita
|
||||||
|
or libdbusmenu installed (CLI, menu installation/removal, both GUI languages,
|
||||||
|
real D-Bus tray events against a simulated device, and extraction-mode startup):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
podman build -t btd700-appimage-test -f packaging/appimage/TestContainerfile .
|
||||||
|
podman run --rm -v "$PWD:/src:z" btd700-appimage-test
|
||||||
|
```
|
||||||
|
|
||||||
|
The output goes to `dist/`. Ubuntu source repositories are enabled in the builder.
|
||||||
|
`assemble.py` copies runtime files and follows their ELF dependencies, retaining
|
||||||
|
package versions and license texts. The build downloads the exact corresponding
|
||||||
|
Ubuntu source archives and publishes them in `*-dependency-sources.tar.gz`.
|
||||||
|
`*-packages.json` lists the binary and source versions. The container receives
|
||||||
|
current Ubuntu 24.04 security updates, so two builds at different times need not
|
||||||
|
be byte-identical. No host packages are installed by this process.
|
||||||
|
|
||||||
|
Appimagetool 1.9.1 and type2-runtime 20251108 are checksum-pinned. The dependency
|
||||||
|
source archive also includes the runtime source, its build scripts and patches,
|
||||||
|
and the libfuse 3.15.0 and squashfuse 0.5.2 sources used by that runtime. To rebuild
|
||||||
|
the runtime, follow `BUILD.md` and `scripts/docker/` in that source archive;
|
||||||
|
the Dockerfile uses Alpine 3.21 and installs its build dependencies. The GNU LGPL
|
||||||
|
libraries in the AppDir remain dynamically linked and replaceable after extracting
|
||||||
|
the image. Repack a modified AppDir using appimagetool with `--runtime-file`.
|
||||||
|
For Ubuntu libraries, unpack their `.dsc` files with `dpkg-source -x`, install
|
||||||
|
the package's build dependencies and build with `dpkg-buildpackage` in Ubuntu 24.04.
|
||||||
|
For app modifications, rebuild from this project's tagged source and Containerfile.
|
||||||
|
|
||||||
|
The release contains `SHA256SUMS` for the binary, dependency sources, package manifest
|
||||||
|
and USB rule. The project itself remains MIT-licensed; dependencies retain their
|
||||||
|
own licenses. See [third-party notices](../THIRD_PARTY_NOTICES.md).
|
||||||
+18
-2
@@ -38,14 +38,30 @@ Connected BTD 700: USB `3542:3001`, firmware 3.11.0.
|
|||||||
- Extraction helper output matched the SHA256 of the original assembly used for
|
- Extraction helper output matched the SHA256 of the original assembly used for
|
||||||
protocol research. No vendor binaries were added to the distribution.
|
protocol research. No vendor binaries were added to the distribution.
|
||||||
|
|
||||||
The CI workflow runs the unit tests on Python 3.10 and 3.14, in both languages.
|
## AppImage 0.3.0
|
||||||
|
|
||||||
|
- Built an x86-64 image from Ubuntu 24.04 runtime packages, with bundled Python
|
||||||
|
3.12, GTK, libadwaita, libdbusmenu, SVG loading, MIME data, icons and fonts.
|
||||||
|
- Normal FUSE startup and `APPIMAGE_EXTRACT_AND_RUN=1` startup both displayed a
|
||||||
|
working demo window and registered the tray on Bazzite/GNOME.
|
||||||
|
- The bundled runtime passed the English and German form and D-Bus menu checks.
|
||||||
|
- The AppImage read the real BTD 700 status successfully: firmware 3.11.0,
|
||||||
|
Gaming, aptX Adaptive, 24-bit / 48 kHz, audio playing. No device settings changed.
|
||||||
|
- Menu installation/removal uses isolated temporary XDG directories. Autostart
|
||||||
|
tests cover persistent AppImage paths/icons, extracted AppDirs and no-FUSE mode.
|
||||||
|
- A minimal Ubuntu test container without Python, GTK, libadwaita or libdbusmenu
|
||||||
|
exercises the packaged CLI, SVG decoding, both GUI languages and D-Bus tray
|
||||||
|
controls with simulated hardware. The release workflow repeats these checks.
|
||||||
|
|
||||||
|
The unit-test CI workflow runs on Python 3.10 and 3.14, in both languages.
|
||||||
It does not run real hardware tests or claim that a particular receiver works.
|
It does not run real hardware tests or claim that a particular receiver works.
|
||||||
|
|
||||||
## Not yet systematically verified
|
## Not yet systematically verified
|
||||||
|
|
||||||
- Hardware writes across different firmware revisions and all individual commands.
|
- Hardware writes across different firmware revisions and all individual commands.
|
||||||
- Real Auracast receiver compatibility, audio quality and reconnect behavior.
|
- Real Auracast receiver compatibility, audio quality and reconnect behavior.
|
||||||
- Other distributions/desktops beyond the tested Bazzite/GNOME setup.
|
- Interactive desktops other than Bazzite/GNOME; Ubuntu GUI checks use Xvfb and
|
||||||
|
a private test tray watcher, not a full GNOME or KDE desktop.
|
||||||
|
|
||||||
`tools/check_hardware.py --run` has not been executed. It deliberately interrupts
|
`tools/check_hardware.py --run` has not been executed. It deliberately interrupts
|
||||||
audio, changes settings including the broadcast password, and attempts to restore
|
audio, changes settings including the broadcast password, and attempts to restore
|
||||||
|
|||||||
+7
-2
@@ -4,7 +4,7 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from btd700.i18n import tr
|
from btd700.i18n import tr
|
||||||
from btd700.integration import APP_ID, autostart_path, desktop_entry
|
from btd700.integration import APP_ID, autostart_path, desktop_entry, install_appimage_icon, installed_icon
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -16,11 +16,16 @@ def main():
|
|||||||
if args.uninstall:
|
if args.uninstall:
|
||||||
destination.unlink(missing_ok=True)
|
destination.unlink(missing_ok=True)
|
||||||
autostart_path().unlink(missing_ok=True)
|
autostart_path().unlink(missing_ok=True)
|
||||||
print(tr('Menüeintrag und Autostart entfernt. Der Projektordner bleibt erhalten.'))
|
installed_icon().unlink(missing_ok=True)
|
||||||
|
print(tr('Menüeintrag und Autostart entfernt. Die Programmdateien bleiben erhalten.'))
|
||||||
return
|
return
|
||||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
install_appimage_icon()
|
||||||
destination.write_text(desktop_entry())
|
destination.write_text(desktop_entry())
|
||||||
print(tr('Installiert: {path}').format(path=destination))
|
print(tr('Installiert: {path}').format(path=destination))
|
||||||
|
if os.environ.get('APPIMAGE') and os.environ.get('APPDIR'):
|
||||||
|
print(tr('Im Anwendungsmenü „BTD 700 Control“ öffnen. Die AppImage-Datei muss an diesem Ort bleiben.'))
|
||||||
|
else:
|
||||||
print(tr('Im Anwendungsmenü „BTD 700 Control“ öffnen. Der Projektordner muss erhalten bleiben.'))
|
print(tr('Im Anwendungsmenü „BTD 700 Control“ öffnen. Der Projektordner muss erhalten bleiben.'))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
APPDIR=${APPDIR:-$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)}
|
||||||
|
export APPDIR
|
||||||
|
. "$APPDIR/usr/share/btd700-control/appimage-environment.sh"
|
||||||
|
case "${1:-}" in
|
||||||
|
--install-desktop) exec "$APPDIR/usr/bin/python3.12" "$APPDIR/usr/share/btd700-control/install.py" ;;
|
||||||
|
--remove-desktop) exec "$APPDIR/usr/bin/python3.12" "$APPDIR/usr/share/btd700-control/install.py" --uninstall ;;
|
||||||
|
esac
|
||||||
|
exec "$APPDIR/usr/bin/python3.12" -m btd700 "$@"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM docker.io/library/ubuntu:24.04
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
RUN sed -i 's/Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get upgrade -y \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
python3 python3-gi python3-gi-cairo gir1.2-gtk-4.0 gir1.2-adw-1 \
|
||||||
|
gir1.2-dbusmenu-glib-0.4 adwaita-icon-theme fonts-dejavu-core \
|
||||||
|
ca-certificates curl squashfs-tools desktop-file-utils binutils libglib2.0-bin \
|
||||||
|
xvfb xauth dbus-x11 \
|
||||||
|
&& rm -rf /var/cache/apt/archives/*.deb
|
||||||
|
RUN apt-get install -y --no-install-recommends librsvg2-common xz-utils file
|
||||||
|
WORKDIR /src
|
||||||
|
CMD ["bash", "packaging/appimage/build.sh"]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
Download **BTD_700_Control-0.3.0-x86_64.AppImage**, make it executable and open it.
|
||||||
|
Python, GTK, libadwaita and the tray library are included. Requires **x86-64 Linux
|
||||||
|
with glibc 2.39+** (such as Ubuntu 24.04 or newer). English and German are supported.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x BTD_700_Control-0.3.0-x86_64.AppImage
|
||||||
|
./BTD_700_Control-0.3.0-x86_64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional `--install-desktop` adds an application-menu entry. Keep the file at that
|
||||||
|
location; enable Start at login in the app for tray autostart. `--remove-desktop`
|
||||||
|
removes both entries. If FUSE is unavailable, prefix the launch command with
|
||||||
|
`APPIMAGE_EXTRACT_AND_RUN=1`.
|
||||||
|
|
||||||
|
GNOME still needs a working AppIndicator/StatusNotifier extension. If USB access
|
||||||
|
is denied, install the attached udev rule following the README instructions.
|
||||||
|
SHA256SUMS covers the binary, package manifest, USB rule and dependency source archive.
|
||||||
|
The large `dependency-sources.tar.gz` is for reviewing/rebuilding libraries and is
|
||||||
|
not needed to run the app. GitHub's Source code archives contain the app itself.
|
||||||
|
|
||||||
|
**Independent, AI-developed (OpenAI Codex), experimental software. No warranty or
|
||||||
|
guarantee of compatibility.** Project code is MIT-licensed; bundled dependencies
|
||||||
|
retain their own licenses. Controls only: no firmware updates, downloads or update APIs.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Deliberately no Python, GTK, libadwaita or libdbusmenu installed on this host.
|
||||||
|
FROM docker.io/library/ubuntu:24.04
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
xvfb xauth dbus-x11 desktop-file-utils
|
||||||
|
WORKDIR /src
|
||||||
|
CMD ["dbus-run-session", "--", "xvfb-run", "-a", "bash", "packaging/appimage/check.sh"]
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Bundle Ubuntu runtime files and record every contributing binary/source package."""
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
APPDIR = ROOT / 'build/appimage/BTD_700_Control.AppDir'
|
||||||
|
if APPDIR.exists():
|
||||||
|
shutil.rmtree(APPDIR)
|
||||||
|
APPDIR.mkdir(parents=True)
|
||||||
|
|
||||||
|
|
||||||
|
def output(*args):
|
||||||
|
return subprocess.check_output(args, text=True)
|
||||||
|
|
||||||
|
|
||||||
|
packages = {}
|
||||||
|
owners = {}
|
||||||
|
for line in output('dpkg-query', '-W', '-f=${binary:Package}\t${Version}\t${source:Package}\t${source:Version}\n').splitlines():
|
||||||
|
name, version, source, source_version = line.split('\t')
|
||||||
|
packages[name] = dict(package=name, version=version, source=source, source_version=source_version)
|
||||||
|
for path in output('dpkg-query', '-L', name).splitlines():
|
||||||
|
owners[path] = name
|
||||||
|
if Path(path).is_file():
|
||||||
|
owners[str(Path(path).resolve())] = name
|
||||||
|
included = set()
|
||||||
|
|
||||||
|
|
||||||
|
def copy(path, destination=None):
|
||||||
|
path = Path(path)
|
||||||
|
if path.name in {'__pycache__', 'icon-theme.cache', 'gschemas.compiled'} or path.suffix == '.pyc':
|
||||||
|
return
|
||||||
|
if path.is_dir():
|
||||||
|
for child in sorted(path.iterdir()):
|
||||||
|
copy(child, Path(destination) / child.name if destination else None)
|
||||||
|
elif path.is_file():
|
||||||
|
target = APPDIR / (str(destination) if destination else str(path).lstrip('/'))
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(path, target)
|
||||||
|
owner = owners.get(str(path)) or owners.get(str(path.resolve()))
|
||||||
|
if not owner and path.is_relative_to('/usr/share/mime'):
|
||||||
|
owner = owners['/usr/share/mime/packages/freedesktop.org.xml']
|
||||||
|
if owner:
|
||||||
|
included.add(owner)
|
||||||
|
elif not path.is_relative_to(ROOT):
|
||||||
|
raise RuntimeError(f'No package provenance for {path}')
|
||||||
|
|
||||||
|
|
||||||
|
for path in ['/usr/bin/python3.12', '/usr/lib/python3.12',
|
||||||
|
'/usr/lib/python3/dist-packages/gi', '/usr/lib/python3/dist-packages/cairo',
|
||||||
|
'/usr/lib/x86_64-linux-gnu/girepository-1.0',
|
||||||
|
'/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders',
|
||||||
|
'/usr/share/glib-2.0/schemas', '/usr/share/icons/Adwaita',
|
||||||
|
'/usr/share/icons/hicolor', '/usr/share/fonts/truetype/dejavu',
|
||||||
|
'/usr/share/mime',
|
||||||
|
'/etc/fonts', '/usr/share/fontconfig']:
|
||||||
|
copy(path)
|
||||||
|
# Include GTK/libadwaita's German translations as well as the app's own catalog.
|
||||||
|
for path in Path('/usr/share/locale').glob('de*/LC_MESSAGES/*.mo'):
|
||||||
|
if path.name.startswith(('gtk40', 'libadwaita', 'glib20', 'gdk-pixbuf')):
|
||||||
|
copy(path)
|
||||||
|
roots = list(APPDIR.rglob('*.so')) + [APPDIR / 'usr/bin/python3.12']
|
||||||
|
roots += [Path('/usr/lib/x86_64-linux-gnu') / name for name in
|
||||||
|
['libgtk-4.so.1', 'libadwaita-1.so.0', 'libdbusmenu-glib.so.4']]
|
||||||
|
# ldd includes the transitive ELF dependencies. glibc and the ELF loader belong
|
||||||
|
# to the host; Ubuntu 24.04 (glibc 2.39) is the documented compatibility floor.
|
||||||
|
host_libraries = re.compile(r'^(ld-linux.*|lib(c|m|dl|rt|pthread|resolv|util|nss_.*)\.so\..*)$')
|
||||||
|
libraries = set(roots[-3:])
|
||||||
|
for path in roots:
|
||||||
|
for line in output('ldd', str(path)).splitlines():
|
||||||
|
if 'not found' in line:
|
||||||
|
raise RuntimeError(f'Missing dependency of {path}: {line}')
|
||||||
|
match = re.search(r'=> (/\S+)', line)
|
||||||
|
if match and not host_libraries.match(Path(match[1]).name):
|
||||||
|
libraries.add(Path(match[1]))
|
||||||
|
for path in sorted(libraries):
|
||||||
|
copy(path, 'usr/lib/x86_64-linux-gnu/' + path.name)
|
||||||
|
|
||||||
|
copy(ROOT / 'btd700', 'usr/share/btd700-control/btd700')
|
||||||
|
for name in ['install.py', 'LICENSE', 'THIRD_PARTY_NOTICES.md']:
|
||||||
|
copy(ROOT / name, 'usr/share/btd700-control/' + name)
|
||||||
|
copy(ROOT / 'packaging/btd700-control.svg', 'btd700-control.svg')
|
||||||
|
copy(ROOT / 'packaging/btd700-control.svg', 'usr/share/icons/hicolor/scalable/apps/btd700-control.svg')
|
||||||
|
copy(ROOT / 'packaging/70-btd700-control.rules', 'usr/share/btd700-control/packaging/70-btd700-control.rules')
|
||||||
|
copy(ROOT / 'packaging/appimage/AppRun', 'AppRun')
|
||||||
|
copy(ROOT / 'packaging/appimage/environment.sh', 'usr/share/btd700-control/appimage-environment.sh')
|
||||||
|
(APPDIR / 'AppRun').chmod(0o755)
|
||||||
|
copy(ROOT / 'packaging/appimage/btd700-control.desktop', 'btd700-control.desktop')
|
||||||
|
(APPDIR / '.DirIcon').symlink_to('btd700-control.svg')
|
||||||
|
subprocess.run(['desktop-file-validate', str(APPDIR / 'btd700-control.desktop')], check=True)
|
||||||
|
subprocess.run(['glib-compile-schemas', str(APPDIR / 'usr/share/glib-2.0/schemas')], check=True)
|
||||||
|
cache = output('/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders')
|
||||||
|
# A bare module name is resolved by dlopen through our LD_LIBRARY_PATH. This
|
||||||
|
# avoids storing a container or temporary AppImage mount path in the cache.
|
||||||
|
cache = re.sub(r'^"[^"\n]*/([^/"\n]+\.so)"$', r'"\1"', cache, flags=re.MULTILINE)
|
||||||
|
(APPDIR / 'usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders.cache').write_text(cache)
|
||||||
|
# Make the bundled fonts available without relying on a host font installation.
|
||||||
|
font_config = APPDIR / 'etc/fonts/fonts.conf'
|
||||||
|
font_config.write_text(font_config.read_text().replace('<fontconfig>',
|
||||||
|
'<fontconfig>\n <dir prefix="relative">../../usr/share/fonts</dir>'))
|
||||||
|
|
||||||
|
# License texts accompany the binary; complete corresponding Ubuntu source
|
||||||
|
# archives are downloaded separately by build.sh and attached to the release.
|
||||||
|
copy('/usr/share/common-licenses')
|
||||||
|
for package in sorted(included):
|
||||||
|
doc = Path('/usr/share/doc') / package.split(':')[0] / 'copyright'
|
||||||
|
if not doc.is_file():
|
||||||
|
raise RuntimeError(f'No copyright file for {package}')
|
||||||
|
copy(doc, 'usr/share/doc/btd700-bundled/' + package.replace(':', '_') + '.copyright')
|
||||||
|
manifest = [packages[name] for name in sorted(included)]
|
||||||
|
(APPDIR / 'usr/share/doc/btd700-bundled/packages.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||||
|
(ROOT / 'build/appimage/packages.json').write_text(json.dumps(manifest, indent=2) + '\n')
|
||||||
|
sources = sorted({f"{p['source']}={p['source_version']}" for p in manifest})
|
||||||
|
(ROOT / 'build/appimage/sources.txt').write_text('\n'.join(sources) + '\n')
|
||||||
|
# Do not ship bytecode, Python tests, or compilation-only metadata.
|
||||||
|
for path in list(APPDIR.rglob('__pycache__')):
|
||||||
|
shutil.rmtree(path)
|
||||||
|
for path in [APPDIR / 'usr/lib/python3.12/test', APPDIR / 'usr/lib/python3.12/config-3.12-x86_64-linux-gnu']:
|
||||||
|
if path.exists():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
print(f'Assembled {APPDIR}: {len(manifest)} binary packages, {len(sources)} source packages')
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=BTD 700 Control
|
||||||
|
Comment=Linux controls for the Sennheiser BTD 700
|
||||||
|
Comment[de]=Linux-Steuerung für den Sennheiser BTD 700
|
||||||
|
Exec=btd700-control
|
||||||
|
Icon=btd700-control
|
||||||
|
Terminal=false
|
||||||
|
Categories=AudioVideo;Audio;
|
||||||
|
Keywords=Sennheiser;Bluetooth;aptX;Auracast;Dongle;
|
||||||
|
StartupWMClass=io.github.btd700linux.Control
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run inside the Ubuntu 24.04 builder (see docs/APPIMAGE.md).
|
||||||
|
set -euo pipefail
|
||||||
|
cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.."
|
||||||
|
repo_dir=$PWD
|
||||||
|
version=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
|
||||||
|
build_dir="$repo_dir/build/appimage"
|
||||||
|
sources_dir="$build_dir/dependency-sources"
|
||||||
|
mkdir -p "$build_dir/downloads" "$sources_dir" dist
|
||||||
|
|
||||||
|
download() {
|
||||||
|
local url=$1 name=$2 checksum=$3
|
||||||
|
if [[ ! -f "$build_dir/downloads/$name" ]]; then
|
||||||
|
curl --fail --location --retry 3 "$url" -o "$build_dir/downloads/$name"
|
||||||
|
fi
|
||||||
|
echo "$checksum $build_dir/downloads/$name" | sha256sum --check
|
||||||
|
}
|
||||||
|
download https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage appimagetool \
|
||||||
|
ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0
|
||||||
|
download https://github.com/AppImage/type2-runtime/releases/download/20251108/runtime-x86_64 runtime-x86_64 \
|
||||||
|
2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d
|
||||||
|
chmod +x "$build_dir/downloads/appimagetool"
|
||||||
|
python3 packaging/appimage/assemble.py
|
||||||
|
|
||||||
|
# Keep the exact corresponding Ubuntu sources alongside the downloadable binary.
|
||||||
|
# apt checks the archives against the signed repository's source index.
|
||||||
|
cd "$sources_dir"
|
||||||
|
while IFS= read -r package; do
|
||||||
|
apt-get source --download-only "$package"
|
||||||
|
done < "$build_dir/sources.txt"
|
||||||
|
curl --fail --location --retry 3 https://github.com/AppImage/type2-runtime/archive/refs/tags/20251108.tar.gz -o type2-runtime-20251108.tar.gz
|
||||||
|
download https://github.com/libfuse/libfuse/releases/download/fuse-3.15.0/fuse-3.15.0.tar.xz fuse-3.15.0.tar.xz \
|
||||||
|
70589cfd5e1cff7ccd6ac91c86c01be340b227285c5e200baa284e401eea2ca0
|
||||||
|
download https://github.com/vasi/squashfuse/archive/0.5.2.tar.gz squashfuse-0.5.2.tar.gz \
|
||||||
|
db0238c5981dabbd80ee09ae15387f390091668ca060a7bc38047912491443d3
|
||||||
|
cp "$build_dir/downloads/fuse-3.15.0.tar.xz" "$build_dir/downloads/squashfuse-0.5.2.tar.gz" .
|
||||||
|
cp "$build_dir/packages.json" "$build_dir/sources.txt" .
|
||||||
|
rm -rf -- "$sources_dir/build-instructions"
|
||||||
|
cp -r "$repo_dir/packaging/appimage" build-instructions
|
||||||
|
cp "$repo_dir/docs/APPIMAGE.md" README.md
|
||||||
|
tar -xOf type2-runtime-20251108.tar.gz type2-runtime-20251108/LICENSE > "$build_dir/BTD_700_Control.AppDir/usr/share/doc/btd700-bundled/AppImage-runtime.LICENSE"
|
||||||
|
tar -xOf fuse-3.15.0.tar.xz fuse-3.15.0/LGPL2.txt > "$build_dir/BTD_700_Control.AppDir/usr/share/doc/btd700-bundled/libfuse.LICENSE"
|
||||||
|
tar -xOf squashfuse-0.5.2.tar.gz squashfuse-0.5.2/LICENSE > "$build_dir/BTD_700_Control.AppDir/usr/share/doc/btd700-bundled/squashfuse.LICENSE"
|
||||||
|
# Permissive runtime dependency notices (the upstream runtime LICENSE links these).
|
||||||
|
curl -fsSL --retry 3 https://git.musl-libc.org/cgit/musl/plain/COPYRIGHT?h=v1.2.5 -o "$build_dir/BTD_700_Control.AppDir/usr/share/doc/btd700-bundled/musl.COPYRIGHT"
|
||||||
|
curl -fsSL --retry 3 https://raw.githubusercontent.com/facebook/zstd/v1.5.6/LICENSE -o "$build_dir/BTD_700_Control.AppDir/usr/share/doc/btd700-bundled/zstd.LICENSE"
|
||||||
|
curl -fsSL --retry 3 https://raw.githubusercontent.com/madler/zlib/v1.3.1/LICENSE -o "$build_dir/BTD_700_Control.AppDir/usr/share/doc/btd700-bundled/zlib.LICENSE"
|
||||||
|
cd "$repo_dir"
|
||||||
|
archive="BTD_700_Control-$version-dependency-sources.tar.gz"
|
||||||
|
tar --exclude='__pycache__' -czf "dist/$archive" -C "$build_dir" dependency-sources
|
||||||
|
ARCH=x86_64 VERSION="$version" APPIMAGE_EXTRACT_AND_RUN=1 "$build_dir/downloads/appimagetool" \
|
||||||
|
--no-appstream --mksquashfs-opt -processors --mksquashfs-opt 2 \
|
||||||
|
--runtime-file "$build_dir/downloads/runtime-x86_64" \
|
||||||
|
"$build_dir/BTD_700_Control.AppDir" "dist/BTD_700_Control-$version-x86_64.AppImage"
|
||||||
|
cp "$build_dir/packages.json" "dist/BTD_700_Control-$version-packages.json"
|
||||||
|
cp packaging/70-btd700-control.rules dist/
|
||||||
|
cd dist
|
||||||
|
sha256sum "BTD_700_Control-$version-x86_64.AppImage" "$archive" \
|
||||||
|
"BTD_700_Control-$version-packages.json" 70-btd700-control.rules > SHA256SUMS
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run in the builder with: dbus-run-session -- xvfb-run -a bash packaging/appimage/check.sh
|
||||||
|
set -euo pipefail
|
||||||
|
repo_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||||
|
appimages=("$repo_dir"/dist/*-x86_64.AppImage)
|
||||||
|
[[ ${#appimages[@]} == 1 ]] || { echo 'Expected exactly one AppImage in dist/' >&2; exit 1; }
|
||||||
|
test_dir=$(mktemp -d)
|
||||||
|
trap 'rm -rf -- "$test_dir"' EXIT
|
||||||
|
export XDG_CONFIG_HOME="$test_dir/config" XDG_DATA_HOME="$test_dir/data"
|
||||||
|
appimage=${appimages[0]}
|
||||||
|
# No accessibility service runs inside this private headless test session.
|
||||||
|
export GTK_A11Y=none
|
||||||
|
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --language en --demo status
|
||||||
|
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --install-desktop
|
||||||
|
desktop-file-validate "$XDG_DATA_HOME/applications/io.github.btd700linux.Control.desktop"
|
||||||
|
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --remove-desktop
|
||||||
|
test ! -e "$XDG_DATA_HOME/applications/io.github.btd700linux.Control.desktop"
|
||||||
|
cd "$test_dir"
|
||||||
|
"$appimage" --appimage-extract > /dev/null
|
||||||
|
export APPDIR="$test_dir/squashfs-root"
|
||||||
|
. "$APPDIR/usr/share/btd700-control/appimage-environment.sh"
|
||||||
|
"$APPDIR/usr/bin/python3.12" - <<'PY'
|
||||||
|
import os, sys, gi
|
||||||
|
from pathlib import Path
|
||||||
|
gi.require_version('GdkPixbuf', '2.0')
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
root = Path(os.environ['APPDIR'])
|
||||||
|
assert Path(sys.prefix).is_relative_to(root)
|
||||||
|
assert Path(gi.__file__).is_relative_to(root)
|
||||||
|
icon = GdkPixbuf.Pixbuf.new_from_file(str(root / 'btd700-control.svg'))
|
||||||
|
assert icon.get_width() == 128
|
||||||
|
print('PASS: bundled Python, PyGObject, SVG loader and MIME recognition')
|
||||||
|
PY
|
||||||
|
export BTD700_TEST_INSTALLED=1 BTD700_TEST_TRAY_HOST=1
|
||||||
|
for language in en de; do
|
||||||
|
BTD700_LANGUAGE=$language "$APPDIR/usr/bin/python3.12" "$repo_dir/tools/check_gui.py"
|
||||||
|
done
|
||||||
|
# Also exercise the actual runtime/extraction entry point into the GUI.
|
||||||
|
"$APPDIR/usr/bin/python3.12" "$repo_dir/tools/tray_test_host.py" > "$test_dir/tray.log" &
|
||||||
|
tray_pid=$!
|
||||||
|
trap 'kill "$tray_pid" 2>/dev/null || true; rm -rf -- "$test_dir"' EXIT
|
||||||
|
for attempt in {1..50}; do
|
||||||
|
if [[ $(cat "$test_dir/tray.log") == TEST_TRAY_READY ]]; then break; fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --demo --smoke-seconds 3
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Shared by the launcher and packaged-runtime verification. APPDIR must be set.
|
||||||
|
export PYTHONHOME="$APPDIR/usr"
|
||||||
|
export PYTHONPATH="$APPDIR/usr/lib/python3/dist-packages:$APPDIR/usr/share/btd700-control"
|
||||||
|
export PYTHONNOUSERSITE=1
|
||||||
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
|
export LD_LIBRARY_PATH="$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||||
|
export GDK_PIXBUF_MODULE_FILE="$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders.cache"
|
||||||
|
export GI_TYPELIB_PATH="$APPDIR/usr/lib/x86_64-linux-gnu/girepository-1.0"
|
||||||
|
export GSETTINGS_SCHEMA_DIR="$APPDIR/usr/share/glib-2.0/schemas"
|
||||||
|
export XDG_DATA_DIRS="$APPDIR/usr/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
||||||
|
export FONTCONFIG_FILE="$APPDIR/etc/fonts/fonts.conf"
|
||||||
|
export FONTCONFIG_PATH="$APPDIR/etc/fonts"
|
||||||
|
# A control panel does not need GPU rendering or host-specific Mesa drivers.
|
||||||
|
export GSK_RENDERER=${GSK_RENDERER:-cairo}
|
||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "btd700-control"
|
name = "btd700-control"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
description = "Independent native Linux controls and tray menu for the Sennheiser BTD 700"
|
description = "Independent native Linux controls and tray menu for the Sennheiser BTD 700"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from btd700.integration import APP_ID, autostart_path, desktop_entry, set_autostart
|
from btd700.integration import APP_ID, autostart_path, desktop_entry, set_autostart, installed_icon
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
@@ -44,3 +44,37 @@ class IntegrationTests(unittest.TestCase):
|
|||||||
text = desktop_entry(background=True)
|
text = desktop_entry(background=True)
|
||||||
self.assertIn(' -m btd700 --background', text)
|
self.assertIn(' -m btd700 --background', text)
|
||||||
self.assertIn('Icon=audio-headphones', text)
|
self.assertIn('Icon=audio-headphones', text)
|
||||||
|
|
||||||
|
def test_appimage_autostart_survives_temporary_mount_removal(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
mount = Path(tmp) / 'temporary-mount'
|
||||||
|
mount.mkdir()
|
||||||
|
(mount / 'btd700-control.svg').write_text('<svg/>')
|
||||||
|
env = dict(APPIMAGE='/home/example/Apps/BTD "700" 100%.AppImage',
|
||||||
|
APPDIR=str(mount), XDG_CONFIG_HOME=tmp+'/config', XDG_DATA_HOME=tmp+'/data')
|
||||||
|
with patch.dict(os.environ, env):
|
||||||
|
set_autostart(True)
|
||||||
|
text = autostart_path().read_text()
|
||||||
|
self.assertIn('Exec="/home/example/Apps/BTD \\"700\\" 100%%.AppImage" --background', text)
|
||||||
|
self.assertNotIn(str(mount), text)
|
||||||
|
self.assertNotIn(' -m btd700', text)
|
||||||
|
(mount / 'btd700-control.svg').unlink()
|
||||||
|
mount.rmdir()
|
||||||
|
self.assertEqual(installed_icon().read_text(), '<svg/>')
|
||||||
|
|
||||||
|
def test_extracted_appimage_launcher_uses_environment_wrapper(self):
|
||||||
|
with patch.dict(os.environ, {'APPDIR': '/tmp/Extracted app', 'APPIMAGE': ''}):
|
||||||
|
text = desktop_entry()
|
||||||
|
self.assertIn('Exec="/tmp/Extracted app/AppRun"', text)
|
||||||
|
self.assertIn('Icon=/tmp/Extracted app/btd700-control.svg', text)
|
||||||
|
self.assertNotIn(' -m btd700', text)
|
||||||
|
|
||||||
|
def test_appimage_launcher_preserves_fuse_free_mode(self):
|
||||||
|
with patch.dict(os.environ, {'APPDIR': '/tmp/mount', 'APPIMAGE': '/tmp/BTD.AppImage', 'APPIMAGE_EXTRACT_AND_RUN': '1'}):
|
||||||
|
text = desktop_entry(background=True)
|
||||||
|
self.assertIn('"/tmp/BTD.AppImage" --appimage-extract-and-run --background', text)
|
||||||
|
|
||||||
|
def test_runtime_extraction_directory_preserves_fuse_free_mode(self):
|
||||||
|
with patch.dict(os.environ, {'APPDIR': '/tmp/appimage_extracted_abc', 'APPIMAGE': '/tmp/BTD.AppImage'}):
|
||||||
|
text = desktop_entry(background=True)
|
||||||
|
self.assertIn(' --appimage-extract-and-run --background', text)
|
||||||
|
|||||||
+16
-5
@@ -1,12 +1,19 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Exercise the GTK UI and real D-Bus menu against an isolated demo device."""
|
"""Exercise the GTK UI and real D-Bus menu against an isolated demo device."""
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
if not os.environ.get('BTD700_TEST_INSTALLED'):
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
from btd700.app import Application
|
from btd700.app import Application
|
||||||
from btd700.i18n import tr
|
from btd700.i18n import tr
|
||||||
from gi.repository import Gio, GLib, Gtk
|
from gi.repository import Gio, GLib, Gtk
|
||||||
|
|
||||||
|
# CI uses a private dbus-run-session; never replace the real desktop's watcher.
|
||||||
|
if os.environ.get('BTD700_TEST_TRAY_HOST') == '1':
|
||||||
|
from tray_test_host import start_host
|
||||||
|
test_host = start_host()
|
||||||
|
|
||||||
app = Application(demo=True)
|
app = Application(demo=True)
|
||||||
step = 0
|
step = 0
|
||||||
wait_ticks = 0
|
wait_ticks = 0
|
||||||
@@ -46,9 +53,10 @@ def screenshot(path):
|
|||||||
paintable.snapshot(snapshot, window.get_width(), window.get_height())
|
paintable.snapshot(snapshot, window.get_width(), window.get_height())
|
||||||
node = snapshot.to_node()
|
node = snapshot.to_node()
|
||||||
if node is None:
|
if node is None:
|
||||||
raise RuntimeError('Window snapshot is not ready')
|
return False # Wayland may still be committing a resize; retry next tick.
|
||||||
texture = window.get_renderer().render_texture(node, None)
|
texture = window.get_renderer().render_texture(node, None)
|
||||||
texture.save_to_png(path)
|
texture.save_to_png(path)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def tick():
|
def tick():
|
||||||
@@ -118,14 +126,17 @@ def tick():
|
|||||||
app.mode_buttons[0].emit('clicked') if app.mode_buttons[0].get_active() else app.mode_buttons[0].set_active(True)
|
app.mode_buttons[0].emit('clicked') if app.mode_buttons[0].get_active() else app.mode_buttons[0].set_active(True)
|
||||||
app.perform('set_mode', 0)
|
app.perform('set_mode', 0)
|
||||||
elif step == 14:
|
elif step == 14:
|
||||||
screenshot('/tmp/btd700-gui-test.png')
|
if not screenshot('/tmp/btd700-gui-test.png'):
|
||||||
|
return True
|
||||||
adjustment = app.scroll.get_vadjustment()
|
adjustment = app.scroll.get_vadjustment()
|
||||||
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size())
|
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size())
|
||||||
elif step == 15:
|
elif step == 15:
|
||||||
screenshot('/tmp/btd700-gui-test-bottom.png')
|
if not screenshot('/tmp/btd700-gui-test-bottom.png'):
|
||||||
|
return True
|
||||||
app.window.set_default_size(420, 600)
|
app.window.set_default_size(420, 600)
|
||||||
elif step == 16:
|
elif step == 16:
|
||||||
screenshot('/tmp/btd700-gui-test-narrow.png')
|
if not screenshot('/tmp/btd700-gui-test-narrow.png'):
|
||||||
|
return True
|
||||||
print('PASS: GTK form, all D-Bus tray control groups, encryption, close/reopen, dirty edits, screenshots', flush=True)
|
print('PASS: GTK form, all D-Bus tray control groups, encryption, close/reopen, dirty edits, screenshots', flush=True)
|
||||||
passed = True
|
passed = True
|
||||||
app.quit()
|
app.quit()
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""Minimal StatusNotifier watcher for GUI tests on a private CI session bus."""
|
||||||
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
|
|
||||||
|
def start_host():
|
||||||
|
bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||||
|
result = bus.call_sync('org.freedesktop.DBus', '/org/freedesktop/DBus',
|
||||||
|
'org.freedesktop.DBus', 'RequestName',
|
||||||
|
GLib.Variant('(su)', ('org.kde.StatusNotifierWatcher', 4)),
|
||||||
|
GLib.VariantType.new('(u)'), Gio.DBusCallFlags.NONE, 2000, None)
|
||||||
|
if result.unpack()[0] != 1:
|
||||||
|
raise RuntimeError('Test watcher refuses to replace an existing tray host')
|
||||||
|
xml = '<node><interface name="org.kde.StatusNotifierWatcher"><method name="RegisterStatusNotifierItem"><arg type="s" direction="in"/></method></interface></node>'
|
||||||
|
def register(_bus, _sender, _path, _interface, _method, _params, invocation):
|
||||||
|
invocation.return_value(None)
|
||||||
|
registration = bus.register_object('/StatusNotifierWatcher', Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], register, None, None)
|
||||||
|
return bus, registration
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
host = start_host()
|
||||||
|
print('TEST_TRAY_READY', flush=True)
|
||||||
|
GLib.MainLoop().run()
|
||||||
Reference in New Issue
Block a user