Changes
12 changed files (+438/-351)
-
cli/src/app.zig (deleted)
-
@@ -1,143 +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 cb = @import("./cb.zig"); const State = @import("./app/State.zig"); pub const Inputs = struct { core_id: ?[]const u8 = null, file_path: ?[]const u8, ephemeral: bool = false, pub const ParseError = error{ MissingCoreId, }; pub fn parse(self: Inputs) ParseError!ParsedInputs { const core_id = self.core_id orelse return error.MissingCoreId; return .{ .core_id = core_id, .file_path = self.file_path orelse State.default_path, .ephemeral = self.ephemeral, }; } }; const ParsedInputs = struct { core_id: []const u8, file_path: []const u8, ephemeral: bool, }; pub const App = struct { allocator: std.mem.Allocator, state: *State, app: *core.App.CApi, pub const InitError = error{ ServerFindError, ServerNotFound, UnableToConnect, }; pub fn init(allocator: std.mem.Allocator, inputs: ParsedInputs) !@This() { var state = try allocator.create(State); state.* = State.init(allocator, .{}) catch |err| { std.log.err("Failed to prepare application state: {s}", .{@errorName(err)}); return err; }; errdefer state.deinit(); var app = core.App.CApi.new() orelse { std.log.err("Unable to create an app: out of memory", .{}); return std.mem.Allocator.Error.OutOfMemory; }; errdefer app.destroy(); var token: ?[:0]const u8 = null; for (state.registrations.items) |r| { if (std.mem.eql(u8, r.core_id, inputs.core_id)) { token = allocator.dupeZ(u8, r.token) catch |err| { std.log.err("Failed to copy token: {s}", .{@errorName(err)}); return err; }; break; } } defer if (token) |tok| allocator.free(tok); const OnConnectionChange = cb.Callback(struct {}); var on_connection_change = OnConnectionChange.init(); const OnServerChange = cb.Callback(struct {}); var on_server_change = OnServerChange.init(); app.onServerChange(OnServerChange.function, on_server_change.userdata()); app.onConnectionChange(OnConnectionChange.function, on_connection_change.userdata()); const server_id = try allocator.dupeZ(u8, inputs.core_id); defer allocator.free(server_id); const token_ptr: ?[*:0]const u8, const token_len: usize = if (token) |t| .{ t.ptr, t.len } else .{ null, 0 }; app.connect(server_id.ptr, server_id.len, token_ptr, token_len); while (true) { on_connection_change.wait(); switch (app.connection) { core.App.CApi.ConnectionState.busy => continue, core.App.CApi.ConnectionState.idle => break, else => |err| { std.log.err("Unable to connect: {s}", .{@tagName(err)}); return InitError.UnableToConnect; }, } } on_server_change.wait(); if (app.server) |server| { const new_token = server.token[0..server.token_len]; state.putToken(inputs.core_id, new_token) catch |err| { std.log.err("Failed to save new token: {s}", .{@errorName(err)}); return err; }; state.save() catch |err| { std.log.err("Failed to save state to disk: {s}", .{@errorName(err)}); return err; }; } return .{ .allocator = allocator, .state = state, .app = app, }; } pub fn deinit(self: *@This()) void { self.state.deinit(); self.allocator.destroy(self.state); self.app.destroy(); } };
-
-
cli/src/app/State.zig (deleted)
-
@@ -1,119 +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 ExtensionRegistration = struct { core_id: []const u8, token: []const u8, }; const FileContent = struct { registrations: []ExtensionRegistration, }; registrations: std.ArrayList(ExtensionRegistration), allocator: std.mem.Allocator, file_path: ?[]const u8, /// Unless user provided custom file path, connection state will be saved to /// and loaded from this file. pub const default_path = ".roon.json"; /// Creates a new empty state. Save operation on the returned state is no-op. pub fn initEphemeral(allocator: std.mem.Allocator) @This() { return .{ .registrations = std.ArrayList(ExtensionRegistration).init(allocator), .allocator = allocator, .file_path = null, }; } pub const InitOptions = struct { file_path: []const u8 = default_path, }; pub fn init(allocator: std.mem.Allocator, opts: InitOptions) !@This() { const file = std.fs.cwd().openFile(opts.file_path, .{}) catch |err| switch (err) { error.FileNotFound => { return .{ .registrations = std.ArrayList(ExtensionRegistration).init(allocator), .allocator = allocator, .file_path = opts.file_path, }; }, else => return err, }; defer file.close(); const contents = try file.readToEndAlloc(allocator, std.math.maxInt(usize)); defer allocator.free(contents); const loaded = try std.json.parseFromSlice(FileContent, allocator, contents, .{}); defer loaded.deinit(); var registrations = try allocator.alloc(ExtensionRegistration, loaded.value.registrations.len); for (loaded.value.registrations, 0..) |r, i| { registrations[i] = .{ .core_id = try allocator.dupe(u8, r.core_id), .token = try allocator.dupe(u8, r.token), }; } return @This(){ .registrations = std.ArrayList(ExtensionRegistration).fromOwnedSlice(allocator, registrations), .allocator = allocator, .file_path = opts.file_path, }; } /// Update or add token. Updated entry copies input buffer: caller can release input buffers /// then access struct fields. pub fn putToken(self: *@This(), core_id: []const u8, token: []const u8) std.mem.Allocator.Error!void { for (self.registrations.items) |*r| { if (std.mem.eql(u8, r.core_id, core_id)) { self.allocator.free(r.token); r.token = try self.allocator.dupe(u8, token); return; } } try self.registrations.append(.{ .core_id = try self.allocator.dupe(u8, core_id), .token = try self.allocator.dupe(u8, token), }); } pub fn save(self: *const @This()) !void { const file_path = self.file_path orelse return; const file = try std.fs.cwd().createFile(file_path, .{}); defer file.close(); try std.json.stringify( FileContent{ .registrations = self.registrations.items }, .{ .whitespace = .indent_tab }, file.writer(), ); } pub fn deinit(self: *@This()) void { for (self.registrations.items) |r| { self.allocator.free(r.core_id); self.allocator.free(r.token); } self.registrations.deinit(); }
-
-
-
@@ -0,0 +1,108 @@// 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 cb = @import("../cb.zig"); const path = @import("../path.zig"); const ExitCode = @import("../exit.zig").ExitCode; const parsers = .{ .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, parsers, 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; }; var app = core.App.CApi.new() orelse { std.log.err("Unable to create an app: out of memory", .{}); return ExitCode.out_of_memory; }; defer app.destroy(); const state_file_path = path.getStateFilePath(allocator) catch |err| { std.log.err("Unable to resolve state file path: {s}", .{@errorName(err)}); return ExitCode.not_ok; }; defer allocator.free(state_file_path); app.setStatePath(state_file_path.ptr, state_file_path.len); const OnConnectionChange = cb.Callback(struct {}); var on_connection_change = OnConnectionChange.init(); app.onConnectionChange(OnConnectionChange.function, on_connection_change.userdata()); const server_id_z = allocator.dupeZ(u8, server_id) catch { std.log.err("Unable to allocate CString for server ID: out of memory", .{}); return ExitCode.out_of_memory; }; defer allocator.free(server_id_z); app.connect(server_id_z.ptr, server_id_z.len, null, 0); while (true) { on_connection_change.wait(); switch (app.connection) { core.App.CApi.ConnectionState.busy => continue, core.App.CApi.ConnectionState.idle => break, else => { std.log.err("Unable to connect: {s}", .{@tagName(app.connection)}); return ExitCode.not_ok; }, } } if (app.server) |server| { std.log.info("Connected to {s}", .{server.name}); return ExitCode.ok; } else { std.log.err("Unable to connect to {s} (server = null)", .{server_id}); return ExitCode.not_ok; } }
-
-
-
@@ -19,7 +19,7 @@ const clap = @import("clap");const core = @import("core"); const cb = @import("../cb.zig"); const App = @import("../app.zig").App; const path = @import("../path.zig"); const ExitCode = @import("../exit.zig").ExitCode; const parsers = .{
-
@@ -32,7 +32,7 @@ \\-z, --zone <string> Name of zone to display playback state.\\ ); pub fn run(allocator: std.mem.Allocator, iter: *std.process.ArgIterator, plac: *const App) ExitCode { pub fn run(allocator: std.mem.Allocator, iter: *std.process.ArgIterator) ExitCode { var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, ¶ms, parsers, iter, .{ .diagnostic = &diag,
-
@@ -55,7 +55,57 @@ std.log.err("--zone is required.", .{});return ExitCode.incorrect_usage; }; const server = plac.app.server orelse unreachable; var app = core.App.CApi.new() orelse { std.log.err("Unable to create an app: out of memory", .{}); return ExitCode.out_of_memory; }; defer app.destroy(); const state_file_path = path.getStateFilePath(allocator) catch |err| { std.log.err("Unable to resolve state file path: {s}", .{@errorName(err)}); return ExitCode.not_ok; }; defer allocator.free(state_file_path); app.setStatePath(state_file_path.ptr, state_file_path.len); const OnRestoreComplete = cb.Callback(struct {}); var on_restore_complete = OnRestoreComplete.init(); app.onRestoreComplete(OnRestoreComplete.function, on_restore_complete.userdata()); const OnConnectionChange = cb.Callback(struct {}); var on_connection_change = OnConnectionChange.init(); app.onConnectionChange(OnConnectionChange.function, on_connection_change.userdata()); app.restoreState(); on_restore_complete.wait(); if (app.connection != .busy) { std.log.err("Not connected, run `connect` command first", .{}); return ExitCode.not_ok; } const server = server: while (true) { on_connection_change.wait(); switch (app.connection) { core.App.CApi.ConnectionState.busy => continue, core.App.CApi.ConnectionState.idle => { if (app.server) |s| { break :server s; } else { std.log.err("Connection lost", .{}); return ExitCode.not_ok; } }, else => { std.log.err("Unable to connect: {s}", .{@tagName(app.connection)}); return ExitCode.not_ok; }, } }; const OnZoneListLoadingChange = cb.Callback(struct {}); var on_zone_list_loading_change = OnZoneListLoadingChange.init();
-
-
cli/src/commands/register.zig (deleted)
-
@@ -1,48 +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 App = @import("../app.zig").App; const ExitCode = @import("../exit.zig").ExitCode; const params = clap.parseParamsComptime( \\-h, --help Prints this message to stdout and exits. \\ ); pub fn run(allocator: std.mem.Allocator, iter: *std.process.ArgIterator, _: *const App) ExitCode { var diag = clap.Diagnostic{}; var res = clap.parseEx(clap.Help, ¶ms, .{}, 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; } return ExitCode.ok; }
-
-
-
@@ -19,10 +19,9 @@ const clap = @import("clap");const build_config = @import("build_config"); const core = @import("core"); const app = @import("./app.zig"); const ExitCode = @import("./exit.zig").ExitCode; const connect = @import("./commands/connect.zig"); const server = @import("./commands/server.zig"); const register = @import("./commands/register.zig"); const playback = @import("./commands/playback.zig"); const version = "0.0.0";
-
@@ -47,9 +46,9 @@ }const Commands = enum { server, register, playback, version, connect, }; const global_parser = .{
-
@@ -63,14 +62,12 @@ 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. \\-c, --core-id <id> Roon Server's ID. \\-s, --state <path> Path to state file to load from and save to. \\<command> \\Available commands: \\* version ... Prints version to stdout and exits. \\* server ... Lists or gets information of Roon Server on network \\* register ... Register extension to Roon Server and exists. \\* playback ... Display playback state of a zone. \\* connect ... Connects to Roon Server and save the state to a disk. \\ );
-
@@ -126,31 +123,7 @@ .version => {try std.fmt.format(std.io.getStdOut().writer(), "{s}\n", .{version}); return @intFromEnum(ExitCode.ok); }, else => |main_command| { const inputs = app.Inputs.parse(.{ .core_id = res.args.@"core-id", .file_path = res.args.state, }) catch |err| { std.log.err("Invalid usage: {s}", .{@errorName(err)}); clap.help(std.io.getStdErr().writer(), clap.Help, &global_params, .{}) catch {}; return @intFromEnum(ExitCode.incorrect_usage); }; var plac = app.App.init(allocator, inputs) catch |err| { std.log.err("Unable to initialize app: {s}", .{@errorName(err)}); return @intFromEnum(switch (err) { error.OutOfMemory => ExitCode.out_of_memory, else => ExitCode.not_ok, }); }; defer plac.deinit(); return switch (main_command) { .register => @intFromEnum(register.run(allocator, &iter, &plac)), .playback => @intFromEnum(playback.run(allocator, &iter, &plac)), // Zig does not support narrowing, thus main_command is still type of `Commands`. else => @intFromEnum(ExitCode.incorrect_usage), }; }, .connect => return @intFromEnum(connect.run(allocator, &iter)), .playback => return @intFromEnum(playback.run(allocator, &iter)), } }
-
-
cli/src/path.zig (new)
-
@@ -0,0 +1,25 @@// 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"); /// Caller owns the returned memory. pub fn getStateFilePath(allocator: std.mem.Allocator) ![:0]const u8 { const cwd = try std.fs.cwd().realpathAlloc(allocator, "."); defer allocator.free(cwd); return try std.fs.path.joinZ(allocator, &.{ cwd, ".roon.json" }); }
-
-
-
@@ -28,6 +28,7 @@const callback = @import("./App/callback.zig"); pub const Server = @import("./App/Server.zig"); pub const ServerSelector = @import("./App/ServerSelector.zig"); const State = @import("./App/State.zig"); const App = @This();
-
@@ -44,6 +45,7 @@ });const OnServerChange = callback.Callback(struct {}); const OnConnectionChange = callback.Callback(struct {}); const OnRestoreComplete = callback.Callback(struct {}); const ServerConnInfo = struct { allocator: std.mem.Allocator,
-
@@ -94,18 +96,29 @@allocator: std.mem.Allocator, on_server_change: OnServerChange.Store, on_connection_change: OnConnectionChange.Store, on_restore_complete: OnRestoreComplete.Store, capi_lock: std.Thread.Mutex, state_file_path: ?[]const u8 = null, restore_thread: ?std.Thread = null, pub fn init(allocator: std.mem.Allocator) App { return .{ .allocator = allocator, .on_server_change = OnServerChange.Store.init(allocator), .on_connection_change = OnConnectionChange.Store.init(allocator), .on_restore_complete = OnRestoreComplete.Store.init(allocator), .capi_lock = std.Thread.Mutex{}, }; } pub fn deinit(self: *const App) void { if (self.restore_thread) |t| { t.join(); } if (self.state_file_path) |path| { self.allocator.free(path); } self.on_restore_complete.deinit(); self.on_connection_change.deinit(); self.on_server_change.deinit(); }
-
@@ -179,24 +192,123 @@ self.deinit(std.heap.c_allocator);std.heap.c_allocator.destroy(self); } pub fn setStatePath(self_ptr: ?*CApi, path_ptr: ?[*:0]const u8, path_len: usize) callconv(.C) void { const self = self_ptr orelse return; const path = if (path_ptr) |ptr| ptr[0..path_len] else { std.log.warn("A path given to plac_app_set_state_path is NULL", .{}); return; }; std.log.debug("Setting state path to {s}...", .{path}); self.internal.state_file_path = self.internal.allocator.dupe(u8, path) catch { std.log.err("Unable to clone state file path: out of memory", .{}); return; }; } pub fn restoreState(self_ptr: ?*CApi) callconv(.C) void { const self = self_ptr orelse return; const file_path = self.internal.state_file_path orelse { std.log.debug("No state file is configured, skipping restore phase", .{}); self.internal.on_restore_complete.runAll(.{}); return; }; if (!std.fs.path.isAbsolute(file_path)) { std.log.err("State file path is not absolute: {s}", .{file_path}); self.internal.on_restore_complete.runAll(.{}); return; } self.internal.restore_thread = std.Thread.spawn( .{}, restoreWorker, .{ self, file_path }, ) catch |err| { std.log.err("Unable to spawn thread for restore job: {s}", .{@errorName(err)}); self.internal.on_restore_complete.runAll(.{}); return; }; } fn restoreWorker(self: *CApi, file_path: []const u8) void { defer self.internal.on_restore_complete.runAll(.{}); const file = std.fs.openFileAbsolute(file_path, .{}) catch |err| { switch (err) { error.FileNotFound => return, else => { std.log.err("Unable to open state file: {s}", .{@errorName(err)}); return; }, } }; var reader = std.json.reader(self.internal.allocator, file.reader()); defer reader.deinit(); const parsed = std.json.parseFromTokenSource( State, self.internal.allocator, &reader, .{}, ) catch |err| { std.log.err("Failed to parse state file: {s}", .{@errorName(err)}); return; }; defer parsed.deinit(); const conn = parsed.value.connection orelse { std.log.debug("No connection stored on state file, skipping", .{}); return; }; std.log.debug("Loaded state, connecting to {s}", .{conn.server_id}); self.connectInner(conn.server_id, conn.token); } fn saveState(self: *CApi, state: State) void { const file_path = self.internal.state_file_path orelse { return; }; std.log.debug("Saving state to {s}...", .{file_path}); const file = std.fs.createFileAbsolute(file_path, .{}) catch |err| { std.log.err("Unable to create or open {s}: {s}", .{ file_path, @errorName(err) }); return; }; defer file.close(); std.json.stringify(state, .{ .whitespace = .indent_tab }, file.writer()) catch |err| { std.log.err("Unable to write state to {s}: {s}", .{ file_path, @errorName(err) }); return; }; } pub fn connect( self_ptr: ?*CApi, server_id: [*:0]const u8, server_id_ptr: [*:0]const u8, server_id_len: usize, saved_token_ptr: ?[*:0]const u8, saved_token_len: usize, ) callconv(.C) void { const self = self_ptr orelse return; const server_id = server_id_ptr[0..server_id_len]; const saved_token = if (saved_token_ptr) |tok| tok[0..saved_token_len] else null; self.connectInner(server_id, saved_token); } fn connectInner(self: *CApi, server_id: []const u8, saved_token: ?[]const u8) void { if (self.connection == .busy) { return; } const id = server_id[0..server_id_len]; const saved_token = if (saved_token_ptr) |tok| tok[0..saved_token_len] else null; if (self.server) |server| { if (std.mem.eql(u8, server.id[0..server.id_len], id)) { if (std.mem.eql(u8, server.id[0..server.id_len], server_id)) { return; } }
-
@@ -209,7 +321,32 @@ self.connection = .busy;self.internal.on_connection_change.runAll(.{}); } const thread = std.Thread.spawn(.{}, connectWorker, .{ self, id, saved_token }) catch { const server_id_t = self.internal.allocator.dupe(u8, server_id) catch { self.internal.capi_lock.lock(); defer self.internal.capi_lock.unlock(); self.connection = .err_out_of_memory; self.internal.on_connection_change.runAll(.{}); return; }; const saved_token_t = if (saved_token) |t| self.internal.allocator.dupe(u8, t) catch { self.internal.allocator.free(server_id_t); self.internal.capi_lock.lock(); defer self.internal.capi_lock.unlock(); self.connection = .err_out_of_memory; self.internal.on_connection_change.runAll(.{}); return; } else null; const thread = std.Thread.spawn(.{}, connectWorker, .{ self, server_id_t, saved_token_t }) catch { self.internal.allocator.free(server_id_t); if (saved_token_t) |t| { self.internal.allocator.free(t); } self.internal.capi_lock.lock(); defer self.internal.capi_lock.unlock();
-
@@ -241,9 +378,27 @@ pub fn onConnectionChangeDisarm(capi_ptr: ?*CApi, cb: OnConnectionChange.Fn) callconv(.C) void {const capi = capi_ptr orelse return; capi.internal.on_connection_change.remove(cb); } pub fn onRestoreComplete(capi_ptr: ?*CApi, cb: OnRestoreComplete.Fn, userdata: callback.UserData) callconv(.C) void { const capi = capi_ptr orelse return; // TODO: Notify error to caller capi.internal.on_restore_complete.add(OnRestoreComplete.init(cb, userdata)) catch {}; } pub fn onRestoreCompleteDisarm(capi_ptr: ?*CApi, cb: OnRestoreComplete.Fn) callconv(.C) void { const capi = capi_ptr orelse return; capi.internal.on_restore_complete.remove(cb); } }; fn connectWorker(self: *CApi, server_id: []const u8, saved_token: ?[]const u8) void { defer { self.internal.allocator.free(server_id); if (saved_token) |t| { self.internal.allocator.free(t); } } if (self.server) |server| { server.internal.conn.deinit(); self.internal.allocator.destroy(server.internal.conn);
-
@@ -355,6 +510,13 @@ self.internal.on_connection_change.runAll(.{});return; }; defer self.internal.allocator.free(token); self.saveState(.{ .connection = .{ .server_id = info.id, .token = token, }, }); const server = self.internal.allocator.create(Server.CApi) catch { self.internal.capi_lock.lock();
-
-
core/src/App/State.zig (new)
-
@@ -0,0 +1,41 @@// 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 = 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, }; } else null, }; }
-
-
-
@@ -366,6 +366,20 @@void plac_app_destroy(plac_app*); /** * Sets a filepath for application state file. Path must be an absolute path. * If this function is not called or called but arguments are invalid (e.g. parent directory * does not exist,) plac_app does neither write to nor read from a file. * * As plac_app copies pointer destination, pointer `path` does not need to be stable. */ void plac_app_set_state_path(plac_app*, char *path, unsigned int path_len); /** * Reconnect and restore previous state if any. */ void plac_app_restore_state(plac_app*); /** * Connects to a server. */ void plac_app_connect(plac_app*,
-
@@ -385,6 +399,14 @@ */typedef void (*plac_cb_connection_change)(void*); void plac_app_on_connection_change(plac_app*, plac_cb_connection_change, void*); void plac_app_on_connection_change_disarm(plac_app*, plac_cb_connection_change); /** * Event fired when `plac_app_restore_state` finished its job, regardless of state * was restored or not. */ typedef void (*plac_cb_restore_complete)(void*); void plac_app_on_restore_complete(plac_app*, plac_cb_restore_complete, void*); void plac_app_on_restore_complete_disarm(plac_app*, plac_cb_restore_complete); /* Application */ // TODO: Remove me
-
-
-
@@ -167,6 +167,9 @@[CCode (cname = "plac_cb_connection_change", has_target = true)] public delegate void OnConnectionChange (); [CCode (cname = "plac_cb_restore_complete", has_target = true)] public delegate void OnRestoreComplete (); [CCode (cname = "plac_app_new")] public App ();
-
@@ -174,6 +177,12 @@ public ServerSelector server_selector;public ConnectionState connection; public Server? server; [CCode (cname = "plac_app_set_state_path")] public void set_state_path(string path); [CCode (cname = "plac_app_restore_state")] public void restore_state(); [CCode (cname = "plac_app_connect")] public void connect(string* server_id, uint server_id_len, string? saved_token, uint saved_token_len);
-
@@ -182,5 +191,8 @@ public void on_server_change (OnServerChange f);[CCode (cname = "plac_app_on_connection_change")] public void on_connection_change (OnConnectionChange f); [CCode (cname = "plac_app_on_restore_complete")] public void on_restore_complete (OnRestoreComplete f); } }
-
-
-
@@ -59,11 +59,15 @@ );@export(&App.CApi.new, .{ .name = "plac_app_new" }); @export(&App.CApi.destroy, .{ .name = "plac_app_destroy" }); @export(&App.CApi.setStatePath, .{ .name = "plac_app_set_state_path" }); @export(&App.CApi.restoreState, .{ .name = "plac_app_restore_state" }); @export(&App.CApi.connect, .{ .name = "plac_app_connect" }); @export(&App.CApi.onServerChange, .{ .name = "plac_app_on_server_change" }); @export(&App.CApi.onServerChangeDisarm, .{ .name = "plac_app_on_server_change_disarm" }); @export(&App.CApi.onConnectionChange, .{ .name = "plac_app_on_connection_change" }); @export(&App.CApi.onConnectionChangeDisarm, .{ .name = "plac_app_on_connection_change_disarm" }); @export(&App.CApi.onRestoreComplete, .{ .name = "plac_app_on_restore_complete" }); @export(&App.CApi.onRestoreCompleteDisarm, .{ .name = "plac_app_on_restore_complete_disarm" }); // -- Old API --
-