From 690e60cd74c44ed1e2d21b27e3152856845ead28 Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Sat, 8 Nov 2025 18:47:51 +0100 Subject: Build an application bundle on macOS This is far from done, but nonetheless constitutes a big improvement. macOS application bundles are more or less necessary for: - showing a nice icon; - having spawned off instances actually be brought to the foreground; - file associations (yet files currently do not open properly); - having a reasonable method of distribution. Also resolving a bunch of minor issues: - The context menu had duplicate items, and might needlessly end up with (null) labels. --- fiv.c | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 164 insertions(+), 6 deletions(-) (limited to 'fiv.c') diff --git a/fiv.c b/fiv.c index 43041b0..cab6c1f 100644 --- a/fiv.c +++ b/fiv.c @@ -73,6 +73,138 @@ slist_to_strv(GSList *slist) return strv; } +// --- macOS utilities --------------------------------------------------------- + +#ifdef __APPLE__ +#include + +static gchar * +cfurlref_to_path(CFURLRef urlref) +{ + CFStringRef path = CFURLCopyFileSystemPath(urlref, kCFURLPOSIXPathStyle); + if (!path) + return NULL; + + CFIndex size = CFStringGetMaximumSizeForEncoding( + CFStringGetLength(path), kCFStringEncodingUTF8) + 1; + gchar *string = g_malloc(size); + + Boolean ok = CFStringGetCString(path, string, size, kCFStringEncodingUTF8); + CFRelease(path); + if (!ok) { + g_free(string); + return NULL; + } + return string; +} + +static gchar * +get_application_bundle_path(void) +{ + gchar *result = NULL; + CFBundleRef bundle = CFBundleGetMainBundle(); + if (!bundle) + goto fail_1; + + // When launched from outside a bundle, it will make up one, + // but these paths will then be equal. + CFURLRef bundle_url = CFBundleCopyBundleURL(bundle); + if (!bundle_url) + goto fail_1; + CFURLRef resources_url = CFBundleCopyResourcesDirectoryURL(bundle); + if (!resources_url) + goto fail_2; + + if (!CFEqual(bundle_url, resources_url)) + result = cfurlref_to_path(bundle_url); + + CFRelease(resources_url); +fail_2: + CFRelease(bundle_url); +fail_1: + return result; +} + +static gchar * +prepend_path_string(const gchar *prepended, const gchar *original) +{ + if (!prepended) + return g_strdup(original ? original : ""); + if (!original || !*original) + return g_strdup(prepended); + + GHashTable *seen = g_hash_table_new(g_str_hash, g_str_equal); + GPtrArray *unique = g_ptr_array_new(); + g_ptr_array_add(unique, (gpointer) prepended); + g_hash_table_add(seen, (gpointer) prepended); + + gchar **components = g_strsplit(original, ":", -1); + for (gchar **p = components; *p; p++) { + if (g_hash_table_contains(seen, *p)) + continue; + + g_ptr_array_add(unique, *p); + g_hash_table_add(seen, *p); + } + + g_ptr_array_add(unique, NULL); + gchar *result = g_strjoinv(":", (gchar **) unique->pdata); + g_hash_table_destroy(seen); + g_ptr_array_free(unique, TRUE); + + g_strfreev(components); + return result; +} + +// We reuse foreign dependencies, so we need to prevent them from loading +// any system-wide files, and point them in the right direction. +static void +adjust_environment(void) +{ + gchar *bundle_dir = get_application_bundle_path(); + if (!bundle_dir) + return; + + gchar *contents_dir = g_build_filename(bundle_dir, "Contents", NULL); + gchar *macos_dir = g_build_filename(contents_dir, "MacOS", NULL); + gchar *resources_dir = g_build_filename(contents_dir, "Resources", NULL); + gchar *datadir = g_build_filename(resources_dir, "share", NULL); + gchar *libdir = g_build_filename(resources_dir, "lib", NULL); + g_free(bundle_dir); + + gchar *new_path = prepend_path_string(macos_dir, g_getenv("PATH")); + g_setenv("PATH", new_path, TRUE); + g_free(new_path); + + const gchar *data_dirs = g_getenv("XDG_DATA_DIRS"); + gchar *new_data_dirs = data_dirs && *data_dirs + ? prepend_path_string(datadir, data_dirs) + : prepend_path_string(datadir, "/usr/local/share:/usr/share"); + g_setenv("XDG_DATA_DIRS", new_data_dirs, TRUE); + g_free(new_data_dirs); + + gchar *schemas_dir = g_build_filename(datadir, "glib-2.0", "schemas", NULL); + g_setenv("GSETTINGS_SCHEMA_DIR", schemas_dir, TRUE); + g_free(schemas_dir); + + gchar *gdk_pixbuf_module_file = + g_build_filename(libdir, "gdk-pixbuf-2.0", "loaders.cache", NULL); + g_setenv("GDK_PIXBUF_MODULE_FILE", gdk_pixbuf_module_file, TRUE); + g_free(gdk_pixbuf_module_file); + + // GTK+ is smart enough to also consider application bundles, + // but let there be a single source of truth. + g_setenv("GTK_EXE_PREFIX", resources_dir, TRUE); + + g_free(libdir); + g_free(datadir); + g_free(resources_dir); + g_free(macos_dir); + g_free(contents_dir); +} + +#endif + // --- Keyboard shortcuts ------------------------------------------------------ // Fuck XML, this can be easily represented in static structures. // Though it would be nice if the accelerators could be customized. @@ -1092,9 +1224,22 @@ on_next(void) static gchar ** build_spawn_argv(const char *uri) { - // Because we only pass URIs, there is no need to prepend "--" here. GPtrArray *a = g_ptr_array_new(); - g_ptr_array_add(a, g_strdup(PROJECT_NAME)); +#ifdef __APPLE__ + // Otherwise we would always launch ourselves in the background. + gchar *bundle_dir = get_application_bundle_path(); + if (bundle_dir) { + g_ptr_array_add(a, g_strdup("open")); + g_ptr_array_add(a, g_strdup("-a")); + g_ptr_array_add(a, bundle_dir); + // At least with G_APPLICATION_NON_UNIQUE, this is necessary: + g_ptr_array_add(a, g_strdup("-n")); + g_ptr_array_add(a, g_strdup("--args")); + } +#endif + // Because we only pass URIs, there is no need to prepend "--" after this. + if (!a->len) + g_ptr_array_add(a, g_strdup(PROJECT_NAME)); // Process-local VFS URIs need to be resolved to globally accessible URIs. // It doesn't seem possible to reliably tell if a GFile is process-local, @@ -1403,15 +1548,24 @@ on_window_state_event(G_GNUC_UNUSED GtkWidget *widget, static void show_help_contents(void) { - gchar *filename = g_strdup_printf("%s.html", PROJECT_NAME); #ifdef G_OS_WIN32 gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL); - gchar *path = g_build_filename(prefix, PROJECT_DOCDIR, filename, NULL); - g_free(prefix); +#elif defined __APPLE__ + gchar *prefix = get_application_bundle_path(); + if (!prefix) { + show_error_dialog(g_error_new( + G_FILE_ERROR, G_FILE_ERROR_FAILED, "Cannot locate bundle")); + return; + } #else - gchar *path = g_build_filename(PROJECT_DOCDIR, filename, NULL); + gchar *prefix = g_strdup(PROJECT_PREFIX); #endif + + gchar *filename = g_strdup_printf("%s.html", PROJECT_NAME); + gchar *path = g_build_filename(prefix, PROJECT_DOCDIR, filename, NULL); + g_free(prefix); g_free(filename); + GError *error = NULL; gchar *uri = g_filename_to_uri(path, NULL, &error); g_free(path); @@ -2640,6 +2794,10 @@ main(int argc, char *argv[]) {}, }; +#ifdef __APPLE__ + adjust_environment(); +#endif + // We never get the ::open signal, thanks to G_OPTION_ARG_FILENAME_ARRAY. GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE); g_application_set_option_context_parameter_string( -- cgit v1.2.3-70-g09d2