budctlf7d0d70
Release 0.1.1: builds for x86_64 and aarch64 Linux, and an install script
- Version 0.1.1 for the whole workspace, and CHANGELOG.md with its notes. - .github/workflows/release.yml: a vX.Y.Z tag checks the tag against Cargo.toml, runs the tests, builds on Ubuntu 22.04 (x86_64 and arm runners, so the binaries need glibc 2.35+; Wayland is loaded at runtime) and publishes the tarballs, their SHA-256 files and install.sh as a GitHub release. - .github/workflows/ci.yml: fmt, clippy and the tests on every push to master. - install.sh: installs budctl, budctl-gui and the launcher entry into ~/.local (or --prefix, with sudo only when needed), from a checked download (curl ... | sh), from the unpacked tarball it is in, or from a checkout with --source. --uninstall leaves the user's config and log alone. - Release profile: stripped, thin LTO.
+317 −8
8 files changed
- A .github/workflows/ci.yml +22 −0
- A .github/workflows/release.yml +66 −0
- A CHANGELOG.md +19 −0
- M CLAUDE.md +7 −0
- M Cargo.lock +3 −3
- M Cargo.toml +7 −1
- M README.md +14 −4
- A install.sh +179 −0
| @@ -0,0 +1,22 @@ | |||
| 1 | + | name: CI | |
| 2 | + | ||
| 3 | + | # Formatting, lints and tests on every push to master and every pull request. | |
| 4 | + | ||
| 5 | + | on: | |
| 6 | + | push: | |
| 7 | + | branches: [master] | |
| 8 | + | pull_request: | |
| 9 | + | ||
| 10 | + | jobs: | |
| 11 | + | check: | |
| 12 | + | runs-on: ubuntu-22.04 | |
| 13 | + | steps: | |
| 14 | + | - uses: actions/checkout@v6 | |
| 15 | + | - uses: dtolnay/rust-toolchain@stable | |
| 16 | + | with: | |
| 17 | + | components: clippy, rustfmt | |
| 18 | + | - run: sudo apt-get update && sudo apt-get install -y pkg-config libwayland-dev libxkbcommon-dev | |
| 19 | + | - run: cargo fmt --all -- --check | |
| 20 | + | - run: cargo clippy --workspace --all-targets --locked -- -D warnings | |
| 21 | + | - run: cargo test --workspace --locked | |
| 22 | + | - run: sh -n install.sh | |
| @@ -0,0 +1,66 @@ | |||
| 1 | + | name: Release | |
| 2 | + | ||
| 3 | + | # A tag like v0.1.1 builds budctl and budctl-gui for x86_64 and aarch64 Linux and publishes them, | |
| 4 | + | # with install.sh and checksums, as a GitHub release. The builds run on Ubuntu 22.04, so they need | |
| 5 | + | # glibc 2.35 or newer and nothing else at build time: Wayland and xkbcommon are loaded at runtime. | |
| 6 | + | ||
| 7 | + | on: | |
| 8 | + | push: | |
| 9 | + | tags: ["v[0-9]+.[0-9]+.[0-9]+"] | |
| 10 | + | ||
| 11 | + | permissions: | |
| 12 | + | contents: write | |
| 13 | + | ||
| 14 | + | jobs: | |
| 15 | + | build: | |
| 16 | + | strategy: | |
| 17 | + | matrix: | |
| 18 | + | include: | |
| 19 | + | - target: x86_64-unknown-linux-gnu | |
| 20 | + | runner: ubuntu-22.04 | |
| 21 | + | - target: aarch64-unknown-linux-gnu | |
| 22 | + | runner: ubuntu-22.04-arm | |
| 23 | + | runs-on: ${{ matrix.runner }} | |
| 24 | + | steps: | |
| 25 | + | - uses: actions/checkout@v6 | |
| 26 | + | - name: The tag matches the version in Cargo.toml | |
| 27 | + | run: | | |
| 28 | + | version=$(sed -n 's/^version = "\(.*\)"$/\1/p' Cargo.toml | head -n 1) | |
| 29 | + | if [ "v$version" != "$GITHUB_REF_NAME" ]; then | |
| 30 | + | echo "tag $GITHUB_REF_NAME, but Cargo.toml says $version" | |
| 31 | + | exit 1 | |
| 32 | + | fi | |
| 33 | + | - uses: dtolnay/rust-toolchain@stable | |
| 34 | + | - run: sudo apt-get update && sudo apt-get install -y pkg-config libwayland-dev libxkbcommon-dev | |
| 35 | + | - run: cargo test --workspace --locked | |
| 36 | + | - run: cargo build --release --locked -p budctl -p budctl-gui | |
| 37 | + | - name: Package | |
| 38 | + | run: | | |
| 39 | + | dir=budctl-${{ matrix.target }} | |
| 40 | + | mkdir "$dir" | |
| 41 | + | cp target/release/budctl target/release/budctl-gui contrib/budctl.desktop install.sh README.md LICENSE "$dir/" | |
| 42 | + | tar -czf "$dir.tar.gz" "$dir" | |
| 43 | + | sha256sum "$dir.tar.gz" > "$dir.tar.gz.sha256" | |
| 44 | + | - uses: actions/upload-artifact@v7 | |
| 45 | + | with: | |
| 46 | + | name: ${{ matrix.target }} | |
| 47 | + | path: budctl-${{ matrix.target }}.tar.gz* | |
| 48 | + | if-no-files-found: error | |
| 49 | + | ||
| 50 | + | publish: | |
| 51 | + | needs: build | |
| 52 | + | runs-on: ubuntu-latest | |
| 53 | + | steps: | |
| 54 | + | - uses: actions/checkout@v6 | |
| 55 | + | - uses: actions/download-artifact@v8 | |
| 56 | + | with: | |
| 57 | + | path: dist | |
| 58 | + | merge-multiple: true | |
| 59 | + | - name: Release notes from CHANGELOG.md | |
| 60 | + | run: | | |
| 61 | + | awk -v v="${GITHUB_REF_NAME#v}" '/^## / { p = ($2 == v); next } p' CHANGELOG.md > notes.md | |
| 62 | + | test -s notes.md || { echo "CHANGELOG.md has no section for ${GITHUB_REF_NAME#v}"; exit 1; } | |
| 63 | + | - name: Publish | |
| 64 | + | env: | |
| 65 | + | GH_TOKEN: ${{ github.token }} | |
| 66 | + | run: gh release create "$GITHUB_REF_NAME" --title "budctl $GITHUB_REF_NAME" --notes-file notes.md dist/* install.sh | |
| @@ -0,0 +1,19 @@ | |||
| 1 | + | # Changelog | |
| 2 | + | ||
| 3 | + | ## 0.1.1 (2026-10-28) | |
| 4 | + | ||
| 5 | + | The first release, with builds for x86_64 and aarch64 Linux and an install script. | |
| 6 | + | ||
| 7 | + | Supported buds: realme Buds T310. | |
| 8 | + | ||
| 9 | + | - `budctl`, the command line: `list`, `status`, noise control and its levels, game mode, EQ | |
| 10 | + | presets and custom EQ presets (create, edit, rename, delete), switches (bass boost, 3D sound, | |
| 11 | + | wind, dual connection), bass level, find (ring the buds), touch controls, the hold cycle, and | |
| 12 | + | the devices dual connection keeps. `--json` for scripts and status bars, `--debug` for hex | |
| 13 | + | dumps. | |
| 14 | + | - `budctl-gui`, the desktop app (Wayland): the same features on five pages, mouse first with | |
| 15 | + | vim-like keys on top, colors from a matugen palette. | |
| 16 | + | - `~/.config/budctl/config.json`: a default device, and the desktop app's idle time and start | |
| 17 | + | page. Created with the defaults when missing. | |
| 18 | + | - The desktop app writes its built-in colors to `~/.config/budctl/theme.json` when there is no | |
| 19 | + | such file, and logs errors and session events to `~/.local/state/budctl/gui.log`. | |
| @@ -137,6 +137,13 @@ Exit codes: | |||
| 137 | 137 | - Keep the encode/decode logic pure and covered by tests, so most changes can be verified without hardware. | |
| 138 | 138 | - Before adding a feature, check the spec says the T310 supports it. The protocol family covers many models, and not every command applies to this one. The T310's command bitmap (spec §5) is the authority: never send a command whose bit is off. | |
| 139 | 139 | ||
| 140 | + | ## Releases | |
| 141 | + | ||
| 142 | + | - One version for the whole workspace (`Cargo.toml`), with a section per release in `CHANGELOG.md`. | |
| 143 | + | - To release: bump the version, add its `CHANGELOG.md` section, commit, then tag `vX.Y.Z` and push the tag. `.github/workflows/release.yml` checks the tag matches `Cargo.toml`, runs the tests, builds x86_64 and aarch64 on Ubuntu 22.04 (glibc builds; Wayland is loaded at runtime), and publishes the tarballs, their `.sha256` files and `install.sh`, with that changelog section as the notes. | |
| 144 | + | - `install.sh` installs into `~/.local` by default: from a release download (`curl … | sh`), from the unpacked tarball it sits in, or from a checkout with `--source`. `--uninstall` removes the binaries and the launcher entry, never the user's config or log. | |
| 145 | + | - `.github/workflows/ci.yml` runs fmt, clippy and the tests on every push to master. | |
| 146 | + | ||
| 140 | 147 | ## License | |
| 141 | 148 | ||
| 142 | 149 | GPL-3.0-or-later (`LICENSE`). Rule 2 forbids copying other projects' code. | |
| @@ -327,7 +327,7 @@ dependencies = [ | |||
| 327 | 327 | ||
| 328 | 328 | [[package]] | |
| 329 | 329 | name = "budctl" | |
| 330 | − | version = "0.1.0" | |
| 330 | + | version = "0.1.1" | |
| 331 | 331 | dependencies = [ | |
| 332 | 332 | "budctl-core", | |
| 333 | 333 | "clap", | |
| @@ -336,7 +336,7 @@ dependencies = [ | |||
| 336 | 336 | ||
| 337 | 337 | [[package]] | |
| 338 | 338 | name = "budctl-core" | |
| 339 | − | version = "0.1.0" | |
| 339 | + | version = "0.1.1" | |
| 340 | 340 | dependencies = [ | |
| 341 | 341 | "libc", | |
| 342 | 342 | "serde", | |
| @@ -346,7 +346,7 @@ dependencies = [ | |||
| 346 | 346 | ||
| 347 | 347 | [[package]] | |
| 348 | 348 | name = "budctl-gui" | |
| 349 | − | version = "0.1.0" | |
| 349 | + | version = "0.1.1" | |
| 350 | 350 | dependencies = [ | |
| 351 | 351 | "budctl-core", | |
| 352 | 352 | "iced", | |
| @@ -3,7 +3,7 @@ resolver = "3" | |||
| 3 | 3 | members = ["crates/budctl-core", "crates/budctl-cli", "crates/budctl-gui"] | |
| 4 | 4 | ||
| 5 | 5 | [workspace.package] | |
| 6 | − | version = "0.1.0" | |
| 6 | + | version = "0.1.1" | |
| 7 | 7 | edition = "2024" | |
| 8 | 8 | rust-version = "1.88" | |
| 9 | 9 | license = "GPL-3.0-or-later" | |
| @@ -21,3 +21,9 @@ panic = "deny" | |||
| 21 | 21 | indexing_slicing = "deny" | |
| 22 | 22 | cast_possible_truncation = "deny" | |
| 23 | 23 | undocumented_unsafe_blocks = "deny" | |
| 24 | + | ||
| 25 | + | # Release builds: the binaries the install script ships. No debug symbols, and whole-crate | |
| 26 | + | # optimization across the dependency tree, for smaller and faster binaries. | |
| 27 | + | [profile.release] | |
| 28 | + | strip = true | |
| 29 | + | lto = "thin" | |
| @@ -15,14 +15,24 @@ touch controls, battery and more. There is a command-line tool, `budctl`, and a | |||
| 15 | 15 | ||
| 16 | 16 | ## Install | |
| 17 | 17 | ||
| 18 | − | You need Rust 1.88 or newer, BlueZ, and for the desktop app a Wayland desktop. | |
| 18 | + | You need Linux with BlueZ, and for the desktop app a Wayland desktop. Release builds are for | |
| 19 | + | x86_64 and aarch64 (glibc 2.35 or newer). | |
| 19 | 20 | ||
| 20 | 21 | ```sh | |
| 21 | − | cargo install --path crates/budctl-cli # budctl | |
| 22 | − | cargo install --path crates/budctl-gui # budctl-gui | |
| 23 | − | cp contrib/budctl.desktop ~/.local/share/applications/ | |
| 22 | + | curl -fsSL https://github.com/krolyxon/budctl/releases/latest/download/install.sh | sh | |
| 24 | 23 | ``` | |
| 25 | 24 | ||
| 25 | + | This puts `budctl` and `budctl-gui` in `~/.local/bin`, and a launcher entry in | |
| 26 | + | `~/.local/share/applications`. The download is checked against its SHA-256 checksum. Options: | |
| 27 | + | ||
| 28 | + | ```sh | |
| 29 | + | sh install.sh --version 0.1.1 # a given release instead of the latest | |
| 30 | + | sh install.sh --prefix /usr/local # elsewhere (uses sudo when it must) | |
| 31 | + | sh install.sh --uninstall # remove them again; your config and log stay | |
| 32 | + | ``` | |
| 33 | + | ||
| 34 | + | To build from source instead (Rust 1.88 or newer), run `./install.sh --source` in a checkout. | |
| 35 | + | ||
| 26 | 36 | Pair the buds and connect them in your Bluetooth settings first. | |
| 27 | 37 | ||
| 28 | 38 | ## Command line | |
| @@ -0,0 +1,179 @@ | |||
| 1 | + | #!/bin/sh | |
| 2 | + | # Install budctl and budctl-gui on Linux, or remove them. | |
| 3 | + | # | |
| 4 | + | # curl -fsSL https://github.com/krolyxon/budctl/releases/latest/download/install.sh | sh | |
| 5 | + | # ./install.sh in an unpacked release: installs the binaries next to it | |
| 6 | + | # ./install.sh --source in a checkout: builds with cargo, then installs | |
| 7 | + | # ./install.sh --uninstall | |
| 8 | + | # | |
| 9 | + | # Installs into ~/.local by default (bin/ and share/applications/), so no root is needed. | |
| 10 | + | ||
| 11 | + | set -eu | |
| 12 | + | ||
| 13 | + | REPO=krolyxon/budctl | |
| 14 | + | PREFIX=$HOME/.local | |
| 15 | + | VERSION=latest | |
| 16 | + | ACTION=install | |
| 17 | + | FROM=auto | |
| 18 | + | ||
| 19 | + | usage() { | |
| 20 | + | cat <<'EOF' | |
| 21 | + | Install budctl (command line) and budctl-gui (desktop app), or remove them. | |
| 22 | + | ||
| 23 | + | Usage: | |
| 24 | + | install.sh [--prefix DIR] [--version X.Y.Z] [--source] | |
| 25 | + | install.sh --uninstall [--prefix DIR] | |
| 26 | + | ||
| 27 | + | Options: | |
| 28 | + | --prefix DIR Install into DIR/bin and DIR/share/applications (default: ~/.local) | |
| 29 | + | --version X.Y.Z Download this release instead of the latest | |
| 30 | + | --source Build from this checkout with cargo instead of downloading | |
| 31 | + | --uninstall Remove what install.sh installed. Your config (~/.config/budctl) | |
| 32 | + | and log (~/.local/state/budctl) are left alone. | |
| 33 | + | -h, --help Show this help | |
| 34 | + | EOF | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | fail() { | |
| 38 | + | printf 'install.sh: %s\n' "$1" >&2 | |
| 39 | + | exit 1 | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | say() { | |
| 43 | + | printf '%s\n' "$1" | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | while [ "$#" -gt 0 ]; do | |
| 47 | + | case "$1" in | |
| 48 | + | --prefix) | |
| 49 | + | [ "$#" -ge 2 ] && [ -n "$2" ] || fail "--prefix needs a directory" | |
| 50 | + | PREFIX=$2 | |
| 51 | + | shift 2 | |
| 52 | + | ;; | |
| 53 | + | --version) | |
| 54 | + | [ "$#" -ge 2 ] && [ -n "$2" ] || fail "--version needs a version, like 0.1.1" | |
| 55 | + | VERSION=${2#v} | |
| 56 | + | shift 2 | |
| 57 | + | ;; | |
| 58 | + | --source) | |
| 59 | + | FROM=source | |
| 60 | + | shift | |
| 61 | + | ;; | |
| 62 | + | --uninstall) | |
| 63 | + | ACTION=uninstall | |
| 64 | + | shift | |
| 65 | + | ;; | |
| 66 | + | -h | --help) | |
| 67 | + | usage | |
| 68 | + | exit 0 | |
| 69 | + | ;; | |
| 70 | + | *) | |
| 71 | + | fail "unknown option: $1 (see --help)" | |
| 72 | + | ;; | |
| 73 | + | esac | |
| 74 | + | done | |
| 75 | + | ||
| 76 | + | [ "$(uname -s)" = Linux ] || fail "budctl runs on Linux only" | |
| 77 | + | ||
| 78 | + | BIN_DIR=$PREFIX/bin | |
| 79 | + | APP_DIR=$PREFIX/share/applications | |
| 80 | + | ||
| 81 | + | # Use sudo only when the prefix cannot be written as this user (such as /usr/local). | |
| 82 | + | SUDO= | |
| 83 | + | writable_dir=$PREFIX | |
| 84 | + | while [ ! -d "$writable_dir" ]; do | |
| 85 | + | writable_dir=$(dirname "$writable_dir") | |
| 86 | + | done | |
| 87 | + | if [ ! -w "$writable_dir" ] && [ "$(id -u)" -ne 0 ]; then | |
| 88 | + | command -v sudo >/dev/null 2>&1 || fail "$PREFIX is not writable and sudo was not found" | |
| 89 | + | SUDO=sudo | |
| 90 | + | fi | |
| 91 | + | ||
| 92 | + | if [ "$ACTION" = uninstall ]; then | |
| 93 | + | for file in "$BIN_DIR/budctl" "$BIN_DIR/budctl-gui" "$APP_DIR/budctl.desktop"; do | |
| 94 | + | if [ -e "$file" ]; then | |
| 95 | + | $SUDO rm -f "$file" | |
| 96 | + | say "Removed $file" | |
| 97 | + | fi | |
| 98 | + | done | |
| 99 | + | say "Your config (~/.config/budctl) and log (~/.local/state/budctl) were left alone." | |
| 100 | + | exit 0 | |
| 101 | + | fi | |
| 102 | + | ||
| 103 | + | WORK=$(mktemp -d) | |
| 104 | + | trap 'rm -rf "$WORK"' EXIT INT TERM | |
| 105 | + | ||
| 106 | + | # Where the files to install come from: a release unpacked next to this script, this checkout | |
| 107 | + | # built with cargo, or a release downloaded from GitHub. | |
| 108 | + | SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) | |
| 109 | + | if [ "$FROM" = auto ]; then | |
| 110 | + | if [ -f "$SCRIPT_DIR/install.sh" ] && [ -x "$SCRIPT_DIR/budctl" ] && [ -x "$SCRIPT_DIR/budctl-gui" ]; then | |
| 111 | + | FROM=bundle | |
| 112 | + | else | |
| 113 | + | FROM=download | |
| 114 | + | fi | |
| 115 | + | fi | |
| 116 | + | ||
| 117 | + | case "$FROM" in | |
| 118 | + | bundle) | |
| 119 | + | FILES=$SCRIPT_DIR | |
| 120 | + | DESKTOP=$SCRIPT_DIR/budctl.desktop | |
| 121 | + | ;; | |
| 122 | + | source) | |
| 123 | + | [ -f "$SCRIPT_DIR/Cargo.toml" ] || fail "--source needs a budctl checkout next to install.sh" | |
| 124 | + | command -v cargo >/dev/null 2>&1 || fail "cargo was not found; install Rust from https://rustup.rs/" | |
| 125 | + | say "Building budctl in $SCRIPT_DIR" | |
| 126 | + | cargo build --release --locked --manifest-path "$SCRIPT_DIR/Cargo.toml" -p budctl -p budctl-gui | |
| 127 | + | FILES=$SCRIPT_DIR/target/release | |
| 128 | + | DESKTOP=$SCRIPT_DIR/contrib/budctl.desktop | |
| 129 | + | ;; | |
| 130 | + | download) | |
| 131 | + | case "$(uname -m)" in | |
| 132 | + | x86_64 | amd64) TARGET=x86_64-unknown-linux-gnu ;; | |
| 133 | + | aarch64 | arm64) TARGET=aarch64-unknown-linux-gnu ;; | |
| 134 | + | *) fail "no release build for $(uname -m); build from a checkout with ./install.sh --source" ;; | |
| 135 | + | esac | |
| 136 | + | ARCHIVE=budctl-$TARGET.tar.gz | |
| 137 | + | if [ "$VERSION" = latest ]; then | |
| 138 | + | URL=https://github.com/$REPO/releases/latest/download | |
| 139 | + | else | |
| 140 | + | URL=https://github.com/$REPO/releases/download/v$VERSION | |
| 141 | + | fi | |
| 142 | + | ||
| 143 | + | if command -v curl >/dev/null 2>&1; then | |
| 144 | + | fetch() { curl -fsSL --proto '=https' --tlsv1.2 -o "$2" "$1"; } | |
| 145 | + | elif command -v wget >/dev/null 2>&1; then | |
| 146 | + | fetch() { wget -q -O "$2" "$1"; } | |
| 147 | + | else | |
| 148 | + | fail "curl or wget is needed to download budctl" | |
| 149 | + | fi | |
| 150 | + | ||
| 151 | + | say "Downloading $URL/$ARCHIVE" | |
| 152 | + | fetch "$URL/$ARCHIVE" "$WORK/$ARCHIVE" || fail "download failed: $URL/$ARCHIVE" | |
| 153 | + | fetch "$URL/$ARCHIVE.sha256" "$WORK/$ARCHIVE.sha256" || fail "download failed: $URL/$ARCHIVE.sha256" | |
| 154 | + | (cd "$WORK" && sha256sum -c --quiet "$ARCHIVE.sha256") || fail "checksum mismatch for $ARCHIVE" | |
| 155 | + | tar -xzf "$WORK/$ARCHIVE" -C "$WORK" | |
| 156 | + | FILES=$WORK/budctl-$TARGET | |
| 157 | + | DESKTOP=$FILES/budctl.desktop | |
| 158 | + | ;; | |
| 159 | + | esac | |
| 160 | + | ||
| 161 | + | for name in budctl budctl-gui; do | |
| 162 | + | [ -x "$FILES/$name" ] || fail "$name was not found in $FILES" | |
| 163 | + | done | |
| 164 | + | [ -f "$DESKTOP" ] || fail "budctl.desktop was not found at $DESKTOP" | |
| 165 | + | ||
| 166 | + | # The launcher entry runs the binary by its full path, so it works even when the launcher's PATH | |
| 167 | + | # lacks the install directory. | |
| 168 | + | sed "s|^Exec=budctl-gui|Exec=$BIN_DIR/budctl-gui|" "$DESKTOP" >"$WORK/budctl.desktop" | |
| 169 | + | ||
| 170 | + | $SUDO install -d "$BIN_DIR" "$APP_DIR" | |
| 171 | + | $SUDO install -m 0755 "$FILES/budctl" "$FILES/budctl-gui" "$BIN_DIR/" | |
| 172 | + | $SUDO install -m 0644 "$WORK/budctl.desktop" "$APP_DIR/budctl.desktop" | |
| 173 | + | ||
| 174 | + | say "Installed $("$BIN_DIR/budctl" --version) to $BIN_DIR (budctl, budctl-gui)" | |
| 175 | + | say "Installed the launcher entry to $APP_DIR/budctl.desktop" | |
| 176 | + | case ":$PATH:" in | |
| 177 | + | *":$BIN_DIR:"*) ;; | |
| 178 | + | *) say "Note: $BIN_DIR is not on your PATH. Add it to run budctl from a terminal." ;; | |
| 179 | + | esac | |