From f2bcec31e13599bff9bff1f05df1649a3d688c9b Mon Sep 17 00:00:00 2001 From: Alexander Vanhee <160625516+AlexanderVanhee@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:35:44 +0200 Subject: [PATCH 1/3] Add daemon process The daemon process is a wrapper around the actual Bazaar application, it's responsibilities are managing the application, forwarding launches with parameters (except --no-window) and implementing the search provider (also auto-updates in the future). The search provider part consumes a ~2mb file with the data needed for the search engine, its generated after the entry-group cache. The daemon does not link to gobject library to keep the memory footprint as low as possible, around 8 mb with a further 12mb for Flatpak stuff --- .../flatpak/io.github.kolunmi.Bazaar.json | 2 +- data/dbus/io.github.kolunmi.Bazaar.service.in | 2 +- data/io.github.kolunmi.Bazaar.desktop.in | 4 +- ...thub.kolunmi.Bazaar.search-provider.ini.in | 2 +- data/io.github.kolunmi.Bazaar.service.in | 2 +- meson.build | 7 + src/bazaar-daemon.c | 553 ++++++++++++++++++ src/bz-application.c | 62 +- src/bz-gnome-shell-search-provider.c | 465 --------------- src/bz-gnome-shell-search-provider.h | 50 -- src/bz-search-index-writer.c | 162 +++++ src/bz-search-index-writer.h | 50 ++ src/meson.build | 22 +- src/search-index.c | 336 +++++++++++ src/search-index.h | 67 +++ 15 files changed, 1222 insertions(+), 564 deletions(-) create mode 100644 src/bazaar-daemon.c delete mode 100644 src/bz-gnome-shell-search-provider.c delete mode 100644 src/bz-gnome-shell-search-provider.h create mode 100644 src/bz-search-index-writer.c create mode 100644 src/bz-search-index-writer.h create mode 100644 src/search-index.c create mode 100644 src/search-index.h diff --git a/build-aux/flatpak/io.github.kolunmi.Bazaar.json b/build-aux/flatpak/io.github.kolunmi.Bazaar.json index 5fa71df0e..fba4ff9c6 100644 --- a/build-aux/flatpak/io.github.kolunmi.Bazaar.json +++ b/build-aux/flatpak/io.github.kolunmi.Bazaar.json @@ -3,7 +3,7 @@ "runtime": "org.gnome.Platform", "runtime-version": "50", "sdk": "org.gnome.Sdk", - "command": "bazaar", + "command": "bazaar-daemon", "sdk-extensions": [ "org.freedesktop.Sdk.Extension.llvm20" ], diff --git a/data/dbus/io.github.kolunmi.Bazaar.service.in b/data/dbus/io.github.kolunmi.Bazaar.service.in index b38d0193c..1be0f780e 100644 --- a/data/dbus/io.github.kolunmi.Bazaar.service.in +++ b/data/dbus/io.github.kolunmi.Bazaar.service.in @@ -1,4 +1,4 @@ [D-BUS Service] Name=io.github.kolunmi.Bazaar SystemdService=io.github.kolunmi.Bazaar.service -Exec=@bindir@/bazaar service +Exec=@bindir@/bazaar-daemon service diff --git a/data/io.github.kolunmi.Bazaar.desktop.in b/data/io.github.kolunmi.Bazaar.desktop.in index 095f00faf..2179bff9c 100644 --- a/data/io.github.kolunmi.Bazaar.desktop.in +++ b/data/io.github.kolunmi.Bazaar.desktop.in @@ -1,7 +1,7 @@ [Desktop Entry] Name=Bazaar Comment=Add, remove or update flatpak software on this computer -Exec=bazaar %U +Exec=bazaar-daemon %U Icon=io.github.kolunmi.Bazaar Terminal=false Type=Application @@ -14,4 +14,4 @@ X-Purism-FormFactor=Workstation;Mobile; [Desktop Action new-window] Name=New Window -Exec=bazaar %U +Exec=bazaar-daemon %U diff --git a/data/io.github.kolunmi.Bazaar.search-provider.ini.in b/data/io.github.kolunmi.Bazaar.search-provider.ini.in index 4cde63728..537beb5c8 100644 --- a/data/io.github.kolunmi.Bazaar.search-provider.ini.in +++ b/data/io.github.kolunmi.Bazaar.search-provider.ini.in @@ -1,5 +1,5 @@ [Shell Search Provider] DesktopId=io.github.kolunmi.Bazaar.desktop -BusName=io.github.kolunmi.Bazaar +BusName=io.github.kolunmi.Bazaar.SearchProvider ObjectPath=/io/github/kolunmi/Bazaar/SearchProvider Version=2 \ No newline at end of file diff --git a/data/io.github.kolunmi.Bazaar.service.in b/data/io.github.kolunmi.Bazaar.service.in index ef38f4ab9..06a53722b 100644 --- a/data/io.github.kolunmi.Bazaar.service.in +++ b/data/io.github.kolunmi.Bazaar.service.in @@ -5,4 +5,4 @@ PartOf=graphical-session.target [Service] Type=dbus BusName=io.github.kolunmi.Bazaar -ExecStart=@bindir@/bazaar --no-window +ExecStart=@bindir@/bazaar-daemon --no-window diff --git a/meson.build b/meson.build index f25b2adea..f74c9d812 100644 --- a/meson.build +++ b/meson.build @@ -105,6 +105,13 @@ else dl_worker_bin_name = 'bazaar-dl-worker' config_h.set_quoted('DL_WORKER_BIN_NAME', dl_worker_bin_name) + daemon_bin_name = 'bazaar-daemon' + config_h.set_quoted('DAEMON_BIN_NAME', daemon_bin_name) + + bazaar_bin_name = 'bazaar' + config_h.set_quoted('BAZAAR_BIN_NAME', bazaar_bin_name) + config_h.set_quoted('BAZAAR_BIN_PATH', get_option('prefix') / get_option('bindir') / bazaar_bin_name) + if get_option('hardcoded_main_config_path') != '' config_h.set_quoted('HARDCODED_MAIN_CONFIG', get_option('hardcoded_main_config_path')) endif diff --git a/src/bazaar-daemon.c b/src/bazaar-daemon.c new file mode 100644 index 000000000..d02c67b42 --- /dev/null +++ b/src/bazaar-daemon.c @@ -0,0 +1,553 @@ +/* bazaar-daemon.c + * + * Copyright 2026 Alexander Vanhee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "search-index.h" + +#define BUS_NAME "io.github.kolunmi.Bazaar.SearchProvider" +#define OBJECT_PATH "/io/github/kolunmi/Bazaar/SearchProvider" +#define DAEMON_IFACE "io.github.kolunmi.Bazaar.Daemon" +#define MAX_SEARCH_RESULTS 25 +#define ACTIVATE_TIMEOUT_US 2000000 + +#define _cleanup_(x) __attribute__ ((cleanup (x))) + +#define SD_BUS_CHECK(expr) \ + do \ + { \ + r = (expr); \ + if (r < 0) \ + goto out; \ + } \ + while (0) + +static sd_event *event = NULL; +static sd_bus *bus = NULL; +static pid_t child_pid = -1; +static SearchIndex *g_index = NULL; +static char *g_index_path = NULL; + +static void log_msg (const char *fmt, ...) __attribute__ ((format (printf, 1, 2))); +static char *build_index_path (void); +static void ensure_index_loaded (void); +static int strv_count_local (char **strv); +static void strv_free_local (char **strv); +static void strv_freep (char ***strv); +static void generic_freep (void *p); +static int on_child_exit (sd_event_source *s, const siginfo_t *si, void *userdata); +static void launch_app (char **extra_args, int n_extra_args); +static int build_and_send_search_reply (sd_bus_message *call, char **terms); +static int method_get_result_set (sd_bus_message *m, void *userdata, sd_bus_error *ret_error); +static int method_get_result_metas (sd_bus_message *m, void *userdata, sd_bus_error *ret_error); +static int method_activate_result (sd_bus_message *m, void *userdata, sd_bus_error *ret_error); +static int method_launch_search (sd_bus_message *m, void *userdata, sd_bus_error *ret_error); +static int method_relaunch (sd_bus_message *m, void *userdata, sd_bus_error *ret_error); +static int method_quit (sd_bus_message *m, void *userdata, sd_bus_error *ret_error); +static int on_signal (sd_event_source *s, const struct signalfd_siginfo *si, void *userdata); +static int forward_to_running_daemon (sd_bus *forward_bus, int argc, char *argv[]); + +static const sd_bus_vtable search_provider_vtable[] = { + SD_BUS_VTABLE_START (0), + SD_BUS_METHOD ("GetInitialResultSet", "as", "as", method_get_result_set, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD ("GetSubsearchResultSet", "asas", "as", method_get_result_set, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD ("GetResultMetas", "as", "aa{sv}", method_get_result_metas, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD ("ActivateResult", "sasu", "", method_activate_result, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD ("LaunchSearch", "asu", "", method_launch_search, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END +}; + +static const sd_bus_vtable daemon_vtable[] = { + SD_BUS_VTABLE_START (0), + SD_BUS_METHOD ("Relaunch", "as", "", method_relaunch, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD ("Quit", "", "", method_quit, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END +}; + +int +main (int argc, char *argv[]) +{ + sigset_t mask = { 0 }; + int r = 0; + int no_window = 0; + _cleanup_ (generic_freep) char **fwd_args = NULL; + int n_fwd_args = 0; + int i = 0; + + sigemptyset (&mask); + sigaddset (&mask, SIGTERM); + sigaddset (&mask, SIGINT); + sigaddset (&mask, SIGCHLD); + sigprocmask (SIG_BLOCK, &mask, NULL); + + r = sd_event_default (&event); + if (r < 0) + return 1; + + sd_event_add_signal (event, NULL, SIGTERM, on_signal, NULL); + sd_event_add_signal (event, NULL, SIGINT, on_signal, NULL); + + r = sd_bus_open_user (&bus); + if (r < 0) + { + log_msg ("Failed to connect to session bus: %s", strerror (-r)); + return 1; + } + + r = sd_bus_request_name (bus, BUS_NAME, 0); + if (r < 0) + { + if (r == -EEXIST || r == -EALREADY) + { + log_msg ("App already running, forwarding"); + forward_to_running_daemon (bus, argc, argv); + sd_bus_flush_close_unref (bus); + sd_event_unref (event); + return 0; + } + + log_msg ("Failed to acquire bus name %s: %s", BUS_NAME, strerror (-r)); + return 1; + } + + r = sd_bus_add_object_vtable (bus, NULL, OBJECT_PATH, + "org.gnome.Shell.SearchProvider2", + search_provider_vtable, NULL); + if (r < 0) + log_msg ("Failed to register search provider object: %s", strerror (-r)); + + r = sd_bus_add_object_vtable (bus, NULL, OBJECT_PATH, + DAEMON_IFACE, + daemon_vtable, NULL); + if (r < 0) + log_msg ("Failed to register daemon object: %s", strerror (-r)); + + r = sd_bus_attach_event (bus, event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + log_msg ("Failed to attach bus to event loop: %s", strerror (-r)); + + ensure_index_loaded (); + + fwd_args = malloc (sizeof (char *) * (size_t) (argc > 0 ? argc : 1)); + for (i = 1; i < argc; i++) + { + if (strcmp (argv[i], "--no-window") == 0) + { + no_window = 1; + continue; + } + + fwd_args[n_fwd_args] = argv[i]; + n_fwd_args++; + } + + if (no_window) + log_msg ("staying headless"); + else + launch_app (fwd_args, n_fwd_args); + + sd_event_loop (event); + + search_index_close (g_index); + free (g_index_path); + + sd_bus_flush_close_unref (bus); + sd_event_unref (event); + + return 0; +} + +static int +forward_to_running_daemon (sd_bus *forward_bus, int argc, char *argv[]) +{ + _cleanup_ (sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_ (sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_ (sd_bus_message_unrefp) sd_bus_message *reply = NULL; + int r = 0; + + r = sd_bus_message_new_method_call (forward_bus, &m, BUS_NAME, OBJECT_PATH, + DAEMON_IFACE, "Relaunch"); + if (r < 0) + return r; + + r = sd_bus_message_append_strv (m, argv + (argc > 0 ? 1 : 0)); + if (r < 0) + return r; + + r = sd_bus_call (forward_bus, m, ACTIVATE_TIMEOUT_US, &error, &reply); + if (r < 0) + log_msg ("Failed to forward relaunch request: %s", + sd_bus_error_is_set (&error) ? error.message : strerror (-r)); + + return r; +} + +static int +method_relaunch (sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + _cleanup_ (strv_freep) char **terms = NULL; + int r = 0; + + r = sd_bus_message_read_strv (m, &terms); + if (r < 0) + return r; + + log_msg ("Relaunch requested via D-Bus"); + + launch_app (terms, strv_count_local (terms)); + + return sd_bus_reply_method_return (m, NULL); +} + +static int +method_quit (sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + int r = 0; + + log_msg ("Quit requested"); + + r = sd_bus_reply_method_return (m, NULL); + + if (child_pid > 0) + kill (child_pid, SIGTERM); + + sd_event_exit (event, 0); + return r; +} + +static void +log_msg (const char *fmt, ...) +{ + va_list args; + + printf ("(bazaar-daemon): "); + + va_start (args, fmt); + vprintf (fmt, args); + va_end (args); + + printf ("\n"); + fflush (stdout); +} + +static char * +build_index_path (void) +{ + const char *cache_home = NULL; + const char *home = NULL; + char buf[4096] = { 0 }; + + cache_home = getenv ("XDG_CACHE_HOME"); + if (cache_home != NULL && *cache_home != '\0') + { + snprintf (buf, sizeof (buf), + "%s/io.github.kolunmi.Bazaar/core/search-index", + cache_home); + return strdup (buf); + } + + home = getenv ("HOME"); + if (home != NULL && *home != '\0') + { + snprintf (buf, sizeof (buf), + "%s/.cache/io.github.kolunmi.Bazaar/core/search-index", + home); + return strdup (buf); + } + + return strdup ("/tmp/search-index"); +} + +static void +ensure_index_loaded (void) +{ + if (g_index_path == NULL) + g_index_path = build_index_path (); + + if (g_index == NULL) + { + g_index = search_index_open (g_index_path); + if (g_index == NULL) + log_msg ("Search index not yet available at %s", g_index_path); + return; + } + + if (search_index_reload_if_stale (&g_index)) + log_msg ("Search index reloaded "); +} + +static int +strv_count_local (char **strv) +{ + int n = 0; + + if (strv == NULL) + return 0; + + while (strv[n] != NULL) + n++; + + return n; +} + +static void +strv_free_local (char **strv) +{ + char **p = NULL; + + if (strv == NULL) + return; + + for (p = strv; *p != NULL; p++) + free (*p); + free (strv); +} + +static void +strv_freep (char ***strv) +{ + strv_free_local (*strv); +} + +static void +generic_freep (void *p) +{ + free (*(void **) p); +} + +static int +on_child_exit (sd_event_source *s, + const siginfo_t *si, + void *userdata) +{ + log_msg ("Application exited (status %d)", si->si_status); + + child_pid = -1; + sd_event_source_unref (s); + ensure_index_loaded (); + + return 0; +} + +static void +launch_app (char **extra_args, + int n_extra_args) +{ + pid_t pid = -1; + _cleanup_ (generic_freep) char **argv = NULL; + int i = 0; + sigset_t mask = { 0 }; + + argv = malloc (sizeof (char *) * (size_t) (n_extra_args + 2)); + argv[0] = (char *) BAZAAR_BIN_PATH; + for (i = 0; i < n_extra_args; i++) + argv[i + 1] = extra_args[i]; + argv[n_extra_args + 1] = NULL; + + log_msg ("Application starting"); + + pid = fork (); + if (pid == 0) + { + sigemptyset (&mask); + sigprocmask (SIG_SETMASK, &mask, NULL); + + execvp (argv[0], argv); + perror ("execvp"); + _exit (127); + } + + if (pid < 0) + { + log_msg ("Failed to spawn application: %s", strerror (errno)); + return; + } + + if (child_pid <= 0) + child_pid = pid; + + sd_event_add_child (event, NULL, pid, WEXITED, on_child_exit, NULL); +} + +static int +build_and_send_search_reply (sd_bus_message *call, + char **terms) +{ + _cleanup_ (sd_bus_message_unrefp) sd_bus_message *reply = NULL; + SearchIndexMatch matches[MAX_SEARCH_RESULTS] = { 0 }; + size_t n_matches = 0; + size_t i = 0; + int r = 0; + + ensure_index_loaded (); + + n_matches = search_index_query ( + g_index, (const char *const *) terms, + strv_count_local (terms), + matches, MAX_SEARCH_RESULTS); + + SD_BUS_CHECK (sd_bus_message_new_method_return (call, &reply)); + SD_BUS_CHECK (sd_bus_message_open_container (reply, 'a', "s")); + + for (i = 0; i < n_matches; i++) + SD_BUS_CHECK (sd_bus_message_append (reply, "s", matches[i].entry->id)); + + SD_BUS_CHECK (sd_bus_message_close_container (reply)); + + return sd_bus_send (NULL, reply, NULL); + +out: + return r; +} + +static int +method_get_result_set (sd_bus_message *m, + void *userdata, + sd_bus_error *ret_error) +{ + _cleanup_ (strv_freep) char **terms = NULL; + const char *sig = NULL; + int r = 0; + + sig = sd_bus_message_get_signature (m, 1); + + if (sig != NULL && strcmp (sig, "asas") == 0) + { + _cleanup_ (strv_freep) char **previous = NULL; + + r = sd_bus_message_read_strv (m, &previous); + if (r < 0) + return r; + } + + r = sd_bus_message_read_strv (m, &terms); + if (r < 0) + return r; + + return build_and_send_search_reply (m, terms); +} + +static int +method_get_result_metas (sd_bus_message *m, + void *userdata, + sd_bus_error *ret_error) +{ + _cleanup_ (sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_ (strv_freep) char **ids = NULL; + char **p = NULL; + int r = 0; + + r = sd_bus_message_read_strv (m, &ids); + if (r < 0) + return r; + + ensure_index_loaded (); + + SD_BUS_CHECK (sd_bus_message_new_method_return (m, &reply)); + SD_BUS_CHECK (sd_bus_message_open_container (reply, 'a', "a{sv}")); + + for (p = ids; p != NULL && *p != NULL; p++) + { + const SearchIndexEntry *e = NULL; + + e = search_index_find (g_index, *p); + + if (e == NULL) + continue; + + SD_BUS_CHECK (sd_bus_message_open_container (reply, 'a', "{sv}")); + SD_BUS_CHECK (sd_bus_message_append (reply, "{sv}", "id", "s", e->id)); + SD_BUS_CHECK (sd_bus_message_append (reply, "{sv}", "name", "s", e->title)); + + if (e->description != NULL) + SD_BUS_CHECK (sd_bus_message_append (reply, "{sv}", "description", "s", e->description)); + + if (e->icon_path != NULL) + SD_BUS_CHECK (sd_bus_message_append (reply, "{sv}", "gicon", "s", e->icon_path)); + + SD_BUS_CHECK (sd_bus_message_close_container (reply)); + } + + SD_BUS_CHECK (sd_bus_message_close_container (reply)); + + return sd_bus_send (NULL, reply, NULL); + +out: + return r; +} + +static int +method_activate_result (sd_bus_message *m, + void *userdata, + sd_bus_error *ret_error) +{ + const char *id = NULL; + _cleanup_ (generic_freep) char *uri_arg = NULL; + char *args[1] = { NULL }; + int r = 0; + + r = sd_bus_message_read (m, "s", &id); + if (r < 0) + return r; + + uri_arg = malloc (strlen ("appstream:") + strlen (id) + 1); + sprintf (uri_arg, "appstream:%s", id); + + args[0] = uri_arg; + launch_app (args, 1); + + return sd_bus_reply_method_return (m, NULL); +} + +static int +method_launch_search (sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + _cleanup_ (strv_freep) char **terms = NULL; + // TODO: Actually open search in app with the terms + launch_app (NULL, 0); + + return sd_bus_reply_method_return (m, NULL); +} + +static int +on_signal (sd_event_source *s, + const struct signalfd_siginfo *si, + void *userdata) +{ + log_msg ("Daemon shutting down"); + + if (child_pid > 0) + kill (child_pid, SIGTERM); + + sd_event_exit (event, 0); + + return 0; +} diff --git a/src/bz-application.c b/src/bz-application.c index d0a6c434c..0fd909da2 100644 --- a/src/bz-application.c +++ b/src/bz-application.c @@ -46,7 +46,6 @@ #include "bz-flatpak-bundle-result.h" #include "bz-flatpak-entry.h" #include "bz-flatpak-instance.h" -#include "bz-gnome-shell-search-provider.h" #include "bz-hash-table-object.h" #include "bz-inspector.h" #include "bz-internal-config.h" @@ -60,6 +59,7 @@ #include "bz-root-blocklist.h" #include "bz-root-curated-config.h" #include "bz-serializable.h" +#include "bz-search-index-writer.h" #include "bz-state-info.h" #include "bz-transaction-manager.h" #include "bz-window.h" @@ -83,7 +83,6 @@ struct _BzApplication BzFlathubState *flathub; BzFlathubState *tmp_flathub; BzFlatpakInstance *flatpak; - BzGnomeShellSearchProvider *gs_search; BzInternalConfig *internal_config; BzMainConfig *config; BzMalcontentService *malcontent; @@ -415,7 +414,6 @@ bz_application_dispose (GObject *object) g_clear_object (&self->group_filter); g_clear_object (&self->group_filter_model); g_clear_object (&self->groups); - g_clear_object (&self->gs_search); g_clear_object (&self->installed_apps); g_clear_object (&self->malcontent); g_clear_object (&self->internal_config); @@ -464,7 +462,6 @@ bz_application_command_line (GApplication *app, gint argc = 0; g_auto (GStrv) argv = NULL; gboolean help = FALSE; - gboolean no_window = FALSE; g_auto (GStrv) blocklists_strv = NULL; g_auto (GStrv) content_configs_strv = NULL; g_auto (GStrv) locations = NULL; @@ -472,7 +469,7 @@ bz_application_command_line (GApplication *app, GOptionEntry main_entries[] = { { "help", 0, 0, G_OPTION_ARG_NONE, &help, "Print help" }, - { "no-window", 0, 0, G_OPTION_ARG_NONE, &no_window, "Ensure the service is running without creating a new window" }, + { "no-window", 0, 0, G_OPTION_ARG_NONE, NULL, "Ensure the service is running without creating a new window (daemon)" }, { "extra-blocklist", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &blocklists_strv, "Add an extra blocklist to read from" }, { "extra-curated-config", 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &content_configs_strv, "Add an extra yaml file with which to configure the app browser" }, /* Here for backwards compat */ @@ -527,8 +524,6 @@ bz_application_command_line (GApplication *app, g_autoptr (GtkStringList) content_configs = NULL; g_autoptr (DexFuture) init = NULL; - g_debug ("Starting daemon!"); - g_application_hold (G_APPLICATION (self)); self->running = TRUE; blocklists = gtk_string_list_new (NULL); @@ -575,7 +570,7 @@ bz_application_command_line (GApplication *app, dex_future_disown (g_steal_pointer (&init)); } - if (!no_window && !preview_metainfo) + if (!preview_metainfo) { if (locations == NULL || *locations == NULL) new_window (self); @@ -610,25 +605,6 @@ bz_application_local_command_line (GApplication *application, return FALSE; } -static gboolean -bz_application_dbus_register (GApplication *application, - GDBusConnection *connection, - const gchar *object_path, - GError **error) -{ - BzApplication *self = BZ_APPLICATION (application); - return bz_gnome_shell_search_provider_set_connection (self->gs_search, connection, error); -} - -static void -bz_application_dbus_unregister (GApplication *application, - GDBusConnection *connection, - const gchar *object_path) -{ - BzApplication *self = BZ_APPLICATION (application); - bz_gnome_shell_search_provider_set_connection (self->gs_search, NULL, NULL); -} - static void bz_application_class_init (BzApplicationClass *klass) { @@ -640,8 +616,6 @@ bz_application_class_init (BzApplicationClass *klass) app_class->activate = bz_application_activate; app_class->command_line = bz_application_command_line; app_class->local_command_line = bz_application_local_command_line; - app_class->dbus_register = bz_application_dbus_register; - app_class->dbus_unregister = bz_application_dbus_unregister; g_type_ensure (BZ_TYPE_RESULT); } @@ -875,9 +849,23 @@ bz_application_quit_action (GSimpleAction *action, GVariant *parameter, gpointer user_data) { - BzApplication *self = user_data; + BzApplication *self = user_data; + g_autoptr (GDBusConnection) conn = NULL; + g_autoptr (GError) error = NULL; - g_assert (BZ_IS_APPLICATION (self)); + conn = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + if (conn != NULL) + { + g_dbus_connection_call_sync ( + conn, "io.github.kolunmi.Bazaar.SearchProvider", + "/io/github/kolunmi/Bazaar/SearchProvider", + "io.github.kolunmi.Bazaar.Daemon", + "Quit", NULL, NULL, G_DBUS_CALL_FLAGS_NONE, + -1, NULL, &error); + } + + if (error != NULL) + g_warning ("Failed to quit daemon: %s", error->message); g_application_quit (G_APPLICATION (self)); } @@ -903,8 +891,6 @@ bz_application_init (BzApplication *self) self->running = FALSE; g_weak_ref_init (&self->main_window, NULL); - self->gs_search = bz_gnome_shell_search_provider_new (); - g_action_map_add_action_entries ( G_ACTION_MAP (self), app_actions, @@ -1583,6 +1569,9 @@ cache_groups_fiber (GWeakRef *wr) g_autoptr (GVariant) variant = NULL; g_autoptr (GBytes) bytes = NULL; guint n_groups = 0; + g_autoptr (GError) index_error = NULL; + g_autofree char *module_dir = NULL; + g_autofree char *index_path = NULL; bz_weak_get_or_return_reject (self, wr); @@ -1620,6 +1609,12 @@ cache_groups_fiber (GWeakRef *wr) if (!result) g_warning ("Failed to cache groups to %s: %s", groups_cache, local_error->message); + module_dir = bz_dup_module_dir (); + index_path = g_build_filename (module_dir, "search-index", NULL); + + if (!bz_write_search_index (G_LIST_MODEL (self->groups), index_path, &index_error)) + g_warning ("Failed to write search index to %s: %s", index_path, index_error->message); + return dex_future_new_true (); } @@ -3518,7 +3513,6 @@ init_service_struct (BzApplication *self, self->search_engine = bz_search_engine_new (); bz_search_engine_set_model (self->search_engine, G_LIST_MODEL (self->group_filter_model)); bz_search_engine_set_biases (self->search_engine, G_LIST_MODEL (self->search_biases)); - bz_gnome_shell_search_provider_set_engine (self->gs_search, self->search_engine); self->curated_provider = bz_content_provider_new (); bz_content_provider_set_input_files ( diff --git a/src/bz-gnome-shell-search-provider.c b/src/bz-gnome-shell-search-provider.c deleted file mode 100644 index fa62acba5..000000000 --- a/src/bz-gnome-shell-search-provider.c +++ /dev/null @@ -1,465 +0,0 @@ -/* bz-gnome-shell-search-provider.c - * - * Copyright 2025 Adam Masciola - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#include "bz-gnome-shell-search-provider.h" -#include "bz-entry-group.h" -#include "bz-finished-search-query.h" -#include "bz-search-result.h" -#include "util.h" -#include "gs-shell-search-provider-generated.h" - -struct _BzGnomeShellSearchProvider -{ - GObject parent_instance; - - BzSearchEngine *engine; - GDBusConnection *connection; - - BzShellSearchProvider2 *skeleton; - DexFuture *task; - - GHashTable *last_results; -}; - -G_DEFINE_FINAL_TYPE (BzGnomeShellSearchProvider, bz_gnome_shell_search_provider, G_TYPE_OBJECT); - -enum -{ - PROP_0, - - PROP_ENGINE, - PROP_CONNECTION, - - LAST_PROP -}; -static GParamSpec *props[LAST_PROP] = { 0 }; - -BZ_DEFINE_DATA ( - request, - Request, - { - BzGnomeShellSearchProvider *self; - GDBusMethodInvocation *invocation; - GApplication *application; - }, - BZ_RELEASE_DATA (invocation, g_object_unref); - BZ_RELEASE_DATA (application, g_application_release);) -static DexFuture * -request_finally (DexFuture *future, - RequestData *data); - -static void -start_request (BzGnomeShellSearchProvider *self, - GDBusMethodInvocation *invocation, - const char *const *terms); - -static void -bz_gnome_shell_search_provider_dispose (GObject *object) -{ - BzGnomeShellSearchProvider *self = BZ_GNOME_SHELL_SEARCH_PROVIDER (object); - - dex_clear (&self->task); - - g_clear_object (&self->engine); - g_clear_object (&self->connection); - g_clear_object (&self->skeleton); - g_clear_pointer (&self->last_results, g_hash_table_unref); - - G_OBJECT_CLASS (bz_gnome_shell_search_provider_parent_class)->dispose (object); -} - -static void -bz_gnome_shell_search_provider_get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec) -{ - BzGnomeShellSearchProvider *self = BZ_GNOME_SHELL_SEARCH_PROVIDER (object); - - switch (prop_id) - { - case PROP_ENGINE: - g_value_set_object (value, bz_gnome_shell_search_provider_get_engine (self)); - break; - case PROP_CONNECTION: - g_value_set_object (value, bz_gnome_shell_search_provider_get_connection (self)); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - } -} - -static void -bz_gnome_shell_search_provider_set_property (GObject *object, - guint prop_id, - const GValue *value, - GParamSpec *pspec) -{ - BzGnomeShellSearchProvider *self = BZ_GNOME_SHELL_SEARCH_PROVIDER (object); - - switch (prop_id) - { - case PROP_ENGINE: - bz_gnome_shell_search_provider_set_engine (self, g_value_get_object (value)); - break; - case PROP_CONNECTION: - bz_gnome_shell_search_provider_set_connection (self, g_value_get_object (value), NULL); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - } -} - -static void -bz_gnome_shell_search_provider_class_init (BzGnomeShellSearchProviderClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS (klass); - - object_class->set_property = bz_gnome_shell_search_provider_set_property; - object_class->get_property = bz_gnome_shell_search_provider_get_property; - object_class->dispose = bz_gnome_shell_search_provider_dispose; - - props[PROP_ENGINE] = - g_param_spec_object ( - "engine", - NULL, NULL, - BZ_TYPE_SEARCH_ENGINE, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); - - props[PROP_CONNECTION] = - g_param_spec_object ( - "connection", - NULL, NULL, - G_TYPE_DBUS_CONNECTION, - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); - - g_object_class_install_properties (object_class, LAST_PROP, props); -} - -static gboolean -get_initial_result_set (BzShellSearchProvider2 *skeleton, - GDBusMethodInvocation *invocation, - gchar **terms, - BzGnomeShellSearchProvider *self) -{ - start_request (self, invocation, (const char *const *) terms); - return TRUE; -} - -static gboolean -get_subsearch_result_set (BzShellSearchProvider2 *skeleton, - GDBusMethodInvocation *invocation, - gchar **previous_results, - gchar **terms, - BzGnomeShellSearchProvider *self) -{ - start_request (self, invocation, (const char *const *) terms); - return TRUE; -} - -static gboolean -get_result_metas (BzShellSearchProvider2 *skeleton, - GDBusMethodInvocation *invocation, - gchar **results, - BzGnomeShellSearchProvider *self) -{ - g_autoptr (GVariantBuilder) builder = NULL; - - builder = g_variant_builder_new (G_VARIANT_TYPE ("aa{sv}")); - - for (char **result = results; *result != NULL; result++) - { - BzEntryGroup *group = NULL; - g_autoptr (GVariantBuilder) meta_builder = NULL; - const char *title = NULL; - const char *description = NULL; - GIcon *icon = NULL; - - group = g_hash_table_lookup (self->last_results, *result); - if (group == NULL) - { - g_warning ("failed to find '%s' in gnome-shell search result cache", *result); - continue; - } - - meta_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); - g_variant_builder_add (meta_builder, "{sv}", "id", g_variant_new_string (*result)); - - title = bz_entry_group_get_title (group); - g_variant_builder_add (meta_builder, "{sv}", "name", g_variant_new_string (title)); - - description = bz_entry_group_get_description (group); - if (description != NULL) - g_variant_builder_add (meta_builder, "{sv}", "description", g_variant_new_string (description)); - - icon = bz_entry_group_get_mini_icon (group); - if (icon != NULL) - { - g_autofree gchar *icon_str = NULL; - - icon_str = g_icon_to_string (icon); - if (icon_str != NULL) - g_variant_builder_add (meta_builder, "{sv}", "gicon", g_variant_new_string (icon_str)); - else - { - g_autoptr (GVariant) icon_serialized = NULL; - - icon_serialized = g_icon_serialize (icon); - if (icon_serialized != NULL) - g_variant_builder_add (meta_builder, "{sv}", "icon", icon_serialized); - } - } - - g_variant_builder_add_value (builder, g_variant_builder_end (meta_builder)); - } - - g_dbus_method_invocation_return_value (invocation, g_variant_new ("(aa{sv})", builder)); - return TRUE; -} - -static gboolean -activate_result (BzShellSearchProvider2 *skeleton, - GDBusMethodInvocation *invocation, - gchar *result, - gchar **terms, - guint32 timestamp, - BzGnomeShellSearchProvider *self) -{ - g_action_group_activate_action ( - G_ACTION_GROUP (g_application_get_default ()), - "show-app-id", - g_variant_new ("s", result)); - - bz_shell_search_provider2_complete_activate_result (skeleton, invocation); - return TRUE; -} - -static gboolean -launch_search (BzShellSearchProvider2 *skeleton, - GDBusMethodInvocation *invocation, - gchar **terms, - guint32 timestamp, - BzGnomeShellSearchProvider *self) -{ - g_autofree gchar *string = NULL; - - string = g_strjoinv (" ", terms); - g_action_group_activate_action ( - G_ACTION_GROUP (g_application_get_default ()), - "show-app-id", - g_variant_new ("s", string)); - - bz_shell_search_provider2_complete_launch_search (skeleton, invocation); - return TRUE; -} - -static void -bz_gnome_shell_search_provider_init (BzGnomeShellSearchProvider *self) -{ - self->skeleton = bz_shell_search_provider2_skeleton_new (); - self->last_results = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); - - g_signal_connect ( - self->skeleton, "handle-get-initial-result-set", - G_CALLBACK (get_initial_result_set), self); - g_signal_connect ( - self->skeleton, "handle-get-subsearch-result-set", - G_CALLBACK (get_subsearch_result_set), self); - g_signal_connect ( - self->skeleton, "handle-get-result-metas", - G_CALLBACK (get_result_metas), self); - g_signal_connect ( - self->skeleton, "handle-activate-result", - G_CALLBACK (activate_result), self); - g_signal_connect ( - self->skeleton, "handle-launch-search", - G_CALLBACK (launch_search), self); -} - -BzGnomeShellSearchProvider * -bz_gnome_shell_search_provider_new (void) -{ - return g_object_new (BZ_TYPE_GNOME_SHELL_SEARCH_PROVIDER, NULL); -} - -BzSearchEngine * -bz_gnome_shell_search_provider_get_engine (BzGnomeShellSearchProvider *self) -{ - g_return_val_if_fail (BZ_IS_GNOME_SHELL_SEARCH_PROVIDER (self), NULL); - return self->engine; -} - -void -bz_gnome_shell_search_provider_set_engine (BzGnomeShellSearchProvider *self, - BzSearchEngine *engine) -{ - g_return_if_fail (BZ_IS_GNOME_SHELL_SEARCH_PROVIDER (self)); - g_return_if_fail (engine == NULL || BZ_IS_SEARCH_ENGINE (engine)); - - g_clear_pointer (&self->engine, g_object_unref); - if (engine != NULL) - self->engine = g_object_ref (engine); - - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENGINE]); -} - -GDBusConnection * -bz_gnome_shell_search_provider_get_connection (BzGnomeShellSearchProvider *self) -{ - g_return_val_if_fail (BZ_IS_GNOME_SHELL_SEARCH_PROVIDER (self), NULL); - return self->connection; -} - -gboolean -bz_gnome_shell_search_provider_set_connection (BzGnomeShellSearchProvider *self, - GDBusConnection *connection, - GError **error) -{ - gboolean success = TRUE; - - g_return_val_if_fail (BZ_IS_GNOME_SHELL_SEARCH_PROVIDER (self), FALSE); - g_return_val_if_fail (connection == NULL || G_IS_DBUS_CONNECTION (connection), FALSE); - - if (self->connection != NULL) - g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->skeleton)); - g_clear_pointer (&self->connection, g_object_unref); - - if (connection != NULL) - { - g_autoptr (GError) local_error = NULL; - - self->connection = g_object_ref (connection); - - success = g_dbus_interface_skeleton_export ( - G_DBUS_INTERFACE_SKELETON (self->skeleton), - connection, - "/io/github/kolunmi/Bazaar/SearchProvider", - &local_error); - if (!success) - { - if (error != NULL) - g_propagate_error (error, g_steal_pointer (&local_error)); - else - g_warning ("Could not register gnome shell search provider: %s", local_error->message); - } - } - - g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CONNECTION]); - return success; -} - -static DexFuture * -request_finally (DexFuture *future, - RequestData *data) -{ - BzGnomeShellSearchProvider *self = data->self; - GDBusMethodInvocation *invocation = data->invocation; - g_autoptr (GError) local_error = NULL; - const GValue *value = NULL; - BzFinishedSearchQuery *finished = NULL; - GPtrArray *results = NULL; - g_autoptr (GVariantBuilder) builder = NULL; - - value = dex_future_get_value (future, &local_error); - if (value != NULL) - { - finished = g_value_get_object (value); - results = bz_finished_search_query_get_results (finished); - builder = g_variant_builder_new (G_VARIANT_TYPE ("as")); - - for (guint i = 0; i < results->len; i++) - { - BzSearchResult *result = NULL; - BzEntryGroup *group = NULL; - const char *id = NULL; - - result = g_ptr_array_index (results, i); - group = bz_search_result_get_group (result); - if (bz_entry_group_get_removable (group) > 0) - /* Skip already installed groups */ - continue; - - id = bz_entry_group_get_id (group); - g_variant_builder_add (builder, "s", id); - g_hash_table_replace ( - self->last_results, - g_strdup (id), - g_object_ref (group)); - } - - g_dbus_method_invocation_return_value ( - invocation, - g_variant_new ("(as)", builder)); - } - else - { - g_warning ("search engine reported an error to the search provider, " - "returning an empty response to invocation: %s", - local_error->message); - g_dbus_method_invocation_return_value (invocation, g_variant_new ("(as)", NULL)); - } - - return NULL; -} - -static void -start_request (BzGnomeShellSearchProvider *self, - GDBusMethodInvocation *invocation, - const char *const *terms) -{ - g_autoptr (RequestData) data = NULL; - g_autoptr (DexFuture) future = NULL; - - dex_clear (&self->task); - g_hash_table_remove_all (self->last_results); - - if (g_strv_length ((gchar **) terms) == 1 && - g_utf8_strlen (terms[0], -1) == 1) - { - g_dbus_method_invocation_return_value ( - invocation, - g_variant_new ("(as)", NULL)); - return; - } - - if (self->engine == NULL) - { - g_warning ("search provider does not have an engine, " - "returning empty response to invocation"); - g_dbus_method_invocation_return_value ( - invocation, - g_variant_new ("(as)", NULL)); - return; - } - - data = request_data_new (); - data->self = self; - data->invocation = g_object_ref (invocation); - data->application = g_application_get_default (); - g_application_hold (data->application); - - future = bz_search_engine_query (self->engine, terms); - future = dex_future_finally ( - future, (DexFutureCallback) request_finally, - request_data_ref (data), request_data_unref); - self->task = g_steal_pointer (&future); -} - -/* End of bz-gnome-shell-search-provider.c */ diff --git a/src/bz-gnome-shell-search-provider.h b/src/bz-gnome-shell-search-provider.h deleted file mode 100644 index 5e4caef2e..000000000 --- a/src/bz-gnome-shell-search-provider.h +++ /dev/null @@ -1,50 +0,0 @@ -/* bz-gnome-shell-search-provider.h - * - * Copyright 2025 Adam Masciola - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#pragma once - -#include "bz-search-engine.h" - -G_BEGIN_DECLS - -#define BZ_TYPE_GNOME_SHELL_SEARCH_PROVIDER (bz_gnome_shell_search_provider_get_type ()) -G_DECLARE_FINAL_TYPE (BzGnomeShellSearchProvider, bz_gnome_shell_search_provider, BZ, GNOME_SHELL_SEARCH_PROVIDER, GObject) - -BzGnomeShellSearchProvider * -bz_gnome_shell_search_provider_new (void); - -BzSearchEngine * -bz_gnome_shell_search_provider_get_engine (BzGnomeShellSearchProvider *self); - -void -bz_gnome_shell_search_provider_set_engine (BzGnomeShellSearchProvider *self, - BzSearchEngine *engine); - -GDBusConnection * -bz_gnome_shell_search_provider_get_connection (BzGnomeShellSearchProvider *self); - -gboolean -bz_gnome_shell_search_provider_set_connection (BzGnomeShellSearchProvider *self, - GDBusConnection *connection, - GError **error); - -G_END_DECLS - -/* End of bz-gnome-shell-search-provider.h */ diff --git a/src/bz-search-index-writer.c b/src/bz-search-index-writer.c new file mode 100644 index 000000000..f6ddc3005 --- /dev/null +++ b/src/bz-search-index-writer.c @@ -0,0 +1,162 @@ +/* bz-search-index-writer.c + * + * Copyright 2026 Alexander Vanhee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#define G_LOG_DOMAIN "BAZAAR::SEARCH-INDEX-WRITER" +#define BAZAAR_MODULE "search-index-writer" + +#include "bz-search-index-writer.h" +#include "bz-entry-group.h" + +#define INDEX_MAGIC "BZSI" +#define INDEX_VERSION 1 +#define DESCRIPTION_MAX_LEN 200 + +static gboolean write_u32 (GOutputStream *out, guint32 value, GError **error); +static gboolean write_string (GOutputStream *out, const char *str, gsize max_len, GError **error); +static gboolean entry_group_is_eligible (BzEntryGroup *group); +static const char *entry_group_icon_path (BzEntryGroup *group); + +gboolean +bz_write_search_index (GListModel *groups, + const char *out_path, + GError **error) +{ + g_autofree char *tmp_path = NULL; + g_autoptr (GFile) tmp_file = NULL; + g_autoptr (GFile) final_file = NULL; + g_autoptr (GOutputStream) out = NULL; + guint n_groups = 0; + guint32 count = 0; + gboolean result = FALSE; + + g_return_val_if_fail (G_IS_LIST_MODEL (groups), FALSE); + g_return_val_if_fail (out_path != NULL, FALSE); + + tmp_path = g_strdup_printf ("%s.tmp", out_path); + tmp_file = g_file_new_for_path (tmp_path); + + out = G_OUTPUT_STREAM (g_file_replace ( + tmp_file, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, error)); + if (out == NULL) + return FALSE; + + if (!g_output_stream_write_all (out, INDEX_MAGIC, 4, NULL, NULL, error)) + return FALSE; + if (!write_u32 (out, INDEX_VERSION, error)) + return FALSE; + + n_groups = g_list_model_get_n_items (groups); + for (guint i = 0; i < n_groups; i++) + { + g_autoptr (BzEntryGroup) group = g_list_model_get_item (groups, i); + if (entry_group_is_eligible (group)) + count++; + } + + if (!write_u32 (out, count, error)) + return FALSE; + + for (guint i = 0; i < n_groups; i++) + { + g_autoptr (BzEntryGroup) group = g_list_model_get_item (groups, i); + const char *icon_path = NULL; + + if (!entry_group_is_eligible (group)) + continue; + + icon_path = entry_group_icon_path (group); + + if (!write_string (out, bz_entry_group_get_id (group), 0, error) || + !write_string (out, bz_entry_group_get_title (group), 0, error) || + !write_string (out, bz_entry_group_get_developer (group), 0, error) || + !write_string (out, bz_entry_group_get_description (group), DESCRIPTION_MAX_LEN, error) || + !write_string (out, bz_entry_group_get_search_tokens (group), 0, error) || + !write_string (out, icon_path, 0, error)) + return FALSE; + } + + if (!g_output_stream_close (out, NULL, error)) + return FALSE; + + final_file = g_file_new_for_path (out_path); + result = g_file_move ( + tmp_file, final_file, + G_FILE_COPY_OVERWRITE, + NULL, NULL, NULL, error); + + return result; +} + +static gboolean +write_u32 (GOutputStream *out, + guint32 value, + GError **error) +{ + guint32 le = GUINT32_TO_LE (value); + return g_output_stream_write_all (out, &le, sizeof (le), NULL, NULL, error); +} + +static gboolean +write_string (GOutputStream *out, + const char *str, + gsize max_len, + GError **error) +{ + gsize len = 0; + + if (str != NULL) + { + len = strlen (str); + if (max_len > 0 && len > max_len) + len = max_len; + } + + if (!write_u32 (out, (guint32) len, error)) + return FALSE; + + if (len > 0) + return g_output_stream_write_all (out, str, len, NULL, NULL, error); + + return TRUE; +} + +static gboolean +entry_group_is_eligible (BzEntryGroup *group) +{ + return bz_entry_group_is_searchable (group) && + !bz_entry_group_is_addon (group) && + bz_entry_group_get_removable (group) == 0 && + bz_entry_group_get_eol (group) == NULL; +} + +static const char * +entry_group_icon_path (BzEntryGroup *group) +{ + GIcon *icon = NULL; + + icon = bz_entry_group_get_mini_icon (group); + if (icon == NULL || !G_IS_FILE_ICON (icon)) + return NULL; + + return g_file_peek_path (g_file_icon_get_file (G_FILE_ICON (icon))); +} diff --git a/src/bz-search-index-writer.h b/src/bz-search-index-writer.h new file mode 100644 index 000000000..0ebcae911 --- /dev/null +++ b/src/bz-search-index-writer.h @@ -0,0 +1,50 @@ +/* bz-search-index-writer.h + * + * Copyright 2026 Alexander Vanhee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +/* + * HEADER: + * char[4] "BZSI" + * uint32 version + * uint32 entry count + * + * CONTENT: + * id + * title + * developer + * description (cut off) + * search_tokens + * icon_path + * + * with strings as uint32 length + uint8[] bytes + * + */ + +gboolean +bz_write_search_index (GListModel *groups, + const char *out_path, + GError **error); + +G_END_DECLS diff --git a/src/meson.build b/src/meson.build index 2c670b379..e4bbea9e6 100644 --- a/src/meson.build +++ b/src/meson.build @@ -17,6 +17,7 @@ libsecret_dep = dependency('libsecret-1', version: '>= 0.20') libproxy_dep = dependency('libproxy-1.0', version: '>= 0.5') malcontent_dep = dependency('malcontent-0', version: '>= 0.12.0') gtksourceview_dep = dependency('gtksourceview-5', version: '>= 5.17') +libsystemd_dep = dependency('libsystemd', version: '>= 245') dl_worker_sources = [ @@ -39,19 +40,22 @@ dl_worker_exe = executable(dl_worker_bin_name, dl_worker_sources, ) +daemon_sources = [ + 'bazaar-daemon.c', + 'search-index.c', +] + +daemon_exe = executable(daemon_bin_name, daemon_sources, + dependencies: [libsystemd_dep], + install: true, +) + marshalers = gnome.genmarshal('bz-marshalers', sources: 'bz-marshalers.list', prefix: 'bz_marshal', valist_marshallers: true, ) -gdbus_src = gnome.gdbus_codegen( - 'gs-shell-search-provider-generated', - 'shell-search-provider-dbus-interfaces.xml', - interface_prefix : 'org.gnome.', - namespace : 'Bz', -) - bz_sources = files( 'bz-addon-tile.c', 'bz-addons-dialog.c', @@ -108,7 +112,6 @@ bz_sources = files( 'bz-flatpak-entry.c', 'bz-flatpak-instance.c', 'bz-full-view.c', - 'bz-gnome-shell-search-provider.c', 'bz-group-tile-css-watcher.c', 'bz-hardware-support-dialog.c', 'bz-inspector.c', @@ -138,6 +141,7 @@ bz_sources = files( 'bz-search-bar.c', 'bz-search-engine.c', 'bz-search-filter-popover.c', + 'bz-search-index-writer.c', 'bz-search-page.c', 'bz-search-pill-list.c', 'bz-section-view.c', @@ -376,7 +380,7 @@ bz_sources += gnome.compile_resources('bz-resources', dependencies: [blueprints, release_notes, countries_gvariant], ) -executable(bazaar_bin_name, bz_sources + ['main.c'], gdbus_src, marshalers, +executable(bazaar_bin_name, bz_sources + ['main.c'], marshalers, dependencies: bz_deps, install: true, ) diff --git a/src/search-index.c b/src/search-index.c new file mode 100644 index 000000000..88164cce6 --- /dev/null +++ b/src/search-index.c @@ -0,0 +1,336 @@ +/* search-index.c + * + * Copyright 2026 Alexander Vanhee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include "search-index.h" + +#include +#include +#include +#include +#include + +#define INDEX_MAGIC "BZSI" + +static int read_u32 (FILE *f, unsigned int *out); +static char *read_field (FILE *f); +static void free_entries (SearchIndexEntry *entries, unsigned int count); +static double score_field (const char *term, const char *field, double weight); +static double score_entry (const SearchIndexEntry *e, const char *const *terms, int n_terms); +static int cmp_matches (const void *a, const void *b); + +SearchIndex * +search_index_open (const char *path) +{ + FILE *f = NULL; + char magic[4] = { 0 }; + unsigned int version = 0; + unsigned int count = 0; + SearchIndexEntry *entries = NULL; + SearchIndex *idx = NULL; + struct stat st = { 0 }; + unsigned int i = 0; + + f = fopen (path, "rb"); + if (f == NULL) + return NULL; + + if (fread (magic, 1, 4, f) != 4 || memcmp (magic, INDEX_MAGIC, 4) != 0) + { + fclose (f); + return NULL; + } + + if (read_u32 (f, &version) < 0 || read_u32 (f, &count) < 0) + { + fclose (f); + return NULL; + } + + entries = calloc (count, sizeof (SearchIndexEntry)); + if (entries == NULL) + { + fclose (f); + return NULL; + } + + for (i = 0; i < count; i++) + { + entries[i].id = read_field (f); + entries[i].title = read_field (f); + entries[i].developer = read_field (f); + entries[i].description = read_field (f); + entries[i].search_tokens = read_field (f); + entries[i].icon_path = read_field (f); + + if (entries[i].id == NULL || entries[i].title == NULL) + { + free_entries (entries, count); + fclose (f); + return NULL; + } + } + + fclose (f); + + idx = calloc (1, sizeof (SearchIndex)); + if (idx == NULL) + { + free_entries (entries, count); + return NULL; + } + + idx->path = strdup (path); + idx->entries = entries; + idx->count = count; + + if (stat (path, &st) == 0) + idx->mtime = st.st_mtime; + + return idx; +} + +void +search_index_close (SearchIndex *idx) +{ + if (idx == NULL) + return; + + free_entries (idx->entries, idx->count); + free (idx->path); + free (idx); +} + +int +search_index_reload_if_stale (SearchIndex **idx_ptr) +{ + struct stat st = { 0 }; + SearchIndex *idx = NULL; + SearchIndex *fresh = NULL; + + idx = *idx_ptr; + + if (idx == NULL) + return 0; + + if (stat (idx->path, &st) != 0) + return 0; + + if (st.st_mtime == idx->mtime) + return 0; + + fresh = search_index_open (idx->path); + if (fresh == NULL) + return 0; + + search_index_close (idx); + *idx_ptr = fresh; + return 1; +} + +size_t +search_index_query (SearchIndex *idx, + const char *const *terms, + int n_terms, + SearchIndexMatch *out, + size_t max_results) +{ + size_t n_matches = 0; + unsigned int i = 0; + double score = 0.0; + + if (idx == NULL || n_terms <= 0) + return 0; + + for (i = 0; i < idx->count && n_matches < max_results * 8; i++) + { + score = score_entry (&idx->entries[i], terms, n_terms); + + if (score > 0.0 && n_matches < max_results * 8) + { + if (n_matches < max_results) + { + out[n_matches].entry = &idx->entries[i]; + out[n_matches].score = score; + } + n_matches++; + } + } + + if (n_matches > max_results) + n_matches = max_results; + + qsort (out, n_matches, sizeof (SearchIndexMatch), cmp_matches); + + return n_matches; +} + +const SearchIndexEntry * +search_index_find (SearchIndex *idx, + const char *id) +{ + unsigned int i = 0; + + if (idx == NULL || id == NULL) + return NULL; + + for (i = 0; i < idx->count; i++) + { + if (strcmp (idx->entries[i].id, id) == 0) + return &idx->entries[i]; + } + + return NULL; +} + +static int +read_u32 (FILE *f, + unsigned int *out) +{ + unsigned char b[4] = { 0 }; + + if (fread (b, 1, 4, f) != 4) + return -1; + + *out = (unsigned int) b[0] | + ((unsigned int) b[1] << 8) | + ((unsigned int) b[2] << 16) | + ((unsigned int) b[3] << 24); + return 0; +} + +static char * +read_field (FILE *f) +{ + unsigned int len = 0; + char *str = NULL; + + if (read_u32 (f, &len) < 0) + return NULL; + + if (len == 0) + return NULL; + + str = malloc (len + 1); + if (str == NULL) + return NULL; + + if (fread (str, 1, len, f) != len) + { + free (str); + return NULL; + } + + str[len] = '\0'; + return str; +} + +static void +free_entries (SearchIndexEntry *entries, + unsigned int count) +{ + unsigned int i = 0; + + if (entries == NULL) + return; + + for (i = 0; i < count; i++) + { + free (entries[i].id); + free (entries[i].title); + free (entries[i].developer); + free (entries[i].description); + free (entries[i].search_tokens); + free (entries[i].icon_path); + } + + free (entries); +} + +static double +score_field (const char *term, + const char *field, + double weight) +{ + const char *hit = NULL; + size_t term_len = 0; + size_t field_len = 0; + + if (field == NULL || term == NULL || *term == '\0') + return 0.0; + + hit = strcasestr (field, term); + if (hit == NULL) + return 0.0; + + term_len = strlen (term); + field_len = strlen (field); + if (field_len == 0) + return 0.0; + + return weight * ((double) (term_len * term_len) / (double) field_len); +} + +static double +score_entry (const SearchIndexEntry *e, + const char *const *terms, + int n_terms) +{ + double total = 0.0; + int i = 0; + const char *term = NULL; + double term_score = 0.0; + + for (i = 0; i < n_terms; i++) + { + term = terms[i]; + term_score = 0.0; + + if (term == NULL || *term == '\0') + continue; + + term_score += score_field (term, e->title, 2.0); + term_score += score_field (term, e->developer, 1.0); + term_score += score_field (term, e->description, 1.0); + term_score += score_field (term, e->search_tokens, 1.5); + + if (term_score <= 0.0) + return 0.0; + + total += term_score; + } + + return total; +} + +static int +cmp_matches (const void *a, + const void *b) +{ + const SearchIndexMatch *ma = a; + const SearchIndexMatch *mb = b; + + if (mb->score > ma->score) + return 1; + if (mb->score < ma->score) + return -1; + return 0; +} diff --git a/src/search-index.h b/src/search-index.h new file mode 100644 index 000000000..8b1295bd1 --- /dev/null +++ b/src/search-index.h @@ -0,0 +1,67 @@ +/* search-index.h + * + * Copyright 2026 Alexander Vanhee + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +typedef struct +{ + char *id; + char *title; + char *developer; + char *description; + char *search_tokens; + char *icon_path; +} SearchIndexEntry; + +typedef struct +{ + char *path; + time_t mtime; + SearchIndexEntry *entries; + unsigned int count; +} SearchIndex; + +typedef struct +{ + const SearchIndexEntry *entry; + double score; +} SearchIndexMatch; + +SearchIndex * +search_index_open (const char *path); + +void +search_index_close (SearchIndex *idx); + +int +search_index_reload_if_stale (SearchIndex **idx); + +size_t +search_index_query (SearchIndex *idx, + const char *const *terms, + int n_terms, + SearchIndexMatch *out, + size_t max_results); + +const SearchIndexEntry * +search_index_find (SearchIndex *idx, const char *id); From 7969393e5e49d2456df80704efffc485f6463d2c Mon Sep 17 00:00:00 2001 From: Alexander Vanhee <160625516+AlexanderVanhee@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:55:38 +0200 Subject: [PATCH 2/3] Switch to Gio.DataOutputStream --- src/bz-search-index-writer.c | 56 ++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/src/bz-search-index-writer.c b/src/bz-search-index-writer.c index f6ddc3005..f1eb466c2 100644 --- a/src/bz-search-index-writer.c +++ b/src/bz-search-index-writer.c @@ -23,15 +23,14 @@ #define G_LOG_DOMAIN "BAZAAR::SEARCH-INDEX-WRITER" #define BAZAAR_MODULE "search-index-writer" -#include "bz-search-index-writer.h" #include "bz-entry-group.h" +#include "bz-search-index-writer.h" #define INDEX_MAGIC "BZSI" #define INDEX_VERSION 1 #define DESCRIPTION_MAX_LEN 200 -static gboolean write_u32 (GOutputStream *out, guint32 value, GError **error); -static gboolean write_string (GOutputStream *out, const char *str, gsize max_len, GError **error); +static gboolean write_string (GDataOutputStream *out, const char *str, gsize max_len, GError **error); static gboolean entry_group_is_eligible (BzEntryGroup *group); static const char *entry_group_icon_path (BzEntryGroup *group); @@ -40,13 +39,14 @@ bz_write_search_index (GListModel *groups, const char *out_path, GError **error) { - g_autofree char *tmp_path = NULL; - g_autoptr (GFile) tmp_file = NULL; - g_autoptr (GFile) final_file = NULL; - g_autoptr (GOutputStream) out = NULL; - guint n_groups = 0; - guint32 count = 0; - gboolean result = FALSE; + g_autofree char *tmp_path = NULL; + g_autoptr (GFile) tmp_file = NULL; + g_autoptr (GFile) final_file = NULL; + g_autoptr (GOutputStream) base_out = NULL; + g_autoptr (GDataOutputStream) out = NULL; + guint n_groups = 0; + guint32 count = 0; + gboolean result = FALSE; g_return_val_if_fail (G_IS_LIST_MODEL (groups), FALSE); g_return_val_if_fail (out_path != NULL, FALSE); @@ -54,15 +54,18 @@ bz_write_search_index (GListModel *groups, tmp_path = g_strdup_printf ("%s.tmp", out_path); tmp_file = g_file_new_for_path (tmp_path); - out = G_OUTPUT_STREAM (g_file_replace ( + base_out = G_OUTPUT_STREAM (g_file_replace ( tmp_file, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, error)); - if (out == NULL) + if (base_out == NULL) return FALSE; - if (!g_output_stream_write_all (out, INDEX_MAGIC, 4, NULL, NULL, error)) + out = g_data_output_stream_new (base_out); + g_data_output_stream_set_byte_order (out, G_DATA_STREAM_BYTE_ORDER_LITTLE_ENDIAN); + + if (!g_output_stream_write_all (G_OUTPUT_STREAM (out), INDEX_MAGIC, 4, NULL, NULL, error)) return FALSE; - if (!write_u32 (out, INDEX_VERSION, error)) + if (!g_data_output_stream_put_uint32 (out, INDEX_VERSION, NULL, error)) return FALSE; n_groups = g_list_model_get_n_items (groups); @@ -73,7 +76,7 @@ bz_write_search_index (GListModel *groups, count++; } - if (!write_u32 (out, count, error)) + if (!g_data_output_stream_put_uint32 (out, count, NULL, error)) return FALSE; for (guint i = 0; i < n_groups; i++) @@ -95,7 +98,7 @@ bz_write_search_index (GListModel *groups, return FALSE; } - if (!g_output_stream_close (out, NULL, error)) + if (!g_output_stream_close (G_OUTPUT_STREAM (out), NULL, error)) return FALSE; final_file = g_file_new_for_path (out_path); @@ -108,19 +111,10 @@ bz_write_search_index (GListModel *groups, } static gboolean -write_u32 (GOutputStream *out, - guint32 value, - GError **error) -{ - guint32 le = GUINT32_TO_LE (value); - return g_output_stream_write_all (out, &le, sizeof (le), NULL, NULL, error); -} - -static gboolean -write_string (GOutputStream *out, - const char *str, - gsize max_len, - GError **error) +write_string (GDataOutputStream *out, + const char *str, + gsize max_len, + GError **error) { gsize len = 0; @@ -131,11 +125,11 @@ write_string (GOutputStream *out, len = max_len; } - if (!write_u32 (out, (guint32) len, error)) + if (!g_data_output_stream_put_uint32 (out, (guint32) len, NULL, error)) return FALSE; if (len > 0) - return g_output_stream_write_all (out, str, len, NULL, NULL, error); + return g_output_stream_write_all (G_OUTPUT_STREAM (out), str, len, NULL, NULL, error); return TRUE; } From 2bcc8645927d0bf882eb5379eb04abeb53b70ef1 Mon Sep 17 00:00:00 2001 From: Alexander Vanhee <160625516+AlexanderVanhee@users.noreply.github.com> Date: Sat, 11 Jul 2026 22:17:46 +0200 Subject: [PATCH 3/3] Rename search index write --- src/bz-application.c | 2 +- src/meson.build | 2 +- src/{bz-search-index-writer.c => search-index-write.c} | 6 +++--- src/{bz-search-index-writer.h => search-index-write.h} | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename src/{bz-search-index-writer.c => search-index-write.c} (97%) rename src/{bz-search-index-writer.h => search-index-write.h} (97%) diff --git a/src/bz-application.c b/src/bz-application.c index 0fd909da2..e6adcd41b 100644 --- a/src/bz-application.c +++ b/src/bz-application.c @@ -59,7 +59,7 @@ #include "bz-root-blocklist.h" #include "bz-root-curated-config.h" #include "bz-serializable.h" -#include "bz-search-index-writer.h" +#include "search-index-write.h" #include "bz-state-info.h" #include "bz-transaction-manager.h" #include "bz-window.h" diff --git a/src/meson.build b/src/meson.build index e4bbea9e6..a0458114c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -141,7 +141,6 @@ bz_sources = files( 'bz-search-bar.c', 'bz-search-engine.c', 'bz-search-filter-popover.c', - 'bz-search-index-writer.c', 'bz-search-page.c', 'bz-search-pill-list.c', 'bz-section-view.c', @@ -172,6 +171,7 @@ bz_sources = files( 'io.c', 'refresh-worker.c', 'safety-calculator.c', + 'search-index-write.c', 'spdx.c', 'template-callbacks.c', ) diff --git a/src/bz-search-index-writer.c b/src/search-index-write.c similarity index 97% rename from src/bz-search-index-writer.c rename to src/search-index-write.c index f1eb466c2..c365ea8f5 100644 --- a/src/bz-search-index-writer.c +++ b/src/search-index-write.c @@ -1,4 +1,4 @@ -/* bz-search-index-writer.c +/* search-index-write.c * * Copyright 2026 Alexander Vanhee * @@ -20,11 +20,11 @@ #include "config.h" -#define G_LOG_DOMAIN "BAZAAR::SEARCH-INDEX-WRITER" +#define G_LOG_DOMAIN "BAZAAR::SEARCH-INDEX-WRITE" #define BAZAAR_MODULE "search-index-writer" #include "bz-entry-group.h" -#include "bz-search-index-writer.h" +#include "search-index-write.h" #define INDEX_MAGIC "BZSI" #define INDEX_VERSION 1 diff --git a/src/bz-search-index-writer.h b/src/search-index-write.h similarity index 97% rename from src/bz-search-index-writer.h rename to src/search-index-write.h index 0ebcae911..9e7883224 100644 --- a/src/bz-search-index-writer.h +++ b/src/search-index-write.h @@ -1,4 +1,4 @@ -/* bz-search-index-writer.h +/* search-index-write.h * * Copyright 2026 Alexander Vanhee *