diff --git a/meson.build b/meson.build index 0c6070b..93b3f65 100644 --- a/meson.build +++ b/meson.build @@ -13,6 +13,7 @@ gio_unix = dependency('gio-unix-2.0') glib = dependency('glib-2.0', version: '>= 2.44' ) +gmodule_export = dependency('gmodule-export-2.0') gtk4 = dependency('gtk4', version: '>= 4.14.0', ) diff --git a/src/jogg-application-window.c b/src/jogg-application-window.c index 452e9e9..fdf3168 100644 --- a/src/jogg-application-window.c +++ b/src/jogg-application-window.c @@ -17,7 +17,7 @@ struct _JoggApplicationWindow GtkSingleSelection *model; GtkFilterListModel *filter_model; GListStore *applications; - GtkCustomFilter *filter; + GtkCustomSorter *custom_sorter; GtkWidget *revealer; GtkWidget *revealer_box; GtkWidget *search_entry; @@ -108,42 +108,24 @@ jogg_application_window_search_entry_on_search_changed (GtkSearchEntry *self, gpointer user_data) { JoggApplicationWindow *window = NULL; + size_t n = 0; + GtkApplication *application = NULL; const char *text = NULL; - g_autofree GStrv *app_infos = NULL; - g_autoptr (GPtrArray) desktop_app_infos = NULL; - size_t n; + g_autoptr (GPtrArray) results = NULL; window = JOGG_APPLICATION_WINDOW (user_data); - text = gtk_editable_get_text (GTK_EDITABLE (self)); - app_infos = g_desktop_app_info_search (text); - desktop_app_infos = g_ptr_array_new_full (64, g_object_unref); n = g_list_model_get_n_items (G_LIST_MODEL (window->applications)); + application = gtk_window_get_application (GTK_WINDOW (window)); + text = gtk_editable_get_text (GTK_EDITABLE (self)); + results = jogg_application_app_info_search (JOGG_APPLICATION (application), text); - g_list_store_splice (window->applications, 0, n, NULL, 0); - - for (size_t i = 0; app_infos[i] != NULL; i++) - { - for (size_t j = 0; app_infos[i][j] != NULL; j++) - { - GDesktopAppInfo *app_info = NULL; - JoggResult *result = NULL; - - app_info = g_desktop_app_info_new (app_infos[i][j]); - if (app_info == NULL) - { - continue; - } - result = jogg_result_new (app_info); - - g_ptr_array_add (desktop_app_infos, result); - } - - g_strfreev (app_infos[i]); - } + g_list_store_splice (window->applications + , 0 + , n + , results->pdata + , results->len + ); - g_list_store_splice (window->applications, 0, 0, - desktop_app_infos->pdata, - desktop_app_infos->len); } static void @@ -203,15 +185,12 @@ jogg_application_window_results_on_activate ( GtkListView *self jogg_application_window_quit (user_data); } -static gboolean -jogg_application_window_app_info_filter_func ( gpointer item - , gpointer user_data) +static gint +jogg_application_window_result_sort_func ( gconstpointer a + , gconstpointer b + , gpointer user_data) { - g_autoptr (GDesktopAppInfo) app_info = NULL; - - app_info = jogg_result_get_app_info (item); - - return !g_desktop_app_info_get_nodisplay (app_info); + return jogg_result_compare (a, b); } static gboolean @@ -336,11 +315,11 @@ jogg_application_window_init (JoggApplicationWindow *self) gtk_search_entry_set_key_capture_widget ( GTK_SEARCH_ENTRY (self->search_entry) , self->results ); - gtk_custom_filter_set_filter_func ( self->filter - , jogg_application_window_app_info_filter_func - , NULL - , NULL - ); + gtk_custom_sorter_set_sort_func ( self->custom_sorter + , jogg_application_window_result_sort_func + , NULL + , NULL + ); controller = gtk_event_controller_key_new (); @@ -408,7 +387,7 @@ jogg_application_window_class_init (JoggApplicationWindowClass *klass) applications); gtk_widget_class_bind_template_child (widget_class, JoggApplicationWindow, - filter); + custom_sorter); gtk_widget_class_bind_template_child (widget_class, JoggApplicationWindow, revealer); diff --git a/src/jogg-application.c b/src/jogg-application.c index 2b1fbb0..57e5f0b 100644 --- a/src/jogg-application.c +++ b/src/jogg-application.c @@ -6,12 +6,17 @@ #include "jogg-application.h" #include "jogg-application-window.h" +#include "jogg-result.h" +#include "jogg-utils.h" +#include #include struct _JoggApplication { GtkApplication parent_instance; + + GList *applications; }; G_DEFINE_TYPE (JoggApplication, jogg_application, GTK_TYPE_APPLICATION); @@ -19,6 +24,7 @@ G_DEFINE_TYPE (JoggApplication, jogg_application, GTK_TYPE_APPLICATION); static void jogg_application_init (JoggApplication *self) { + self->applications = g_app_info_get_all (); } static void @@ -41,6 +47,16 @@ jogg_application_window_on_realize (GtkWidget *self, geometry.height / 3); } +static void +jogg_application_finalize (GObject *object) +{ + JoggApplication *self = NULL; + + self = JOGG_APPLICATION (object); + + g_list_free_full (g_steal_pointer (&self->applications), g_object_unref); +} + static void jogg_application_activate (GApplication *app) { @@ -76,11 +92,102 @@ jogg_application_activate (GApplication *app) static void jogg_application_class_init (JoggApplicationClass *klass) { + GObjectClass *object_class = G_OBJECT_CLASS (klass); GApplicationClass *app_class = G_APPLICATION_CLASS (klass); + object_class->finalize = jogg_application_finalize; app_class->activate = jogg_application_activate; } +GPtrArray * +jogg_application_app_info_search ( JoggApplication *self + , const char *query) +{ + GPtrArray *results = NULL; + + g_return_val_if_fail (JOGG_IS_APPLICATION (self), NULL); + g_return_val_if_fail (query != NULL, NULL); + + results = g_ptr_array_new_full (64, g_object_unref); + + if (*query == '\0') + { + return results; + } + + for (GList *l = self->applications; l != NULL; l = g_list_next (l)) + { + GDesktopAppInfo *app_info = NULL; + const char *haystack = NULL; + bool prefix = false; + const char *const *haystacks = NULL; + JoggMatchType match_type = JOGG_MATCH_TYPE_INVALID; + JoggResult *result = NULL; + + app_info = G_DESKTOP_APP_INFO (l->data); + haystack = g_app_info_get_name (G_APP_INFO (app_info)); + if (jogg_has_substring (haystack, query, &prefix)) + { + match_type = JOGG_MATCH_TYPE_NAME; + + goto match_found; + } + haystack = g_app_info_get_display_name (G_APP_INFO (app_info)); + if (jogg_has_substring (haystack, query, &prefix)) + { + match_type = JOGG_MATCH_TYPE_GENERIC_NAME; + + goto match_found; + } + haystacks = g_desktop_app_info_get_keywords (app_info); + for (; haystacks != NULL && *haystacks != NULL; haystacks++) + { + haystack = *haystacks; + + if (jogg_has_substring (haystack, query, &prefix)) + { + match_type = JOGG_MATCH_TYPE_KEYWORDS; + + goto match_found; + } + } + haystack = g_app_info_get_executable (G_APP_INFO (app_info)); + if (jogg_has_substring (haystack, query, &prefix)) + { + match_type = JOGG_MATCH_TYPE_EXEC; + } + +match_found: + if (match_type != JOGG_MATCH_TYPE_INVALID) + { + result = jogg_result_new (app_info, NULL, match_type, prefix); + + g_ptr_array_add (results, result); + + continue; + } + + haystacks = g_desktop_app_info_list_actions (app_info); + for (; haystacks != NULL && *haystacks != NULL; haystacks++) + { + haystack = g_desktop_app_info_get_action_name (app_info, *haystacks); + + if (jogg_has_substring (haystack, query, &prefix)) + { + result = jogg_result_new ( app_info + , haystack + , JOGG_MATCH_TYPE_ACTIONS + , prefix + ); + + g_ptr_array_add (results, result); + } + } + } + + return results; +} + JoggApplication * jogg_application_new (void) { diff --git a/src/jogg-application.h b/src/jogg-application.h index 076b2ce..dc2ca01 100644 --- a/src/jogg-application.h +++ b/src/jogg-application.h @@ -16,6 +16,9 @@ G_DECLARE_FINAL_TYPE (JoggApplication, jogg_application, JOGG, APPLICATION, GtkApplication); +GPtrArray *jogg_application_app_info_search ( JoggApplication *self + , const char *query); + JoggApplication *jogg_application_new (void); G_END_DECLS diff --git a/src/jogg-enum-types.c.template b/src/jogg-enum-types.c.template new file mode 100644 index 0000000..184ac76 --- /dev/null +++ b/src/jogg-enum-types.c.template @@ -0,0 +1,26 @@ +/*** BEGIN file-header ***/ +/* + * SPDX-License-Identifier: GPL-2.0-only + * + * Copyright (C) 2024 Ernestas Kulik + */ + +#include "jogg-enum-types.h" +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* Enumerations from "@basename@" */ +#include "@basename@" +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +G_DEFINE_ENUM_TYPE ( @EnumName@ + , @enum_name@ +/*** END value-header ***/ +/*** BEGIN value-production ***/ + , G_DEFINE_ENUM_VALUE (@VALUENAME@, "@valuenick@") +/*** END value-production ***/ +/*** BEGIN value-tail ***/ + ); +/*** END value-tail ***/ diff --git a/src/jogg-enum-types.h.template b/src/jogg-enum-types.h.template new file mode 100644 index 0000000..535460b --- /dev/null +++ b/src/jogg-enum-types.h.template @@ -0,0 +1,30 @@ +/*** BEGIN file-header ***/ +/* + * SPDX-License-Identifier: GPL-2.0-only + * + * Copyright (C) 2024 Ernestas Kulik + */ + +#pragma once + +#include + +G_BEGIN_DECLS +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* Enumerations from "@basename@" */ + +/*** END file-production ***/ + +/*** BEGIN enumeration-production ***/ +#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) +GType @enum_name@_get_type (void); + +/*** END enumeration-production ***/ + +/*** BEGIN file-tail ***/ + +G_END_DECLS +/*** END file-tail ***/ diff --git a/src/jogg-enums.h b/src/jogg-enums.h new file mode 100644 index 0000000..3e988e1 --- /dev/null +++ b/src/jogg-enums.h @@ -0,0 +1,23 @@ +#pragma once + +/* + * SPDX-License-Identifier: GPL-2.0-only + * + * Copyright (C) 2024 Ernestas Kulik + */ + +#include + +G_BEGIN_DECLS + +typedef enum +{ + JOGG_MATCH_TYPE_INVALID, + JOGG_MATCH_TYPE_NAME, + JOGG_MATCH_TYPE_GENERIC_NAME, + JOGG_MATCH_TYPE_ACTIONS, + JOGG_MATCH_TYPE_KEYWORDS, + JOGG_MATCH_TYPE_EXEC, +} JoggMatchType; + +G_END_DECLS diff --git a/src/jogg-result.c b/src/jogg-result.c index 80c6291..3e51b16 100644 --- a/src/jogg-result.c +++ b/src/jogg-result.c @@ -4,6 +4,7 @@ * Copyright (C) 2024 Ernestas Kulik */ +#include "jogg-enum-types.h" #include "jogg-result.h" struct _JoggResult @@ -11,26 +12,57 @@ struct _JoggResult GObject parent_instance; GDesktopAppInfo *app_info; + char *action; + JoggMatchType match_type; + bool prefix_match; }; -enum -{ - PROP_APP_INFO = 1, - PROP_ICON, - PROP_LABEL, - N_PROPERTIES, -}; +enum { PROP_APP_INFO = 1 + , PROP_ACTION + , PROP_MATCH_TYPE + , PROP_PREFIX_MATCH + , PROP_HIDDEN + , PROP_ICON + , PROP_LABEL + , N_PROPERTIES + }; static GParamSpec *properties[N_PROPERTIES]; G_DEFINE_TYPE (JoggResult, jogg_result, G_TYPE_OBJECT); +gboolean +jogg_result_is_action_visible (GObject *object, + JoggResult *self) +{ + JoggMatchType match_type; + + if (NULL == self) + { + return FALSE; + } + + match_type = jogg_result_get_match_type (self); + + return match_type == JOGG_MATCH_TYPE_ACTIONS; +} + static void jogg_result_init (JoggResult *self) { (void) self; } +static void +jogg_result_finalize (GObject *object) +{ + JoggResult *self = NULL; + + self = JOGG_RESULT (object); + + g_free (g_steal_pointer (&self->action)); +} + static void jogg_result_get_property ( GObject *object , guint property_id @@ -38,6 +70,7 @@ jogg_result_get_property ( GObject *object , GParamSpec *pspec) { JoggResult *self = NULL; + gboolean nodisplay = false; GIcon *icon = NULL; const char *name = NULL; @@ -48,6 +81,20 @@ jogg_result_get_property ( GObject *object case PROP_APP_INFO: g_value_set_object (value, self->app_info); break; + case PROP_ACTION: + g_value_set_string (value, self->action); + break; + case PROP_MATCH_TYPE: + g_value_set_enum (value, self->match_type); + break; + case PROP_PREFIX_MATCH: + g_value_set_boolean (value, self->prefix_match); + break; + case PROP_HIDDEN: + nodisplay = g_desktop_app_info_get_nodisplay (self->app_info); + + g_value_set_boolean (value, nodisplay); + break; case PROP_ICON: icon = g_app_info_get_icon (G_APP_INFO (self->app_info)); @@ -79,6 +126,15 @@ jogg_result_set_property ( GObject *object case PROP_APP_INFO: self->app_info = g_value_get_object (value); break; + case PROP_ACTION: + self->action = g_value_dup_string (value); + break; + case PROP_MATCH_TYPE: + self->match_type = g_value_get_enum (value); + break; + case PROP_PREFIX_MATCH: + self->prefix_match = g_value_get_boolean (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -101,6 +157,42 @@ jogg_result_class_init (JoggResultClass *klass) | G_PARAM_STATIC_STRINGS ) ); + properties[PROP_ACTION] = g_param_spec_string ( "action" + , NULL + , NULL + , NULL + , ( G_PARAM_READWRITE + | G_PARAM_CONSTRUCT_ONLY + | G_PARAM_STATIC_STRINGS + ) + ); + properties[PROP_MATCH_TYPE] = g_param_spec_enum ( "match-type" + , NULL + , NULL + , JOGG_TYPE_MATCH_TYPE + , JOGG_MATCH_TYPE_INVALID + , ( G_PARAM_READWRITE + | G_PARAM_CONSTRUCT_ONLY + | G_PARAM_STATIC_STRINGS + ) + ); + properties[PROP_PREFIX_MATCH] = g_param_spec_boolean ( "prefix-match" + , NULL + , NULL + , FALSE + , ( G_PARAM_READWRITE + | G_PARAM_CONSTRUCT_ONLY + | G_PARAM_STATIC_STRINGS + ) + ); + properties[PROP_HIDDEN] = g_param_spec_boolean ( "hidden" + , NULL + , NULL + , FALSE + , ( G_PARAM_READABLE + | G_PARAM_STATIC_STRINGS + ) + ); properties[PROP_ICON] = g_param_spec_object ( "icon" , NULL , NULL @@ -118,6 +210,7 @@ jogg_result_class_init (JoggResultClass *klass) ) ); + object_class->finalize = jogg_result_finalize; object_class->get_property = jogg_result_get_property; object_class->set_property = jogg_result_set_property; @@ -135,13 +228,51 @@ jogg_result_get_app_info (JoggResult *self) return g_object_ref (self->app_info); } +bool +jogg_result_is_prefix_match (JoggResult *self) +{ + g_return_val_if_fail (JOGG_IS_RESULT (self), false); + + return self->prefix_match; +} + +JoggMatchType +jogg_result_get_match_type (JoggResult *self) +{ + g_return_val_if_fail (JOGG_IS_RESULT (self), JOGG_MATCH_TYPE_INVALID); + + return self->match_type; +} + +GtkOrdering +jogg_result_compare ( const JoggResult *lhs + , const JoggResult *rhs) +{ + if (lhs->match_type < rhs->match_type) + { + return GTK_ORDERING_SMALLER; + } + else if (lhs->match_type > rhs->match_type) + { + return GTK_ORDERING_LARGER; + } + + return GTK_ORDERING_EQUAL; +} + JoggResult * -jogg_result_new (GDesktopAppInfo *app_info) +jogg_result_new ( GDesktopAppInfo *app_info + , const char *action + , JoggMatchType match_type + , bool prefix_match) { g_return_val_if_fail (app_info != NULL, NULL); return g_object_new ( JOGG_TYPE_RESULT + , "action", action , "app-info", app_info + , "match-type", match_type + , "prefix-match", prefix_match , NULL ); } diff --git a/src/jogg-result.h b/src/jogg-result.h index dbf17a0..c156cd9 100644 --- a/src/jogg-result.h +++ b/src/jogg-result.h @@ -6,10 +6,13 @@ #pragma once +#include "jogg-enums.h" #include "jogg-types.h" #include #include +#include +#include G_BEGIN_DECLS @@ -20,8 +23,16 @@ G_DECLARE_FINAL_TYPE ( JoggResult , GObject ); -GDesktopAppInfo *jogg_result_get_app_info (JoggResult *); +GDesktopAppInfo *jogg_result_get_app_info (JoggResult *self); +bool jogg_result_is_prefix_match (JoggResult *self); +JoggMatchType jogg_result_get_match_type (JoggResult *self); -JoggResult *jogg_result_new (GDesktopAppInfo *); +GtkOrdering jogg_result_compare ( const JoggResult *lhs + , const JoggResult *rhs); + +JoggResult *jogg_result_new ( GDesktopAppInfo *app_info + , const char *action + , JoggMatchType match_type + , bool prefix_match); G_END_DECLS diff --git a/src/jogg-utils.c b/src/jogg-utils.c new file mode 100644 index 0000000..9725fc5 --- /dev/null +++ b/src/jogg-utils.c @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: GPL-2.0-only + * + * Copyright (C) 2024 Ernestas Kulik + */ + +#include "jogg-utils.h" + +#include + +bool +jogg_has_substring ( const char *haystack + , const char *needle + , bool *prefix_match) +{ + g_autofree char *haystack_folded = NULL; + g_autofree char *needle_folded = NULL; + const char *p = NULL; + + if (NULL == haystack) + { + return false; + } + if (NULL == needle) + { + return false; + } + + haystack_folded = g_utf8_casefold (haystack, -1); + needle_folded = g_utf8_casefold (needle, -1); + p = g_strstr_len (haystack_folded, -1, needle_folded); + if (NULL == p) + { + return false; + } + + if (prefix_match != NULL) + { + *prefix_match = (p == haystack_folded); + } + + return true; +} diff --git a/src/jogg-utils.h b/src/jogg-utils.h new file mode 100644 index 0000000..8054fc5 --- /dev/null +++ b/src/jogg-utils.h @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: GPL-2.0-only + * + * Copyright (C) 2024 Ernestas Kulik + */ + +#pragma once + +#include + +bool jogg_has_substring ( const char *haystack + , const char *needle + , bool *prefix); diff --git a/src/meson.build b/src/meson.build index 7dee94e..5b495f8 100644 --- a/src/meson.build +++ b/src/meson.build @@ -7,13 +7,24 @@ jogg_sources = [ source_dir: 'res', c_name: 'jogg' ), + gnome.mkenums( + 'jogg-enum-types', + c_template: 'jogg-enum-types.c.template', + h_template: 'jogg-enum-types.h.template', + sources: [ + 'jogg-enums.h', + ], + ), 'jogg-application.c', 'jogg-application.h', 'jogg-application-window.c', 'jogg-application-window.h', + 'jogg-enums.h', 'jogg-result.c', 'jogg-result.h', 'jogg-types.h', + 'jogg-utils.c', + 'jogg-utils.h', 'main.c', ] @@ -22,8 +33,10 @@ executable('jogg', dependencies: [ gio_unix, glib, + gmodule_export, gtk4_layer_shell, gtk4, ], + export_dynamic: true, install: true, ) diff --git a/src/res/ui/result.ui b/src/res/ui/result.ui index d90f446..fb21c1a 100644 --- a/src/res/ui/result.ui +++ b/src/res/ui/result.ui @@ -21,12 +21,46 @@ - - - - GtkListItem - - + + horizontal + 6 + + + + + GtkListItem + + + + + + + horizontal + 6 + + + GtkListItem + + + + + + pan-end-symbolic + + + + + + + GtkListItem + + + + + + diff --git a/src/res/ui/window.ui b/src/res/ui/window.ui index c0e3aa4..f3ca54a 100644 --- a/src/res/ui/window.ui +++ b/src/res/ui/window.ui @@ -44,14 +44,38 @@ - + - - JoggResult + + + + JoggResult + + + + + + + + true + + - - + + + + + + + + + + + descending + + +