Changes
4 changed files (+178/-1)
-
cli/src/State.zig (new)
-
@@ -0,0 +1,52 @@// 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"); 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, }; } fn open(flags: std.fs.File.CreateFlags) std.fs.File.OpenError!std.fs.File { return std.fs.cwd().createFile(".roon.json", flags); } pub fn write(self: *const @This()) !void { var file = try open(.{}); defer file.close(); try std.json.stringify(self, .{ .whitespace = .indent_tab }, file.writer()); }
-
-
-
@@ -0,0 +1,120 @@// 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) 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 => {}, } } }
-
-
-
@@ -19,6 +19,7 @@ const clap = @import("clap");const core = @import("core"); const ExitCode = @import("./exit.zig").ExitCode; const connect = @import("./commands/connect.zig"); const server = @import("./commands/server.zig"); const version = "0.0.0";
-
@@ -42,6 +43,7 @@ }} const Commands = enum { connect, server, version, };
-
@@ -61,6 +63,7 @@ \\<command>\\Available commands: \\* version ... Prints version to stdout and exits. \\* server ... Lists or gets information of Roon Server on network \\* connect ... Connects to Roon server and save state to a file. \\ );
-
@@ -110,5 +113,6 @@ .version => {try std.fmt.format(std.io.getStdOut().writer(), "{s}\n", .{version}); return @intFromEnum(ExitCode.ok); }, .connect => return @intFromEnum(connect.run(allocator, &iter)), } }
-
-
-
@@ -199,11 +199,12 @@ pub fn initFindResult(self: *ScanResult, found: *Server) std.mem.Allocator.Error!void {const servers = try allocator.alloc(*Server, 1); errdefer allocator.free(servers); servers[0] = found; servers[0] = found.retain(); self.internal.servers = servers; self.servers_ptr = servers.ptr; self.servers_len = 1; self.code = .ok; } pub fn retain(ptr: ?*ScanResult) callconv(.C) *ScanResult {
-