feat(qbzd): scaffold slint-free qbzd crate with clap verb tree and CI gate

IV6 build peak: 795 MB max RSS (summed across the cargo-capped build's
process tree via /proc VmRSS polling; /usr/bin/time -v is not installed
on this box). aws-lc-rs in graph: 0 (single rustls provider: ring
v0.17.14 throughout).

cc
This commit is contained in:
vicrodh committed 2026-07-14 12:53:09 -06:00
1 parent 90d9d1c66a
commit 02c2dba68d
6 files changed
+263

No files matched your search

+9
View File
@@ -94,3 +94,12 @@ jobs:
./scripts/cargo-test.sh
END=$(date +%s)
echo "cargo test wall time: $((END - START))s"
- name: qbzd slint-free dep-graph gate (01-architecture.md §1.2)
run: |
set -euo pipefail
hits=$(cargo tree --manifest-path crates/Cargo.toml -p qbzd -e normal \
| grep -E '\b(slint|qbz-ui|qbz-slint-common|qbz-dac-wizard) v' || true)
if [ -n "$hits" ]; then
echo "::error::qbzd graph resolves Slint crates:"; echo "$hits"; exit 1
fi
+81
View File
@@ -1090,6 +1090,55 @@ dependencies = [
"libloading 0.8.9",
]
[[package]]
name = "clap"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_complete"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "clipboard-win"
version = "5.4.1"
@@ -5944,6 +5993,38 @@ dependencies = [
"slint-build",
]
[[package]]
name = "qbzd"
version = "2.0.1"
dependencies = [
"async-trait",
"clap",
"clap_complete",
"dirs 6.0.0",
"libc",
"log",
"qbz-app",
"qbz-audio",
"qbz-core",
"qbz-credentials",
"qbz-log",
"qbz-models",
"qbz-player",
"qbz-qobuz",
"qconnect-app",
"qconnect-core",
"qconnect-protocol",
"qconnect-transport-ws",
"rand 0.10.1",
"reqwest",
"serde",
"serde_json",
"tempfile",
"tiny_http",
"tokio",
"toml",
]
[[package]]
name = "qconnect-app"
version = "2.0.1"
+1
View File
@@ -38,6 +38,7 @@ members = [
"qbz-reco",
"qbz-external-reco",
"qbz-log",
"qbzd",
]
[workspace.package]
+44
View File
@@ -0,0 +1,44 @@
[package]
name = "qbzd"
version.workspace = true
edition = "2021"
description = "QBZ headless Qobuz playback daemon (daemon + CLI + setup TUI in one binary)"
license = "MIT"
[[bin]]
name = "qbzd"
path = "src/main.rs"
[dependencies]
# workspace crates — the slint-free column ONLY (01-architecture.md §1.2)
qbz-models = { path = "../qbz-models" }
qbz-app = { path = "../qbz-app" }
qbz-core = { path = "../qbz-core" }
qbz-player = { path = "../qbz-player" }
qbz-audio = { path = "../qbz-audio" } # PROTECTED — read-only consumer
qbz-qobuz = { path = "../qbz-qobuz" }
qbz-credentials = { path = "../qbz-credentials" }
qbz-log = { path = "../qbz-log" }
qconnect-core = { path = "../qconnect-core" }
qconnect-protocol = { path = "../qconnect-protocol" }
qconnect-transport-ws = { path = "../qconnect-transport-ws" }
qconnect-app = { path = "../qconnect-app" }
# runtime / wire
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
reqwest = { workspace = true }
dirs = "6"
rand = { workspace = true }
log = { workspace = true }
async-trait = { workspace = true }
# new direct deps (versions already in lock where noted)
tiny_http = "0.12" # in lock via qbz-cast
toml = "0.9" # in lock transitively
libc = "0.2" # flock
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
# ratatui arrives in T13 — do NOT add it here
[dev-dependencies]
tempfile = "3"
+110
View File
@@ -0,0 +1,110 @@
use clap::{Parser, Subcommand};
pub const API_VERSION: u32 = 1; // 02-cli-and-api.md §1.6
#[derive(Parser)]
#[command(name = "qbzd", version, arg_required_else_help = true,
about = "QBZ headless Qobuz playback daemon")]
struct Cli {
/// Target daemon (default 127.0.0.1:8182; env QBZD_HOST)
#[arg(long, global = true)]
host: Option<String>,
/// API token (default: auto-read ~/.config/qbzd/api_token; env QBZD_TOKEN)
#[arg(long, global = true)]
token: Option<String>,
#[arg(short, long, global = true)]
quiet: bool,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
/// Run the daemon in the foreground (systemd ExecStart)
Run,
/// Log in to Qobuz (one-shot browser listener; --paste; --token)
Login {
#[arg(long)] callback_host: Option<String>,
#[arg(long)] paste: bool,
#[arg(long)] token: Option<String>,
},
Logout,
/// Interactive configurator (six screens)
Setup,
/// Composite daemon diagnostic
Status { #[arg(long)] json: bool },
Ping { #[arg(long)] json: bool },
/// One-line now-playing
Now { #[arg(long)] json: bool },
Play, Pause, Toggle, Stop, Next, Prev,
/// Absolute secs, +N/-N, or mm:ss
Seek { position: String },
/// Bare = read; 0-100, +N, -N
Volume { value: Option<String>, #[arg(long)] json: bool },
/// Bare = toggle
Mute { state: Option<String> },
Queue { #[command(subcommand)] cmd: QueueCmd },
Settings { #[command(subcommand)] cmd: SettingsCmd },
Qconnect { #[command(subcommand)] cmd: QconnectCmd },
Config { #[command(subcommand)] cmd: ConfigCmd },
Version { #[arg(long)] json: bool },
/// Shell completions (hidden; packaged by T14)
#[command(hide = true)]
Completions { shell: clap_complete::Shell },
}
#[derive(Subcommand)]
enum QueueCmd {
List { #[arg(long)] json: bool },
Add { track_id: u64, #[arg(long)] next: bool },
Remove{ index: usize },
Clear { #[arg(long)] keep_current: bool },
}
#[derive(Subcommand)]
enum SettingsCmd {
Export {
file: Option<String>,
#[arg(long, default_value = "daemon")] from: String, // daemon|desktop
#[arg(long)] include_auth: bool,
},
Import {
file: String,
#[arg(long)] include_auth: bool,
#[arg(long)] trust_dsd: bool,
#[arg(long)] remap: Vec<String>, // OLD=NEW, repeatable
#[arg(long)] dry_run: bool,
},
Show { #[arg(long)] json: bool },
Set { key: String, value: String },
}
#[derive(Subcommand)]
enum QconnectCmd { Enable, Disable, Name { name: String } }
#[derive(Subcommand)]
enum ConfigCmd { Path, Show { #[arg(long)] json: bool }, RegenToken }
#[tokio::main]
async fn main() {
let cli = Cli::parse();
let code = match cli.cmd {
Cmd::Version { json } => {
if json {
println!("{{\"version\":\"{}\",\"api_version\":{}}}",
env!("CARGO_PKG_VERSION"), API_VERSION);
} else {
println!("qbzd {} (api v{})", env!("CARGO_PKG_VERSION"), API_VERSION);
}
0
}
Cmd::Completions { shell } => {
use clap::CommandFactory;
clap_complete::generate(shell, &mut Cli::command(), "qbzd",
&mut std::io::stdout());
0
}
_ => { eprintln!("not implemented yet"); 1 } // burned down task by task
};
std::process::exit(code);
}
+18
View File
@@ -0,0 +1,18 @@
use std::process::Command;
#[test]
fn bare_qbzd_prints_help_and_exits_2() {
// 01-architecture.md §1.1: a typo'd verb must never leave a daemon running.
let out = Command::new(env!("CARGO_BIN_EXE_qbzd")).output().unwrap();
assert_eq!(out.status.code(), Some(2));
let text = String::from_utf8_lossy(&out.stdout) + String::from_utf8_lossy(&out.stderr);
assert!(text.contains("Usage"), "help text missing: {text}");
}
#[test]
fn version_answers_locally() {
// 02-cli-and-api.md §2.2: `qbzd version` needs no daemon, no network.
let out = Command::new(env!("CARGO_BIN_EXE_qbzd")).arg("version").output().unwrap();
assert_eq!(out.status.code(), Some(0));
assert!(String::from_utf8_lossy(&out.stdout).contains("api v1"));
}