diff --git a/.gitignore b/.gitignore index 94a9222..21d65b7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ way-shell way-sh/way-sh gresources.c gresources.h +/.cache/ diff --git a/src/activities/activities.c b/src/activities/activities.c index 63eb37a..ba9a4c0 100644 --- a/src/activities/activities.c +++ b/src/activities/activities.c @@ -404,8 +404,10 @@ static void activities_init_layout(Activities *self) { gtk_widget_set_halign(GTK_WIDGET(self->search_entry), GTK_ALIGN_CENTER); gtk_widget_set_size_request(GTK_WIDGET(self->search_entry), 480, 120); - gtk_entry_set_placeholder_text( - GTK_ENTRY(self->search_entry), + // gtk_entry_set_placeholder_text( + // GTK_ENTRY(self->search_entry), + gtk_search_entry_set_placeholder_text( // fixes (way-shell:13960): GLib-GObject-CRITICAL **: 12:12:03.013: invalid cast from 'GtkSearchEntry' to 'GtkEntry' + self->search_entry, "Search Applications...ctrl-g for next, ctrl-s-g for prev"); // wire into search-changed signal for GtkSearchEntry diff --git a/src/app_switcher/app_switcher.c b/src/app_switcher/app_switcher.c index f9c80e0..9a9572b 100644 --- a/src/app_switcher/app_switcher.c +++ b/src/app_switcher/app_switcher.c @@ -419,6 +419,13 @@ void app_switcher_activate(AdwApplication *app, gpointer user_data) { void app_switcher_show(AppSwitcher *self) { g_debug("app_switcher.c:app_switcher_show called"); + // fixes crash when app switcher is invoked without any apps open + if (self->widget_n == 0) { + g_debug("app_switcher.c:app_switcher_show no widgets available, hiding"); + app_switcher_hide(self); + return; + } + if (self->widget_n > 1) { if (self->select_alternative_app) app_switcher_focus_widget_at_index(self, 1); diff --git a/src/panel/message_tray/message_tray.c b/src/panel/message_tray/message_tray.c index 3e8e4e1..7a99c13 100644 --- a/src/panel/message_tray/message_tray.c +++ b/src/panel/message_tray/message_tray.c @@ -5,6 +5,7 @@ #include "./notifications/notifications_list.h" #include "calendar/calendar.h" +#include "glib-object.h" #include "gtk/gtk.h" enum signals { @@ -194,6 +195,7 @@ static void message_tray_init_layout(MessageTray *self) { // get GdkSurface from self->win GtkNative *native = gtk_widget_get_native(GTK_WIDGET(self->win)); + gtk_widget_realize(GTK_WIDGET(native)); // fixes (way-shell:15127): GLib-GObject-CRITICAL **: 12:18:35.939: invalid (NULL) pointer instance GdkSurface *surface = gtk_native_get_surface(native); g_signal_connect(surface, "layout", G_CALLBACK(on_layout_changed), self); @@ -273,7 +275,8 @@ static void message_tray_init(MessageTray *self) { // create dependent widgets self->notifications_list = g_object_new(NOTIFICATIONS_LIST_TYPE, NULL); - g_object_unref(self->calendar); + // g_object_unref(self->calendar); + g_clear_object(&self->calendar); // fixes (way-shell:14434): GLib-GObject-CRITICAL **: 12:15:40.810: g_object_unref: assertion 'G_IS_OBJECT (object)' failed // setup the layout message_tray_init_layout(self); diff --git a/src/services/wayland/wlr-foreign-toplevel-management-unstable-v1.c b/src/services/wayland/wlr-foreign-toplevel-management-unstable-v1.c index 84e328a..7f91453 100644 --- a/src/services/wayland/wlr-foreign-toplevel-management-unstable-v1.c +++ b/src/services/wayland/wlr-foreign-toplevel-management-unstable-v1.c @@ -1,4 +1,4 @@ -/* Generated by wayland-scanner 1.23.0 */ +/* Generated by wayland-scanner 1.24.0 */ /* * Copyright © 2018 Ilia Bozhinov diff --git a/src/services/wayland/wlr-foreign-toplevel-management-unstable-v1.h b/src/services/wayland/wlr-foreign-toplevel-management-unstable-v1.h index d5b808d..12bc285 100644 --- a/src/services/wayland/wlr-foreign-toplevel-management-unstable-v1.h +++ b/src/services/wayland/wlr-foreign-toplevel-management-unstable-v1.h @@ -1,4 +1,4 @@ -/* Generated by wayland-scanner 1.23.0 */ +/* Generated by wayland-scanner 1.24.0 */ #ifndef WLR_FOREIGN_TOPLEVEL_MANAGEMENT_UNSTABLE_V1_CLIENT_PROTOCOL_H #define WLR_FOREIGN_TOPLEVEL_MANAGEMENT_UNSTABLE_V1_CLIENT_PROTOCOL_H diff --git a/src/services/wayland/wlr-gamma-control-unstable-v1.c b/src/services/wayland/wlr-gamma-control-unstable-v1.c index 17c4e7b..2f70939 100644 --- a/src/services/wayland/wlr-gamma-control-unstable-v1.c +++ b/src/services/wayland/wlr-gamma-control-unstable-v1.c @@ -1,4 +1,4 @@ -/* Generated by wayland-scanner 1.23.0 */ +/* Generated by wayland-scanner 1.24.0 */ /* * Copyright © 2015 Giulio camuffo diff --git a/src/services/wayland/wlr-gamma-control-unstable-v1.h b/src/services/wayland/wlr-gamma-control-unstable-v1.h index db8d5fc..4d15246 100644 --- a/src/services/wayland/wlr-gamma-control-unstable-v1.h +++ b/src/services/wayland/wlr-gamma-control-unstable-v1.h @@ -1,4 +1,4 @@ -/* Generated by wayland-scanner 1.23.0 */ +/* Generated by wayland-scanner 1.24.0 */ #ifndef WLR_GAMMA_CONTROL_UNSTABLE_V1_CLIENT_PROTOCOL_H #define WLR_GAMMA_CONTROL_UNSTABLE_V1_CLIENT_PROTOCOL_H diff --git a/src/services/window_manager_service/niri/niri_client.c b/src/services/window_manager_service/niri/niri_client.c new file mode 100644 index 0000000..69cba09 --- /dev/null +++ b/src/services/window_manager_service/niri/niri_client.c @@ -0,0 +1,352 @@ +#include "niri_client.h" + +#include +#include +#include +#include +#include +#include + +#include "../../window_manager_service/window_manager_service.h" + +WMWorkspaceEventType niri_client_event_map(const gchar *event_type) { + if (g_strcmp0(event_type, "WorkspaceAdded") == 0) return WMWORKSPACE_EVENT_CREATED; + if (g_strcmp0(event_type, "WorkspaceRemoved") == 0) return WMWORKSPACE_EVENT_DESTROYED; + if (g_strcmp0(event_type, "WorkspaceActivated") == 0) return WMWORKSPACE_EVENT_FOCUSED; + if (g_strcmp0(event_type, "WorkspaceMoved") == 0) return WMWORKSPACE_EVENT_MOVED; + if (g_strcmp0(event_type, "WorkspaceRenamed") == 0) return WMWORKSPACE_EVENT_RENAMED; + return -1; +} + +gchar *niri_client_find_socket_path() { + char *socket_path; + + g_debug("niri_client.c:niri_client_find_socket_path() called"); + + socket_path = getenv("NIRI_SOCKET"); + if (!socket_path) { + g_warning("NIRI_SOCKET environment variable not set"); + return nullptr; + } + + return g_strdup(socket_path); +} + +GIOChannel *niri_client_connect(gchar *socket_path) { + int socket_fd = -1; + g_debug("niri_client.c:niri_client_connect() called"); + + if (!socket_path) return nullptr; + + struct sockaddr_un addr = {0}; + + if ((strlen(socket_path) + 1) > sizeof(addr.sun_path)) + return nullptr; + + addr.sun_family = AF_UNIX; + strcpy(addr.sun_path, socket_path); + + socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_fd == -1) return nullptr; + + if (connect(socket_fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) + return nullptr; + + GIOChannel *channel = g_io_channel_unix_new(socket_fd); + GError *error = nullptr; + g_io_channel_set_encoding(channel, "UTF-8", &error); + if (error) { + g_error("niri_client.c:niri_client_connect() failed to set encoding: %s", error->message); + g_error_free(error); + return nullptr; + } + return channel; +} + +int niri_client_send_request(GIOChannel *channel, const gchar *request_json, gchar **response_json) { + g_debug("niri_client.c:niri_client_send_request() sending: %s", request_json); + + // Send request with newline + gchar *request_with_newline = g_strdup_printf("%s\n", request_json); + gsize bytes_written; + GError *error = nullptr; + GIOStatus status = g_io_channel_write_chars(channel, request_with_newline, -1, &bytes_written, &error); + if (status == G_IO_STATUS_ERROR) { + g_warning("niri_client.c:niri_client_send_request() send failed: %s", error->message); + g_error_free(error); + return -1; + } + error = nullptr; + g_io_channel_flush(channel, &error); + if (status == G_IO_STATUS_ERROR) { + g_warning("niri_client.c:niri_client_send_request() flush failed: %s", error->message); + g_error_free(error); + return -1; + } + g_free(request_with_newline); + + // Read response line + gsize bytes_read; + gsize terminator_pos; + status = g_io_channel_read_line(channel, response_json, &bytes_read, &terminator_pos, &error); + + if (status == G_IO_STATUS_ERROR) { + g_warning("niri_client.c:niri_client_send_request() recv failed: %s", error->message); + g_error_free(error); + return -1; + } + + g_debug("niri_client.c:niri_client_send_request() received: %s", *response_json); + return 0; +} + +GPtrArray *niri_client_get_workspaces(GIOChannel *channel) { + g_debug("niri_client.c:niri_client_get_workspaces() called"); + + gchar *response_json = nullptr; + if (niri_client_send_request(channel, "\"Workspaces\"", &response_json) != 0) { + return nullptr; + } + + JsonParser *parser = json_parser_new(); + GError *error = nullptr; + + if (!json_parser_load_from_data(parser, response_json, -1, &error)) { + g_warning("niri_client.c:niri_client_get_workspaces() JSON parse error: %s", error->message); + g_error_free(error); + g_object_unref(parser); + g_free(response_json); + return nullptr; + } + + JsonNode *root = json_parser_get_root(parser); + if (!JSON_NODE_HOLDS_OBJECT(root)) { + g_warning("niri_client.c:niri_client_get_workspaces() root is not an object"); + g_object_unref(parser); + g_free(response_json); + return nullptr; + } + JsonObject *root_obj = json_node_get_object(root); + + JsonNode *ok_node = json_object_get_member(root_obj, "Ok"); + if (!ok_node || !JSON_NODE_HOLDS_OBJECT(ok_node)) { + g_warning("niri_client.c:niri_client_get_workspaces(): no Ok object found"); + g_object_unref(parser); + g_free(response_json); + return nullptr; + } + JsonObject *ok_obj = json_node_get_object(ok_node); + + JsonNode *workspaces_node = json_object_get_member(ok_obj, "Workspaces"); + if (!workspaces_node || !JSON_NODE_HOLDS_ARRAY(workspaces_node)) { + g_warning("niri_client.c:niri_client_get_workspaces() no Workspaces array found"); + g_object_unref(parser); + g_free(response_json); + return nullptr; + } + JsonArray *workspaces_array = json_node_get_array(workspaces_node); + + GPtrArray *workspaces = g_ptr_array_new_with_free_func(g_free); + for (guint i = 0; i < json_array_get_length(workspaces_array); i++) { + JsonNode *ws_node = json_array_get_element(workspaces_array, i); + if (!JSON_NODE_HOLDS_OBJECT(ws_node)) continue; + + JsonObject *ws_obj = json_node_get_object(ws_node); + WMWorkspace *ws = g_new0(WMWorkspace, 1); + + JsonNode *name_node = json_object_get_member(ws_obj, "name"); + if (name_node && JSON_NODE_HOLDS_VALUE(name_node)) { + ws->name = g_strdup(json_node_get_string(name_node)); + } else { + // Use idx as fallback name + JsonNode *idx_node = json_object_get_member(ws_obj, "idx"); + if (idx_node && JSON_NODE_HOLDS_VALUE(idx_node)) { + ws->name = g_strdup_printf("%" G_GINT64_FORMAT, json_node_get_int(idx_node)); + ws->num = json_node_get_int(idx_node); + } + } + + JsonNode *id_node = json_object_get_member(ws_obj, "id"); + if (id_node && JSON_NODE_HOLDS_VALUE(id_node)) { + ws->id = json_node_get_int(id_node); + } + + JsonNode *output_node = json_object_get_member(ws_obj, "output"); + if (output_node && JSON_NODE_HOLDS_VALUE(output_node)) { + ws->output = g_strdup(json_node_get_string(output_node)); + } + + JsonNode *is_active_node = json_object_get_member(ws_obj, "is_active"); + if (is_active_node && JSON_NODE_HOLDS_VALUE(is_active_node)) { + ws->focused = json_node_get_boolean(is_active_node); + } + + JsonNode *is_focused_node = json_object_get_member(ws_obj, "is_focused"); + if (is_focused_node && JSON_NODE_HOLDS_VALUE(is_focused_node)) { + ws->visible = json_node_get_boolean(is_focused_node); + } + + JsonNode *is_urgent_node = json_object_get_member(ws_obj, "is_urgent"); + if (is_urgent_node && JSON_NODE_HOLDS_VALUE(is_urgent_node)) { + ws->urgent = json_node_get_boolean(is_urgent_node); + } + + g_ptr_array_add(workspaces, ws); + } + + // Sort workspaces by idx field + g_ptr_array_sort(workspaces, (GCompareFunc)compare_workspaces_by_idx); + + g_object_unref(parser); + g_free(response_json); + return workspaces; +} + +gint compare_workspaces_by_idx(gconstpointer a, gconstpointer b) { + const WMWorkspace *ws_a = *(const WMWorkspace **)a; + const WMWorkspace *ws_b = *(const WMWorkspace **)b; + + return ws_a->num - ws_b->num; +} + +GPtrArray *niri_client_get_outputs(GIOChannel *channel) { + g_debug("niri_client.c:niri_client_get_outputs() called"); + + gchar *response_json = nullptr; + if (niri_client_send_request(channel, "\"Outputs\"", &response_json) != 0) { + return nullptr; + } + + JsonParser *parser = json_parser_new(); + GError *error = nullptr; + + if (!json_parser_load_from_data(parser, response_json, -1, &error)) { + g_warning("niri_client.c:niri_client_get_outputs() JSON parse error: %s", error->message); + g_error_free(error); + g_object_unref(parser); + g_free(response_json); + return nullptr; + } + + JsonNode *root = json_parser_get_root(parser); + if (!JSON_NODE_HOLDS_OBJECT(root)) { + g_warning("niri_client.c:niri_client_get_outputs() root is not an object"); + g_object_unref(parser); + g_free(response_json); + return nullptr; + } + JsonObject *root_obj = json_node_get_object(root); + + // Iterate over output names (keys in the object) + GPtrArray *outputs = g_ptr_array_new_with_free_func(g_free); + GList *output_names = json_object_get_members(root_obj); + for (GList *iter = output_names; iter != nullptr; iter = iter->next) { + const gchar *output_name = (const gchar *)iter->data; + JsonNode *output_node = json_object_get_member(root_obj, output_name); + + if (!JSON_NODE_HOLDS_OBJECT(output_node)) continue; + + JsonObject *output_obj = json_node_get_object(output_node); + WMOutput *output = g_new0(WMOutput, 1); + + // Name is the key + output->name = g_strdup(output_name); + + JsonNode *make_node = json_object_get_member(output_obj, "make"); + if (make_node && JSON_NODE_HOLDS_VALUE(make_node)) { + output->make = g_strdup(json_node_get_string(make_node)); + } + + JsonNode *model_node = json_object_get_member(output_obj, "model"); + if (model_node && JSON_NODE_HOLDS_VALUE(model_node)) { + output->model = g_strdup(json_node_get_string(model_node)); + } + + // TODO: output->serial is wrapped in an Option, handle this later + // TODO: implement output->current_workspace later + + g_ptr_array_add(outputs, output); + } + g_list_free(output_names); + + g_object_unref(parser); + g_free(response_json); + return outputs; +} + +int niri_client_focus_workspace(GIOChannel *channel, const gchar *workspace_name) { + g_debug("niri_client.c:niri_client_focus_workspace() workspace: %s", workspace_name); + + gchar *request; + // For niri, we use WorkspaceReferenceArg which can be index or name + // If workspace_name is numeric, use it as index, otherwise use as name + if (g_ascii_isdigit(workspace_name[0])) { + // Use as index + request = g_strdup_printf("{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Index\":%s}}}}", workspace_name); + } else { + // Use as name + request = g_strdup_printf("{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Name\":\"%s\"}}}}", workspace_name); + } + gchar *response = nullptr; + + int result = niri_client_send_request(channel, request, &response); + + g_free(request); + g_free(response); + return result; +} + +int niri_client_move_window_to_workspace(GIOChannel *channel, const gchar *workspace_name) { + g_debug("niri_client.c:niri_client_move_window_to_workspace() workspace: %s", workspace_name); + + gchar *request; + // For niri, we use WorkspaceReferenceArg which can be index or name + // If workspace_name is numeric, use it as index, otherwise use as name + if (g_ascii_isdigit(workspace_name[0])) { + // Use as index + request = g_strdup_printf("{\"Action\":{\"MoveWindowToWorkspace\":{\"window_id\":null,\"reference\":{\"Index\":%s},\"focus\":false}}}", workspace_name); + } else { + // Use as name + request = g_strdup_printf("{\"Action\":{\"MoveWindowToWorkspace\":{\"window_id\":null,\"reference\":{\"Name\":\"%s\"},\"focus\":false}}}", workspace_name); + } + gchar *response = nullptr; + + int result = niri_client_send_request(channel, request, &response); + + g_free(request); + g_free(response); + return result; +} + +int niri_client_rename_current_workspace(GIOChannel *channel, const gchar *new_name) { + g_debug("niri_client.c:niri_client_rename_workspace() new_name: %s", new_name); + + gchar *request = g_strdup_printf("{\"Action\":{\"SetWorkspaceName\":{\"name\":\"%s\",\"workspace\":null}}}", new_name); + gchar *response = nullptr; + + int result = niri_client_send_request(channel, request, &response); + + g_free(request); + g_free(response); + return result; +} + +int niri_client_move_workspace_to_output(GIOChannel *channel, const gchar *output_name) { + g_debug("niri_client.c:niri_client_move_workspace_to_output() output: %s", output_name); + + gchar *request = g_strdup_printf("{\"Action\":{\"MoveWorkspaceToMonitor\":{\"output\":\"%s\",\"reference\":null}}}", output_name); + gchar *response = nullptr; + + int result = niri_client_send_request(channel, request, &response); + + g_free(request); + g_free(response); + return result; +} + +int niri_client_subscribe_events(GIOChannel *channel) { + g_debug("niri_client.c:niri_client_subscribe_events() called"); + + gchar *response = nullptr; + return niri_client_send_request(channel, "\"EventStream\"", &response); +} diff --git a/src/services/window_manager_service/niri/niri_client.h b/src/services/window_manager_service/niri/niri_client.h new file mode 100644 index 0000000..3059371 --- /dev/null +++ b/src/services/window_manager_service/niri/niri_client.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include "../window_manager_service.h" + +gchar *niri_client_find_socket_path(); +GIOChannel *niri_client_connect(gchar *socket_path); +int niri_client_send_request(GIOChannel *channel, const gchar *request_json, gchar **response_json); +GPtrArray *niri_client_get_workspaces(GIOChannel *channel); +GPtrArray *niri_client_get_outputs(GIOChannel *channel); +int niri_client_focus_workspace(GIOChannel *channel, const gchar *workspace_name); +int niri_client_move_window_to_workspace(GIOChannel *channel, const gchar *workspace_name); +int niri_client_rename_current_workspace(GIOChannel *channel, const gchar *new_name); +int niri_client_move_workspace_to_output(GIOChannel *channel, const gchar *output_name); +int niri_client_subscribe_events(GIOChannel *channel); +WMWorkspaceEventType niri_client_event_map(const gchar *event_type); +gint compare_workspaces_by_idx(gconstpointer a, gconstpointer b); diff --git a/src/services/window_manager_service/niri/window_manager_service_niri.c b/src/services/window_manager_service/niri/window_manager_service_niri.c new file mode 100644 index 0000000..bc25c7e --- /dev/null +++ b/src/services/window_manager_service/niri/window_manager_service_niri.c @@ -0,0 +1,305 @@ +#include "window_manager_service_niri.h" + +#include +#include +#include +#include + +#include "glib-object.h" +#include "glib.h" +#include "niri_client.h" + +enum signals { + workspaces_changed, + outputs_changed, // NOTE: niri doesn't have output events: https://docs.rs/niri-ipc/25.8.0/niri_ipc/enum.Event.html + signals_n +}; + +struct _WMServiceNiri { + GObject parent_instance; + GPtrArray *workspaces; + GPtrArray *outputs; + char *socket_path; + GIOChannel *event_channel; + GIOChannel *command_channel; + guint poll_id; + gboolean subscribed; + gchar *focused_workspace; + GSettings *settings; +}; + +static guint service_signals[signals_n] = {0}; +G_DEFINE_TYPE(WMServiceNiri, wm_service_niri, G_TYPE_OBJECT); + +static void wm_service_niri_dispose(GObject *gobject) { + WMServiceNiri *self = WM_SERVICE_NIRI(gobject); + + // close sockets + g_io_channel_unref(self->event_channel); + g_io_channel_unref(self->command_channel); + + // g_free socket path + g_free(self->socket_path); + + if (self->workspaces) g_ptr_array_unref(self->workspaces); + if (self->outputs) g_ptr_array_unref(self->outputs); + + // Chain-up + G_OBJECT_CLASS(wm_service_niri_parent_class)->dispose(gobject); +}; + +static void wm_service_niri_finalize(GObject *gobject) { + // Chain-up + G_OBJECT_CLASS(wm_service_niri_parent_class)->finalize(gobject); +}; + +static void wm_service_niri_class_init(WMServiceNiriClass *klass) { + GObjectClass *object_class = G_OBJECT_CLASS(klass); + object_class->dispose = wm_service_niri_dispose; + object_class->finalize = wm_service_niri_finalize; + + service_signals[workspaces_changed] = g_signal_new( + "workspaces-changed", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, + NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_PTR_ARRAY); + + // NOTE: doesn't actually do anything, niri doesn't have output events + service_signals[outputs_changed] = g_signal_new( + "outputs-changed", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, + NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_PTR_ARRAY); +} + +static void wm_service_niri_init(WMServiceNiri *self) { + // initialize only sockets here i.e. niri specific stuff + // generic stuff is initialized in wm_service_niri_window_manager_init + self->socket_path = niri_client_find_socket_path(); + if (!self->socket_path) + g_error( + "window_manager_service_niri.c:wm_service_niri_init " + "failed to find socket path."); + + g_debug( + "window_manager_service_niri.c:wm_service_niri_init " + "found socket path: %s", + self->socket_path); + self->event_channel = niri_client_connect(self->socket_path); + if (self->event_channel == 0) + g_error( + "window_manager_service_niri.c:wm_service_niri_init " + "failed to connect to event socket."); + + self->command_channel = niri_client_connect(self->socket_path); + if (self->command_channel == 0) + g_error( + "window_manager_service_niri.c:wm_service_niri_init " + "failed to connect to command socket."); + + self->poll_id = 0; + self->subscribed = FALSE; + + // connect to 'org.ldelossa.way-shell.window-manager.workspaces' setting + self->settings = g_settings_new("org.ldelossa.way-shell.window-manager"); + + self->workspaces = NULL; + self->outputs = NULL; + self->focused_workspace = NULL; +} + +static gboolean on_ipc_recv(GIOChannel *channel, GIOCondition condition, WMServiceNiri *self) { + g_debug( + "window_manager_service_niri.c:on_ipc_recv() " + "received ipc message."); + + // TODO: implement recovery from this. + if (condition & G_IO_HUP || condition & G_IO_ERR) { + g_debug( + "window_manager_service_niri.c:handle_ipc_recv() " + "received G_IO_HUP or G_IO_ERR, GLib polling stopped."); + return false; + } + + if (condition & G_IO_IN) { + gchar *line = nullptr; + gsize length = 0; + gsize terminator_pos = -1; + GError *error = nullptr; + GIOStatus status = g_io_channel_read_line(channel, &line, &length, &terminator_pos, &error); + + if (status == G_IO_STATUS_NORMAL) { + g_debug("window_manager_service_niri.c:on_socket_ready() received event: %s", line); + + // Check for workspace-related events + if (g_strstr_len(line, -1, "WorkspacesChanged") != NULL || + g_strstr_len(line, -1, "WorkspaceActivated") != NULL || + g_strstr_len(line, -1, "WorkspaceUrgencyChanged") != NULL || + g_strstr_len(line, -1, "WorkspaceActiveWindowChanged") != NULL) { + + // Refresh workspace list on any workspace event + g_debug("window_manager_service_niri.c:on_socket_ready() refreshing workspaces due to workspace event"); + if (self->workspaces) g_ptr_array_unref(self->workspaces); + self->workspaces = niri_client_get_workspaces(self->command_channel); + + if (self->workspaces) { + g_signal_emit(self, service_signals[workspaces_changed], 0, self->workspaces); + } + } + g_free(line); + } else { + // TODO: handle other conditions + } + + } + + return true; +} + +static void wm_service_niri_setup_polling(WMServiceNiri *self) { + g_debug("window_manager_service_niri.c:wm_service_niri_setup_polling() called"); + + // Subscribe to events + if (!self->subscribed) { + if (niri_client_subscribe_events(self->event_channel) == 0) { + self->subscribed = TRUE; + } else { + g_warning("window_manager_service_niri.c:wm_service_niri_setup_polling() failed to subscribe to events"); + return; + } + } + + // add our connected channel to GLib event loop. + self->poll_id = g_io_add_watch(self->event_channel, G_IO_IN | G_IO_ERR | G_IO_HUP, (GIOFunc)on_ipc_recv, self); +} + +// WindowManager interface implementation +GPtrArray *wm_service_niri_get_workspaces(WindowManager *wm) { + WMServiceNiri *self = wm->private; + + if (!self->workspaces) { + g_warning( + "window_manager_service_niri.c:wm_service_niri_get_workspaces() " + "workspaces not initialized."); + return NULL; + } + + return g_ptr_array_ref(self->workspaces); +} + +GPtrArray *wm_service_niri_get_outputs(WindowManager *wm) { + WMServiceNiri *self = wm->private; + + if (!self->outputs) { + g_warning( + "window_manager_service_niri.c:wm_service_niri_get_outputs() " + "outputs not initialized."); + return NULL; + } + + return g_ptr_array_ref(self->outputs); +} + +int wm_service_niri_focus_workspace(WindowManager *wm, WMWorkspace *ws) { + WMServiceNiri *self = wm->private; + + if (!self->workspaces) { + g_warning( + "window_manager_service_niri.c:wm_service_niri_focus_workspace() " + "workspaces not initialized."); + return -1; + } + return niri_client_focus_workspace(self->command_channel, ws->name); +} + +int wm_service_niri_rename_current_workspace(WindowManager *wm, const gchar *name) { + WMServiceNiri *self = wm->private; + + if (strlen(name) == 0) return -1; + + return niri_client_rename_current_workspace(self->command_channel, name); +} + +int wm_service_niri_current_ws_to_output(WindowManager *wm, WMOutput *o) { + WMServiceNiri *self = wm->private; + + if (!o) { + g_warning( + "window_manager_service_niri.c:wm_service_niri_current_ws_to_" + "output() " + "outputs not initialized."); + return -1; + } + return niri_client_move_workspace_to_output(self->command_channel, o->name); +} + +int wm_service_niri_current_app_to_workspace(WindowManager *wm, WMWorkspace *ws) { + WMServiceNiri *self = wm->private; + + if (!ws) { + g_warning( + "window_manager_service_niri.c:wm_service_niri_current_app_to_" + "workspace() " + "workspaces not initialized."); + return -1; + } + return niri_client_move_window_to_workspace(self->command_channel, ws->name); +} + +guint wm_service_niri_register_on_workspaces_changed(WindowManager *wm, wm_on_workspaces_changed cb, void *data) { + WMServiceNiri *self = wm->private; + + // we use swapped here because workspaces_changed functions should not + // leak the private workspace service's implementation in their signatures. + return g_signal_connect_swapped(self, "workspaces-changed", G_CALLBACK(cb), + data); +} + +guint wm_service_niri_unregister_on_workspaces_changed(WindowManager *wm, wm_on_workspaces_changed cb, void *data) { + WMServiceNiri *self = wm->private; + + return g_signal_handlers_disconnect_by_func(self, cb, data); +} + +// NOTE: doesn't actually do anything, niri doesn't have events for outputs +guint wm_service_niri_register_on_outputs_changed(WindowManager *wm, wm_on_outputs_changed cb, void *data) { + WMServiceNiri *self = wm->private; + + // we use swapped here because workspaces_changed functions should not + // leak the private workspace service's implementation in their signatures. + return g_signal_connect_swapped(self, "outputs-changed", G_CALLBACK(cb), + data); +} + +// NOTE: doesn't actually do anything, niri doesn't have events for outputs +guint wm_service_niri_unregister_on_outputs_changed(WindowManager *wm, wm_on_outputs_changed cb, void *data) { + WMServiceNiri *self = wm->private; + + return g_signal_handlers_disconnect_by_func(self, cb, data); +} + +WindowManager *wm_service_niri_window_manager_init() { + WindowManager *wm = g_malloc(sizeof(WindowManager)); + + WMServiceNiri *self = g_object_new(WM_SERVICE_NIRI_TYPE, NULL); + + // write virt func table + wm->private = self; + wm->get_workspaces = wm_service_niri_get_workspaces; + wm->get_outputs = wm_service_niri_get_outputs; + wm->focus_workspace = wm_service_niri_focus_workspace; + wm->rename_workspace = wm_service_niri_rename_current_workspace; + wm->current_ws_to_output = wm_service_niri_current_ws_to_output; + wm->current_app_to_workspace = wm_service_niri_current_app_to_workspace; + wm->register_on_workspaces_changed = wm_service_niri_register_on_workspaces_changed; + wm->unregister_on_workspaces_changed = wm_service_niri_unregister_on_workspaces_changed; + wm->register_on_outputs_changed = wm_service_niri_register_on_outputs_changed; + wm->unregister_on_outputs_changed = wm_service_niri_unregister_on_outputs_changed; + + // subscribe to events + wm_service_niri_setup_polling(self); + + // get initial listing of workspaces + self->workspaces = niri_client_get_workspaces(self->command_channel); + + // get initial listing of outputs + self->outputs = niri_client_get_outputs(self->command_channel); + + return wm; +} diff --git a/src/services/window_manager_service/niri/window_manager_service_niri.h b/src/services/window_manager_service/niri/window_manager_service_niri.h new file mode 100644 index 0000000..a0ff0e6 --- /dev/null +++ b/src/services/window_manager_service/niri/window_manager_service_niri.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "../window_manager_service.h" + +G_BEGIN_DECLS + +struct _WMServiceNiri; +#define WM_SERVICE_NIRI_TYPE wm_service_niri_get_type() +G_DECLARE_FINAL_TYPE(WMServiceNiri, wm_service_niri, WM_SERVICE, NIRI, GObject); + +G_END_DECLS + +WindowManager *wm_service_niri_window_manager_init(); diff --git a/src/services/window_manager_service/window_manager_service.c b/src/services/window_manager_service/window_manager_service.c index ce0d0c8..3a3bb1f 100644 --- a/src/services/window_manager_service/window_manager_service.c +++ b/src/services/window_manager_service/window_manager_service.c @@ -1,6 +1,7 @@ #include "window_manager_service.h" #include "./sway/window_manager_service_sway.h" +#include "./niri/window_manager_service_niri.h" static WindowManager *global = NULL; @@ -17,6 +18,8 @@ int window_manager_service_init() { if (g_strcmp0(backend, "sway") == 0) { global = wm_service_sway_window_manager_init(); + } else if (g_strcmp0(backend, "niri") == 0) { + global = wm_service_niri_window_manager_init(); } else { g_warning("Unknown backend: %s", backend); return -1;