Changes
15 changed files (+0/-1287)
-
cli/.gitignore (deleted)
-
@@ -1,23 +0,0 @@# Copyright 2025 Shota FUJI # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # What: Connection state file, containing tokens and IDs. # Why: CLI automatically generates and loads this file, and the content is unique to each # local network and changes by user operations. .roon.json # What: Valgrind's core dump. vgcore.*
-
-
cli/README.md (deleted)
-
@@ -1,42 +0,0 @@<!-- Copyright 2025 Shota FUJI Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. SPDX-License-Identifier: Apache-2.0 --> # CLI Simple command line application for accessing Roon API. This is not intended for real-world uses—the sole reason of this program is to test core's functionality. Because this program does not have any runtime dependencies, you can easily check memory errors and profile using tools such as Valgrind. ## Development ### Building ``` zig build ``` This command builds and outputs binary to `zig-out/bin/plac`. ### Enable freelog To output debug log for releasing memory, enable `-Dfreelog` build option: ``` zig build -Dfreelog ```
-
-
cli/build.zig (deleted)
-
@@ -1,66 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 //! This file defines how `zig build` command behaves. //! Run `zig build --help` for available subcommands and options. //! //! Learn more at //! https://ziglang.org/learn/build-system/ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const freelog = b.option(bool, "freelog", "Enable logging memory releases") orelse false; const clap = clap: { const dep = b.dependency("clap", .{}); break :clap dep.module("clap"); }; const core = core: { const dep = b.dependency("plac_core", .{ .target = target, .optimize = optimize, .freelog = freelog, .@"extension-id" = @as([]const u8, "jp.pocka.plac.cli"), .@"extension-name" = @as([]const u8, "Plac CLI"), }); break :core dep.module("core"); }; // Executable { const exe = b.addExecutable(.{ .name = "plac", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }), }); exe.root_module.addImport("core", core); exe.root_module.addImport("clap", clap); exe.linkLibC(); b.installArtifact(exe); } }
-
-
cli/build.zig.zon (deleted)
-
@@ -1,35 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 .{ .name = .plac_cli, .version = "0.0.0", .fingerprint = 0xbc872ed057582c78, .minimum_zig_version = "0.14.0", .dependencies = .{ .plac_core = .{ .path = "../core", }, .clap = .{ .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.10.0.tar.gz", .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", }, }, .paths = .{ "build.zig", "build.zig.zon", "src/", }, }
-
-
cli/src/State.zig (deleted)
-
@@ -1,59 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const filename: []const u8 = ".roon.json"; connection: ?Connection = null, const Connection = struct { server_id: []const u8, token: []const u8, }; pub fn clone(self: *const @This(), allocator: std.mem.Allocator) @This() { return .{ .connection = if (self.connection) |conn| conn: { const server_id = try allocator.dupe(u8, conn.server_id); errdefer allocator.free(server_id); const token = allocator.dupe(u8, conn.token); errdefer allocator.free(token); break :conn .{ .server_id = server_id, .token = token, }; } catch null, }; } pub fn write(self: *const @This()) !void { var file = try std.fs.cwd().createFile(filename, .{}); defer file.close(); try std.json.stringify(self, .{ .whitespace = .indent_tab }, file.writer()); } pub fn read(allocator: std.mem.Allocator) !std.json.Parsed(@This()) { const contents = try std.fs.cwd().readFileAlloc(allocator, filename, std.math.maxInt(usize)); defer allocator.free(contents); return try std.json.parseFromSlice(@This(), allocator, contents, .{ .allocate = .alloc_always, }); }
-
-
cli/src/commands/albums.zig (deleted)
-
@@ -1,194 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const clap = @import("clap"); const core = @import("core"); const connection = @import("../connection.zig"); const State = @import("../State.zig"); const ExitCode = @import("../exit.zig").ExitCode; const OutputFormat = enum { text, tsv, jsonl, }; const parser = .{ .format = clap.parsers.enumeration(OutputFormat), }; const params = clap.parseParamsComptime( \\-h, --help Prints this message to stdout and exits. \\--header Whether emits header row (only on --format=tsv) \\-f, --format <format> Output format. \\Available values are: \\* text ... Plain text format for human consumption. \\* tsv ... TSV (tab-separated values). \\* jsonl ... Newline delimited list of JSON documents. \\ ); pub fn run(allocator: std.mem.Allocator, iter: *std.process.ArgIterator) ExitCode { var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, ¶ms, parser, iter, .{ .diagnostic = &diag, .allocator = allocator, }) catch |err| { diag.report(std.io.getStdErr().writer(), err) catch {}; return ExitCode.incorrect_usage; }; defer res.deinit(); if (res.args.help > 0) { clap.help(std.io.getStdOut().writer(), clap.Help, ¶ms, .{}) catch { return ExitCode.stdout_write_failed; }; return ExitCode.ok; } const conn = connection.make(allocator) catch |err| { std.log.err("Unable to establish a connection: {s}", .{@errorName(err)}); return switch (err) { error.OutOfMemory => ExitCode.out_of_memory, else => ExitCode.not_ok, }; }; defer conn.release(); while (true) { const ev = conn.getEvent() orelse { std.log.err("Got empty event, terminating", .{}); return ExitCode.not_ok; }; defer ev.release(); switch (ev.internal.payload) { .connection_error => |err_ev| { std.log.err("Failed to read event: {s}", .{@tagName(err_ev.code)}); return ExitCode.not_ok; }, .connected => { break; }, else => { continue; }, } } const loop_thread = std.Thread.spawn(.{}, consumeEvent, .{conn}) catch |err| { std.log.err("Failed to spawn message read thread: {s}", .{@errorName(err)}); return ExitCode.not_ok; }; defer { conn.disconnect(); loop_thread.join(); } const browse_result = conn.requestBrowse(.albums, null, null, false) orelse { std.log.err("Failed to browse albums: OutOfMemory", .{}); return ExitCode.out_of_memory; }; defer browse_result.release(); if (browse_result.code != .ok) { std.log.err("Failed to browse albums: {s}", .{@tagName(browse_result.code)}); return if (browse_result.code == .out_of_memory) ExitCode.out_of_memory else ExitCode.not_ok; } if (browse_result.action != .list) { std.log.err("Expected `list` response, found `{s}`", .{@tagName(browse_result.action)}); return ExitCode.not_ok; } const list = browse_result.internal.payload.list; const stdout = std.io.getStdOut().writer(); switch (res.args.format orelse .text) { .text => { for (list.items_ptr[0..list.items_len]) |item| { std.fmt.format( stdout, "Title=\"{s}\" Artist=\"{s}\"\n", .{ item.title, item.subtitle orelse "" }, ) catch |err| { std.log.err("Failed to write to stdout: {s}", .{@errorName(err)}); return ExitCode.stdout_write_failed; }; } }, .tsv => { if (res.args.header > 0) { stdout.writeAll("Title\tArtist\n") catch |err| { std.log.err("Failed to write to stdout: {s}", .{@errorName(err)}); return ExitCode.stdout_write_failed; }; } for (list.items_ptr[0..list.items_len]) |item| { stdout.print( "{s}\t{s}\n", .{ item.title, item.subtitle orelse "" }, ) catch |err| { std.log.err("Failed to write to stdout: {s}", .{@errorName(err)}); return ExitCode.stdout_write_failed; }; } }, .jsonl => { for (list.items_ptr[0..list.items_len]) |item| { stdout.print("{}\n", .{ std.json.fmt(ItemPrinter{ .item = item }, .{ .whitespace = .minified }), }) catch |err| { std.log.err("Failed to write to stdout: {s}", .{@errorName(err)}); return ExitCode.stdout_write_failed; }; } }, } return ExitCode.ok; } const ItemPrinter = struct { item: *const core.browse.Item, pub fn jsonStringify(self: *const ItemPrinter, jws: anytype) !void { try jws.beginObject(); try jws.objectField("title"); try jws.write(self.item.title); try jws.objectField("artist"); try jws.write(self.item.subtitle); if (self.item.image_key) |image_key| { try jws.objectField("image_key"); try jws.write(image_key); } try jws.endObject(); } }; fn consumeEvent(conn: *core.connection.Connection) void { const ev = conn.getEvent() orelse { return; }; defer ev.release(); }
-
-
cli/src/commands/connect.zig (deleted)
-
@@ -1,120 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const clap = @import("clap"); const core = @import("core"); const State = @import("../State.zig"); const ExitCode = @import("../exit.zig").ExitCode; const parser = .{ .server_id = clap.parsers.string, }; const params = clap.parseParamsComptime( \\-h, --help Prints this message to stdout and exits. \\<server_id> \\ ); pub fn run(allocator: std.mem.Allocator, iter: *std.process.ArgIterator) ExitCode { var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, ¶ms, parser, iter, .{ .diagnostic = &diag, .allocator = allocator, }) catch |err| { diag.report(std.io.getStdErr().writer(), err) catch {}; return ExitCode.incorrect_usage; }; defer res.deinit(); if (res.args.help > 0) { clap.help(std.io.getStdOut().writer(), clap.Help, ¶ms, .{}) catch { return ExitCode.stdout_write_failed; }; return ExitCode.ok; } const server_id: []const u8 = res.positionals[0] orelse { std.log.err("server_id is required", .{}); clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}) catch { return ExitCode.stdout_write_failed; }; return ExitCode.incorrect_usage; }; const server_id_z = allocator.dupeZ(u8, server_id) catch { std.log.err("Unable to allocate memory for server ID (Out of memory)", .{}); return ExitCode.out_of_memory; }; defer allocator.free(server_id_z); const find_result = core.discovery.find(server_id_z.ptr) orelse { std.log.err("Unable to get server info: Out of memory", .{}); return ExitCode.out_of_memory; }; defer find_result.release(); if (find_result.code != .ok) { std.log.err("Failed to get server info: {s}", .{@tagName(find_result.code)}); return ExitCode.not_ok; } if (find_result.servers_len == 0) { std.log.err("Server not found for ID={s}", .{server_id}); return ExitCode.not_ok; } const server = find_result.servers_ptr[0]; const conn = core.connection.Connection.make(server, null) orelse { std.log.err("Unable to connect: Out of memory", .{}); return ExitCode.out_of_memory; }; defer conn.release(); while (true) { const ev = conn.getEvent() orelse { std.log.err("Got empty event, terminating", .{}); return ExitCode.not_ok; }; defer ev.release(); switch (ev.internal.payload) { .connection_error => |err_ev| { std.log.err("Failed to read event: {s}", .{@tagName(err_ev.code)}); return ExitCode.not_ok; }, .connected => |conn_ev| { const state = State{ .connection = .{ .server_id = server_id, .token = std.mem.span(conn_ev.token), }, }; state.write() catch |err| { std.log.err("Failed to write state file: {s}", .{@errorName(err)}); return ExitCode.not_ok; }; std.log.info("Connected to {s}", .{server.name}); return ExitCode.ok; }, else => {}, } } }
-
-
cli/src/commands/image.zig (deleted)
-
@@ -1,172 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const clap = @import("clap"); const core = @import("core"); const connection = @import("../connection.zig"); const State = @import("../State.zig"); const ExitCode = @import("../exit.zig").ExitCode; const parser = .{ .format = clap.parsers.enumeration(core.image.ContentType), .px = clap.parsers.int(usize, 10), .scaling = clap.parsers.enumeration(core.image.ScalingMethod), .image_key = clap.parsers.string, }; const params = clap.parseParamsComptime( \\-h, --help Prints this message to stdout and exits. \\-f, --format <format> Image format. \\Available values are: \\* jpeg \\* png \\-s, --scaling <scaling> Scaling method to use. \\Available values are: \\* fit \\* fill \\* stretch \\-w, --width <px> Required when --scaling is set. \\-h, --height <px> Required when --scaling is set. \\<image_key> \\ ); pub fn run(allocator: std.mem.Allocator, iter: *std.process.ArgIterator) ExitCode { var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, ¶ms, parser, iter, .{ .diagnostic = &diag, .allocator = allocator, }) catch |err| { diag.report(std.io.getStdErr().writer(), err) catch {}; return ExitCode.incorrect_usage; }; defer res.deinit(); if (res.args.help > 0) { clap.help(std.io.getStdOut().writer(), clap.Help, ¶ms, .{}) catch { return ExitCode.stdout_write_failed; }; return ExitCode.ok; } const image_key: []const u8 = res.positionals[0] orelse { std.log.err("image_key is required", .{}); clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}) catch { return ExitCode.stdout_write_failed; }; return ExitCode.incorrect_usage; }; const image_key_z = allocator.dupeZ(u8, image_key) catch { std.log.err("Unable to allocate memory for image key (Out of memory)", .{}); return ExitCode.out_of_memory; }; defer allocator.free(image_key_z); const opts = core.image.GetOptions.makeRetained() orelse { std.log.err("Unable to allocate options struct (Out of memory)", .{}); return ExitCode.out_of_memory; }; defer opts.release(); if (res.args.format) |format| { opts.setContentType(format); } if (res.args.scaling) |scaling| { const width = res.args.width orelse { std.log.err("--width is required when --scaling is set", .{}); return ExitCode.incorrect_usage; }; const height = res.args.height orelse { std.log.err("--height is required when --scaling is set", .{}); return ExitCode.incorrect_usage; }; opts.setSize(scaling, width, height); } const conn = connection.make(allocator) catch |err| { std.log.err("Unable to establish a connection: {s}", .{@errorName(err)}); return switch (err) { error.OutOfMemory => ExitCode.out_of_memory, else => ExitCode.not_ok, }; }; defer conn.release(); while (true) { const ev = conn.getEvent() orelse { std.log.err("Got empty event, terminating", .{}); return ExitCode.not_ok; }; defer ev.release(); switch (ev.internal.payload) { .connection_error => |err_ev| { std.log.err("Failed to read event: {s}", .{@tagName(err_ev.code)}); return ExitCode.not_ok; }, .connected => { break; }, else => { continue; }, } } const loop_thread = std.Thread.spawn(.{}, consumeEvent, .{conn}) catch |err| { std.log.err("Failed to spawn message read thread: {s}", .{@errorName(err)}); return ExitCode.not_ok; }; defer { conn.disconnect(); loop_thread.join(); } const result = conn.getImage(image_key_z.ptr, opts) orelse { std.log.err("Failed to get image: OutOfMemory", .{}); return ExitCode.out_of_memory; }; defer result.release(); if (result.code != .ok) { std.log.err("Failed to get image: {s}", .{@tagName(result.code)}); return if (result.code == .out_of_memory) ExitCode.out_of_memory else ExitCode.not_ok; } const data = result.image orelse { std.log.err("get_image returned ok result, but image field is null", .{}); return ExitCode.not_ok; }; std.io.getStdOut().writeAll(data.data_ptr[0..data.data_len]) catch |err| { std.log.err("Failed to write to stdouit: {s}", .{@errorName(err)}); return ExitCode.stdout_write_failed; }; return ExitCode.ok; } fn consumeEvent(conn: *core.connection.Connection) void { const ev = conn.getEvent() orelse { return; }; defer ev.release(); }
-
-
cli/src/commands/playback.zig (deleted)
-
@@ -1,106 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const clap = @import("clap"); const core = @import("core"); const connection = @import("../connection.zig"); const State = @import("../State.zig"); const ExitCode = @import("../exit.zig").ExitCode; const parser = .{ .string = clap.parsers.string, }; const params = clap.parseParamsComptime( \\-h, --help Prints this message to stdout and exits. \\-z, --zone <string> Name of zone to display playback state. \\ ); pub fn run(allocator: std.mem.Allocator, iter: *std.process.ArgIterator) ExitCode { var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, ¶ms, parser, iter, .{ .diagnostic = &diag, .allocator = allocator, }) catch |err| { diag.report(std.io.getStdErr().writer(), err) catch {}; return ExitCode.incorrect_usage; }; defer res.deinit(); if (res.args.help > 0) { clap.help(std.io.getStdOut().writer(), clap.Help, ¶ms, .{}) catch { return ExitCode.stdout_write_failed; }; return ExitCode.ok; } const zone_name: []const u8 = res.args.zone orelse { std.log.err("--zone is required", .{}); return ExitCode.incorrect_usage; }; const conn = connection.make(allocator) catch |err| { std.log.err("Unable to establish a connection: {s}", .{@errorName(err)}); return switch (err) { error.OutOfMemory => ExitCode.out_of_memory, else => ExitCode.not_ok, }; }; defer conn.release(); while (true) { const ev = conn.getEvent() orelse { std.log.err("Got empty event, terminating", .{}); return ExitCode.not_ok; }; defer ev.release(); switch (ev.internal.payload) { .connection_error => |err_ev| { std.log.err("Failed to read event: {s}", .{@tagName(err_ev.code)}); return ExitCode.not_ok; }, .connected => { conn.subscribeZones(); continue; }, .zone_list => |event| { const added_zones = event.added_zones_ptr[0..event.added_zones_len]; const changed_zones = event.changed_zones_ptr[0..event.changed_zones_len]; inline for (.{ added_zones, changed_zones }) |zones| { for (zones) |zone| { if (std.mem.eql(u8, std.mem.span(zone.name), zone_name)) { std.fmt.format( std.io.getStdOut().writer(), "{s}\n", .{@tagName(zone.playback)}, ) catch |err| { std.log.err("Failed to write to stdout: {s}", .{@errorName(err)}); return ExitCode.stdout_write_failed; }; return ExitCode.ok; } } } continue; }, } } }
-
-
cli/src/commands/server.zig (deleted)
-
@@ -1,67 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const clap = @import("clap"); const ExitCode = @import("../exit.zig").ExitCode; const list = @import("./server/list.zig"); const Commands = enum { list, ls, }; const parser = .{ .command = clap.parsers.enumeration(Commands), }; const params = clap.parseParamsComptime( \\-h, --help Prints this message to stdout and exits. \\<command> \\Available commands: \\* list ... List available Roon Server on network (alias: ls) \\ ); pub fn run(allocator: std.mem.Allocator, iter: *std.process.ArgIterator) ExitCode { var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, ¶ms, parser, iter, .{ .diagnostic = &diag, .allocator = allocator, .terminating_positional = 0, }) catch |err| { diag.report(std.io.getStdErr().writer(), err) catch {}; return ExitCode.incorrect_usage; }; defer res.deinit(); if (res.args.help > 0) { clap.help(std.io.getStdOut().writer(), clap.Help, ¶ms, .{}) catch { return ExitCode.stdout_write_failed; }; return ExitCode.ok; } const command = res.positionals[0] orelse { clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}) catch {}; return ExitCode.incorrect_usage; }; return switch (command) { .ls, .list => list.run(allocator, iter), }; }
-
-
cli/src/commands/server/list.zig (deleted)
-
@@ -1,141 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const clap = @import("clap"); const core = @import("core"); const ExitCode = @import("../../exit.zig").ExitCode; const OutputFormat = enum { text, tsv, jsonl, }; const parser = .{ .format = clap.parsers.enumeration(OutputFormat), }; const params = clap.parseParamsComptime( \\-h, --help Prints this message to stdout and exits. \\--header Whether emits header row (only on --format=tsv) \\-f, --format <format> Output format. \\Available values are: \\* text ... Plain text format for human consumption. \\* tsv ... TSV (tab-separated values). \\* jsonl ... Newline delimited list of JSON documents. \\ ); pub fn run(allocator: std.mem.Allocator, iter: *std.process.ArgIterator) ExitCode { var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, ¶ms, parser, iter, .{ .diagnostic = &diag, .allocator = allocator, }) catch |err| { diag.report(std.io.getStdErr().writer(), err) catch {}; return ExitCode.incorrect_usage; }; defer res.deinit(); if (res.args.help > 0) { clap.help(std.io.getStdOut().writer(), clap.Help, ¶ms, .{}) catch { return ExitCode.stdout_write_failed; }; return ExitCode.ok; } const result = core.discovery.scan() orelse { std.log.err("Unable to scan: out of memory", .{}); return ExitCode.out_of_memory; }; defer result.release(); switch (result.code) { .ok => {}, .out_of_memory => { std.log.err("Failed to scan: out of memory", .{}); return ExitCode.out_of_memory; }, else => |code| { std.log.err("Failed to scan: {s}", .{@tagName(code)}); return ExitCode.not_ok; }, } const servers = result.servers_ptr[0..result.servers_len]; if (servers.len == 0) { std.log.info("No servers found.", .{}); return ExitCode.ok; } const stdout = std.io.getStdOut().writer(); switch (res.args.format orelse .text) { .text => { for (servers) |server| { stdout.print("ID={s} IP={} VERSION=\"{s}\"\n", .{ server.id, server.internal.address, server.version, }) catch { return ExitCode.stdout_write_failed; }; } }, .tsv => { if (res.args.header > 0) { stdout.writeAll("ID\tName\tIP address\tVersion\n") catch { return ExitCode.stdout_write_failed; }; } for (servers) |server| { const name = allocator.dupe(u8, std.mem.span(server.name)) catch { return ExitCode.out_of_memory; }; std.mem.replaceScalar(u8, name, '\t', ' '); const version = allocator.dupe(u8, std.mem.span(server.version)) catch { return ExitCode.out_of_memory; }; std.mem.replaceScalar(u8, version, '\t', ' '); stdout.print("{s}\t{s}\t{}\t{s}\n", .{ server.id, name, // Only CLI needs this... handling POSIX addr is messy. server.internal.address, version, }) catch { return ExitCode.stdout_write_failed; }; } }, .jsonl => { for (servers) |server| { stdout.print("{}\n", .{ std.json.fmt(server, .{ .whitespace = .minified }), }) catch { return ExitCode.stdout_write_failed; }; } }, } return ExitCode.ok; }
-
-
cli/src/connection.zig (deleted)
-
@@ -1,80 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const core = @import("core"); const State = @import("./State.zig"); pub const MakeError = error{ NoSavedConnection, FailedToFindServer, ServerNotFound, }; pub fn make(allocator: std.mem.Allocator) !*core.connection.Connection { const server_id, const saved_token = state: { const state = State.read(allocator) catch |err| { std.log.err("Failed to read state file: {s}", .{@errorName(err)}); return err; }; defer state.deinit(); const conn = state.value.connection orelse { std.log.err("No previous connection: connect to server first", .{}); return MakeError.NoSavedConnection; }; const server_id = allocator.dupeZ(u8, conn.server_id) catch { std.log.err("Failed to restore server ID from state file: Out of memory", .{}); return std.mem.Allocator.Error.OutOfMemory; }; errdefer allocator.free(server_id); const saved_token = allocator.dupeZ(u8, conn.token) catch { std.log.err("Failed to restore token from state file: Out of memory", .{}); return std.mem.Allocator.Error.OutOfMemory; }; break :state .{ server_id, saved_token }; }; defer allocator.free(server_id); defer allocator.free(saved_token); const find_result = core.discovery.find(server_id.ptr) orelse { std.log.err("Unable to get server info: Out of memory", .{}); return std.mem.Allocator.Error.OutOfMemory; }; defer find_result.release(); if (find_result.code != .ok) { std.log.err("Failed to get server info: {s}", .{@tagName(find_result.code)}); return MakeError.FailedToFindServer; } if (find_result.servers_len == 0) { std.log.err("Server not found for ID={s}", .{server_id}); return MakeError.ServerNotFound; } const server = find_result.servers_ptr[0]; return core.connection.Connection.make(server, saved_token.ptr) orelse { std.log.err("Unable to connect: Out of memory", .{}); return std.mem.Allocator.Error.OutOfMemory; }; }
-
-
cli/src/exit.zig (deleted)
-
@@ -1,33 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); pub const ExitCode = enum(u8) { ok = 0, /// Generic error code. Something prevented process from running successfully. not_ok = 1, /// Unknown flags, invalid option types, etc. incorrect_usage = 2, /// Out of memory, or a NULL pointer passed to a function due to bug. out_of_memory = 3, /// Unable to write command output to stdout. stdout_write_failed = 4, };
-
-
cli/src/main.zig (deleted)
-
@@ -1,130 +0,0 @@// Copyright 2025 Shota FUJI // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const std = @import("std"); const clap = @import("clap"); const core = @import("core"); const ExitCode = @import("./exit.zig").ExitCode; const albums = @import("./commands/albums.zig"); const connect = @import("./commands/connect.zig"); const image = @import("./commands/image.zig"); const playback = @import("./commands/playback.zig"); const server = @import("./commands/server.zig"); const version = "0.0.0"; pub const std_options = std.Options{ .log_level = .debug, .logFn = log, }; var log_level: std.log.Level = .debug; pub fn log( comptime level: std.log.Level, comptime scope: @Type(.enum_literal), comptime format: []const u8, args: anytype, ) void { if (@intFromEnum(level) <= @intFromEnum(log_level)) { std.log.defaultLog(level, scope, format, args); } } const Commands = enum { connect, albums, image, playback, server, version, }; const global_parser = .{ .command = clap.parsers.enumeration(Commands), .id = clap.parsers.string, .path = clap.parsers.string, .level = clap.parsers.enumeration(std.log.Level), }; const global_params = clap.parseParamsComptime( \\-h, --help Prints this message to stdout and exits. \\-l, --log-level <level> Log level to output. \\-v, --verbose Enables debug logging. \\<command> \\Available commands: \\* version ... Prints version to stdout and exits. \\* albums ... List albums in library. \\* image ... Download image. \\* playback ... Display playback state of a zone. \\* server ... Lists or gets information of Roon Server on network \\* connect ... Connects to Roon server and save state to a file. \\ ); pub fn main() !u8 { var debug_allocator = std.heap.DebugAllocator(.{ .safety = true }){ .backing_allocator = std.heap.c_allocator, }; const allocator = debug_allocator.allocator(); defer { _ = debug_allocator.deinit(); } var iter = try std.process.ArgIterator.initWithAllocator(allocator); defer iter.deinit(); // Skip process name. _ = iter.next(); var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, &global_params, global_parser, &iter, .{ .diagnostic = &diag, .allocator = allocator, .terminating_positional = 0, }) catch |err| { diag.report(std.io.getStdErr().writer(), err) catch {}; return @intFromEnum(ExitCode.incorrect_usage); }; defer res.deinit(); log_level = res.args.@"log-level" orelse .info; if (res.args.help > 0) { clap.help(std.io.getStdOut().writer(), clap.Help, &global_params, .{}) catch {}; return @intFromEnum(ExitCode.ok); } const command = res.positionals[0] orelse { clap.help(std.io.getStdErr().writer(), clap.Help, &global_params, .{}) catch {}; return @intFromEnum(ExitCode.incorrect_usage); }; switch (command) { .albums => return @intFromEnum(albums.run(allocator, &iter)), .image => return @intFromEnum(image.run(allocator, &iter)), .playback => return @intFromEnum(playback.run(allocator, &iter)), .server => return @intFromEnum(server.run(allocator, &iter)), .version => { try std.fmt.format(std.io.getStdOut().writer(), "{s}\n", .{version}); return @intFromEnum(ExitCode.ok); }, .connect => return @intFromEnum(connect.run(allocator, &iter)), } }
-
-
-
@@ -121,25 +121,6 @@ allocator.destroy(self);} } /// For CLI pub fn jsonStringify(self: *const Server, jws: anytype) !void { try jws.beginObject(); try jws.objectField("unique_id"); try jws.write(std.mem.span(self.id)); try jws.objectField("name"); try jws.write(std.mem.span(self.name)); try jws.objectField("version"); try jws.write(std.mem.span(self.version)); try jws.objectField("address"); try jws.print("\"{}\"", .{self.internal.address}); try jws.endObject(); } pub fn export_capi() void { @export(&retain, .{ .name = std.fmt.comptimePrint("{s}_retain", .{cname}) }); @export(&release, .{ .name = std.fmt.comptimePrint("{s}_release", .{cname}) });
-