diff options
| author | Hannes Schulze <contact@haschu.me> | 2026-04-26 01:03:47 +0200 |
|---|---|---|
| committer | Hannes Schulze <contact@haschu.me> | 2026-04-26 01:03:47 +0200 |
| commit | e80a97f5603658af3a444a3a8213024c0c9d36d6 (patch) | |
| tree | 8b5326361b0f6a93533d7c31fd37b205fc9d290b /src | |
| parent | 77a4c25a2e5aeda0f0d1f1980eb7c85c99041c45 (diff) | |
split up workspace service
Diffstat (limited to 'src')
| -rw-r--r-- | src/application.c | 17 | ||||
| -rw-r--r-- | src/services/meson.build | 1 | ||||
| -rw-r--r-- | src/services/workspace-service.c | 1325 | ||||
| -rw-r--r-- | src/services/workspace-service.h | 27 | ||||
| -rw-r--r-- | src/services/workspaces/hyprland-workspace-service.c | 1368 | ||||
| -rw-r--r-- | src/services/workspaces/hyprland-workspace-service.h | 13 | ||||
| -rw-r--r-- | src/services/workspaces/meson.build | 3 |
7 files changed, 1489 insertions, 1265 deletions
diff --git a/src/application.c b/src/application.c index bad62b1..cb05f65 100644 --- a/src/application.c +++ b/src/application.c @@ -1,4 +1,5 @@ #include "application.h" +#include "glib-object.h" #include "panel.h" #include "launcher.h" #include "control-center.h" @@ -9,6 +10,7 @@ #include "services/brightness-service.h" #include "services/quick-answer-service.h" #include "services/workspace-service.h" +#include "services/workspaces/hyprland-workspace-service.h" #include "services/notification-service.h" #include "services/audio-service.h" #include "services/network-service.h" @@ -91,6 +93,7 @@ static void foobar_application_destroy_panels ( FoobarApplicati static void foobar_application_create_panels ( FoobarApplication* self ); static guint foobar_application_create_panel ( FoobarApplication* self, GdkMonitor* monitor ); +static void foobar_application_initialize_workspaces ( FoobarApplication* self ); G_DEFINE_FINAL_TYPE( FoobarApplication, foobar_application, GTK_TYPE_APPLICATION ) @@ -176,7 +179,7 @@ void foobar_application_activate( GApplication* app ) self->battery_service = foobar_battery_service_new( ); self->clock_service = foobar_clock_service_new( ); self->brightness_service = foobar_brightness_service_new( ); - self->workspace_service = foobar_workspace_service_new( ); + foobar_application_initialize_workspaces( self ); self->notification_service = foobar_notification_service_new( ); self->audio_service = foobar_audio_service_new( ); self->network_service = foobar_network_service_new( ); @@ -689,3 +692,15 @@ guint foobar_application_create_panel( return gtk_application_window_get_id( GTK_APPLICATION_WINDOW( panel ) ); } + +// +// Initialize the instance's workspace_service, attempting multiple window manager implementations. +// +void foobar_application_initialize_workspaces( FoobarApplication* self ) +{ + g_clear_object( &self->workspace_service ); + self->workspace_service = foobar_hyprland_workspace_service_new( ); + if ( foobar_workspace_service_is_available( self->workspace_service ) ) { return; } + + g_warning( "No suitable workspace manager implementation found." ); +} diff --git a/src/services/meson.build b/src/services/meson.build index 368c2f1..a3c148f 100644 --- a/src/services/meson.build +++ b/src/services/meson.build @@ -13,3 +13,4 @@ foobar_sources += files( ) subdir('quick-answers') +subdir('workspaces') diff --git a/src/services/workspace-service.c b/src/services/workspace-service.c index 75ea06a..f9a6ecd 100644 --- a/src/services/workspace-service.c +++ b/src/services/workspace-service.c @@ -1,10 +1,5 @@ #include "services/workspace-service.h" -#include <json-glib/json-glib.h> -#include <gtk/gtk.h> -#include <string.h> - -typedef struct _EventData EventData; -typedef struct _EventHandler EventHandler; +#include <stdint.h> // // FoobarWorkspaceFlags: @@ -28,74 +23,19 @@ G_DEFINE_FLAGS_TYPE( // Representation of a workspace. // -struct _FoobarWorkspace -{ - GObject parent_instance; - FoobarWorkspaceService* service; - gint64 id; - gchar* name; - gchar* monitor; - FoobarWorkspaceFlags flags; -}; - -enum -{ - WORKSPACE_PROP_ID = 1, - WORKSPACE_PROP_NAME, - WORKSPACE_PROP_MONITOR, - WORKSPACE_PROP_FLAGS, - N_WORKSPACE_PROPS, -}; - -static GParamSpec* workspace_props[N_WORKSPACE_PROPS] = { 0 }; - -static void foobar_workspace_class_init ( FoobarWorkspaceClass* klass ); -static void foobar_workspace_init ( FoobarWorkspace* self ); -static void foobar_workspace_get_property( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_workspace_finalize ( GObject* object ); -static FoobarWorkspace* foobar_workspace_new ( FoobarWorkspaceService* service ); -static void foobar_workspace_set_id ( FoobarWorkspace* self, - gint64 value ); -static void foobar_workspace_set_name ( FoobarWorkspace* self, - gchar const* value ); -static void foobar_workspace_set_monitor ( FoobarWorkspace* self, - gchar const* value ); -static void foobar_workspace_set_flags ( FoobarWorkspace* self, - FoobarWorkspaceFlags value ); +G_DEFINE_INTERFACE( FoobarWorkspace, foobar_workspace, G_TYPE_OBJECT ) -G_DEFINE_FINAL_TYPE( FoobarWorkspace, foobar_workspace, G_TYPE_OBJECT ) +static void foobar_workspace_default_init( FoobarWorkspaceInterface* iface ); // // FoobarWorkspaceService: // -// Service monitoring the workspaces provided by the window manager. This is implemented by communicating directly with -// Hyprland through its IPC socket. -// -// Based on the waybar implementation: -// https://github.com/Alexays/Waybar/blob/master/src/modules/hyprland/workspaces.cpp +// Service monitoring the workspaces provided by the window manager. The implementation depends on the window manager. // -struct _FoobarWorkspaceService -{ - GObject parent_instance; - GListStore* workspaces; - GtkSortListModel* sorted_workspaces; - GCancellable* event_cancellable; - GThread* event_thread; - gchar* rx_path; - gchar* tx_path; -}; +G_DEFINE_INTERFACE( FoobarWorkspaceService, foobar_workspace_service, G_TYPE_OBJECT ) -enum -{ - PROP_WORKSPACES = 1, - N_PROPS, -}; - -static GParamSpec* props[N_PROPS] = { 0 }; +static void foobar_workspace_service_default_init( FoobarWorkspaceServiceInterface* iface ); enum { @@ -104,99 +44,6 @@ enum }; static unsigned signals[N_SIGNALS] = { 0 }; -static void foobar_workspace_service_class_init ( FoobarWorkspaceServiceClass* klass ); -static void foobar_workspace_service_init ( FoobarWorkspaceService* self ); -static void foobar_workspace_service_get_property ( GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ); -static void foobar_workspace_service_dispose ( GObject* object ); -static void foobar_workspace_service_finalize ( GObject* object ); -static gboolean foobar_workspace_service_handle_workspace_created ( EventData* data ); -static gboolean foobar_workspace_service_handle_workspace_destroyed ( EventData* data ); -static gboolean foobar_workspace_service_handle_workspace_moved ( EventData* data ); -static gboolean foobar_workspace_service_handle_workspace_renamed ( EventData* data ); -static gboolean foobar_workspace_service_handle_workspace_activated ( EventData* data ); -static gboolean foobar_workspace_service_handle_special_workspace_activated( EventData* data ); -static gboolean foobar_workspace_service_handle_monitor_focused ( EventData* data ); -static gboolean foobar_workspace_service_handle_window_urgent ( EventData* data ); -static gboolean foobar_workspace_service_handle_config_reloaded ( EventData* data ); -static gpointer foobar_workspace_service_event_thread_func ( gpointer userdata ); -static void foobar_workspace_service_dispatch_event ( FoobarWorkspaceService* self, - gchar* message ); -static gchar* foobar_workspace_service_send_request ( FoobarWorkspaceService* self, - gchar const* request, - GError** error ); -static JsonNode* foobar_workspace_service_send_json_request ( FoobarWorkspaceService* self, - gchar const* request, - GError** error ); -static void foobar_workspace_service_initialize ( FoobarWorkspaceService* self ); -static void foobar_workspace_service_update_active ( FoobarWorkspaceService* self, - GListStore* workspaces ); -static void foobar_workspace_service_add_workspace ( FoobarWorkspaceService* self, - GListStore* workspaces, - JsonObject* workspace_object, - gboolean is_persistent ); -static FoobarWorkspace* foobar_workspace_service_find_workspace ( GListModel* workspaces, - gint64 id, - guint* out_index ); -static gboolean foobar_workspace_service_is_invalid_name ( gchar const* name ); -static gint64 foobar_workspace_service_parse_id ( gchar const* str ); -static gchar* foobar_workspace_service_get_hyprland_base_path ( void ); -static gint foobar_workspace_service_compare_ids ( gconstpointer a, - gconstpointer b ); -static gint foobar_workspace_service_sort_func ( gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ); - -G_DEFINE_FINAL_TYPE( FoobarWorkspaceService, foobar_workspace_service, G_TYPE_OBJECT ) - -// -// EventData: -// -// Custom userdata passed to hyprland event handlers in a single pointer. -// - -struct _EventData -{ - FoobarWorkspaceService* service; - gchar* name; - gchar* payload; -}; - -static EventData* event_data_new ( FoobarWorkspaceService* service, - gchar const* name, - gchar const* payload ); -static void event_data_free( EventData* self ); - -// -// EventHandler: -// -// Description for the handler of a single hyprland event. -// - -struct _EventHandler -{ - gchar const* name; - gboolean ( *fn )( EventData* data ); -}; - -// -// Static list of registered event handlers. -// -static EventHandler const event_handlers[] = - { - { .name = "createworkspacev2", .fn = foobar_workspace_service_handle_workspace_created }, - { .name = "destroyworkspacev2", .fn = foobar_workspace_service_handle_workspace_destroyed }, - { .name = "moveworkspacev2", .fn = foobar_workspace_service_handle_workspace_moved }, - { .name = "renameworkspace", .fn = foobar_workspace_service_handle_workspace_renamed }, - { .name = "workspace", .fn = foobar_workspace_service_handle_workspace_activated }, - { .name = "activespecial", .fn = foobar_workspace_service_handle_special_workspace_activated }, - { .name = "focusedmon", .fn = foobar_workspace_service_handle_monitor_focused }, - { .name = "urgent", .fn = foobar_workspace_service_handle_window_urgent }, - { .name = "configreloaded", .fn = foobar_workspace_service_handle_config_reloaded }, - }; - // --------------------------------------------------------------------------------------------------------------------- // Workspace // --------------------------------------------------------------------------------------------------------------------- @@ -204,102 +51,43 @@ static EventHandler const event_handlers[] = // // Static initialization for workspaces. // -void foobar_workspace_class_init( FoobarWorkspaceClass* klass ) -{ - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_workspace_get_property; - object_klass->finalize = foobar_workspace_finalize; - - workspace_props[WORKSPACE_PROP_ID] = g_param_spec_int64( - "id", - "ID", - "Numeric ID of the workspace.", - INT64_MIN, - INT64_MAX, - 0, - G_PARAM_READABLE ); - workspace_props[WORKSPACE_PROP_NAME] = g_param_spec_string( - "name", - "Name", - "Optional human-readable workspace name.", - NULL, - G_PARAM_READABLE ); - workspace_props[WORKSPACE_PROP_MONITOR] = g_param_spec_string( - "monitor", - "Monitor", - "Connector ID of the workspace's monitor.", - NULL, - G_PARAM_READABLE ); - workspace_props[WORKSPACE_PROP_FLAGS] = g_param_spec_flags( - "flags", - "Flags", - "Flags indicating the current state of the workspace.", - FOOBAR_TYPE_WORKSPACE_FLAGS, - FOOBAR_WORKSPACE_FLAGS_NONE, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_WORKSPACE_PROPS, workspace_props ); -} - -// -// Instance initialization for workspaces. -// -void foobar_workspace_init( FoobarWorkspace* self ) +void foobar_workspace_default_init( FoobarWorkspaceInterface* iface ) { - (void)self; -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_workspace_get_property( - GObject* object, - guint prop_id, - GValue* value, - GParamSpec* pspec ) -{ - FoobarWorkspace* self = (FoobarWorkspace*)object; - - switch ( prop_id ) - { - case WORKSPACE_PROP_ID: - g_value_set_int64( value, foobar_workspace_get_id( self ) ); - break; - case WORKSPACE_PROP_NAME: - g_value_set_string( value, foobar_workspace_get_name( self ) ); - break; - case WORKSPACE_PROP_MONITOR: - g_value_set_string( value, foobar_workspace_get_monitor( self ) ); - break; - case WORKSPACE_PROP_FLAGS: - g_value_set_flags( value, foobar_workspace_get_flags( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance cleanup for workspaces. -// -void foobar_workspace_finalize( GObject* object ) -{ - FoobarWorkspace* self = (FoobarWorkspace*)object; - - g_clear_pointer( &self->name, g_free ); - g_clear_pointer( &self->monitor, g_free ); - - G_OBJECT_CLASS( foobar_workspace_parent_class )->finalize( object ); -} - -// -// Create a new workspace that is owned by the given service (captured as an unowned reference). -// -FoobarWorkspace* foobar_workspace_new( FoobarWorkspaceService* service ) -{ - FoobarWorkspace* self = g_object_new( FOOBAR_TYPE_WORKSPACE, NULL ); - self->service = service; - return self; + g_object_interface_install_property( + iface, + g_param_spec_int64( + "id", + "ID", + "Numeric ID of the workspace.", + INT64_MIN, + INT64_MAX, + 0, + G_PARAM_READABLE ) ); + g_object_interface_install_property( + iface, + g_param_spec_string( + "name", + "Name", + "Optional human-readable workspace name.", + NULL, + G_PARAM_READABLE ) ); + g_object_interface_install_property( + iface, + g_param_spec_string( + "monitor", + "Monitor", + "Connector ID of the workspace's monitor.", + NULL, + G_PARAM_READABLE ) ); + g_object_interface_install_property( + iface, + g_param_spec_flags( + "flags", + "Flags", + "Flags indicating the current state of the workspace.", + FOOBAR_TYPE_WORKSPACE_FLAGS, + FOOBAR_WORKSPACE_FLAGS_NONE, + G_PARAM_READABLE ) ); } // @@ -308,7 +96,8 @@ FoobarWorkspace* foobar_workspace_new( FoobarWorkspaceService* service ) gint64 foobar_workspace_get_id( FoobarWorkspace* self ) { g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), 0 ); - return self->id; + + return FOOBAR_WORKSPACE_GET_IFACE( self )->get_id( self ); } // @@ -317,7 +106,8 @@ gint64 foobar_workspace_get_id( FoobarWorkspace* self ) gchar const* foobar_workspace_get_name( FoobarWorkspace* self ) { g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), NULL ); - return self->name; + + return FOOBAR_WORKSPACE_GET_IFACE( self )->get_name( self ); } // @@ -326,7 +116,8 @@ gchar const* foobar_workspace_get_name( FoobarWorkspace* self ) gchar const* foobar_workspace_get_monitor( FoobarWorkspace* self ) { g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), NULL ); - return self->monitor; + + return FOOBAR_WORKSPACE_GET_IFACE( self )->get_monitor( self ); } // @@ -335,73 +126,8 @@ gchar const* foobar_workspace_get_monitor( FoobarWorkspace* self ) FoobarWorkspaceFlags foobar_workspace_get_flags( FoobarWorkspace* self ) { g_return_val_if_fail( FOOBAR_IS_WORKSPACE( self ), FOOBAR_WORKSPACE_FLAGS_NONE ); - return self->flags; -} - -// -// Numeric ID of the workspace. -// -void foobar_workspace_set_id( - FoobarWorkspace* self, - gint64 value ) -{ - g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); - - if ( self->id != value ) - { - self->id = value; - g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_ID] ); - } -} - -// -// Optional human-readable workspace name. -// -void foobar_workspace_set_name( - FoobarWorkspace* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); - - if ( g_strcmp0( self->name, value ) ) - { - g_clear_pointer( &self->name, g_free ); - self->name = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_NAME] ); - } -} - -// -// Connector ID of the workspace's monitor. -// -void foobar_workspace_set_monitor( - FoobarWorkspace* self, - gchar const* value ) -{ - g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); - if ( g_strcmp0( self->monitor, value ) ) - { - g_clear_pointer( &self->monitor, g_free ); - self->monitor = g_strdup( value ); - g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_MONITOR] ); - } -} - -// -// Flags indicating the current state of the workspace. -// -void foobar_workspace_set_flags( - FoobarWorkspace* self, - FoobarWorkspaceFlags value ) -{ - g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); - - if ( self->flags != value ) - { - self->flags = value; - g_object_notify_by_pspec( G_OBJECT( self ), workspace_props[WORKSPACE_PROP_FLAGS] ); - } + return FOOBAR_WORKSPACE_GET_IFACE( self )->get_flags( self ); } // @@ -410,59 +136,27 @@ void foobar_workspace_set_flags( void foobar_workspace_activate( FoobarWorkspace* self ) { g_return_if_fail( FOOBAR_IS_WORKSPACE( self ) ); - if ( !self->service ) { return; } - - g_autofree gchar* request = NULL; - if ( foobar_workspace_get_id( self ) > 0 ) - { - // Normal - request = g_strdup_printf( "dispatch workspace %lld", (long long)foobar_workspace_get_id( self ) ); - } - else if ( !( foobar_workspace_get_flags( self ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) ) - { - // Named (this includes persistent) - request = g_strdup_printf( "dispatch workspace name:%s", foobar_workspace_get_name( self ) ); - } - else if ( foobar_workspace_get_id( self ) != -99 ) - { - // Named special - request = g_strdup_printf( "dispatch togglespecialworkspace %s", foobar_workspace_get_name( self ) ); - } - else - { - // Special - request = g_strdup( "dispatch togglespecialworkspace" ); - } - g_autoptr( GError ) error = NULL; - g_free( foobar_workspace_service_send_request( self->service, request, &error ) ); - if ( error ) - { - g_warning( "Unable to activate workspace: %s", error->message ); - } + FOOBAR_WORKSPACE_GET_IFACE( self )->activate( self ); } // --------------------------------------------------------------------------------------------------------------------- -// Service Implementation +// Service Interface // --------------------------------------------------------------------------------------------------------------------- // // Static initialization for the workspace service. // -void foobar_workspace_service_class_init( FoobarWorkspaceServiceClass* klass ) +void foobar_workspace_service_default_init( FoobarWorkspaceServiceInterface* iface ) { - GObjectClass* object_klass = G_OBJECT_CLASS( klass ); - object_klass->get_property = foobar_workspace_service_get_property; - object_klass->dispose = foobar_workspace_service_dispose; - object_klass->finalize = foobar_workspace_service_finalize; - - props[PROP_WORKSPACES] = g_param_spec_object( - "workspaces", - "Workspaces", - "Sorted list of all workspaces.", - G_TYPE_LIST_MODEL, - G_PARAM_READABLE ); - g_object_class_install_properties( object_klass, N_PROPS, props ); + g_object_interface_install_property( + iface, + g_param_spec_object( + "workspaces", + "Workspaces", + "Sorted list of all workspaces.", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE ) ); signals[SIGNAL_MONITOR_CONFIGURATION_CHANGED] = g_signal_new( "monitor-configuration-changed", @@ -477,910 +171,21 @@ void foobar_workspace_service_class_init( FoobarWorkspaceServiceClass* klass ) } // -// Instance initialization for the workspace service. -// -void foobar_workspace_service_init( FoobarWorkspaceService* self ) -{ - self->workspaces = g_list_store_new( FOOBAR_TYPE_WORKSPACE ); - - GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_workspace_service_sort_func, NULL, NULL ); - self->sorted_workspaces = gtk_sort_list_model_new( - G_LIST_MODEL( g_object_ref( self->workspaces ) ), - GTK_SORTER( sorter ) ); - - g_autofree gchar* base_path = foobar_workspace_service_get_hyprland_base_path( ); - if ( !base_path ) - { - g_warning( "Unable to connect to hyprland -- is it currently running?" ); - return; - } - - self->tx_path = g_strdup_printf( "%s/.socket.sock", base_path ); - self->rx_path = g_strdup_printf( "%s/.socket2.sock", base_path ); - self->event_cancellable = g_cancellable_new( ); - self->event_thread = g_thread_new( "workspace-event-listener", foobar_workspace_service_event_thread_func, self ); - - foobar_workspace_service_initialize( self ); -} - -// -// Property getter implementation, mapping a property id to a method. -// -void foobar_workspace_service_get_property( GObject* object, guint prop_id, GValue* value, GParamSpec* pspec ) -{ - FoobarWorkspaceService* self = (FoobarWorkspaceService*)object; - - switch ( prop_id ) - { - case PROP_WORKSPACES: - g_value_set_object( value, foobar_workspace_service_get_workspaces( self ) ); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); - break; - } -} - -// -// Instance de-initialization for the workspace service. -// -// This is where the event thread is joined because it might still create new object references which is a bad thing to -// do in finalize. -// -void foobar_workspace_service_dispose( GObject* object ) -{ - FoobarWorkspaceService* self = (FoobarWorkspaceService*)object; - - if ( self->event_thread ) - { - g_cancellable_cancel( self->event_cancellable ); - g_thread_join( self->event_thread ); - self->event_thread = NULL; - } - - G_OBJECT_CLASS( foobar_workspace_service_parent_class )->dispose( object ); -} - -// -// Instance cleanup for the workspace service. -// -void foobar_workspace_service_finalize( GObject* object ) -{ - FoobarWorkspaceService* self = (FoobarWorkspaceService*)object; - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->workspaces ) ); ++i ) - { - FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( self->workspaces ), i ); - workspace->service = NULL; - } - g_clear_object( &self->event_cancellable ); - g_clear_object( &self->sorted_workspaces ); - g_clear_object( &self->workspaces ); - g_clear_pointer( &self->rx_path, g_free ); - g_clear_pointer( &self->tx_path, g_free ); - - G_OBJECT_CLASS( foobar_workspace_service_parent_class )->finalize( object ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------------------------------------------------- - -// -// Create a new workspace service instance. -// -FoobarWorkspaceService* foobar_workspace_service_new( void ) -{ - return g_object_new( FOOBAR_TYPE_WORKSPACE_SERVICE, NULL ); -} - -// // Get a sorted list of all workspaces. // GListModel* foobar_workspace_service_get_workspaces( FoobarWorkspaceService* self ) { g_return_val_if_fail( FOOBAR_IS_WORKSPACE_SERVICE( self ), NULL ); - return G_LIST_MODEL( self->sorted_workspaces ); -} - -// --------------------------------------------------------------------------------------------------------------------- -// Event Handlers -// --------------------------------------------------------------------------------------------------------------------- - -// -// Handler for the "createworkspacev2" event. -// -// The payload has the format "<id>,<name>" where <id> is the numeric ID. -// -gboolean foobar_workspace_service_handle_workspace_created( EventData* data ) -{ - gchar* save; - gchar const* id_str = strtok_r( data->payload, ",", &save ); - gchar const* name = strtok_r( NULL, ",", &save ); - g_return_val_if_fail( id_str && name, G_SOURCE_REMOVE ); - - if ( foobar_workspace_service_is_invalid_name( name ) ) { return G_SOURCE_REMOVE; } - gint64 id = foobar_workspace_service_parse_id( id_str ); - - // Get additional information about the workspace. - - g_autoptr( GError ) error = NULL; - g_autoptr( JsonNode ) workspaces_node = foobar_workspace_service_send_json_request( - data->service, - "j/workspaces", - &error ); - if ( !workspaces_node ) - { - g_warning( "Unable to load workspaces: %s", error->message ); - return G_SOURCE_REMOVE; - } - - JsonArray* workspaces_array = json_node_get_array( workspaces_node ); - JsonObject* workspace_object = NULL; - for ( guint i = 0; i < json_array_get_length( workspaces_array ); ++i ) - { - JsonObject* it = json_array_get_object_element( workspaces_array, i ); - if ( id == json_object_get_int_member( it, "id" ) ) - { - workspace_object = it; - break; - } - } - - if ( !workspace_object ) - { - g_warning( "Unable to find new workspace." ); - return G_SOURCE_REMOVE; - } - - // Check if the workspace is persistent by looking for a rule with its ID. - - gboolean is_persistent = FALSE; - - g_autoptr( JsonNode ) rules_node = foobar_workspace_service_send_json_request( - data->service, - "j/workspacerules", - &error ); - if ( !rules_node ) - { - g_warning( "Unable to load workspace rules: %s", error->message ); - return G_SOURCE_REMOVE; - } - - JsonArray* rules_array = json_node_get_array( rules_node ); - for ( guint i = 0; i < json_array_get_length( rules_array ); ++i ) - { - JsonObject* rule_object = json_array_get_object_element( rules_array, i ); - gchar const* rule_id_str = json_object_get_string_member_with_default( rule_object, "workspaceString", NULL ); - if ( id == foobar_workspace_service_parse_id( rule_id_str ) ) - { - is_persistent = json_object_get_boolean_member_with_default( rule_object, "persistent", FALSE ); - break; - } - } - foobar_workspace_service_add_workspace( data->service, data->service->workspaces, workspace_object, is_persistent ); - foobar_workspace_service_update_active( data->service, data->service->workspaces ); - return G_SOURCE_REMOVE; + return FOOBAR_WORKSPACE_SERVICE_GET_IFACE( self )->get_workspaces( self ); } // -// Handler for the "destroyworkspacev2" event. +// Check whether this implementation is available (i.e., the window manager is running). // -// The payload has the format "<id>,<name>" where <id> is the numeric ID. -// -gboolean foobar_workspace_service_handle_workspace_destroyed( EventData* data ) -{ - gchar* save; - gchar const* id_str = strtok_r( data->payload, ",", &save ); - gchar const* name = strtok_r( NULL, ",", &save ); - g_return_val_if_fail( id_str && name, G_SOURCE_REMOVE ); - - if ( foobar_workspace_service_is_invalid_name( name ) ) { return G_SOURCE_REMOVE; } - gint64 id = foobar_workspace_service_parse_id( id_str ); - - GListStore* workspaces = data->service->workspaces; - guint index; - FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( G_LIST_MODEL( workspaces ), id, &index ); - if ( workspace ) - { - workspace->service = NULL; - g_list_store_remove( workspaces, index ); - } - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "moveworkspacev2" event. -// -// The payload has the format "<id>,<name>,<monitor>" where <id> is the numeric ID of the workspace. -// -gboolean foobar_workspace_service_handle_workspace_moved( EventData* data ) -{ - gchar* save; - gchar const* workspace_id_str = strtok_r( data->payload, ",", &save ); - gchar const* workspace_name = strtok_r( NULL, ",", &save ); - gchar const* monitor_name = strtok_r( NULL, ",", &save ); - g_return_val_if_fail( workspace_id_str && workspace_name && monitor_name, G_SOURCE_REMOVE ); - - gint64 workspace_id = foobar_workspace_service_parse_id( workspace_id_str ); - FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( - G_LIST_MODEL( data->service->workspaces ), - workspace_id, - NULL ); - if ( workspace ) - { - foobar_workspace_set_monitor( workspace, monitor_name ); - g_signal_emit( data->service, signals[SIGNAL_MONITOR_CONFIGURATION_CHANGED], 0 ); - } - foobar_workspace_service_update_active( data->service, data->service->workspaces ); - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "renameworkspace" event. -// -// The payload has the format "<id>,<name>" where <id> is the numeric ID of the workspace. -// -gboolean foobar_workspace_service_handle_workspace_renamed( EventData* data ) +gboolean foobar_workspace_service_is_available( FoobarWorkspaceService* self ) { - gchar* save; - gchar const* id_str = strtok_r( data->payload, ",", &save ); - gchar const* new_name = strtok_r( NULL, ",", &save ); - g_return_val_if_fail( id_str && new_name, G_SOURCE_REMOVE ); - - gint64 id = foobar_workspace_service_parse_id( id_str ); - FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( - G_LIST_MODEL( data->service->workspaces ), - id, - NULL ); - if ( workspace ) - { - foobar_workspace_set_name( workspace, new_name ); - gtk_sorter_changed( - gtk_sort_list_model_get_sorter( data->service->sorted_workspaces ), - GTK_SORTER_CHANGE_DIFFERENT ); - } + g_return_val_if_fail( FOOBAR_IS_WORKSPACE_SERVICE( self ), FALSE ); - return G_SOURCE_REMOVE; -} - -// -// Handler for the "workspace" event. -// -gboolean foobar_workspace_service_handle_workspace_activated( EventData* data ) -{ - foobar_workspace_service_update_active( data->service, data->service->workspaces ); - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "activespecial" event. -// -gboolean foobar_workspace_service_handle_special_workspace_activated( EventData* data ) -{ - foobar_workspace_service_update_active( data->service, data->service->workspaces ); - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "focusedmon" event. -// -gboolean foobar_workspace_service_handle_monitor_focused( EventData* data ) -{ - foobar_workspace_service_update_active( data->service, data->service->workspaces ); - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "urgent" event. -// -// The payload has the format "<address>" where <address> is the sender window's address. -// -gboolean foobar_workspace_service_handle_window_urgent( EventData* data ) -{ - gchar const* address = data->payload; - - // Find the client with the given address. - - g_autoptr( GError ) error = NULL; - g_autoptr( JsonNode ) clients_node = foobar_workspace_service_send_json_request( - data->service, - "j/clients", - &error ); - if ( !clients_node ) - { - g_warning( "Unable to load clients: %s", error->message ); - return G_SOURCE_REMOVE; - } - - JsonArray* clients_array = json_node_get_array( clients_node ); - JsonObject* client_object = NULL; - for ( guint i = 0; i < json_array_get_length( clients_array ); ++i ) - { - JsonObject* it = json_array_get_object_element( clients_array, i ); - gchar const* it_address = json_object_get_string_member_with_default( it, "address", "" ); - if ( g_str_has_suffix( it_address, address ) ) - { - client_object = it; - break; - } - } - - if ( !client_object ) { return G_SOURCE_REMOVE; } - - // Find the workspace this window belongs to and mark it as "urgent". - - JsonObject* workspace_object = json_object_get_object_member( client_object, "workspace" ); - gint64 id = json_object_get_int_member( workspace_object, "id" ); - FoobarWorkspace* workspace = foobar_workspace_service_find_workspace( - G_LIST_MODEL( data->service->workspaces ), - id, - NULL ); - if ( workspace ) - { - FoobarWorkspaceFlags flags = foobar_workspace_get_flags( workspace ) | FOOBAR_WORKSPACE_FLAGS_URGENT; - foobar_workspace_set_flags( workspace, flags ); - } - - return G_SOURCE_REMOVE; -} - -// -// Handler for the "configreloaded" event. -// -gboolean foobar_workspace_service_handle_config_reloaded( EventData* data ) -{ - foobar_workspace_service_initialize( data->service ); - - return G_SOURCE_REMOVE; -} - -// --------------------------------------------------------------------------------------------------------------------- -// Helper Methods -// --------------------------------------------------------------------------------------------------------------------- - -// -// Implementation of the event thread. -// -// This thread listens on hyprland's event socket (socket.2.sock) for incoming event notifications and then submits -// their registered handlers (if any) to the main loop. -// -gpointer foobar_workspace_service_event_thread_func( gpointer userdata ) -{ - FoobarWorkspaceService* self = (FoobarWorkspaceService*)userdata; - - g_autoptr( GError ) error = NULL; - g_autoptr( GSocket ) sock = g_socket_new( - G_SOCKET_FAMILY_UNIX, - G_SOCKET_TYPE_STREAM, - G_SOCKET_PROTOCOL_DEFAULT, - &error ); - if ( !sock ) - { - g_warning( "Unable to create socket for hyprland communication: %s", error->message ); - return NULL; - } - - g_autoptr( GSocketAddress ) addr = g_unix_socket_address_new( self->rx_path ); - if ( !g_socket_connect( sock, addr, self->event_cancellable, &error ) && - !g_cancellable_is_cancelled( self->event_cancellable ) ) - { - g_warning( "Unable to connect socket to hyprland: %s", error->message ); - return NULL; - } - - // Each event is sent as a complete line, separated by a newline character. - - GString* prev_buf = NULL; // for longer, incomplete messages - gchar buf[1024]; - while ( !g_cancellable_is_cancelled( self->event_cancellable ) ) - { - // Synchronously receive data from the socket, but support cancellation using Gio. - - gssize received = g_socket_receive( sock, buf, sizeof( buf ), self->event_cancellable, &error ); - if ( error && !g_cancellable_is_cancelled( self->event_cancellable ) ) - { - g_warning( "Unable to receive events from hyprland: %s", error->message ); - } - if ( received <= 0 ) { break; } - - gchar* start = buf; - gchar* end = start + received; - while ( start < end ) - { - gchar* next_newline = g_strstr_len( start, end - start, "\n" ); - if ( !next_newline ) - { - // Incomplete message (missing newline) -> store it in the buffer - - prev_buf = ( prev_buf != NULL ) - ? g_string_append_len( prev_buf, start, end - start ) - : g_string_new_len( start, end - start ); - break; - } - - if ( prev_buf ) - { - // Prepend buffered data before the actual data. - - prev_buf = g_string_append_len( prev_buf, start, next_newline - start ); - foobar_workspace_service_dispatch_event( self, prev_buf->str ); - - g_string_free( prev_buf, TRUE ); - prev_buf = NULL; - } - else - { - // Skip allocation and use the buffer contents on the stack directly. - - *next_newline = '\0'; - foobar_workspace_service_dispatch_event( self, start ); - } - - start = next_newline + 1; - } - } - - if ( prev_buf ) { g_string_free( prev_buf, TRUE ); } - - return NULL; -} - -// -// Called when an entire event has been received on the event thread. -// -// This will look through the registered handlers and (if found) invoke the event's handler on the main thread. -// -void foobar_workspace_service_dispatch_event( - FoobarWorkspaceService* self, - gchar* message ) -{ - gchar* delimiter = strstr( message, ">>" ); - if ( !delimiter ) - { - g_warning( "Received an invalid message from hyprland: %s", message ); - return; - } - - *delimiter = '\0'; - gchar* name = message; - gchar* payload = delimiter + 2; - for ( gsize i = 0; i < G_N_ELEMENTS( event_handlers ); ++i ) - { - EventHandler handler = event_handlers[i]; - if ( !strcmp( name, handler.name ) ) - { - g_idle_add_full( - G_PRIORITY_DEFAULT, - G_SOURCE_FUNC( handler.fn ), - event_data_new( self, name, payload ), - (GDestroyNotify)event_data_free ); - break; - } - } -} - -// -// Synchronously send a command to hyprland's control socket (socket.sock), returning the entire response. -// -gchar* foobar_workspace_service_send_request( - FoobarWorkspaceService* self, - gchar const* request, - GError** error ) -{ - g_autoptr( GSocket ) sock = g_socket_new( - G_SOCKET_FAMILY_UNIX, - G_SOCKET_TYPE_STREAM, - G_SOCKET_PROTOCOL_DEFAULT, - error ); - if ( !sock ) { return NULL; } - - g_autoptr( GSocketAddress ) addr = g_unix_socket_address_new( self->tx_path ); - if ( !g_socket_connect( sock, addr, NULL, error ) ) { return NULL; } - - if ( g_socket_send( sock, request, strlen( request ), NULL, error ) == -1 ) { return NULL; } - - GString* str = g_string_sized_new( 0 ); - gchar buf[8192]; - while ( TRUE ) - { - gssize received = g_socket_receive( sock, buf, sizeof( buf ), self->event_cancellable, error ); - if ( received < 0 ) { return g_string_free( str, TRUE ); } - - if ( received == 0 ) { break; } - str = g_string_append_len( str, buf, received ); - } - - return g_string_free( str, FALSE ); -} - -// -// Synchronously send a command to hyprland's control socket (socket.sock), returning the parsed the JSON response. -// -JsonNode* foobar_workspace_service_send_json_request( - FoobarWorkspaceService* self, - gchar const* request, - GError** error ) -{ - g_autofree gchar* json = foobar_workspace_service_send_request( self, request, error ); - if ( !json ) { return NULL; } - - g_autoptr( JsonParser ) parser = json_parser_new( ); - if ( !json_parser_load_from_data( parser, json, (gssize)strlen( json ), error ) ) { return NULL; } - - return json_parser_steal_root( parser ); -} - -// -// Refresh the list of workspaces by actively sending a request to hyprland. -// -void foobar_workspace_service_initialize( FoobarWorkspaceService* self ) -{ - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->workspaces ) ); ++i ) - { - FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( self->workspaces ), i ); - workspace->service = NULL; - } - g_list_store_remove_all( self->workspaces ); - g_autoptr( GListStore ) workspaces = g_list_store_new( FOOBAR_TYPE_WORKSPACE ); - - { - g_autoptr( GError ) error = NULL; - g_autoptr( JsonNode ) workspaces_node = foobar_workspace_service_send_json_request( - self, - "j/workspaces", - &error ); - if ( !workspaces_node ) - { - g_warning( "Unable to load workspaces: %s", error->message ); - return; - } - - JsonArray* workspaces_array = json_node_get_array( workspaces_node ); - for ( guint i = 0; i < json_array_get_length( workspaces_array ); ++i ) - { - JsonObject* workspace_object = json_array_get_object_element( workspaces_array, i ); - foobar_workspace_service_add_workspace( self, workspaces, workspace_object, FALSE ); - } - } - - { - // Load configured persistent workspaces. - - g_autoptr( GError ) error = NULL; - g_autoptr( JsonNode ) rules_node = foobar_workspace_service_send_json_request( - self, - "j/workspacerules", - &error ); - if ( rules_node ) - { - JsonArray* rules_array = json_node_get_array( rules_node ); - for ( guint i = 0; i < json_array_get_length( rules_array ); ++i ) - { - JsonObject* rule_object = json_array_get_object_element( rules_array, i ); - gchar const* name = json_object_get_string_member_with_default( rule_object, "workspaceString", NULL ); - if ( !name ) - { - g_warning( "Found an invalid workspaceString value, skipping." ); - continue; - } - - if ( !json_object_get_boolean_member_with_default( rule_object, "persistent", FALSE ) ) { continue; } - - gint64 id = foobar_workspace_service_parse_id( name ); - json_object_set_int_member( rule_object, "id", id ); - json_object_set_string_member( rule_object, "name", name ); - - foobar_workspace_service_add_workspace( self, workspaces, rule_object, TRUE ); - } - } - else { g_warning( "Unable to load workspace rules: %s", error->message ); } - } - - foobar_workspace_service_update_active( self, workspaces ); - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( workspaces ) ); ++i ) - { - FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( workspaces ), i ); - g_list_store_append( self->workspaces, workspace ); - } -} - -// -// Update the currently active/visible workspaces by actively sending a request to hyprland. -// -void foobar_workspace_service_update_active( - FoobarWorkspaceService* self, - GListStore* workspaces ) -{ - g_autoptr( GArray ) visible_workspace_ids = g_array_new( FALSE, FALSE, sizeof( gint64 ) ); - gint64 active_workspace_id = 0; - gint64 active_special_workspace_id = 0; - - { - // Get all active workspaces - - g_autoptr( GError ) error = NULL; - g_autoptr( JsonNode ) monitors_node = foobar_workspace_service_send_json_request( - self, - "j/monitors", - &error ); - if ( monitors_node ) - { - JsonArray* monitors_array = json_node_get_array( monitors_node ); - for ( guint i = 0; i < json_array_get_length( monitors_array ); ++i ) - { - JsonObject* monitor_object = json_array_get_object_element( monitors_array, i ); - gint64 workspace_id = 0; - gint64 special_workspace_id = 0; - - if ( json_object_has_member( monitor_object, "activeWorkspace" ) ) - { - JsonObject* workspace_object = json_object_get_object_member( monitor_object, "activeWorkspace" ); - workspace_id = json_object_get_int_member_with_default( workspace_object, "id", 0 ); - if ( workspace_id ) { g_array_append_val( visible_workspace_ids, workspace_id ); } - } - - if ( json_object_has_member( monitor_object, "specialWorkspace" ) ) - { - JsonObject* workspace_object = json_object_get_object_member( monitor_object, "specialWorkspace" ); - special_workspace_id = json_object_get_int_member_with_default( workspace_object, "id", 0 ); - if ( special_workspace_id ) { g_array_append_val( visible_workspace_ids, special_workspace_id ); } - } - - // If this is the focused monitor, then the visible workspace is also the active one. - - if ( json_object_get_boolean_member_with_default( monitor_object, "focused", FALSE ) ) - { - active_workspace_id = workspace_id; - active_special_workspace_id = special_workspace_id; - } - } - } - else { g_warning( "Unable to load monitors: %s", error->message ); } - } - - // Sort visible workspaces to allow binary search. - - g_array_sort( visible_workspace_ids, foobar_workspace_service_compare_ids ); - - // Go through all workspaces and update their state. - - for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( workspaces ) ); ++i ) - { - FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( workspaces ), i ); - FoobarWorkspaceFlags flags = foobar_workspace_get_flags( workspace ); - gint64 id = foobar_workspace_get_id( workspace ); - - // Active - gboolean is_active = id == active_workspace_id || id == active_special_workspace_id; - flags = is_active ? flags | FOOBAR_WORKSPACE_FLAGS_ACTIVE : flags & ~FOOBAR_WORKSPACE_FLAGS_ACTIVE; - - // Disable urgency if workspace is active - if ( is_active ) { flags = flags & ~FOOBAR_WORKSPACE_FLAGS_URGENT; } - - // Visible - gboolean is_visible = g_array_binary_search( - visible_workspace_ids, - &id, - foobar_workspace_service_compare_ids, - NULL ); - flags = is_visible ? flags | FOOBAR_WORKSPACE_FLAGS_VISIBLE : flags & ~FOOBAR_WORKSPACE_FLAGS_VISIBLE; - - foobar_workspace_set_flags( workspace, flags ); - } -} - -// -// Add or update a workspace described by a JSON object. -// -void foobar_workspace_service_add_workspace( - FoobarWorkspaceService* self, - GListStore* workspaces, - JsonObject* workspace_object, - gboolean is_persistent ) -{ - gchar const* name = json_object_get_string_member( workspace_object, "name" ); - gint64 id = json_object_get_int_member( workspace_object, "id" ); - gboolean is_special = g_str_has_prefix( name, "special" ); - if ( g_str_has_prefix( name, "name:" ) ) { name += strlen( "name:" ); } - else if ( g_str_has_prefix( name, "special:" ) ) { name += strlen( "special:" ); } - - FoobarWorkspace* existing = foobar_workspace_service_find_workspace( G_LIST_MODEL( workspaces ), id, NULL ); - if ( existing ) - { - // Only update persistence. - - FoobarWorkspaceFlags flags = foobar_workspace_get_flags( existing ); - flags = is_persistent ? flags | FOOBAR_WORKSPACE_FLAGS_PERSISTENT : flags & ~FOOBAR_WORKSPACE_FLAGS_PERSISTENT; - foobar_workspace_set_flags( existing, flags ); - } - else - { - // Found a new workspace -> create a new object for it. - - FoobarWorkspaceFlags flags = FOOBAR_WORKSPACE_FLAGS_NONE; - flags = is_persistent ? flags | FOOBAR_WORKSPACE_FLAGS_PERSISTENT : flags; - flags = is_special ? flags | FOOBAR_WORKSPACE_FLAGS_SPECIAL : flags; - - g_autoptr( FoobarWorkspace ) workspace = foobar_workspace_new( self ); - foobar_workspace_set_id( workspace, id ); - foobar_workspace_set_name( workspace, name ); - foobar_workspace_set_monitor( workspace, json_object_get_string_member( workspace_object, "monitor" ) ); - foobar_workspace_set_flags( workspace, flags ); - g_list_store_append( workspaces, workspace ); - } -} - -// -// Find an existing workspace object by its ID. -// -FoobarWorkspace* foobar_workspace_service_find_workspace( - GListModel* workspaces, - gint64 id, - guint* out_index ) -{ - for ( guint i = 0; i < g_list_model_get_n_items( workspaces ); ++i ) - { - FoobarWorkspace* workspace = g_list_model_get_item( workspaces, i ); - if ( foobar_workspace_get_id( workspace ) == id ) - { - if ( out_index ) { *out_index = i; } - return workspace; - } - } - - return NULL; -} - -// -// Hyprland's IPC sometimes reports the creation of workspaces strangely named `special:special:<some_name>`. This -// function checks for that and is used to avoid creating (and then removing) such workspaces. -// -// See hyprwm/Hyprland#3424 for more info. -// -gboolean foobar_workspace_service_is_invalid_name( gchar const* name ) -{ - return g_str_has_prefix( name, "special:special:" ); -} - -// -// Parse an ID string and return its numeric code. -// -gint64 foobar_workspace_service_parse_id( gchar const* str ) -{ - if ( !g_strcmp0( str, "special" ) ) { return -99; } - - return strtoll( str, NULL, 10 ); -} - -// -// Get the base path for hyprland socket files. -// -gchar* foobar_workspace_service_get_hyprland_base_path( void ) -{ - gchar const* instance_signature = g_getenv( "HYPRLAND_INSTANCE_SIGNATURE" ); - if ( !instance_signature ) - { - return NULL; - } - - // for Hyprland >= 0.40 - gchar* result = g_strdup_printf( "%s/hypr/%s", g_get_user_runtime_dir( ), instance_signature ); - if ( g_file_test( result, G_FILE_TEST_EXISTS ) ) - { - return result; - } - - // for Hyprland < 0.40 - g_free( result ); - result = g_strdup_printf( "%s/hypr/%s", g_get_tmp_dir( ), instance_signature ); - if ( g_file_test( result, G_FILE_TEST_EXISTS ) ) - { - return result; - } - - g_free( result ); - return NULL; -} - -// -// Comparison function for two workspace IDs, used for binary search. -// -gint foobar_workspace_service_compare_ids( - gconstpointer a, - gconstpointer b ) -{ - gint64 const* a_int = a; - gint64 const* b_int = b; - - if ( *a_int < *b_int ) { return -1; } - if ( *a_int > *b_int ) { return 1; } - return 0; -} - -// -// Sorting callback for workspaces. Items are sorted to be in the following order: -// 1. normal -// 2. named persistent -// 3. named -// 4. special -// 5. named special -// -gint foobar_workspace_service_sort_func( - gconstpointer item_a, - gconstpointer item_b, - gpointer userdata ) -{ - (void)userdata; - - FoobarWorkspace* workspace_a = (FoobarWorkspace*)item_a; - FoobarWorkspace* workspace_b = (FoobarWorkspace*)item_b; - gint64 id_a = foobar_workspace_get_id( workspace_a ); - gint64 id_b = foobar_workspace_get_id( workspace_b ); - gboolean special_a = ( foobar_workspace_get_flags( workspace_a ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) != 0; - gboolean special_b = ( foobar_workspace_get_flags( workspace_b ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) != 0; - gboolean named_a = id_a < 0; - gboolean named_b = id_b < 0; - - // both normal (includes numbered persistent) => sort by ID - if ( !named_a && !named_b ) - { - if ( id_a < id_b ) { return -1; } - if ( id_a > id_b ) { return 1; } - return 0; - } - - // one normal, one special => normal first - if ( special_a && !special_b ) { return 1; } - if ( !special_a && special_b ) { return -1; } - - // only one normal, one named - if ( named_a && !named_b ) { return -1; } - if ( !named_a && named_b ) { return 1; } - - // both special - if ( special_a && special_b ) - { - // if one is -99 => put it last - if ( id_a == -99 && id_b != -99 ) { return 1; } - if ( id_a != -99 && id_b == -99 ) { return -1; } - } - - return g_strcmp0( foobar_workspace_get_name( workspace_a ), foobar_workspace_get_name( workspace_b ) ); -} - -// -// Create a new EventData structure. -// -static EventData* event_data_new( - FoobarWorkspaceService* service, - gchar const* name, - gchar const* payload ) -{ - EventData* self = g_new0( EventData, 1 ); - self->service = g_object_ref( service ); - self->name = g_strdup( name ); - self->payload = g_strdup( payload ); - return self; -} - -// -// Destroy an EventData structure. -// -static void event_data_free( EventData* self ) -{ - if ( self ) - { - g_object_unref( self->service ); - g_free( self->name ); - g_free( self->payload ); - g_free( self ); - } + return FOOBAR_WORKSPACE_SERVICE_GET_IFACE( self )->is_available( self ); } diff --git a/src/services/workspace-service.h b/src/services/workspace-service.h index c80e7dc..62259d7 100644 --- a/src/services/workspace-service.h +++ b/src/services/workspace-service.h @@ -26,7 +26,18 @@ typedef enum GType foobar_workspace_flags_get_type( void ); -G_DECLARE_FINAL_TYPE( FoobarWorkspace, foobar_workspace, FOOBAR, WORKSPACE, GObject ) +G_DECLARE_INTERFACE( FoobarWorkspace, foobar_workspace, FOOBAR, WORKSPACE, GObject ) + +struct _FoobarWorkspaceInterface +{ + GTypeInterface parent_iface; + + gint64 ( *get_id ) ( FoobarWorkspace* self ); + gchar const* ( *get_name ) ( FoobarWorkspace* self ); + gchar const* ( *get_monitor )( FoobarWorkspace* self ); + FoobarWorkspaceFlags ( *get_flags ) ( FoobarWorkspace* self ); + void ( *activate ) ( FoobarWorkspace* self ); +}; gint64 foobar_workspace_get_id ( FoobarWorkspace* self ); gchar const* foobar_workspace_get_name ( FoobarWorkspace* self ); @@ -34,9 +45,17 @@ gchar const* foobar_workspace_get_monitor( FoobarWorkspace* self ); FoobarWorkspaceFlags foobar_workspace_get_flags ( FoobarWorkspace* self ); void foobar_workspace_activate ( FoobarWorkspace* self ); -G_DECLARE_FINAL_TYPE( FoobarWorkspaceService, foobar_workspace_service, FOOBAR, WORKSPACE_SERVICE, GObject ) +G_DECLARE_INTERFACE( FoobarWorkspaceService, foobar_workspace_service, FOOBAR, WORKSPACE_SERVICE, GObject ) + +struct _FoobarWorkspaceServiceInterface +{ + GTypeInterface parent_iface; + + GListModel* ( *get_workspaces )( FoobarWorkspaceService* self ); + gboolean ( *is_available ) ( FoobarWorkspaceService* self ); +}; -FoobarWorkspaceService* foobar_workspace_service_new ( void ); -GListModel* foobar_workspace_service_get_workspaces( FoobarWorkspaceService* self ); +GListModel* foobar_workspace_service_get_workspaces( FoobarWorkspaceService* self ); +gboolean foobar_workspace_service_is_available ( FoobarWorkspaceService* self ); G_END_DECLS diff --git a/src/services/workspaces/hyprland-workspace-service.c b/src/services/workspaces/hyprland-workspace-service.c new file mode 100644 index 0000000..27a01b7 --- /dev/null +++ b/src/services/workspaces/hyprland-workspace-service.c @@ -0,0 +1,1368 @@ +#include "services/workspaces/hyprland-workspace-service.h" +#include "glib-object.h" +#include "services/workspace-service.h" +#include <json-glib/json-glib.h> +#include <gtk/gtk.h> +#include <string.h> + +typedef struct _EventData EventData; +typedef struct _EventHandler EventHandler; + +// +// FoobarHyprlandWorkspace: +// +// Representation of a workspace in Hyprland. +// + +#define FOOBAR_TYPE_HYPRLAND_WORKSPACE foobar_hyprland_workspace_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarHyprlandWorkspace, foobar_hyprland_workspace, FOOBAR, HYPRLAND_WORKSPACE, GObject ) + +struct _FoobarHyprlandWorkspace +{ + GObject parent_instance; + FoobarHyprlandWorkspaceService* service; + gint64 id; + gchar* name; + gchar* monitor; + FoobarWorkspaceFlags flags; +}; + +enum +{ + WORKSPACE_PROP_ID = 1, + WORKSPACE_PROP_NAME, + WORKSPACE_PROP_MONITOR, + WORKSPACE_PROP_FLAGS, + N_WORKSPACE_PROPS, +}; + +static void foobar_hyprland_workspace_class_init ( FoobarHyprlandWorkspaceClass* klass ); +static void foobar_hyprland_workspace_interface_init( FoobarWorkspaceInterface* iface ); +static void foobar_hyprland_workspace_init ( FoobarHyprlandWorkspace* self ); +static void foobar_hyprland_workspace_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_hyprland_workspace_finalize ( GObject* object ); +static FoobarHyprlandWorkspace* foobar_hyprland_workspace_new ( FoobarHyprlandWorkspaceService* service ); +static gint64 foobar_hyprland_workspace_get_id ( FoobarWorkspace* self ); +static gchar const* foobar_hyprland_workspace_get_name ( FoobarWorkspace* workspace ); +static gchar const* foobar_hyprland_workspace_get_monitor ( FoobarWorkspace* workspace ); +static FoobarWorkspaceFlags foobar_hyprland_workspace_get_flags ( FoobarWorkspace* workspace ); +static void foobar_hyprland_workspace_activate ( FoobarWorkspace* workspace ); +static void foobar_hyprland_workspace_set_id ( FoobarHyprlandWorkspace* self, + gint64 value ); +static void foobar_hyprland_workspace_set_name ( FoobarHyprlandWorkspace* self, + gchar const* value ); +static void foobar_hyprland_workspace_set_monitor ( FoobarHyprlandWorkspace* self, + gchar const* value ); +static void foobar_hyprland_workspace_set_flags ( FoobarHyprlandWorkspace* self, + FoobarWorkspaceFlags value ); + +G_DEFINE_TYPE_WITH_CODE( + FoobarHyprlandWorkspace, foobar_hyprland_workspace, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_WORKSPACE, foobar_hyprland_workspace_interface_init ) ); + +// +// FoobarHyprlandWorkspaceService: +// +// Service monitoring the workspaces provided by the window manager. This is implemented by communicating directly with +// Hyprland through its IPC socket. +// +// Based on the waybar implementation: +// https://github.com/Alexays/Waybar/blob/master/src/modules/hyprland/workspaces.cpp +// + +struct _FoobarHyprlandWorkspaceService +{ + GObject parent_instance; + GListStore* workspaces; + GtkSortListModel* sorted_workspaces; + GCancellable* event_cancellable; + GThread* event_thread; + gchar* rx_path; + gchar* tx_path; +}; + +enum +{ + PROP_WORKSPACES = 1, + N_PROPS, +}; + +static void foobar_hyprland_workspace_service_class_init ( FoobarHyprlandWorkspaceServiceClass* klass ); +static void foobar_hyprland_workspace_service_interface_init ( FoobarWorkspaceServiceInterface* iface ); +static void foobar_hyprland_workspace_service_init ( FoobarHyprlandWorkspaceService* self ); +static void foobar_hyprland_workspace_service_get_property ( GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ); +static void foobar_hyprland_workspace_service_dispose ( GObject* object ); +static void foobar_hyprland_workspace_service_finalize ( GObject* object ); +static GListModel* foobar_hyprland_workspace_service_get_workspaces ( FoobarWorkspaceService* service ); +static gboolean foobar_hyprland_workspace_service_is_available ( FoobarWorkspaceService* service ); +static gboolean foobar_hyprland_workspace_service_handle_workspace_created ( EventData* data ); +static gboolean foobar_hyprland_workspace_service_handle_workspace_destroyed ( EventData* data ); +static gboolean foobar_hyprland_workspace_service_handle_workspace_moved ( EventData* data ); +static gboolean foobar_hyprland_workspace_service_handle_workspace_renamed ( EventData* data ); +static gboolean foobar_hyprland_workspace_service_handle_workspace_activated ( EventData* data ); +static gboolean foobar_hyprland_workspace_service_handle_special_workspace_activated( EventData* data ); +static gboolean foobar_hyprland_workspace_service_handle_monitor_focused ( EventData* data ); +static gboolean foobar_hyprland_workspace_service_handle_window_urgent ( EventData* data ); +static gboolean foobar_hyprland_workspace_service_handle_config_reloaded ( EventData* data ); +static gpointer foobar_hyprland_workspace_service_event_thread_func ( gpointer userdata ); +static void foobar_hyprland_workspace_service_dispatch_event ( FoobarHyprlandWorkspaceService* self, + gchar* message ); +static gchar* foobar_hyprland_workspace_service_send_request ( FoobarHyprlandWorkspaceService* self, + gchar const* request, + GError** error ); +static JsonNode* foobar_hyprland_workspace_service_send_json_request ( FoobarHyprlandWorkspaceService* self, + gchar const* request, + GError** error ); +static void foobar_hyprland_workspace_service_initialize ( FoobarHyprlandWorkspaceService* self ); +static void foobar_hyprland_workspace_service_update_active ( FoobarHyprlandWorkspaceService* self, + GListStore* workspaces ); +static void foobar_hyprland_workspace_service_add_workspace ( FoobarHyprlandWorkspaceService* self, + GListStore* workspaces, + JsonObject* workspace_object, + gboolean is_persistent ); +static FoobarHyprlandWorkspace* foobar_hyprland_workspace_service_find_workspace ( GListModel* workspaces, + gint64 id, + guint* out_index ); +static gboolean foobar_hyprland_workspace_service_is_invalid_name ( gchar const* name ); +static gint64 foobar_hyprland_workspace_service_parse_id ( gchar const* str ); +static gchar* foobar_hyprland_workspace_service_get_hyprland_base_path ( void ); +static gint foobar_hyprland_workspace_service_compare_ids ( gconstpointer a, + gconstpointer b ); +static gint foobar_hyprland_workspace_service_sort_func ( gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ); + + +G_DEFINE_TYPE_WITH_CODE( + FoobarHyprlandWorkspaceService, foobar_hyprland_workspace_service, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE( FOOBAR_TYPE_WORKSPACE_SERVICE, foobar_hyprland_workspace_service_interface_init ) ); + +// +// EventData: +// +// Custom userdata passed to hyprland event handlers in a single pointer. +// + +struct _EventData +{ + FoobarHyprlandWorkspaceService* service; + gchar* name; + gchar* payload; +}; + +static EventData* event_data_new ( FoobarHyprlandWorkspaceService* service, + gchar const* name, + gchar const* payload ); +static void event_data_free( EventData* self ); + +// +// EventHandler: +// +// Description for the handler of a single hyprland event. +// + +struct _EventHandler +{ + gchar const* name; + gboolean ( *fn )( EventData* data ); +}; + +// +// Static list of registered event handlers. +// +static EventHandler const event_handlers[] = + { + { .name = "createworkspacev2", .fn = foobar_hyprland_workspace_service_handle_workspace_created }, + { .name = "destroyworkspacev2", .fn = foobar_hyprland_workspace_service_handle_workspace_destroyed }, + { .name = "moveworkspacev2", .fn = foobar_hyprland_workspace_service_handle_workspace_moved }, + { .name = "renameworkspace", .fn = foobar_hyprland_workspace_service_handle_workspace_renamed }, + { .name = "workspace", .fn = foobar_hyprland_workspace_service_handle_workspace_activated }, + { .name = "activespecial", .fn = foobar_hyprland_workspace_service_handle_special_workspace_activated }, + { .name = "focusedmon", .fn = foobar_hyprland_workspace_service_handle_monitor_focused }, + { .name = "urgent", .fn = foobar_hyprland_workspace_service_handle_window_urgent }, + { .name = "configreloaded", .fn = foobar_hyprland_workspace_service_handle_config_reloaded }, + }; + +// --------------------------------------------------------------------------------------------------------------------- +// Workspace +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for workspaces. +// +void foobar_hyprland_workspace_class_init( FoobarHyprlandWorkspaceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_hyprland_workspace_get_property; + object_klass->finalize = foobar_hyprland_workspace_finalize; + + g_object_class_override_property( object_klass, WORKSPACE_PROP_ID, "id" ); + g_object_class_override_property( object_klass, WORKSPACE_PROP_NAME, "name" ); + g_object_class_override_property( object_klass, WORKSPACE_PROP_MONITOR, "monitor" ); + g_object_class_override_property( object_klass, WORKSPACE_PROP_FLAGS, "flags" ); +} + +// +// Interface initialization for workspaces. +// +void foobar_hyprland_workspace_interface_init( FoobarWorkspaceInterface* iface ) +{ + iface->get_id = foobar_hyprland_workspace_get_id; + iface->get_name = foobar_hyprland_workspace_get_name; + iface->get_monitor = foobar_hyprland_workspace_get_monitor; + iface->get_flags = foobar_hyprland_workspace_get_flags; + iface->activate = foobar_hyprland_workspace_activate; +} + +// +// Instance initialization for workspaces. +// +void foobar_hyprland_workspace_init( FoobarHyprlandWorkspace* self ) +{ + (void)self; +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_hyprland_workspace_get_property( + GObject* object, + guint prop_id, + GValue* value, + GParamSpec* pspec ) +{ + FoobarHyprlandWorkspace* self = (FoobarHyprlandWorkspace*)object; + + switch ( prop_id ) + { + case WORKSPACE_PROP_ID: + g_value_set_int64( value, foobar_workspace_get_id( FOOBAR_WORKSPACE( self ) ) ); + break; + case WORKSPACE_PROP_NAME: + g_value_set_string( value, foobar_workspace_get_name( FOOBAR_WORKSPACE( self ) ) ); + break; + case WORKSPACE_PROP_MONITOR: + g_value_set_string( value, foobar_workspace_get_monitor( FOOBAR_WORKSPACE( self ) ) ); + break; + case WORKSPACE_PROP_FLAGS: + g_value_set_flags( value, foobar_workspace_get_flags( FOOBAR_WORKSPACE( self ) ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance cleanup for workspaces. +// +void foobar_hyprland_workspace_finalize( GObject* object ) +{ + FoobarHyprlandWorkspace* self = (FoobarHyprlandWorkspace*)object; + + g_clear_pointer( &self->name, g_free ); + g_clear_pointer( &self->monitor, g_free ); + + G_OBJECT_CLASS( foobar_hyprland_workspace_parent_class )->finalize( object ); +} + +// +// Create a new workspace that is owned by the given service (captured as an unowned reference). +// +FoobarHyprlandWorkspace* foobar_hyprland_workspace_new( FoobarHyprlandWorkspaceService* service ) +{ + FoobarHyprlandWorkspace* self = g_object_new( FOOBAR_TYPE_HYPRLAND_WORKSPACE, NULL ); + self->service = service; + return self; +} + +// +// Numeric ID of the workspace. +// +gint64 foobar_hyprland_workspace_get_id( FoobarWorkspace* workspace ) +{ + FoobarHyprlandWorkspace* self = (FoobarHyprlandWorkspace*)workspace; + return self->id; +} + +// +// Optional human-readable workspace name. +// +gchar const* foobar_hyprland_workspace_get_name( FoobarWorkspace* workspace ) +{ + FoobarHyprlandWorkspace* self = (FoobarHyprlandWorkspace*)workspace; + return self->name; +} + +// +// Connector ID of the workspace's monitor. +// +gchar const* foobar_hyprland_workspace_get_monitor( FoobarWorkspace* workspace ) +{ + FoobarHyprlandWorkspace* self = (FoobarHyprlandWorkspace*)workspace; + return self->monitor; +} + +// +// Flags indicating the current state of the workspace. +// +FoobarWorkspaceFlags foobar_hyprland_workspace_get_flags( FoobarWorkspace* workspace ) +{ + FoobarHyprlandWorkspace* self = (FoobarHyprlandWorkspace*)workspace; + return self->flags; +} + +// +// Let this workspace become the active one. +// +void foobar_hyprland_workspace_activate( FoobarWorkspace* workspace ) +{ + FoobarHyprlandWorkspace* self = (FoobarHyprlandWorkspace*)workspace; + + if ( !self->service ) { return; } + + g_autofree gchar* request = NULL; + if ( foobar_workspace_get_id( FOOBAR_WORKSPACE( self ) ) > 0 ) + { + // Normal + request = g_strdup_printf( "dispatch workspace %lld", (long long)foobar_workspace_get_id( FOOBAR_WORKSPACE( self ) ) ); + } + else if ( !( foobar_workspace_get_flags( FOOBAR_WORKSPACE( self ) ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) ) + { + // Named (this includes persistent) + request = g_strdup_printf( "dispatch workspace name:%s", foobar_workspace_get_name( FOOBAR_WORKSPACE( self ) ) ); + } + else if ( foobar_workspace_get_id( FOOBAR_WORKSPACE( self ) ) != -99 ) + { + // Named special + request = g_strdup_printf( "dispatch togglespecialworkspace %s", foobar_workspace_get_name( FOOBAR_WORKSPACE( self ) ) ); + } + else + { + // Special + request = g_strdup( "dispatch togglespecialworkspace" ); + } + + g_autoptr( GError ) error = NULL; + g_free( foobar_hyprland_workspace_service_send_request( self->service, request, &error ) ); + if ( error ) + { + g_warning( "Unable to activate workspace: %s", error->message ); + } +} + +// +// Numeric ID of the workspace. +// +void foobar_hyprland_workspace_set_id( + FoobarHyprlandWorkspace* self, + gint64 value ) +{ + g_return_if_fail( FOOBAR_IS_HYPRLAND_WORKSPACE( self ) ); + + if ( self->id != value ) + { + self->id = value; + g_object_notify( G_OBJECT( self ), "id" ); + } +} + +// +// Optional human-readable workspace name. +// +void foobar_hyprland_workspace_set_name( + FoobarHyprlandWorkspace* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_HYPRLAND_WORKSPACE( self ) ); + + if ( g_strcmp0( self->name, value ) ) + { + g_clear_pointer( &self->name, g_free ); + self->name = g_strdup( value ); + g_object_notify( G_OBJECT( self ), "name" ); + } +} + +// +// Connector ID of the workspace's monitor. +// +void foobar_hyprland_workspace_set_monitor( + FoobarHyprlandWorkspace* self, + gchar const* value ) +{ + g_return_if_fail( FOOBAR_IS_HYPRLAND_WORKSPACE( self ) ); + + if ( g_strcmp0( self->monitor, value ) ) + { + g_clear_pointer( &self->monitor, g_free ); + self->monitor = g_strdup( value ); + g_object_notify( G_OBJECT( self ), "monitor" ); + } +} + +// +// Flags indicating the current state of the workspace. +// +void foobar_hyprland_workspace_set_flags( + FoobarHyprlandWorkspace* self, + FoobarWorkspaceFlags value ) +{ + g_return_if_fail( FOOBAR_IS_HYPRLAND_WORKSPACE( self ) ); + + if ( self->flags != value ) + { + self->flags = value; + g_object_notify( G_OBJECT( self ), "flags" ); + } +} + +// --------------------------------------------------------------------------------------------------------------------- +// Service Implementation +// --------------------------------------------------------------------------------------------------------------------- + +// +// Static initialization for the workspace service. +// +void foobar_hyprland_workspace_service_class_init( FoobarHyprlandWorkspaceServiceClass* klass ) +{ + GObjectClass* object_klass = G_OBJECT_CLASS( klass ); + object_klass->get_property = foobar_hyprland_workspace_service_get_property; + object_klass->dispose = foobar_hyprland_workspace_service_dispose; + object_klass->finalize = foobar_hyprland_workspace_service_finalize; + + g_object_class_override_property( object_klass, PROP_WORKSPACES, "workspaces" ); +} + +// +// Interface initialization for the workspace service. +// +void foobar_hyprland_workspace_service_interface_init( FoobarWorkspaceServiceInterface* iface ) +{ + iface->get_workspaces = foobar_hyprland_workspace_service_get_workspaces; + iface->is_available = foobar_hyprland_workspace_service_is_available; +} + +// +// Instance initialization for the workspace service. +// +void foobar_hyprland_workspace_service_init( FoobarHyprlandWorkspaceService* self ) +{ + self->workspaces = g_list_store_new( FOOBAR_TYPE_WORKSPACE ); + + GtkCustomSorter* sorter = gtk_custom_sorter_new( foobar_hyprland_workspace_service_sort_func, NULL, NULL ); + self->sorted_workspaces = gtk_sort_list_model_new( + G_LIST_MODEL( g_object_ref( self->workspaces ) ), + GTK_SORTER( sorter ) ); + + g_autofree gchar* base_path = foobar_hyprland_workspace_service_get_hyprland_base_path( ); + if ( !base_path ) + { + return; + } + + self->tx_path = g_strdup_printf( "%s/.socket.sock", base_path ); + self->rx_path = g_strdup_printf( "%s/.socket2.sock", base_path ); + self->event_cancellable = g_cancellable_new( ); + self->event_thread = g_thread_new( "workspace-event-listener", foobar_hyprland_workspace_service_event_thread_func, self ); + + foobar_hyprland_workspace_service_initialize( self ); +} + +// +// Property getter implementation, mapping a property id to a method. +// +void foobar_hyprland_workspace_service_get_property( GObject* object, guint prop_id, GValue* value, GParamSpec* pspec ) +{ + FoobarHyprlandWorkspaceService* self = (FoobarHyprlandWorkspaceService*)object; + + switch ( prop_id ) + { + case PROP_WORKSPACES: + g_value_set_object( value, foobar_workspace_service_get_workspaces( FOOBAR_WORKSPACE_SERVICE( self ) ) ); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec ); + break; + } +} + +// +// Instance de-initialization for the workspace service. +// +// This is where the event thread is joined because it might still create new object references which is a bad thing to +// do in finalize. +// +void foobar_hyprland_workspace_service_dispose( GObject* object ) +{ + FoobarHyprlandWorkspaceService* self = (FoobarHyprlandWorkspaceService*)object; + + if ( self->event_thread ) + { + g_cancellable_cancel( self->event_cancellable ); + g_thread_join( self->event_thread ); + self->event_thread = NULL; + } + + G_OBJECT_CLASS( foobar_hyprland_workspace_service_parent_class )->dispose( object ); +} + +// +// Instance cleanup for the workspace service. +// +void foobar_hyprland_workspace_service_finalize( GObject* object ) +{ + FoobarHyprlandWorkspaceService* self = (FoobarHyprlandWorkspaceService*)object; + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->workspaces ) ); ++i ) + { + FoobarHyprlandWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( self->workspaces ), i ); + workspace->service = NULL; + } + g_clear_object( &self->event_cancellable ); + g_clear_object( &self->sorted_workspaces ); + g_clear_object( &self->workspaces ); + g_clear_pointer( &self->rx_path, g_free ); + g_clear_pointer( &self->tx_path, g_free ); + + G_OBJECT_CLASS( foobar_hyprland_workspace_service_parent_class )->finalize( object ); +} + +// +// Get a sorted list of all workspaces. +// +GListModel* foobar_hyprland_workspace_service_get_workspaces( FoobarWorkspaceService* service ) +{ + FoobarHyprlandWorkspaceService* self = (FoobarHyprlandWorkspaceService*)service; + return G_LIST_MODEL( self->sorted_workspaces ); +} + +// +// Check whether this service is connected to Hyprland. +// +gboolean foobar_hyprland_workspace_service_is_available( FoobarWorkspaceService* service ) +{ + FoobarHyprlandWorkspaceService* self = (FoobarHyprlandWorkspaceService*)service; + return self->event_thread != NULL; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------------------------------------------------- + +// +// Create a new Hyprland workspace service instance. +// +FoobarWorkspaceService* foobar_hyprland_workspace_service_new( void ) +{ + return g_object_new( FOOBAR_TYPE_HYPRLAND_WORKSPACE_SERVICE, NULL ); +} + +// --------------------------------------------------------------------------------------------------------------------- +// Event Handlers +// --------------------------------------------------------------------------------------------------------------------- + +// +// Handler for the "createworkspacev2" event. +// +// The payload has the format "<id>,<name>" where <id> is the numeric ID. +// +gboolean foobar_hyprland_workspace_service_handle_workspace_created( EventData* data ) +{ + gchar* save; + gchar const* id_str = strtok_r( data->payload, ",", &save ); + gchar const* name = strtok_r( NULL, ",", &save ); + g_return_val_if_fail( id_str && name, G_SOURCE_REMOVE ); + + if ( foobar_hyprland_workspace_service_is_invalid_name( name ) ) { return G_SOURCE_REMOVE; } + gint64 id = foobar_hyprland_workspace_service_parse_id( id_str ); + + // Get additional information about the workspace. + + g_autoptr( GError ) error = NULL; + g_autoptr( JsonNode ) workspaces_node = foobar_hyprland_workspace_service_send_json_request( + data->service, + "j/workspaces", + &error ); + if ( !workspaces_node ) + { + g_warning( "Unable to load workspaces: %s", error->message ); + return G_SOURCE_REMOVE; + } + + JsonArray* workspaces_array = json_node_get_array( workspaces_node ); + JsonObject* workspace_object = NULL; + for ( guint i = 0; i < json_array_get_length( workspaces_array ); ++i ) + { + JsonObject* it = json_array_get_object_element( workspaces_array, i ); + if ( id == json_object_get_int_member( it, "id" ) ) + { + workspace_object = it; + break; + } + } + + if ( !workspace_object ) + { + g_warning( "Unable to find new workspace." ); + return G_SOURCE_REMOVE; + } + + // Check if the workspace is persistent by looking for a rule with its ID. + + gboolean is_persistent = FALSE; + + g_autoptr( JsonNode ) rules_node = foobar_hyprland_workspace_service_send_json_request( + data->service, + "j/workspacerules", + &error ); + if ( !rules_node ) + { + g_warning( "Unable to load workspace rules: %s", error->message ); + return G_SOURCE_REMOVE; + } + + JsonArray* rules_array = json_node_get_array( rules_node ); + for ( guint i = 0; i < json_array_get_length( rules_array ); ++i ) + { + JsonObject* rule_object = json_array_get_object_element( rules_array, i ); + gchar const* rule_id_str = json_object_get_string_member_with_default( rule_object, "workspaceString", NULL ); + if ( id == foobar_hyprland_workspace_service_parse_id( rule_id_str ) ) + { + is_persistent = json_object_get_boolean_member_with_default( rule_object, "persistent", FALSE ); + break; + } + } + + foobar_hyprland_workspace_service_add_workspace( data->service, data->service->workspaces, workspace_object, is_persistent ); + foobar_hyprland_workspace_service_update_active( data->service, data->service->workspaces ); + return G_SOURCE_REMOVE; +} + +// +// Handler for the "destroyworkspacev2" event. +// +// The payload has the format "<id>,<name>" where <id> is the numeric ID. +// +gboolean foobar_hyprland_workspace_service_handle_workspace_destroyed( EventData* data ) +{ + gchar* save; + gchar const* id_str = strtok_r( data->payload, ",", &save ); + gchar const* name = strtok_r( NULL, ",", &save ); + g_return_val_if_fail( id_str && name, G_SOURCE_REMOVE ); + + if ( foobar_hyprland_workspace_service_is_invalid_name( name ) ) { return G_SOURCE_REMOVE; } + gint64 id = foobar_hyprland_workspace_service_parse_id( id_str ); + + GListStore* workspaces = data->service->workspaces; + guint index; + FoobarHyprlandWorkspace* workspace = foobar_hyprland_workspace_service_find_workspace( G_LIST_MODEL( workspaces ), id, &index ); + if ( workspace ) + { + workspace->service = NULL; + g_list_store_remove( workspaces, index ); + } + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "moveworkspacev2" event. +// +// The payload has the format "<id>,<name>,<monitor>" where <id> is the numeric ID of the workspace. +// +gboolean foobar_hyprland_workspace_service_handle_workspace_moved( EventData* data ) +{ + gchar* save; + gchar const* workspace_id_str = strtok_r( data->payload, ",", &save ); + gchar const* workspace_name = strtok_r( NULL, ",", &save ); + gchar const* monitor_name = strtok_r( NULL, ",", &save ); + g_return_val_if_fail( workspace_id_str && workspace_name && monitor_name, G_SOURCE_REMOVE ); + + gint64 workspace_id = foobar_hyprland_workspace_service_parse_id( workspace_id_str ); + FoobarHyprlandWorkspace* workspace = foobar_hyprland_workspace_service_find_workspace( + G_LIST_MODEL( data->service->workspaces ), + workspace_id, + NULL ); + if ( workspace ) + { + foobar_hyprland_workspace_set_monitor( workspace, monitor_name ); + g_signal_emit_by_name( data->service, "monitor-configuration-changed" ); + } + foobar_hyprland_workspace_service_update_active( data->service, data->service->workspaces ); + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "renameworkspace" event. +// +// The payload has the format "<id>,<name>" where <id> is the numeric ID of the workspace. +// +gboolean foobar_hyprland_workspace_service_handle_workspace_renamed( EventData* data ) +{ + gchar* save; + gchar const* id_str = strtok_r( data->payload, ",", &save ); + gchar const* new_name = strtok_r( NULL, ",", &save ); + g_return_val_if_fail( id_str && new_name, G_SOURCE_REMOVE ); + + gint64 id = foobar_hyprland_workspace_service_parse_id( id_str ); + FoobarHyprlandWorkspace* workspace = foobar_hyprland_workspace_service_find_workspace( + G_LIST_MODEL( data->service->workspaces ), + id, + NULL ); + if ( workspace ) + { + foobar_hyprland_workspace_set_name( workspace, new_name ); + gtk_sorter_changed( + gtk_sort_list_model_get_sorter( data->service->sorted_workspaces ), + GTK_SORTER_CHANGE_DIFFERENT ); + } + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "workspace" event. +// +gboolean foobar_hyprland_workspace_service_handle_workspace_activated( EventData* data ) +{ + foobar_hyprland_workspace_service_update_active( data->service, data->service->workspaces ); + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "activespecial" event. +// +gboolean foobar_hyprland_workspace_service_handle_special_workspace_activated( EventData* data ) +{ + foobar_hyprland_workspace_service_update_active( data->service, data->service->workspaces ); + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "focusedmon" event. +// +gboolean foobar_hyprland_workspace_service_handle_monitor_focused( EventData* data ) +{ + foobar_hyprland_workspace_service_update_active( data->service, data->service->workspaces ); + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "urgent" event. +// +// The payload has the format "<address>" where <address> is the sender window's address. +// +gboolean foobar_hyprland_workspace_service_handle_window_urgent( EventData* data ) +{ + gchar const* address = data->payload; + + // Find the client with the given address. + + g_autoptr( GError ) error = NULL; + g_autoptr( JsonNode ) clients_node = foobar_hyprland_workspace_service_send_json_request( + data->service, + "j/clients", + &error ); + if ( !clients_node ) + { + g_warning( "Unable to load clients: %s", error->message ); + return G_SOURCE_REMOVE; + } + + JsonArray* clients_array = json_node_get_array( clients_node ); + JsonObject* client_object = NULL; + for ( guint i = 0; i < json_array_get_length( clients_array ); ++i ) + { + JsonObject* it = json_array_get_object_element( clients_array, i ); + gchar const* it_address = json_object_get_string_member_with_default( it, "address", "" ); + if ( g_str_has_suffix( it_address, address ) ) + { + client_object = it; + break; + } + } + + if ( !client_object ) { return G_SOURCE_REMOVE; } + + // Find the workspace this window belongs to and mark it as "urgent". + + JsonObject* workspace_object = json_object_get_object_member( client_object, "workspace" ); + gint64 id = json_object_get_int_member( workspace_object, "id" ); + FoobarHyprlandWorkspace* workspace = foobar_hyprland_workspace_service_find_workspace( + G_LIST_MODEL( data->service->workspaces ), + id, + NULL ); + if ( workspace ) + { + FoobarWorkspaceFlags flags = foobar_workspace_get_flags( FOOBAR_WORKSPACE( workspace ) ) | FOOBAR_WORKSPACE_FLAGS_URGENT; + foobar_hyprland_workspace_set_flags( workspace, flags ); + } + + return G_SOURCE_REMOVE; +} + +// +// Handler for the "configreloaded" event. +// +gboolean foobar_hyprland_workspace_service_handle_config_reloaded( EventData* data ) +{ + foobar_hyprland_workspace_service_initialize( data->service ); + + return G_SOURCE_REMOVE; +} + +// --------------------------------------------------------------------------------------------------------------------- +// Helper Methods +// --------------------------------------------------------------------------------------------------------------------- + +// +// Implementation of the event thread. +// +// This thread listens on hyprland's event socket (socket.2.sock) for incoming event notifications and then submits +// their registered handlers (if any) to the main loop. +// +gpointer foobar_hyprland_workspace_service_event_thread_func( gpointer userdata ) +{ + FoobarHyprlandWorkspaceService* self = (FoobarHyprlandWorkspaceService*)userdata; + + g_autoptr( GError ) error = NULL; + g_autoptr( GSocket ) sock = g_socket_new( + G_SOCKET_FAMILY_UNIX, + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + &error ); + if ( !sock ) + { + g_warning( "Unable to create socket for hyprland communication: %s", error->message ); + return NULL; + } + + g_autoptr( GSocketAddress ) addr = g_unix_socket_address_new( self->rx_path ); + if ( !g_socket_connect( sock, addr, self->event_cancellable, &error ) && + !g_cancellable_is_cancelled( self->event_cancellable ) ) + { + g_warning( "Unable to connect socket to hyprland: %s", error->message ); + return NULL; + } + + // Each event is sent as a complete line, separated by a newline character. + + GString* prev_buf = NULL; // for longer, incomplete messages + gchar buf[1024]; + while ( !g_cancellable_is_cancelled( self->event_cancellable ) ) + { + // Synchronously receive data from the socket, but support cancellation using Gio. + + gssize received = g_socket_receive( sock, buf, sizeof( buf ), self->event_cancellable, &error ); + if ( error && !g_cancellable_is_cancelled( self->event_cancellable ) ) + { + g_warning( "Unable to receive events from hyprland: %s", error->message ); + } + if ( received <= 0 ) { break; } + + gchar* start = buf; + gchar* end = start + received; + while ( start < end ) + { + gchar* next_newline = g_strstr_len( start, end - start, "\n" ); + if ( !next_newline ) + { + // Incomplete message (missing newline) -> store it in the buffer + + prev_buf = ( prev_buf != NULL ) + ? g_string_append_len( prev_buf, start, end - start ) + : g_string_new_len( start, end - start ); + break; + } + + if ( prev_buf ) + { + // Prepend buffered data before the actual data. + + prev_buf = g_string_append_len( prev_buf, start, next_newline - start ); + foobar_hyprland_workspace_service_dispatch_event( self, prev_buf->str ); + + g_string_free( prev_buf, TRUE ); + prev_buf = NULL; + } + else + { + // Skip allocation and use the buffer contents on the stack directly. + + *next_newline = '\0'; + foobar_hyprland_workspace_service_dispatch_event( self, start ); + } + + start = next_newline + 1; + } + } + + if ( prev_buf ) { g_string_free( prev_buf, TRUE ); } + + return NULL; +} + +// +// Called when an entire event has been received on the event thread. +// +// This will look through the registered handlers and (if found) invoke the event's handler on the main thread. +// +void foobar_hyprland_workspace_service_dispatch_event( + FoobarHyprlandWorkspaceService* self, + gchar* message ) +{ + gchar* delimiter = strstr( message, ">>" ); + if ( !delimiter ) + { + g_warning( "Received an invalid message from hyprland: %s", message ); + return; + } + + *delimiter = '\0'; + gchar* name = message; + gchar* payload = delimiter + 2; + for ( gsize i = 0; i < G_N_ELEMENTS( event_handlers ); ++i ) + { + EventHandler handler = event_handlers[i]; + if ( !strcmp( name, handler.name ) ) + { + g_idle_add_full( + G_PRIORITY_DEFAULT, + G_SOURCE_FUNC( handler.fn ), + event_data_new( self, name, payload ), + (GDestroyNotify)event_data_free ); + break; + } + } +} + +// +// Synchronously send a command to hyprland's control socket (socket.sock), returning the entire response. +// +gchar* foobar_hyprland_workspace_service_send_request( + FoobarHyprlandWorkspaceService* self, + gchar const* request, + GError** error ) +{ + g_autoptr( GSocket ) sock = g_socket_new( + G_SOCKET_FAMILY_UNIX, + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + error ); + if ( !sock ) { return NULL; } + + g_autoptr( GSocketAddress ) addr = g_unix_socket_address_new( self->tx_path ); + if ( !g_socket_connect( sock, addr, NULL, error ) ) { return NULL; } + + if ( g_socket_send( sock, request, strlen( request ), NULL, error ) == -1 ) { return NULL; } + + GString* str = g_string_sized_new( 0 ); + gchar buf[8192]; + while ( TRUE ) + { + gssize received = g_socket_receive( sock, buf, sizeof( buf ), self->event_cancellable, error ); + if ( received < 0 ) { return g_string_free( str, TRUE ); } + + if ( received == 0 ) { break; } + str = g_string_append_len( str, buf, received ); + } + + return g_string_free( str, FALSE ); +} + +// +// Synchronously send a command to hyprland's control socket (socket.sock), returning the parsed the JSON response. +// +JsonNode* foobar_hyprland_workspace_service_send_json_request( + FoobarHyprlandWorkspaceService* self, + gchar const* request, + GError** error ) +{ + g_autofree gchar* json = foobar_hyprland_workspace_service_send_request( self, request, error ); + if ( !json ) { return NULL; } + + g_autoptr( JsonParser ) parser = json_parser_new( ); + if ( !json_parser_load_from_data( parser, json, (gssize)strlen( json ), error ) ) { return NULL; } + + return json_parser_steal_root( parser ); +} + +// +// Refresh the list of workspaces by actively sending a request to hyprland. +// +void foobar_hyprland_workspace_service_initialize( FoobarHyprlandWorkspaceService* self ) +{ + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( self->workspaces ) ); ++i ) + { + FoobarHyprlandWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( self->workspaces ), i ); + workspace->service = NULL; + } + g_list_store_remove_all( self->workspaces ); + g_autoptr( GListStore ) workspaces = g_list_store_new( FOOBAR_TYPE_WORKSPACE ); + + { + g_autoptr( GError ) error = NULL; + g_autoptr( JsonNode ) workspaces_node = foobar_hyprland_workspace_service_send_json_request( + self, + "j/workspaces", + &error ); + if ( !workspaces_node ) + { + g_warning( "Unable to load workspaces: %s", error->message ); + return; + } + + JsonArray* workspaces_array = json_node_get_array( workspaces_node ); + for ( guint i = 0; i < json_array_get_length( workspaces_array ); ++i ) + { + JsonObject* workspace_object = json_array_get_object_element( workspaces_array, i ); + foobar_hyprland_workspace_service_add_workspace( self, workspaces, workspace_object, FALSE ); + } + } + + { + // Load configured persistent workspaces. + + g_autoptr( GError ) error = NULL; + g_autoptr( JsonNode ) rules_node = foobar_hyprland_workspace_service_send_json_request( + self, + "j/workspacerules", + &error ); + if ( rules_node ) + { + JsonArray* rules_array = json_node_get_array( rules_node ); + for ( guint i = 0; i < json_array_get_length( rules_array ); ++i ) + { + JsonObject* rule_object = json_array_get_object_element( rules_array, i ); + gchar const* name = json_object_get_string_member_with_default( rule_object, "workspaceString", NULL ); + if ( !name ) + { + g_warning( "Found an invalid workspaceString value, skipping." ); + continue; + } + + if ( !json_object_get_boolean_member_with_default( rule_object, "persistent", FALSE ) ) { continue; } + + gint64 id = foobar_hyprland_workspace_service_parse_id( name ); + json_object_set_int_member( rule_object, "id", id ); + json_object_set_string_member( rule_object, "name", name ); + + foobar_hyprland_workspace_service_add_workspace( self, workspaces, rule_object, TRUE ); + } + } + else { g_warning( "Unable to load workspace rules: %s", error->message ); } + } + + foobar_hyprland_workspace_service_update_active( self, workspaces ); + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( workspaces ) ); ++i ) + { + FoobarWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( workspaces ), i ); + g_list_store_append( self->workspaces, workspace ); + } +} + +// +// Update the currently active/visible workspaces by actively sending a request to hyprland. +// +void foobar_hyprland_workspace_service_update_active( + FoobarHyprlandWorkspaceService* self, + GListStore* workspaces ) +{ + g_autoptr( GArray ) visible_workspace_ids = g_array_new( FALSE, FALSE, sizeof( gint64 ) ); + gint64 active_workspace_id = 0; + gint64 active_special_workspace_id = 0; + + { + // Get all active workspaces + + g_autoptr( GError ) error = NULL; + g_autoptr( JsonNode ) monitors_node = foobar_hyprland_workspace_service_send_json_request( + self, + "j/monitors", + &error ); + if ( monitors_node ) + { + JsonArray* monitors_array = json_node_get_array( monitors_node ); + for ( guint i = 0; i < json_array_get_length( monitors_array ); ++i ) + { + JsonObject* monitor_object = json_array_get_object_element( monitors_array, i ); + gint64 workspace_id = 0; + gint64 special_workspace_id = 0; + + if ( json_object_has_member( monitor_object, "activeWorkspace" ) ) + { + JsonObject* workspace_object = json_object_get_object_member( monitor_object, "activeWorkspace" ); + workspace_id = json_object_get_int_member_with_default( workspace_object, "id", 0 ); + if ( workspace_id ) { g_array_append_val( visible_workspace_ids, workspace_id ); } + } + + if ( json_object_has_member( monitor_object, "specialWorkspace" ) ) + { + JsonObject* workspace_object = json_object_get_object_member( monitor_object, "specialWorkspace" ); + special_workspace_id = json_object_get_int_member_with_default( workspace_object, "id", 0 ); + if ( special_workspace_id ) { g_array_append_val( visible_workspace_ids, special_workspace_id ); } + } + + // If this is the focused monitor, then the visible workspace is also the active one. + + if ( json_object_get_boolean_member_with_default( monitor_object, "focused", FALSE ) ) + { + active_workspace_id = workspace_id; + active_special_workspace_id = special_workspace_id; + } + } + } + else { g_warning( "Unable to load monitors: %s", error->message ); } + } + + // Sort visible workspaces to allow binary search. + + g_array_sort( visible_workspace_ids, foobar_hyprland_workspace_service_compare_ids ); + + // Go through all workspaces and update their state. + + for ( guint i = 0; i < g_list_model_get_n_items( G_LIST_MODEL( workspaces ) ); ++i ) + { + FoobarHyprlandWorkspace* workspace = g_list_model_get_item( G_LIST_MODEL( workspaces ), i ); + FoobarWorkspaceFlags flags = foobar_workspace_get_flags( FOOBAR_WORKSPACE( workspace ) ); + gint64 id = foobar_workspace_get_id( FOOBAR_WORKSPACE( workspace ) ); + + // Active + gboolean is_active = id == active_workspace_id || id == active_special_workspace_id; + flags = is_active ? flags | FOOBAR_WORKSPACE_FLAGS_ACTIVE : flags & ~FOOBAR_WORKSPACE_FLAGS_ACTIVE; + + // Disable urgency if workspace is active + if ( is_active ) { flags = flags & ~FOOBAR_WORKSPACE_FLAGS_URGENT; } + + // Visible + gboolean is_visible = g_array_binary_search( + visible_workspace_ids, + &id, + foobar_hyprland_workspace_service_compare_ids, + NULL ); + flags = is_visible ? flags | FOOBAR_WORKSPACE_FLAGS_VISIBLE : flags & ~FOOBAR_WORKSPACE_FLAGS_VISIBLE; + + foobar_hyprland_workspace_set_flags( workspace, flags ); + } +} + +// +// Add or update a workspace described by a JSON object. +// +void foobar_hyprland_workspace_service_add_workspace( + FoobarHyprlandWorkspaceService* self, + GListStore* workspaces, + JsonObject* workspace_object, + gboolean is_persistent ) +{ + gchar const* name = json_object_get_string_member( workspace_object, "name" ); + gint64 id = json_object_get_int_member( workspace_object, "id" ); + gboolean is_special = g_str_has_prefix( name, "special" ); + if ( g_str_has_prefix( name, "name:" ) ) { name += strlen( "name:" ); } + else if ( g_str_has_prefix( name, "special:" ) ) { name += strlen( "special:" ); } + + FoobarHyprlandWorkspace* existing = foobar_hyprland_workspace_service_find_workspace( G_LIST_MODEL( workspaces ), id, NULL ); + if ( existing ) + { + // Only update persistence. + + FoobarWorkspaceFlags flags = foobar_workspace_get_flags( FOOBAR_WORKSPACE( existing ) ); + flags = is_persistent ? flags | FOOBAR_WORKSPACE_FLAGS_PERSISTENT : flags & ~FOOBAR_WORKSPACE_FLAGS_PERSISTENT; + foobar_hyprland_workspace_set_flags( existing, flags ); + } + else + { + // Found a new workspace -> create a new object for it. + + FoobarWorkspaceFlags flags = FOOBAR_WORKSPACE_FLAGS_NONE; + flags = is_persistent ? flags | FOOBAR_WORKSPACE_FLAGS_PERSISTENT : flags; + flags = is_special ? flags | FOOBAR_WORKSPACE_FLAGS_SPECIAL : flags; + + g_autoptr( FoobarHyprlandWorkspace ) workspace = foobar_hyprland_workspace_new( self ); + foobar_hyprland_workspace_set_id( workspace, id ); + foobar_hyprland_workspace_set_name( workspace, name ); + foobar_hyprland_workspace_set_monitor( workspace, json_object_get_string_member( workspace_object, "monitor" ) ); + foobar_hyprland_workspace_set_flags( workspace, flags ); + g_list_store_append( workspaces, workspace ); + } +} + +// +// Find an existing workspace object by its ID. +// +FoobarHyprlandWorkspace* foobar_hyprland_workspace_service_find_workspace( + GListModel* workspaces, + gint64 id, + guint* out_index ) +{ + for ( guint i = 0; i < g_list_model_get_n_items( workspaces ); ++i ) + { + FoobarHyprlandWorkspace* workspace = g_list_model_get_item( workspaces, i ); + if ( foobar_workspace_get_id( FOOBAR_WORKSPACE( workspace ) ) == id ) + { + if ( out_index ) { *out_index = i; } + return workspace; + } + } + + return NULL; +} + +// +// Hyprland's IPC sometimes reports the creation of workspaces strangely named `special:special:<some_name>`. This +// function checks for that and is used to avoid creating (and then removing) such workspaces. +// +// See hyprwm/Hyprland#3424 for more info. +// +gboolean foobar_hyprland_workspace_service_is_invalid_name( gchar const* name ) +{ + return g_str_has_prefix( name, "special:special:" ); +} + +// +// Parse an ID string and return its numeric code. +// +gint64 foobar_hyprland_workspace_service_parse_id( gchar const* str ) +{ + if ( !g_strcmp0( str, "special" ) ) { return -99; } + + return strtoll( str, NULL, 10 ); +} + +// +// Get the base path for hyprland socket files. +// +gchar* foobar_hyprland_workspace_service_get_hyprland_base_path( void ) +{ + gchar const* instance_signature = g_getenv( "HYPRLAND_INSTANCE_SIGNATURE" ); + if ( !instance_signature ) + { + return NULL; + } + + // for Hyprland >= 0.40 + gchar* result = g_strdup_printf( "%s/hypr/%s", g_get_user_runtime_dir( ), instance_signature ); + if ( g_file_test( result, G_FILE_TEST_EXISTS ) ) + { + return result; + } + + // for Hyprland < 0.40 + g_free( result ); + result = g_strdup_printf( "%s/hypr/%s", g_get_tmp_dir( ), instance_signature ); + if ( g_file_test( result, G_FILE_TEST_EXISTS ) ) + { + return result; + } + + g_free( result ); + return NULL; +} + +// +// Comparison function for two workspace IDs, used for binary search. +// +gint foobar_hyprland_workspace_service_compare_ids( + gconstpointer a, + gconstpointer b ) +{ + gint64 const* a_int = a; + gint64 const* b_int = b; + + if ( *a_int < *b_int ) { return -1; } + if ( *a_int > *b_int ) { return 1; } + return 0; +} + +// +// Sorting callback for workspaces. Items are sorted to be in the following order: +// 1. normal +// 2. named persistent +// 3. named +// 4. special +// 5. named special +// +gint foobar_hyprland_workspace_service_sort_func( + gconstpointer item_a, + gconstpointer item_b, + gpointer userdata ) +{ + (void)userdata; + + FoobarWorkspace* workspace_a = (FoobarWorkspace*)item_a; + FoobarWorkspace* workspace_b = (FoobarWorkspace*)item_b; + gint64 id_a = foobar_workspace_get_id( workspace_a ); + gint64 id_b = foobar_workspace_get_id( workspace_b ); + gboolean special_a = ( foobar_workspace_get_flags( workspace_a ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) != 0; + gboolean special_b = ( foobar_workspace_get_flags( workspace_b ) & FOOBAR_WORKSPACE_FLAGS_SPECIAL ) != 0; + gboolean named_a = id_a < 0; + gboolean named_b = id_b < 0; + + // both normal (includes numbered persistent) => sort by ID + if ( !named_a && !named_b ) + { + if ( id_a < id_b ) { return -1; } + if ( id_a > id_b ) { return 1; } + return 0; + } + + // one normal, one special => normal first + if ( special_a && !special_b ) { return 1; } + if ( !special_a && special_b ) { return -1; } + + // only one normal, one named + if ( named_a && !named_b ) { return -1; } + if ( !named_a && named_b ) { return 1; } + + // both special + if ( special_a && special_b ) + { + // if one is -99 => put it last + if ( id_a == -99 && id_b != -99 ) { return 1; } + if ( id_a != -99 && id_b == -99 ) { return -1; } + } + + return g_strcmp0( foobar_workspace_get_name( workspace_a ), foobar_workspace_get_name( workspace_b ) ); +} + +// +// Create a new EventData structure. +// +static EventData* event_data_new( + FoobarHyprlandWorkspaceService* service, + gchar const* name, + gchar const* payload ) +{ + EventData* self = g_new0( EventData, 1 ); + self->service = g_object_ref( service ); + self->name = g_strdup( name ); + self->payload = g_strdup( payload ); + return self; +} + +// +// Destroy an EventData structure. +// +static void event_data_free( EventData* self ) +{ + if ( self ) + { + g_object_unref( self->service ); + g_free( self->name ); + g_free( self->payload ); + g_free( self ); + } +} diff --git a/src/services/workspaces/hyprland-workspace-service.h b/src/services/workspaces/hyprland-workspace-service.h new file mode 100644 index 0000000..6482a03 --- /dev/null +++ b/src/services/workspaces/hyprland-workspace-service.h @@ -0,0 +1,13 @@ +#pragma once + +#include "services/workspace-service.h" + +G_BEGIN_DECLS + +#define FOOBAR_TYPE_HYPRLAND_WORKSPACE_SERVICE foobar_hyprland_workspace_service_get_type( ) + +G_DECLARE_FINAL_TYPE( FoobarHyprlandWorkspaceService, foobar_hyprland_workspace_service, FOOBAR, HYPRLAND_WORKSPACE_SERVICE, GObject ) + +FoobarWorkspaceService* foobar_hyprland_workspace_service_new( void ); + +G_END_DECLS diff --git a/src/services/workspaces/meson.build b/src/services/workspaces/meson.build new file mode 100644 index 0000000..7de31dd --- /dev/null +++ b/src/services/workspaces/meson.build @@ -0,0 +1,3 @@ +foobar_sources += files( + 'hyprland-workspace-service.c', +) |
