From 4ba24fe91eb6142384e3b98f4eb5fa5a42dff261 Mon Sep 17 00:00:00 2001 From: Mitchell Date: Sun, 5 Apr 2026 14:54:13 -0500 Subject: [PATCH] feat: user settings - restore capture source - Add new user setting to restore the video capture source on app startup. closes #47 --- src/state/actor.zig | 12 +++-- src/state/user_settings.zig | 81 ++++++++++++++++++++++++++++++ src/state/user_settings_state.zig | 82 ++----------------------------- src/ui/draw_left_column.zig | 32 +++++++++--- src/util.zig | 2 + src/vulkan/vulkan.zig | 8 ++- 6 files changed, 124 insertions(+), 93 deletions(-) create mode 100644 src/state/user_settings.zig diff --git a/src/state/actor.zig b/src/state/actor.zig index dfe9a64..d736b1b 100644 --- a/src/state/actor.zig +++ b/src/state/actor.zig @@ -137,9 +137,11 @@ pub const Actor = struct { const thread_2 = try std.Thread.spawn(.{}, struct { fn run(_self: *Self) void { - _self.handle_action(.restore_capture_session) catch |err| { - log.err("[capture_startup] restore_capture_session error: {}\n", .{err}); - }; + if (_self.state.user_settings.settings.restore_capture_source_on_startup) { + _self.handle_action(.restore_capture_session) catch |err| { + log.err("[capture_startup] restore_capture_session error: {}\n", .{err}); + }; + } } }.run, .{self}); errdefer thread_2.join(); @@ -149,7 +151,9 @@ pub const Actor = struct { fn run(_self: *Self, t1: std.Thread, t2: std.Thread) void { t1.join(); t2.join(); - if (_self.state.user_settings.settings.start_replay_buffer_on_startup) { + if (_self.state.user_settings.settings.restore_capture_source_on_startup and + _self.state.user_settings.settings.start_replay_buffer_on_startup) + { _self.start_record() catch |err| { log.err("[capture_startup] start_record error: {}", .{err}); }; diff --git a/src/state/user_settings.zig b/src/state/user_settings.zig new file mode 100644 index 0000000..98f9be8 --- /dev/null +++ b/src/state/user_settings.zig @@ -0,0 +1,81 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// NOTE: This MUST remain serializable. +pub const UserSettings = struct { + const AudioDeviceSettings = struct { + id: []const u8, + selected: bool = false, + gain: f32 = 1.0, + }; + + // NOTE: Default values here are default user settings. + gui_foreground_fps: u32 = 120, + gui_background_fps: u32 = 30, + capture_fps: u32 = 60, + /// In bits per second (bps). + capture_bit_rate: u64 = 10_000_000, + replay_seconds: u32 = 30, + start_replay_buffer_on_startup: bool = false, + restore_capture_source_on_startup: bool = true, + audio_devices: std.json.ArrayHashMap(AudioDeviceSettings) = .{}, + + pub fn deinit(self: *@This(), allocator: Allocator) void { + self.clear_audio_device_settings(allocator); + self.audio_devices.deinit(allocator); + } + + pub fn update_audio_device_settings( + self: *@This(), + allocator: Allocator, + id: []const u8, + selected: bool, + gain: f32, + ) !void { + const id_copy = try allocator.dupe(u8, id); + errdefer allocator.free(id_copy); + + const audio_device_settings = try self.audio_devices.map.getOrPut(allocator, id_copy); + if (audio_device_settings.found_existing) { + allocator.free(id_copy); + const audio_device = audio_device_settings.value_ptr; + audio_device.selected = selected; + audio_device.gain = gain; + } else { + audio_device_settings.value_ptr.* = .{ + .id = audio_device_settings.key_ptr.*, + .selected = selected, + .gain = gain, + }; + } + } + + fn clear_audio_device_settings(self: *@This(), allocator: Allocator) void { + var iter = self.audio_devices.map.iterator(); + while (iter.next()) |entry| { + allocator.free(entry.key_ptr.*); + } + self.audio_devices.map.clearRetainingCapacity(); + } + + /// Deep copy user settings. + pub fn clone(self: @This(), allocator: Allocator) !@This() { + var settings_copy = self; + settings_copy.audio_devices = .{}; + errdefer settings_copy.deinit(allocator); + + var iter = self.audio_devices.map.iterator(); + while (iter.next()) |entry| { + const audio_device = entry.value_ptr.*; + const device_id = if (audio_device.id.len > 0) audio_device.id else entry.key_ptr.*; + try settings_copy.update_audio_device_settings( + allocator, + device_id, + audio_device.selected, + audio_device.gain, + ); + } + + return settings_copy; + } +}; diff --git a/src/state/user_settings_state.zig b/src/state/user_settings_state.zig index 00f0de8..3e45052 100644 --- a/src/state/user_settings_state.zig +++ b/src/state/user_settings_state.zig @@ -4,6 +4,7 @@ const Actor = @import("./actor.zig").Actor; const ActionPayload = @import("./action_payload.zig").ActionPayload; const util = @import("../util.zig"); const Actions = @import("./actor.zig").Actions; +const UserSettings = @import("./user_settings.zig").UserSettings; const log = std.log.scoped(.user_settings_state); @@ -13,6 +14,7 @@ pub const UserSettingsActions = union(enum) { set_replay_seconds: u32, set_gui_foreground_fps: u32, set_gui_background_fps: u32, + set_restore_capture_source_on_startup: bool, set_start_replay_buffer_on_startup: bool, set_audio_device_settings: *ActionPayload(struct { device_id: []u8, @@ -75,6 +77,9 @@ pub const UserSettingsState = struct { .set_start_replay_buffer_on_startup => |start_replay_buffer_on_startup| { try self.set_state(actor, "start_replay_buffer_on_startup", start_replay_buffer_on_startup); }, + .set_restore_capture_source_on_startup => |restore_capture_source_on_startup| { + try self.set_state(actor, "restore_capture_source_on_startup", restore_capture_source_on_startup); + }, .set_audio_device_settings => |_action| { defer _action.deinit(); const payload = _action.payload; @@ -186,80 +191,3 @@ pub const UserSettingsState = struct { try stringify.write(settings.*); } }; - -/// NOTE: This MUST remain serializable. -const UserSettings = struct { - const AudioDeviceSettings = struct { - id: []const u8, - selected: bool = false, - gain: f32 = 1.0, - }; - - gui_foreground_fps: u32 = 120, - gui_background_fps: u32 = 30, - capture_fps: u32 = 60, - /// In bits per second (bps). - capture_bit_rate: u64 = 10_000_000, - replay_seconds: u32 = 30, - start_replay_buffer_on_startup: bool = false, - audio_devices: std.json.ArrayHashMap(AudioDeviceSettings) = .{}, - - fn deinit(self: *@This(), allocator: Allocator) void { - self.clear_audio_device_settings(allocator); - self.audio_devices.deinit(allocator); - } - - fn update_audio_device_settings( - self: *@This(), - allocator: Allocator, - id: []const u8, - selected: bool, - gain: f32, - ) !void { - const id_copy = try allocator.dupe(u8, id); - errdefer allocator.free(id_copy); - - const audio_device_settings = try self.audio_devices.map.getOrPut(allocator, id_copy); - if (audio_device_settings.found_existing) { - allocator.free(id_copy); - const audio_device = audio_device_settings.value_ptr; - audio_device.selected = selected; - audio_device.gain = gain; - } else { - audio_device_settings.value_ptr.* = .{ - .id = audio_device_settings.key_ptr.*, - .selected = selected, - .gain = gain, - }; - } - } - - fn clear_audio_device_settings(self: *@This(), allocator: Allocator) void { - var iter = self.audio_devices.map.iterator(); - while (iter.next()) |entry| { - allocator.free(entry.key_ptr.*); - } - self.audio_devices.map.clearRetainingCapacity(); - } - - /// Deep copy user settings. - fn clone(self: @This(), allocator: Allocator) !@This() { - var settings_copy = self; - settings_copy.audio_devices = .{}; - errdefer settings_copy.deinit(allocator); - - var iter = self.audio_devices.map.iterator(); - while (iter.next()) |entry| { - const audio_device = entry.value_ptr.*; - const device_id = if (audio_device.id.len > 0) audio_device.id else entry.key_ptr.*; - try settings_copy.update_audio_device_settings( - allocator, - device_id, - audio_device.selected, - audio_device.gain, - ); - } - - return settings_copy; - } -}; diff --git a/src/ui/draw_left_column.zig b/src/ui/draw_left_column.zig index 3f8d44d..37727d5 100644 --- a/src/ui/draw_left_column.zig +++ b/src/ui/draw_left_column.zig @@ -346,10 +346,12 @@ pub fn draw_left_column(allocator: std.mem.Allocator, actor: *Actor) !void { // c.ImGui_SameLineEx(0, spacing); // imgui_util.help_marker("This button may not work. Configure shortcuts with your system settings."); - c.ImGui_SeparatorText("IMGUI Debug"); + if (util.DEBUG) { + c.ImGui_SeparatorText("IMGUI Debug"); - if (c.ImGui_ButtonEx("Show Demo", .{ .x = c.ImGui_GetContentRegionAvail().x, .y = 0 })) { - try actor.dispatch(.show_demo); + if (c.ImGui_ButtonEx("Show Demo", .{ .x = c.ImGui_GetContentRegionAvail().x, .y = 0 })) { + try actor.dispatch(.show_demo); + } } } } @@ -456,11 +458,27 @@ fn draw_capture_settings(allocator: std.mem.Allocator, actor: *Actor) !void { c.ImGui_PopTextWrapPos(); } - c.ImGui_Text("Start replay buffer on startup"); - var start_replay_buffer_on_startup = actor.state.user_settings.settings.start_replay_buffer_on_startup; - if (c.ImGui_Checkbox("##start_replay_buffer_on_startup", &start_replay_buffer_on_startup)) { + c.ImGui_Text("Restore capture source on startup"); + c.ImGui_SameLine(); + imgui_util.help_marker("Try to restore the last capture source when Spacecap starts."); + var restore_capture_source_on_startup = actor.state.user_settings.settings.restore_capture_source_on_startup; + if (c.ImGui_Checkbox("##restore_capture_source_on_startup", &restore_capture_source_on_startup)) { try actor.dispatch(.{ .user_settings = .{ - .set_start_replay_buffer_on_startup = start_replay_buffer_on_startup, + .set_restore_capture_source_on_startup = restore_capture_source_on_startup, } }); } + + { + c.ImGui_Text("Start replay buffer on startup"); + c.ImGui_SameLine(); + imgui_util.help_marker("Start the replay buffer when Spacecap starts. Requires 'Restore capture source on startup'."); + c.ImGui_BeginDisabled(!actor.state.user_settings.settings.restore_capture_source_on_startup); + defer c.ImGui_EndDisabled(); + var start_replay_buffer_on_startup = actor.state.user_settings.settings.start_replay_buffer_on_startup; + if (c.ImGui_Checkbox("##start_replay_buffer_on_startup", &start_replay_buffer_on_startup)) { + try actor.dispatch(.{ .user_settings = .{ + .set_start_replay_buffer_on_startup = start_replay_buffer_on_startup, + } }); + } + } } diff --git a/src/util.zig b/src/util.zig index fc46744..188ac1f 100644 --- a/src/util.zig +++ b/src/util.zig @@ -1,5 +1,7 @@ const std = @import("std"); +pub const DEBUG = @import("builtin").mode == .Debug; + pub fn is_windows() bool { return @import("builtin").os.tag == .windows; } diff --git a/src/vulkan/vulkan.zig b/src/vulkan/vulkan.zig index 319f1dc..1f3cff5 100644 --- a/src/vulkan/vulkan.zig +++ b/src/vulkan/vulkan.zig @@ -20,8 +20,6 @@ pub const Device = vk.DeviceProxy; pub const CommandBuffer = vk.CommandBufferProxy; pub const API_VERSION = vk.API_VERSION_1_4; -const DEBUG = @import("builtin").mode == .Debug; - const INSTANCE_EXTENSIONS = [_][*:0]const u8{ vk.extensions.khr_get_physical_device_properties_2.name, }; @@ -149,7 +147,7 @@ pub const Vulkan = struct { // enable with vkEnumerateInstanceExtensionProperties. // See imgui example_sdl3_vulkan for reference. - if (DEBUG) { + if (util.DEBUG) { try extension_names.append(allocator, vk.extensions.ext_debug_utils.name); // TODO: check if this extension is enabled //try extension_names.append(vk.extensions.ext_device_address_binding_report.name); @@ -157,7 +155,7 @@ pub const Vulkan = struct { } const validation_layers = [_][*:0]const u8{"VK_LAYER_KHRONOS_validation"}; - const enabled_layers: []const [*:0]const u8 = if (DEBUG) &validation_layers else &.{}; + const enabled_layers: []const [*:0]const u8 = if (util.DEBUG) &validation_layers else &.{}; const instance_def = try vkbd.createInstance(&.{ .p_application_info = &app_info, @@ -177,7 +175,7 @@ pub const Vulkan = struct { var debug_messenger: ?vk.DebugUtilsMessengerEXT = null; - if (DEBUG) { + if (util.DEBUG) { debug_messenger = try instance.createDebugUtilsMessengerEXT(&.{ .message_severity = .{ .error_bit_ext = true,