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:
6 files changed
+263
No files matched your search
@@ -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
|
||||
Generated
+81
@@ -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"
|
||||
|
||||
@@ -38,6 +38,7 @@ members = [
|
||||
"qbz-reco",
|
||||
"qbz-external-reco",
|
||||
"qbz-log",
|
||||
"qbzd",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
Reference in new issue
Block a user