xworkmate-app/linux/runner/desktop_platform_channel.cc
2026-03-21 01:28:33 +08:00

735 lines
25 KiB
C++

#include "desktop_platform_channel.h"
#include <errno.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <limits.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <memory>
#include <optional>
#include <regex>
#include <sstream>
#include <string>
#include <vector>
struct _DesktopPlatformChannel {
MyApplication* application;
GtkWindow* window;
FlView* view;
FlMethodChannel* channel;
GtkStatusIcon* status_icon;
GtkWidget* menu;
std::string preferred_mode = "proxy";
std::string vpn_connection_name = "XWorkmate Tunnel";
std::string proxy_host = "127.0.0.1";
int proxy_port = 7890;
bool tray_enabled = true;
bool tray_available = true;
bool autostart_enabled = false;
bool network_manager_available = false;
std::string desktop_environment = "unknown";
std::string status_message;
};
namespace {
constexpr char kChannelName[] = "plus.svc.xworkmate/desktop_platform";
constexpr char kDesktopFileId[] = "plus.svc.xworkmate.desktop";
struct CommandResult {
bool ok = false;
int exit_status = -1;
std::string stdout_text;
std::string stderr_text;
};
std::string json_escape(const std::string& input) {
std::ostringstream escaped;
for (const char ch : input) {
switch (ch) {
case '\\':
escaped << "\\\\";
break;
case '"':
escaped << "\\\"";
break;
case '\n':
escaped << "\\n";
break;
default:
escaped << ch;
break;
}
}
return escaped.str();
}
std::string trim_quotes(const std::string& value) {
if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
return value.substr(1, value.size() - 2);
}
return value;
}
std::optional<std::string> json_string(const std::string& payload,
const std::string& key) {
const std::regex pattern("\"" + key + "\"\\s*:\\s*\"((?:\\\\.|[^\"])*)\"");
std::smatch match;
if (!std::regex_search(payload, match, pattern) || match.size() < 2) {
return std::nullopt;
}
std::string value = match[1].str();
value = std::regex_replace(value, std::regex("\\\\\""), "\"");
value = std::regex_replace(value, std::regex("\\\\\\\\"), "\\");
return value;
}
std::optional<int> json_int(const std::string& payload, const std::string& key) {
const std::regex pattern("\"" + key + "\"\\s*:\\s*(\\d+)");
std::smatch match;
if (!std::regex_search(payload, match, pattern) || match.size() < 2) {
return std::nullopt;
}
return std::stoi(match[1].str());
}
std::optional<bool> json_bool(const std::string& payload,
const std::string& key) {
const std::regex pattern("\"" + key + "\"\\s*:\\s*(true|false)");
std::smatch match;
if (!std::regex_search(payload, match, pattern) || match.size() < 2) {
return std::nullopt;
}
return match[1].str() == "true";
}
CommandResult run_command(const std::vector<std::string>& args) {
std::vector<std::unique_ptr<gchar, decltype(&g_free)>> quoted;
quoted.reserve(args.size());
std::ostringstream command;
for (size_t index = 0; index < args.size(); index++) {
if (index > 0) {
command << ' ';
}
quoted.emplace_back(g_shell_quote(args[index].c_str()), g_free);
command << quoted.back().get();
}
gchar* stdout_text = nullptr;
gchar* stderr_text = nullptr;
gint exit_status = -1;
GError* error = nullptr;
const gboolean ok = g_spawn_command_line_sync(command.str().c_str(),
&stdout_text, &stderr_text,
&exit_status, &error);
CommandResult result;
result.ok = ok && error == nullptr;
result.exit_status = exit_status;
if (stdout_text != nullptr) {
result.stdout_text = stdout_text;
}
if (stderr_text != nullptr) {
result.stderr_text = stderr_text;
}
if (error != nullptr) {
result.stderr_text = error->message;
g_error_free(error);
}
g_free(stdout_text);
g_free(stderr_text);
return result;
}
bool command_succeeds(const std::vector<std::string>& args) {
const CommandResult result = run_command(args);
return result.ok && result.exit_status == 0;
}
std::string detect_desktop_environment() {
const char* current = g_getenv("XDG_CURRENT_DESKTOP");
const std::string desktop = current == nullptr ? "" : current;
std::unique_ptr<gchar, decltype(&g_free)> lowered_raw(
g_ascii_strdown(desktop.c_str(), -1), g_free);
const std::string lowered =
lowered_raw == nullptr ? std::string() : lowered_raw.get();
if (lowered.find("gnome") != std::string::npos) {
return "gnome";
}
if (lowered.find("kde") != std::string::npos ||
lowered.find("plasma") != std::string::npos) {
return "kde";
}
if (g_getenv("KDE_FULL_SESSION") != nullptr) {
return "kde";
}
return "unknown";
}
std::string autostart_path() {
const char* config_home = g_get_user_config_dir();
std::ostringstream path;
path << config_home << "/autostart/" << kDesktopFileId;
return path.str();
}
std::string executable_path() {
gchar buffer[PATH_MAX];
const ssize_t size = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1);
if (size <= 0) {
return "xworkmate";
}
buffer[size] = '\0';
return buffer;
}
bool write_autostart_file() {
const std::string path = autostart_path();
const std::string directory = path.substr(0, path.find_last_of('/'));
if (g_mkdir_with_parents(directory.c_str(), 0755) != 0) {
return false;
}
std::ostringstream contents;
contents << "[Desktop Entry]\n";
contents << "Type=Application\n";
contents << "Version=1.0\n";
contents << "Name=XWorkmate\n";
contents << "Exec=" << executable_path() << "\n";
contents << "Icon=xworkmate\n";
contents << "Terminal=false\n";
contents << "Categories=Network;Utility;\n";
contents << "StartupNotify=true\n";
return g_file_set_contents(path.c_str(), contents.str().c_str(), -1,
nullptr);
}
bool remove_autostart_file() {
return g_remove(autostart_path().c_str()) == 0 || errno == ENOENT;
}
bool autostart_enabled() {
return g_file_test(autostart_path().c_str(), G_FILE_TEST_EXISTS);
}
bool network_manager_available() {
return command_succeeds({"nmcli", "--version"});
}
bool tunnel_profile_exists(const std::string& connection_name) {
const CommandResult result = run_command(
{"nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"});
if (!result.ok || result.exit_status != 0) {
return false;
}
std::istringstream lines(result.stdout_text);
std::string line;
while (std::getline(lines, line)) {
if (line.rfind(connection_name + ":", 0) == 0) {
return true;
}
}
return false;
}
bool tunnel_connected(const std::string& connection_name) {
const CommandResult result = run_command(
{"nmcli", "-t", "-f", "NAME", "connection", "show", "--active"});
if (!result.ok || result.exit_status != 0) {
return false;
}
std::istringstream lines(result.stdout_text);
std::string line;
while (std::getline(lines, line)) {
if (line == connection_name) {
return true;
}
}
return false;
}
std::string gsettings_read(const std::vector<std::string>& args) {
const CommandResult result = run_command(args);
if (!result.ok || result.exit_status != 0) {
return "";
}
std::string value = result.stdout_text;
value.erase(value.find_last_not_of(" \n\r\t") + 1);
return trim_quotes(value);
}
bool apply_gnome_proxy(const DesktopPlatformChannel* self) {
const bool mode_ok = command_succeeds({
"gsettings", "set", "org.gnome.system.proxy", "mode", "manual"});
const bool http_host = command_succeeds({
"gsettings", "set", "org.gnome.system.proxy.http", "host",
self->proxy_host});
const bool http_port = command_succeeds({
"gsettings", "set", "org.gnome.system.proxy.http", "port",
std::to_string(self->proxy_port)});
const bool https_host = command_succeeds({
"gsettings", "set", "org.gnome.system.proxy.https", "host",
self->proxy_host});
const bool https_port = command_succeeds({
"gsettings", "set", "org.gnome.system.proxy.https", "port",
std::to_string(self->proxy_port)});
const bool socks_host = command_succeeds({
"gsettings", "set", "org.gnome.system.proxy.socks", "host",
self->proxy_host});
const bool socks_port = command_succeeds({
"gsettings", "set", "org.gnome.system.proxy.socks", "port",
std::to_string(self->proxy_port)});
return mode_ok && http_host && http_port && https_host && https_port &&
socks_host && socks_port;
}
bool disable_gnome_proxy() {
return command_succeeds(
{"gsettings", "set", "org.gnome.system.proxy", "mode", "none"});
}
bool apply_kde_proxy(const DesktopPlatformChannel* self) {
const bool type_ok = command_succeeds({
"kwriteconfig5", "--file", "kioslaverc", "--group",
"Proxy Settings", "--key", "ProxyType", "1"});
const std::string proxy_value =
"http://" + self->proxy_host + " " + std::to_string(self->proxy_port);
const bool http_ok = command_succeeds({
"kwriteconfig5", "--file", "kioslaverc", "--group",
"Proxy Settings", "--key", "httpProxy", proxy_value});
const bool https_ok = command_succeeds({
"kwriteconfig5", "--file", "kioslaverc", "--group",
"Proxy Settings", "--key", "httpsProxy", proxy_value});
const bool socks_ok = command_succeeds({
"kwriteconfig5", "--file", "kioslaverc", "--group",
"Proxy Settings", "--key", "socksProxy", proxy_value});
command_succeeds({"qdbus", "org.kde.KIO", "/KIO/Scheduler",
"org.kde.KIO.Scheduler.reparseConfiguration", ""});
return type_ok && http_ok && https_ok && socks_ok;
}
bool disable_kde_proxy() {
const bool ok = command_succeeds({
"kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings",
"--key", "ProxyType", "0"});
command_succeeds({"qdbus", "org.kde.KIO", "/KIO/Scheduler",
"org.kde.KIO.Scheduler.reparseConfiguration", ""});
return ok;
}
bool apply_proxy_mode(DesktopPlatformChannel* self) {
if (self->desktop_environment == "gnome") {
return apply_gnome_proxy(self);
}
if (self->desktop_environment == "kde") {
return apply_kde_proxy(self);
}
return false;
}
bool disable_system_proxy(DesktopPlatformChannel* self) {
if (self->desktop_environment == "gnome") {
return disable_gnome_proxy();
}
if (self->desktop_environment == "kde") {
return disable_kde_proxy();
}
return false;
}
std::string gnome_proxy_mode() {
return gsettings_read(
{"gsettings", "get", "org.gnome.system.proxy", "mode"});
}
std::string gnome_proxy_host(const std::string& group) {
const std::string schema = "org.gnome.system.proxy." + group;
return gsettings_read({"gsettings", "get", schema, "host"});
}
int gnome_proxy_port(const std::string& group) {
const std::string schema = "org.gnome.system.proxy." + group;
const std::string value =
gsettings_read({"gsettings", "get", schema, "port"});
return value.empty() ? 0 : std::atoi(value.c_str());
}
std::string kde_proxy_value(const char* key) {
const CommandResult result = run_command({"kreadconfig5", "--file", "kioslaverc",
"--group", "Proxy Settings",
"--key", key});
if (!result.ok || result.exit_status != 0) {
return "";
}
std::string value = result.stdout_text;
value.erase(value.find_last_not_of(" \n\r\t") + 1);
return value;
}
void refresh_runtime_state(DesktopPlatformChannel* self) {
self->desktop_environment = detect_desktop_environment();
self->network_manager_available = network_manager_available();
self->autostart_enabled = autostart_enabled();
}
std::string state_json(DesktopPlatformChannel* self) {
refresh_runtime_state(self);
bool proxy_enabled = false;
std::string proxy_backend;
std::string proxy_host = self->proxy_host;
int proxy_port = self->proxy_port;
if (self->desktop_environment == "gnome") {
proxy_backend = "gsettings";
proxy_enabled = gnome_proxy_mode() == "manual";
if (proxy_enabled) {
const std::string detected_host = gnome_proxy_host("http");
const int detected_port = gnome_proxy_port("http");
if (!detected_host.empty()) {
proxy_host = detected_host;
}
if (detected_port > 0) {
proxy_port = detected_port;
}
}
} else if (self->desktop_environment == "kde") {
proxy_backend = "kioslaverc";
const std::string detected = kde_proxy_value("httpProxy");
proxy_enabled = !detected.empty();
if (proxy_enabled) {
const std::regex pattern(R"(http://([^ ]+)\s+(\d+))");
std::smatch match;
if (std::regex_search(detected, match, pattern) && match.size() >= 3) {
proxy_host = match[1].str();
proxy_port = std::stoi(match[2].str());
}
}
}
const bool tunnel_available =
self->network_manager_available &&
tunnel_profile_exists(self->vpn_connection_name);
const bool tunnel_is_connected =
tunnel_available && tunnel_connected(self->vpn_connection_name);
const std::string mode =
tunnel_is_connected ? "tunnel" : (proxy_enabled ? "proxy" : self->preferred_mode);
std::ostringstream json;
json << "{";
json << "\"isSupported\":true,";
json << "\"environment\":\"" << json_escape(self->desktop_environment) << "\",";
json << "\"mode\":\"" << json_escape(mode) << "\",";
json << "\"trayAvailable\":" << (self->tray_available ? "true" : "false") << ",";
json << "\"trayEnabled\":" << (self->tray_enabled ? "true" : "false") << ",";
json << "\"autostartEnabled\":" << (self->autostart_enabled ? "true" : "false") << ",";
json << "\"networkManagerAvailable\":"
<< (self->network_manager_available ? "true" : "false") << ",";
json << "\"systemProxy\":{";
json << "\"enabled\":" << (proxy_enabled ? "true" : "false") << ",";
json << "\"host\":\"" << json_escape(proxy_host) << "\",";
json << "\"port\":" << proxy_port << ",";
json << "\"backend\":\"" << json_escape(proxy_backend) << "\",";
json << "\"lastAppliedMode\":\"" << json_escape(self->preferred_mode) << "\"";
json << "},";
json << "\"tunnel\":{";
json << "\"available\":" << (tunnel_available ? "true" : "false") << ",";
json << "\"connected\":" << (tunnel_is_connected ? "true" : "false") << ",";
json << "\"connectionName\":\"" << json_escape(self->vpn_connection_name) << "\",";
json << "\"backend\":\"nmcli\",";
json << "\"lastError\":\"" << json_escape(self->status_message) << "\"";
json << "},";
json << "\"statusMessage\":\"" << json_escape(self->status_message) << "\"";
json << "}";
return json.str();
}
void update_status_icon(DesktopPlatformChannel* self) {
if (self->status_icon == nullptr) {
return;
}
gtk_status_icon_set_visible(self->status_icon, self->tray_enabled);
gtk_status_icon_set_from_icon_name(self->status_icon, "network-vpn-symbolic");
const std::string json = state_json(self);
const std::string tooltip =
"XWorkmate • " + self->desktop_environment + "" + self->preferred_mode;
gtk_status_icon_set_tooltip_text(self->status_icon, tooltip.c_str());
}
void show_window(DesktopPlatformChannel* self) {
gtk_widget_show_all(GTK_WIDGET(self->window));
gtk_window_present(self->window);
}
void on_open_activate(GtkMenuItem*, gpointer user_data) {
show_window(static_cast<DesktopPlatformChannel*>(user_data));
}
void on_status_icon_activate(GtkStatusIcon*, gpointer user_data) {
show_window(static_cast<DesktopPlatformChannel*>(user_data));
}
void on_quit_activate(GtkMenuItem*, gpointer user_data) {
auto* self = static_cast<DesktopPlatformChannel*>(user_data);
g_application_quit(G_APPLICATION(self->application));
}
void on_use_proxy_activate(GtkMenuItem*, gpointer user_data) {
auto* self = static_cast<DesktopPlatformChannel*>(user_data);
self->preferred_mode = "proxy";
if (!apply_proxy_mode(self)) {
self->status_message =
"Failed to apply system proxy; verify gsettings/kwriteconfig5";
} else {
self->status_message = "System proxy enabled";
}
update_status_icon(self);
}
void on_use_tunnel_activate(GtkMenuItem*, gpointer user_data) {
auto* self = static_cast<DesktopPlatformChannel*>(user_data);
self->preferred_mode = "tunnel";
if (!disable_system_proxy(self)) {
self->status_message = "Tunnel mode selected; proxy disable may require manual follow-up";
} else {
self->status_message = "Tunnel mode selected";
}
update_status_icon(self);
}
void on_connect_tunnel_activate(GtkMenuItem*, gpointer user_data) {
auto* self = static_cast<DesktopPlatformChannel*>(user_data);
self->preferred_mode = "tunnel";
disable_system_proxy(self);
if (!command_succeeds({"nmcli", "connection", "up", "id",
self->vpn_connection_name})) {
self->status_message = "Failed to connect NetworkManager tunnel";
} else {
self->status_message = "Tunnel connected";
}
update_status_icon(self);
}
void on_disconnect_tunnel_activate(GtkMenuItem*, gpointer user_data) {
auto* self = static_cast<DesktopPlatformChannel*>(user_data);
if (!command_succeeds({"nmcli", "connection", "down", "id",
self->vpn_connection_name})) {
self->status_message = "Failed to disconnect tunnel";
} else {
self->status_message = "Tunnel disconnected";
}
update_status_icon(self);
}
void on_status_icon_popup(GtkStatusIcon* status_icon,
guint button,
guint activate_time,
gpointer user_data) {
auto* self = static_cast<DesktopPlatformChannel*>(user_data);
gtk_menu_popup(GTK_MENU(self->menu), nullptr, nullptr,
gtk_status_icon_position_menu, status_icon, button,
activate_time);
}
GtkWidget* build_menu(DesktopPlatformChannel* self) {
GtkWidget* menu = gtk_menu_new();
GtkWidget* open_item = gtk_menu_item_new_with_label("Open XWorkmate");
GtkWidget* connect_item = gtk_menu_item_new_with_label("Connect Tunnel");
GtkWidget* disconnect_item = gtk_menu_item_new_with_label("Disconnect Tunnel");
GtkWidget* proxy_item = gtk_menu_item_new_with_label("Use Proxy Mode");
GtkWidget* tunnel_item = gtk_menu_item_new_with_label("Use Tunnel Mode");
GtkWidget* quit_item = gtk_menu_item_new_with_label("Quit");
g_signal_connect(open_item, "activate", G_CALLBACK(on_open_activate), self);
g_signal_connect(connect_item, "activate",
G_CALLBACK(on_connect_tunnel_activate), self);
g_signal_connect(disconnect_item, "activate",
G_CALLBACK(on_disconnect_tunnel_activate), self);
g_signal_connect(proxy_item, "activate", G_CALLBACK(on_use_proxy_activate),
self);
g_signal_connect(tunnel_item, "activate", G_CALLBACK(on_use_tunnel_activate),
self);
g_signal_connect(quit_item, "activate", G_CALLBACK(on_quit_activate), self);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), open_item);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), connect_item);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), disconnect_item);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), proxy_item);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), tunnel_item);
gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
gtk_menu_shell_append(GTK_MENU_SHELL(menu), quit_item);
gtk_widget_show_all(menu);
return menu;
}
void ensure_status_icon(DesktopPlatformChannel* self) {
if (self->status_icon == nullptr) {
self->status_icon = gtk_status_icon_new();
self->menu = build_menu(self);
g_signal_connect(self->status_icon, "popup-menu",
G_CALLBACK(on_status_icon_popup), self);
g_signal_connect(self->status_icon, "activate",
G_CALLBACK(on_status_icon_activate),
self);
}
update_status_icon(self);
}
FlMethodResponse* success_response_with_json(const std::string& payload) {
g_autoptr(FlValue) result = fl_value_new_string(payload.c_str());
return FL_METHOD_RESPONSE(fl_method_success_response_new(result));
}
FlMethodResponse* method_error(const char* code, const std::string& message) {
return FL_METHOD_RESPONSE(
fl_method_error_response_new(code, message.c_str(), nullptr));
}
FlMethodResponse* handle_method_call(DesktopPlatformChannel* self,
FlMethodCall* method_call) {
const gchar* method = fl_method_call_get_name(method_call);
FlValue* args = fl_method_call_get_args(method_call);
if (strcmp(method, "getState") == 0) {
return success_response_with_json(state_json(self));
}
if (strcmp(method, "configure") == 0) {
const char* payload = args == nullptr ? nullptr : fl_value_get_string(args);
const std::string json = payload == nullptr ? "" : payload;
if (const auto value = json_string(json, "preferredMode"); value.has_value()) {
self->preferred_mode = *value;
}
if (const auto value = json_string(json, "vpnConnectionName"); value.has_value()) {
self->vpn_connection_name = *value;
}
if (const auto value = json_string(json, "proxyHost"); value.has_value()) {
self->proxy_host = *value;
}
if (const auto value = json_int(json, "proxyPort"); value.has_value()) {
self->proxy_port = *value;
}
if (const auto value = json_bool(json, "trayEnabled"); value.has_value()) {
self->tray_enabled = *value;
}
ensure_status_icon(self);
return success_response_with_json(state_json(self));
}
if (strcmp(method, "setMode") == 0) {
const char* value = args == nullptr ? nullptr : fl_value_get_string(args);
if (value == nullptr) {
return method_error("INVALID_ARGS", "mode is required");
}
self->preferred_mode = value;
if (self->preferred_mode == "proxy") {
if (!apply_proxy_mode(self)) {
self->status_message = "Failed to apply system proxy";
} else {
self->status_message = "System proxy enabled";
}
} else {
disable_system_proxy(self);
self->status_message = "Tunnel mode selected";
}
update_status_icon(self);
return success_response_with_json(state_json(self));
}
if (strcmp(method, "connectTunnel") == 0) {
self->preferred_mode = "tunnel";
disable_system_proxy(self);
if (!command_succeeds({"nmcli", "connection", "up", "id",
self->vpn_connection_name})) {
return method_error("NM_CONNECT_FAILED",
"Failed to connect NetworkManager tunnel");
}
self->status_message = "Tunnel connected";
update_status_icon(self);
return success_response_with_json(state_json(self));
}
if (strcmp(method, "disconnectTunnel") == 0) {
if (!command_succeeds({"nmcli", "connection", "down", "id",
self->vpn_connection_name})) {
return method_error("NM_DISCONNECT_FAILED", "Failed to disconnect tunnel");
}
self->status_message = "Tunnel disconnected";
update_status_icon(self);
return success_response_with_json(state_json(self));
}
if (strcmp(method, "setAutostart") == 0) {
const bool enabled = args != nullptr && fl_value_get_bool(args);
const bool ok = enabled ? write_autostart_file() : remove_autostart_file();
if (!ok) {
return method_error("AUTOSTART_FAILED", "Failed to update autostart");
}
self->status_message =
enabled ? "Autostart enabled" : "Autostart disabled";
update_status_icon(self);
return success_response_with_json(state_json(self));
}
if (strcmp(method, "showWindow") == 0) {
show_window(self);
return success_response_with_json(state_json(self));
}
return FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
}
void method_call_cb(FlMethodChannel* channel,
FlMethodCall* method_call,
gpointer user_data) {
auto* self = static_cast<DesktopPlatformChannel*>(user_data);
g_autoptr(FlMethodResponse) response = handle_method_call(self, method_call);
GError* error = nullptr;
if (!fl_method_call_respond(method_call, response, &error) && error != nullptr) {
g_warning("Failed to send response: %s", error->message);
g_error_free(error);
}
}
} // namespace
DesktopPlatformChannel* desktop_platform_channel_new(MyApplication* application,
GtkWindow* window,
FlView* view) {
auto* self = new DesktopPlatformChannel();
self->application = application;
self->window = window;
self->view = view;
self->desktop_environment = detect_desktop_environment();
ensure_status_icon(self);
FlEngine* engine = fl_view_get_engine(view);
FlBinaryMessenger* messenger = fl_engine_get_binary_messenger(engine);
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
self->channel =
fl_method_channel_new(messenger, kChannelName, FL_METHOD_CODEC(codec));
fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self,
nullptr);
return self;
}
void desktop_platform_channel_free(DesktopPlatformChannel* channel) {
if (channel == nullptr) {
return;
}
if (channel->status_icon != nullptr) {
gtk_status_icon_set_visible(channel->status_icon, FALSE);
g_object_unref(channel->status_icon);
}
if (channel->menu != nullptr) {
gtk_widget_destroy(channel->menu);
}
if (channel->channel != nullptr) {
g_object_unref(channel->channel);
}
delete channel;
}