aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2024-08-06 05:57:21 +0200
committerdec05eba <dec05eba@protonmail.com>2024-08-06 05:57:21 +0200
commit9f1fddc47ce10fbc65cdeaa70461063b9921434e (patch)
treee9a8f19fa2ea5445e62ac2f4f8a488c591805347 /src
parentb778fd7cc654f28a2bfe0ff74537f120241b289c (diff)
Copy Config from gpu-screen-recorder-gtk, make it more modern and efficient with string_view and variant, use string_view in gsr info parsing
Diffstat (limited to 'src')
-rw-r--r--src/Config.cpp153
-rw-r--r--src/GsrInfo.cpp126
-rw-r--r--src/Utils.cpp176
-rw-r--r--src/gui/Button.cpp6
-rw-r--r--src/main.cpp2
5 files changed, 387 insertions, 76 deletions
diff --git a/src/Config.cpp b/src/Config.cpp
new file mode 100644
index 0000000..51f14f1
--- /dev/null
+++ b/src/Config.cpp
@@ -0,0 +1,153 @@
+#include "../include/Config.hpp"
+#include <variant>
+#include <limits.h>
+#include <inttypes.h>
+#include <libgen.h>
+
+namespace gsr {
+ #define FORMAT_I32 "%" PRIi32
+ #define FORMAT_I64 "%" PRIi64
+ #define FORMAT_U32 "%" PRIu32
+
+ using ConfigValue = std::variant<bool*, std::string*, int32_t*, ConfigHotkey*, std::vector<std::string>*>;
+
+ static std::map<std::string_view, ConfigValue> get_config_options(Config &config) {
+ return {
+ {"main.record_area_option", &config.main_config.record_area_option},
+ {"main.record_area_width", &config.main_config.record_area_width},
+ {"main.record_area_height", &config.main_config.record_area_height},
+ {"main.fps", &config.main_config.fps},
+ {"main.merge_audio_tracks", &config.main_config.merge_audio_tracks},
+ {"main.audio_input", &config.main_config.audio_input},
+ {"main.color_range", &config.main_config.color_range},
+ {"main.quality", &config.main_config.quality},
+ {"main.codec", &config.main_config.video_codec},
+ {"main.audio_codec", &config.main_config.audio_codec},
+ {"main.framerate_mode", &config.main_config.framerate_mode},
+ {"main.advanced_view", &config.main_config.advanced_view},
+ {"main.overclock", &config.main_config.overclock},
+ {"main.show_recording_started_notifications", &config.main_config.show_recording_started_notifications},
+ {"main.show_recording_stopped_notifications", &config.main_config.show_recording_stopped_notifications},
+ {"main.show_recording_saved_notifications", &config.main_config.show_recording_saved_notifications},
+ {"main.record_cursor", &config.main_config.record_cursor},
+ {"main.hide_window_when_recording", &config.main_config.hide_window_when_recording},
+ {"main.software_encoding_warning_shown", &config.main_config.software_encoding_warning_shown},
+ {"main.restore_portal_session", &config.main_config.restore_portal_session},
+
+ {"streaming.service", &config.streaming_config.streaming_service},
+ {"streaming.youtube.key", &config.streaming_config.youtube.stream_key},
+ {"streaming.twitch.key", &config.streaming_config.twitch.stream_key},
+ {"streaming.custom.url", &config.streaming_config.custom.url},
+ {"streaming.custom.container", &config.streaming_config.custom.container},
+ {"streaming.start_stop_recording_hotkey", &config.streaming_config.start_stop_recording_hotkey},
+
+ {"record.save_directory", &config.record_config.save_directory},
+ {"record.container", &config.record_config.container},
+ {"record.start_stop_recording_hotkey", &config.record_config.start_stop_recording_hotkey},
+ {"record.pause_unpause_recording_hotkey", &config.record_config.pause_unpause_recording_hotkey},
+
+ {"replay.save_directory", &config.replay_config.save_directory},
+ {"replay.container", &config.replay_config.container},
+ {"replay.time", &config.replay_config.replay_time},
+ {"replay.start_stop_recording_hotkey", &config.replay_config.start_stop_recording_hotkey},
+ {"replay.save_recording_hotkey", &config.replay_config.save_recording_hotkey}
+ };
+ }
+
+ Config read_config(bool &config_empty) {
+ Config config;
+
+ const std::string config_path = get_config_dir() + "/config";
+ std::string file_content;
+ if(!file_get_content(config_path.c_str(), file_content)) {
+ fprintf(stderr, "Warning: Failed to read config file: %s\n", config_path.c_str());
+ config_empty = true;
+ return config;
+ }
+
+ auto config_options = get_config_options(config);
+
+ string_split_char(file_content, '\n', [&](std::string_view line) {
+ const std::optional<KeyValue> key_value = parse_key_value(line);
+ if(!key_value) {
+ fprintf(stderr, "Warning: Invalid config option format: %.*s\n", (int)line.size(), line.data());
+ return true;
+ }
+
+ if(key_value->key.empty() || key_value->value.empty())
+ return true;
+
+ auto it = config_options.find(key_value->key);
+ if(it == config_options.end())
+ return true;
+
+ if(std::holds_alternative<bool*>(it->second)) {
+ *std::get<bool*>(it->second) = key_value->value == "true";
+ } else if(std::holds_alternative<std::string*>(it->second)) {
+ std::get<std::string*>(it->second)->assign(key_value->value.data(), key_value->value.size());
+ } else if(std::holds_alternative<int32_t*>(it->second)) {
+ std::string value_str(key_value->value);
+ int32_t *value = std::get<int32_t*>(it->second);
+ if(sscanf(value_str.c_str(), FORMAT_I32, value) != 1) {
+ fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data());
+ *value = 0;
+ }
+ } else if(std::holds_alternative<ConfigHotkey*>(it->second)) {
+ std::string value_str(key_value->value);
+ ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it->second);
+ if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->keysym, &config_hotkey->modifiers) != 2) {
+ fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data());
+ config_hotkey->keysym = 0;
+ config_hotkey->modifiers = 0;
+ }
+ } else if(std::holds_alternative<ConfigHotkey*>(it->second)) {
+ std::string array_value(key_value->value);
+ std::get<std::vector<std::string>*>(it->second)->push_back(std::move(array_value));
+ }
+
+ return true;
+ });
+
+ return config;
+ }
+
+ void save_config(Config &config) {
+ const std::string config_path = get_config_dir() + "/config";
+
+ char dir_tmp[PATH_MAX];
+ snprintf(dir_tmp, sizeof(dir_tmp), "%s", config_path.c_str());
+ char *dir = dirname(dir_tmp);
+
+ if(create_directory_recursive(dir) != 0) {
+ fprintf(stderr, "Warning: Failed to create config directory: %s\n", dir);
+ return;
+ }
+
+ FILE *file = fopen(config_path.c_str(), "wb");
+ if(!file) {
+ fprintf(stderr, "Warning: Failed to create config file: %s\n", config_path.c_str());
+ return;
+ }
+
+ const auto config_options = get_config_options(config);
+ for(auto it : config_options) {
+ if(std::holds_alternative<bool*>(it.second)) {
+ fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), *std::get<bool*>(it.second) ? "true" : "false");
+ } else if(std::holds_alternative<std::string*>(it.second)) {
+ fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), std::get<std::string*>(it.second)->c_str());
+ } else if(std::holds_alternative<int32_t*>(it.second)) {
+ fprintf(file, "%.*s " FORMAT_I32 "\n", (int)it.first.size(), it.first.data(), *std::get<int32_t*>(it.second));
+ } else if(std::holds_alternative<ConfigHotkey*>(it.second)) {
+ const ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it.second);
+ fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->keysym, config_hotkey->modifiers);
+ } else if(std::holds_alternative<ConfigHotkey*>(it.second)) {
+ std::vector<std::string> *array = std::get<std::vector<std::string>*>(it.second);
+ for(const std::string &value : *array) {
+ fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), value.c_str());
+ }
+ }
+ }
+
+ fclose(file);
+ }
+} \ No newline at end of file
diff --git a/src/GsrInfo.cpp b/src/GsrInfo.cpp
index f147ecb..dfd18af 100644
--- a/src/GsrInfo.cpp
+++ b/src/GsrInfo.cpp
@@ -1,57 +1,38 @@
#include "../include/GsrInfo.hpp"
+#include "../include/Utils.hpp"
+#include <optional>
#include <string.h>
-#include <functional>
namespace gsr {
- using StringSplitCallback = std::function<bool(std::string_view line)>;
-
- static void string_split_char(const std::string &str, char delimiter, StringSplitCallback callback_func) {
- size_t index = 0;
- while(index < str.size()) {
- size_t new_index = str.find(delimiter, index);
- if(new_index == std::string::npos)
- new_index = str.size();
-
- if(!callback_func({str.data() + index, new_index - index}))
- break;
-
- index = new_index + 1;
- }
- }
-
- static void parse_system_info_line(GsrInfo *gsr_info, const std::string &line) {
- const size_t space_index = line.find(' ');
- if(space_index == std::string::npos)
+ static void parse_system_info_line(GsrInfo *gsr_info, std::string_view line) {
+ const std::optional<KeyValue> key_value = parse_key_value(line);
+ if(!key_value)
return;
- const std::string_view attribute_name = {line.c_str(), space_index};
- const std::string_view attribute_value = {line.c_str() + space_index + 1, line.size() - (space_index + 1)};
- if(attribute_name == "display_server") {
- if(attribute_value == "x11")
+ if(key_value->key == "display_server") {
+ if(key_value->value == "x11")
gsr_info->system_info.display_server = DisplayServer::X11;
- else if(attribute_value == "wayland")
+ else if(key_value->value == "wayland")
gsr_info->system_info.display_server = DisplayServer::WAYLAND;
}
}
- static void parse_gpu_info_line(GsrInfo *gsr_info, const std::string &line) {
- const size_t space_index = line.find(' ');
- if(space_index == std::string::npos)
+ static void parse_gpu_info_line(GsrInfo *gsr_info, std::string_view line) {
+ const std::optional<KeyValue> key_value = parse_key_value(line);
+ if(!key_value)
return;
- const std::string_view attribute_name = {line.c_str(), space_index};
- const std::string_view attribute_value = {line.c_str() + space_index + 1, line.size() - (space_index + 1)};
- if(attribute_name == "vendor") {
- if(attribute_value == "amd")
+ if(key_value->key == "vendor") {
+ if(key_value->value == "amd")
gsr_info->gpu_info.vendor = GpuVendor::AMD;
- else if(attribute_value == "intel")
+ else if(key_value->value == "intel")
gsr_info->gpu_info.vendor = GpuVendor::INTEL;
- else if(attribute_value == "nvidia")
+ else if(key_value->value == "nvidia")
gsr_info->gpu_info.vendor = GpuVendor::NVIDIA;
}
}
- static void parse_video_codecs_line(GsrInfo *gsr_info, const std::string &line) {
+ static void parse_video_codecs_line(GsrInfo *gsr_info, std::string_view line) {
if(line == "h264")
gsr_info->supported_video_codecs.h264 = true;
else if(line == "h264_software")
@@ -66,19 +47,23 @@ namespace gsr {
gsr_info->supported_video_codecs.vp9 = true;
}
- static GsrMonitor capture_option_line_to_monitor(const std::string &line) {
- size_t space_index = line.find(' ');
- if(space_index == std::string::npos)
- return { line, {0, 0} };
+ static std::optional<GsrMonitor> capture_option_line_to_monitor(std::string_view line) {
+ std::optional<GsrMonitor> monitor;
+ const std::optional<KeyValue> key_value = parse_key_value(line);
+ if(!key_value)
+ return monitor;
+
+ char value_buffer[256];
+ snprintf(value_buffer, sizeof(value_buffer), "%.*s", (int)key_value->value.size(), key_value->value.data());
- mgl::vec2i size = {0, 0};
- if(sscanf(line.c_str() + space_index + 1, "%dx%d", &size.x, &size.y) != 2)
- size = {0, 0};
+ monitor = GsrMonitor{std::string(key_value->key), mgl::vec2i{0, 0}};
+ if(sscanf(value_buffer, "%dx%d", &monitor->size.x, &monitor->size.y) != 2)
+ monitor->size = {0, 0};
- return { line.substr(0, space_index), size };
+ return monitor;
}
- static void parse_capture_options_line(GsrInfo *gsr_info, const std::string &line) {
+ static void parse_capture_options_line(GsrInfo *gsr_info, std::string_view line) {
if(line == "window")
gsr_info->supported_capture_options.window = true;
else if(line == "focused")
@@ -87,8 +72,11 @@ namespace gsr {
gsr_info->supported_capture_options.screen = true;
else if(line == "portal")
gsr_info->supported_capture_options.portal = true;
- else
- gsr_info->supported_capture_options.monitors.push_back(capture_option_line_to_monitor(line));
+ else {
+ std::optional<GsrMonitor> monitor = capture_option_line_to_monitor(line);
+ if(monitor)
+ gsr_info->supported_capture_options.monitors.push_back(std::move(monitor.value()));
+ }
}
enum class GsrInfoSection {
@@ -99,7 +87,7 @@ namespace gsr {
CAPTURE_OPTIONS
};
- static bool starts_with(const std::string &str, const char *substr) {
+ static bool starts_with(std::string_view str, const char *substr) {
size_t len = strlen(substr);
return str.size() >= len && memcmp(str.data(), substr, len) == 0;
}
@@ -123,18 +111,16 @@ namespace gsr {
output[bytes_read] = '\0';
GsrInfoSection section = GsrInfoSection::UNKNOWN;
- string_split_char(output, '\n', [&](std::string_view line) {
- const std::string line_str(line.data(), line.size());
-
- if(starts_with(line_str, "section=")) {
- const char *section_name = line_str.c_str() + 8;
- if(strcmp(section_name, "system_info") == 0)
+ string_split_char({output, (size_t)bytes_read}, '\n', [&](std::string_view line) {
+ if(starts_with(line, "section=")) {
+ const std::string_view section_name = line.substr(8);
+ if(section_name == "system_info")
section = GsrInfoSection::SYSTEM_INFO;
- else if(strcmp(section_name, "gpu_info") == 0)
+ else if(section_name == "gpu_info")
section = GsrInfoSection::GPU_INFO;
- else if(strcmp(section_name, "video_codecs") == 0)
+ else if(section_name == "video_codecs")
section = GsrInfoSection::VIDEO_CODECS;
- else if(strcmp(section_name, "capture_options") == 0)
+ else if(section_name == "capture_options")
section = GsrInfoSection::CAPTURE_OPTIONS;
else
section = GsrInfoSection::UNKNOWN;
@@ -146,19 +132,19 @@ namespace gsr {
break;
}
case GsrInfoSection::SYSTEM_INFO: {
- parse_system_info_line(gsr_info, line_str);
+ parse_system_info_line(gsr_info, line);
break;
}
case GsrInfoSection::GPU_INFO: {
- parse_gpu_info_line(gsr_info, line_str);
+ parse_gpu_info_line(gsr_info, line);
break;
}
case GsrInfoSection::VIDEO_CODECS: {
- parse_video_codecs_line(gsr_info, line_str);
+ parse_video_codecs_line(gsr_info, line);
break;
}
case GsrInfoSection::CAPTURE_OPTIONS: {
- parse_capture_options_line(gsr_info, line_str);
+ parse_capture_options_line(gsr_info, line);
break;
}
}
@@ -179,16 +165,13 @@ namespace gsr {
return GsrInfoExitStatus::FAILED_TO_RUN_COMMAND;
}
- static AudioDevice parse_audio_device_line(const std::string &line) {
- AudioDevice audio_device;
- const size_t space_index = line.find(' ');
- if(space_index == std::string::npos)
+ static std::optional<AudioDevice> parse_audio_device_line(std::string_view line) {
+ std::optional<AudioDevice> audio_device;
+ const std::optional<KeyValue> key_value = parse_key_value(line);
+ if(!key_value)
return audio_device;
- const std::string_view audio_input_name = {line.c_str(), space_index};
- const std::string_view audio_input_description = {line.c_str() + space_index + 1, line.size() - (space_index + 1)};
- audio_device.name.assign(audio_input_name.data(), audio_input_name.size());
- audio_device.description.assign(audio_input_description.data(), audio_input_description.size());
+ audio_device = AudioDevice{std::string(key_value->key), std::string(key_value->value)};
return audio_device;
}
@@ -210,9 +193,10 @@ namespace gsr {
}
output[bytes_read] = '\0';
- string_split_char(output, '\n', [&](std::string_view line) {
- const std::string line_str(line.data(), line.size());
- audio_devices.push_back(parse_audio_device_line(line_str));
+ string_split_char({output, (size_t)bytes_read}, '\n', [&](std::string_view line) {
+ std::optional<AudioDevice> audio_device = parse_audio_device_line(line);
+ if(audio_device)
+ audio_devices.push_back(std::move(audio_device.value()));
return true;
});
diff --git a/src/Utils.cpp b/src/Utils.cpp
new file mode 100644
index 0000000..4252de8
--- /dev/null
+++ b/src/Utils.cpp
@@ -0,0 +1,176 @@
+#include "../include/Utils.hpp"
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <limits.h>
+#include <string.h>
+#include <sys/stat.h>
+
+namespace gsr {
+ void string_split_char(std::string_view str, char delimiter, StringSplitCallback callback_func) {
+ size_t index = 0;
+ while(index < str.size()) {
+ size_t new_index = str.find(delimiter, index);
+ if(new_index == std::string_view::npos)
+ new_index = str.size();
+
+ if(!callback_func(str.substr(index, new_index - index)))
+ break;
+
+ index = new_index + 1;
+ }
+ }
+
+ std::optional<KeyValue> parse_key_value(std::string_view line) {
+ const size_t space_index = line.find(' ');
+ if(space_index == std::string_view::npos)
+ return std::nullopt;
+ return KeyValue{line.substr(0, space_index), line.substr(space_index + 1)};
+ }
+
+ std::string get_home_dir() {
+ const char *home_dir = getenv("HOME");
+ if(!home_dir) {
+ passwd *pw = getpwuid(getuid());
+ home_dir = pw->pw_dir;
+ }
+
+ if(!home_dir) {
+ fprintf(stderr, "Error: Failed to get home directory of user, using /tmp directory\n");
+ home_dir = "/tmp";
+ }
+
+ return home_dir;
+ }
+
+ std::string get_config_dir() {
+ std::string config_dir;
+ const char *xdg_config_home = getenv("XDG_CONFIG_HOME");
+ if(xdg_config_home) {
+ config_dir = xdg_config_home;
+ } else {
+ config_dir = get_home_dir() + "/.config";
+ }
+ config_dir += "/gpu-screen-recorder";
+ return config_dir;
+ }
+
+ // Whoever designed xdg-user-dirs is retarded. Why are some XDG variables environment variables
+ // while others are in this pseudo shell config file ~/.config/user-dirs.dirs
+ std::map<std::string, std::string> get_xdg_variables() {
+ std::string user_dirs_filepath;
+ const char *xdg_config_home = getenv("XDG_CONFIG_HOME");
+ if(xdg_config_home) {
+ user_dirs_filepath = xdg_config_home;
+ } else {
+ user_dirs_filepath = get_home_dir() + "/.config";
+ }
+
+ user_dirs_filepath += "/user-dirs.dirs";
+
+ std::map<std::string, std::string> result;
+ FILE *f = fopen(user_dirs_filepath.c_str(), "rb");
+ if(!f)
+ return result;
+
+ char line[PATH_MAX];
+ while(fgets(line, sizeof(line), f)) {
+ int len = strlen(line);
+ if(len < 2)
+ continue;
+
+ if(line[0] == '#')
+ continue;
+
+ if(line[len - 1] == '\n') {
+ line[len - 1] = '\0';
+ len--;
+ }
+
+ if(line[len - 1] != '"')
+ continue;
+
+ line[len - 1] = '\0';
+ len--;
+
+ const char *sep = strchr(line, '=');
+ if(!sep)
+ continue;
+
+ if(sep[1] != '\"')
+ continue;
+
+ std::string value(sep + 2);
+ if(strncmp(value.c_str(), "$HOME/", 6) == 0)
+ value = get_home_dir() + value.substr(5);
+
+ std::string key(line, sep - line);
+ result[std::move(key)] = std::move(value);
+ }
+
+ fclose(f);
+ return result;
+ }
+
+ std::string get_videos_dir() {
+ auto xdg_vars = get_xdg_variables();
+ std::string xdg_videos_dir = xdg_vars["XDG_VIDEOS_DIR"];
+ if(xdg_videos_dir.empty())
+ xdg_videos_dir = get_home_dir() + "/Videos";
+ return xdg_videos_dir;
+ }
+
+ int create_directory_recursive(char *path) {
+ int path_len = strlen(path);
+ char *p = path;
+ char *end = path + path_len;
+ for(;;) {
+ char *slash_p = strchr(p, '/');
+
+ // Skips first '/', we don't want to try and create the root directory
+ if(slash_p == path) {
+ ++p;
+ continue;
+ }
+
+ if(!slash_p)
+ slash_p = end;
+
+ char prev_char = *slash_p;
+ *slash_p = '\0';
+ int err = mkdir(path, S_IRWXU);
+ *slash_p = prev_char;
+
+ if(err == -1 && errno != EEXIST)
+ return err;
+
+ if(slash_p == end)
+ break;
+ else
+ p = slash_p + 1;
+ }
+ return 0;
+ }
+
+ bool file_get_content(const char *filepath, std::string &file_content) {
+ file_content.clear();
+ bool success = false;
+
+ FILE *file = fopen(filepath, "rb");
+ if(!file)
+ return success;
+
+ fseek(file, 0, SEEK_END);
+ long file_size = ftell(file);
+ if(file_size != -1) {
+ file_content.resize(file_size);
+ fseek(file, 0, SEEK_SET);
+ if((long)fread(&file_content[0], 1, file_size, file) == file_size)
+ success = true;
+ }
+
+ fclose(file);
+ return success;
+ }
+} \ No newline at end of file
diff --git a/src/gui/Button.cpp b/src/gui/Button.cpp
index e8137c9..9662c34 100644
--- a/src/gui/Button.cpp
+++ b/src/gui/Button.cpp
@@ -18,10 +18,8 @@ namespace gsr {
bool Button::on_event(mgl::Event &event, mgl::Window&, mgl::vec2f offset) {
const mgl::vec2f item_size = get_size().floor();
- if(event.type == mgl::Event::MouseMoved) {
- mouse_inside = mgl::FloatRect(position + offset, item_size).contains({ (float)event.mouse_move.x, (float)event.mouse_move.y });
- } else if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
- const bool clicked_inside = mgl::FloatRect(position + offset, item_size).contains({ (float)event.mouse_button.x, (float)event.mouse_button.y });;
+ if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
+ const bool clicked_inside = mgl::FloatRect(position + offset, item_size).contains({ (float)event.mouse_button.x, (float)event.mouse_button.y });
if(clicked_inside && on_click)
on_click();
}
diff --git a/src/main.cpp b/src/main.cpp
index a6ca0d8..4ec211b 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -253,7 +253,7 @@ static void add_widgets_to_settings_page(mgl::vec2i window_size, mgl::vec2f sett
auto audio_device_list = std::make_unique<gsr::List>(gsr::List::Orientation::HORIZONTAL, gsr::List::Alignment::CENTER);
gsr::List *audio_device_list_ptr = audio_device_list.get();
{
- audio_device_list->add_widget(std::make_unique<gsr::Label>(&gsr::get_theme().body_font, "*", gsr::get_theme().text_color));
+ audio_device_list->add_widget(std::make_unique<gsr::Label>(&gsr::get_theme().body_font, " ", gsr::get_theme().text_color));
auto audio_device_box = std::make_unique<gsr::ComboBox>(&gsr::get_theme().body_font);
for(const auto &audio_device : audio_devices) {
audio_device_box->add_item(audio_device.description, audio_device.name);