summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/common/backend.h4
-rw-r--r--src/common/desktop.c81
-rw-r--r--src/common/desktop.h19
-rw-r--r--src/common/meson.build2
-rw-r--r--src/platform/cocoa/desktop.m145
-rw-r--r--src/platform/cocoa/meson.build2
-rw-r--r--src/platform/cocoa/window.m10
-rw-r--r--src/platform/freedesktop/dbus.c187
-rw-r--r--src/platform/freedesktop/dbus.h16
-rw-r--r--src/platform/freedesktop/desktop.c150
-rw-r--r--src/platform/freedesktop/meson.build1
-rw-r--r--src/platform/meson.build4
-rw-r--r--src/platform/wayland/api.h1
-rw-r--r--src/platform/wayland/backend.c6
-rw-r--r--src/platform/wayland/internal.h2
-rw-r--r--src/platform/wayland/shell.c9
-rw-r--r--src/platform/wayland/surface.c2
-rw-r--r--src/platform/x11/window.c14
18 files changed, 653 insertions, 2 deletions
diff --git a/src/common/backend.h b/src/common/backend.h
index f0fae03..fd60418 100644
--- a/src/common/backend.h
+++ b/src/common/backend.h
@@ -15,6 +15,8 @@ struct yasl_display {
struct yasl_display_cbs callbacks;
struct yasl_loop *loop;
void *app_runner;
+ /* Backend's copy of yasl_display_spec.app_id, or NULL; for desktop services. */
+ const char *app_id;
};
struct yasl_surface {
@@ -89,6 +91,8 @@ struct yasl_backend_ops {
bool (*surface_show_window_menu)(struct yasl_surface *s, struct yasl_vector2i pos);
bool (*surface_drag_carry)(struct yasl_surface *s, struct yasl_vector2i offset);
bool (*surface_place_under_pointer)(struct yasl_surface *s, struct yasl_vector2i at);
+ /* Optional; NULL means the bell is unsupported. */
+ bool (*surface_bell)(struct yasl_surface *s);
void *(*display_get_native_handle)(struct yasl_display *d);
void *(*surface_get_native_handle)(struct yasl_surface *s);
bool (*surface_get_native_frame)(struct yasl_surface *s,
diff --git a/src/common/desktop.c b/src/common/desktop.c
new file mode 100644
index 0000000..23c62d7
--- /dev/null
+++ b/src/common/desktop.c
@@ -0,0 +1,81 @@
+#include "yasl/desktop.h"
+#include "common/backend.h"
+#include "common/desktop.h"
+
+#include <string.h>
+
+/* Large enough for any path the platform APIs return. */
+#define DESKTOP_STR_MAX 4096
+
+/* An RFC 3986 scheme, then ':', and no control characters anywhere. */
+static bool uri_valid(const char *uri)
+{
+ unsigned char c = (unsigned char)uri[0];
+ if ((unsigned)((c | 32) - 'a') >= 26) return false;
+ const char *p = uri + 1;
+ for (; *p != ':'; p++) {
+ c = (unsigned char)*p;
+ bool alpha = (unsigned)((c | 32) - 'a') < 26;
+ bool digit = (unsigned)(c - '0') < 10;
+ if (!alpha && !digit && c != '+' && c != '-' && c != '.') return false;
+ }
+ for (; *p; p++) {
+ c = (unsigned char)*p;
+ if (c < 0x20 || c == 0x7f) return false;
+ }
+ return true;
+}
+
+/* Copy len bytes and a NUL when they fit, and report the full length. */
+static size_t copy_out(char *out, size_t cap, const char *s, size_t len)
+{
+ if (len && len < cap) {
+ memcpy(out, s, len);
+ out[len] = '\0';
+ }
+ return len;
+}
+
+bool yasl_display_open_uri(struct yasl_display *d, const char *uri)
+{
+ if (!d || !uri || !uri_valid(uri)) return false;
+ return desktop_open_uri(uri);
+}
+
+bool yasl_display_notify(struct yasl_display *d, const char *title, const char *body)
+{
+ if (!d || !title) return false;
+ return desktop_notify(d->app_id, title, body ? body : "");
+}
+
+uint32_t yasl_display_appearance(struct yasl_display *d)
+{
+ return d ? desktop_appearance() : 0;
+}
+
+bool yasl_surface_bell(struct yasl_surface *s)
+{
+ if (!s || !s->display->ops->surface_bell) return false;
+ return s->display->ops->surface_bell(s);
+}
+
+size_t yasl_executable_path(char *out, size_t cap)
+{
+ if (!out && cap) return 0;
+ char buf[DESKTOP_STR_MAX];
+ return copy_out(out, cap, buf, desktop_executable_path(buf, sizeof(buf)));
+}
+
+size_t yasl_resource_dir(char *out, size_t cap)
+{
+ if (!out && cap) return 0;
+ char buf[DESKTOP_STR_MAX];
+ return copy_out(out, cap, buf, desktop_resource_dir(buf, sizeof(buf)));
+}
+
+size_t yasl_default_locale(char *out, size_t cap)
+{
+ if (!out && cap) return 0;
+ char buf[DESKTOP_STR_MAX];
+ return copy_out(out, cap, buf, desktop_default_locale(buf, sizeof(buf)));
+}
diff --git a/src/common/desktop.h b/src/common/desktop.h
new file mode 100644
index 0000000..9af5952
--- /dev/null
+++ b/src/common/desktop.h
@@ -0,0 +1,19 @@
+#ifndef YASL_COMMON_DESKTOP_H
+#define YASL_COMMON_DESKTOP_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/* Operating-system desktop services behind yasl/desktop.h. Common code
+ * validates arguments first. Path and locale functions write a NUL-terminated
+ * string to buf and return its length, or zero on failure or when it does not
+ * fit in cap. */
+bool desktop_open_uri(const char *uri);
+bool desktop_notify(const char *app_id, const char *title, const char *body);
+uint32_t desktop_appearance(void);
+size_t desktop_executable_path(char *buf, size_t cap);
+size_t desktop_resource_dir(char *buf, size_t cap);
+size_t desktop_default_locale(char *buf, size_t cap);
+
+#endif
diff --git a/src/common/meson.build b/src/common/meson.build
index 015c61c..26565ee 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -1 +1 @@
-sources += files('app.c', 'backend.c', 'input.c', 'native.c', 'timing.c', 'utf8.c')
+sources += files('app.c', 'backend.c', 'desktop.c', 'input.c', 'native.c', 'timing.c', 'utf8.c')
diff --git a/src/platform/cocoa/desktop.m b/src/platform/cocoa/desktop.m
new file mode 100644
index 0000000..526cbb9
--- /dev/null
+++ b/src/platform/cocoa/desktop.m
@@ -0,0 +1,145 @@
+#import <AppKit/AppKit.h>
+#import <UserNotifications/UserNotifications.h>
+
+#include "common/desktop.h"
+#include "yasl/desktop.h"
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <limits.h>
+#include <mach-o/dyld.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static size_t copy_str(char *buf, size_t cap, const char *s)
+{
+ size_t len = s ? strlen(s) : 0;
+ if (!len || len >= cap) return 0;
+ memcpy(buf, s, len + 1);
+ return len;
+}
+
+bool desktop_open_uri(const char *uri)
+{
+ @autoreleasepool {
+ NSString *str = [NSString stringWithUTF8String:uri];
+ NSURL *url = str ? [NSURL URLWithString:str] : nil;
+ return url && [[NSWorkspace sharedWorkspace] openURL:url];
+ }
+}
+
+/* Show banners while the application is in front, too. */
+@interface YaslNotificationDelegate : NSObject <UNUserNotificationCenterDelegate>
+@end
+
+@implementation YaslNotificationDelegate
+- (void)userNotificationCenter:(UNUserNotificationCenter *)center
+ willPresentNotification:(UNNotification *)notification
+ withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
+{
+ (void)center;
+ (void)notification;
+ completionHandler(UNNotificationPresentationOptionBanner);
+}
+@end
+
+static YaslNotificationDelegate *notification_delegate;
+static bool authorization_requested;
+
+/* UNUserNotificationCenter raises an exception outside an app bundle. */
+static bool notifications_available(void)
+{
+ NSBundle *bundle = [NSBundle mainBundle];
+ return bundle.bundleIdentifier && [bundle.bundlePath.pathExtension isEqualToString:@"app"];
+}
+
+bool desktop_notify(const char *app_id, const char *title, const char *body)
+{
+ (void)app_id;
+ if (!notifications_available()) return false;
+ @autoreleasepool {
+ NSString *heading = [NSString stringWithUTF8String:title];
+ NSString *message = [NSString stringWithUTF8String:body];
+ if (!heading || !message) return false;
+ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
+ if (!notification_delegate) {
+ notification_delegate = [[YaslNotificationDelegate alloc] init];
+ center.delegate = notification_delegate;
+ }
+ if (!authorization_requested) {
+ authorization_requested = true;
+ [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert
+ completionHandler:^(BOOL granted, NSError *error) {
+ (void)granted;
+ (void)error;
+ }];
+ }
+ UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
+ content.title = heading;
+ content.body = message;
+ UNNotificationRequest *request =
+ [UNNotificationRequest requestWithIdentifier:[[NSUUID UUID] UUIDString]
+ content:content
+ trigger:nil];
+ [content release];
+ [center addNotificationRequest:request withCompletionHandler:nil];
+ return true;
+ }
+}
+
+uint32_t desktop_appearance(void)
+{
+ @autoreleasepool {
+ uint32_t flags = 0;
+ NSAppearanceName name = [NSApp.effectiveAppearance
+ bestMatchFromAppearancesWithNames:@[NSAppearanceNameAqua, NSAppearanceNameDarkAqua]];
+ if ([name isEqualToString:NSAppearanceNameDarkAqua])
+ flags |= YASL_APPEARANCE_DARK;
+ else if (name)
+ flags |= YASL_APPEARANCE_LIGHT;
+ if ([NSWorkspace sharedWorkspace].accessibilityDisplayShouldIncreaseContrast)
+ flags |= YASL_APPEARANCE_HIGH_CONTRAST;
+ return flags;
+ }
+}
+
+size_t desktop_executable_path(char *buf, size_t cap)
+{
+ char raw[PATH_MAX];
+ uint32_t size = sizeof(raw);
+ char resolved[PATH_MAX];
+ if (_NSGetExecutablePath(raw, &size) != 0 || !realpath(raw, resolved)) return 0;
+ return copy_str(buf, cap, resolved);
+}
+
+size_t desktop_resource_dir(char *buf, size_t cap)
+{
+ @autoreleasepool {
+ NSBundle *bundle = [NSBundle mainBundle];
+ if (!bundle.bundleIdentifier || ![bundle.bundlePath.pathExtension isEqualToString:@"app"])
+ return 0;
+ NSString *path = bundle.resourcePath;
+ return path ? copy_str(buf, cap, path.fileSystemRepresentation) : 0;
+ }
+}
+
+size_t desktop_default_locale(char *buf, size_t cap)
+{
+ /* Identifiers such as "zh-Hans_CN" are not POSIX names, so build the name
+ * from the language and region codes alone. */
+ char name[64] = "en_US.UTF-8";
+ CFLocaleRef locale = CFLocaleCopyCurrent();
+ if (locale) {
+ CFStringRef language = CFLocaleGetValue(locale, kCFLocaleLanguageCode);
+ CFStringRef region = CFLocaleGetValue(locale, kCFLocaleCountryCode);
+ char lang[16], reg[16];
+ if (language && CFStringGetCString(language, lang, sizeof(lang), kCFStringEncodingUTF8) && lang[0]) {
+ if (region && CFStringGetCString(region, reg, sizeof(reg), kCFStringEncodingUTF8) && reg[0])
+ snprintf(name, sizeof(name), "%s_%s.UTF-8", lang, reg);
+ else
+ snprintf(name, sizeof(name), "%s.UTF-8", lang);
+ }
+ CFRelease(locale);
+ }
+ return copy_str(buf, cap, name);
+}
diff --git a/src/platform/cocoa/meson.build b/src/platform/cocoa/meson.build
index 8230376..5fc5f5e 100644
--- a/src/platform/cocoa/meson.build
+++ b/src/platform/cocoa/meson.build
@@ -1 +1 @@
-sources += files('window.m', 'keymap.c')
+sources += files('window.m', 'desktop.m', 'keymap.c')
diff --git a/src/platform/cocoa/window.m b/src/platform/cocoa/window.m
index 811a7c6..75dce54 100644
--- a/src/platform/cocoa/window.m
+++ b/src/platform/cocoa/window.m
@@ -1366,6 +1366,7 @@ struct cocoa_display *cocoa_display_open(struct yasl_loop *loop, const struct ya
darwin_ctx.cbs = &darwin_ctx.base.callbacks;
darwin_ctx.ud = spec->ud;
if (spec->app_id) darwin_ctx.app_id = strdup(spec->app_id);
+ darwin_ctx.base.app_id = darwin_ctx.app_id;
}
return &darwin_ctx;
}
@@ -2191,6 +2192,14 @@ static bool impl_surface_place_under_pointer(struct yasl_surface *s, struct yasl
return cocoa_surface_place_under_pointer((struct cocoa_surface *)s, at);
}
+static bool impl_surface_bell(struct yasl_surface *s)
+{
+ assert([NSThread isMainThread]);
+ (void)s;
+ NSBeep();
+ return true;
+}
+
static void *impl_display_get_native_handle(struct yasl_display *d)
{
return d ? (void *)NSApp : NULL;
@@ -2249,6 +2258,7 @@ const struct yasl_backend_ops yasl_cocoa_backend = {
.surface_show_window_menu = impl_surface_show_window_menu,
.surface_drag_carry = impl_surface_drag_carry,
.surface_place_under_pointer = impl_surface_place_under_pointer,
+ .surface_bell = impl_surface_bell,
.display_get_native_handle = impl_display_get_native_handle,
.surface_get_native_handle = impl_surface_get_native_handle,
.surface_get_native_frame = impl_surface_get_native_frame,
diff --git a/src/platform/freedesktop/dbus.c b/src/platform/freedesktop/dbus.c
new file mode 100644
index 0000000..abca900
--- /dev/null
+++ b/src/platform/freedesktop/dbus.c
@@ -0,0 +1,187 @@
+#define _GNU_SOURCE
+#include "platform/freedesktop/dbus.h"
+
+#if YASL_HAS_DBUS
+
+#include <dbus/dbus.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+/* The portal may need activation on first use; bound how long a query blocks. */
+#define PORTAL_TIMEOUT_MS 1000
+
+static DBusConnection *bus;
+
+/* The session bus address, without libdbus's X11 autolaunch fallback. */
+static char *session_address(void)
+{
+ const char *env = getenv("DBUS_SESSION_BUS_ADDRESS");
+ if (env && *env) return strdup(env);
+ const char *run = getenv("XDG_RUNTIME_DIR");
+ if (!run || !*run) return NULL;
+ char path[4096];
+ struct stat st;
+ if (snprintf(path, sizeof(path), "%s/bus", run) >= (int)sizeof(path) ||
+ stat(path, &st) != 0 || !S_ISSOCK(st.st_mode))
+ return NULL;
+ char *escaped = dbus_address_escape_value(path);
+ if (!escaped) return NULL;
+ char *address = NULL;
+ if (asprintf(&address, "unix:path=%s", escaped) < 0) address = NULL;
+ dbus_free(escaped);
+ return address;
+}
+
+static DBusConnection *session_bus(void)
+{
+ if (bus && dbus_connection_get_is_connected(bus)) return bus;
+ if (bus) {
+ dbus_connection_close(bus);
+ dbus_connection_unref(bus);
+ bus = NULL;
+ }
+ char *address = session_address();
+ if (!address) return NULL;
+ DBusError err;
+ dbus_error_init(&err);
+ DBusConnection *c = dbus_connection_open_private(address, &err);
+ free(address);
+ if (!c) {
+ dbus_error_free(&err);
+ return NULL;
+ }
+ dbus_connection_set_exit_on_disconnect(c, FALSE);
+ if (!dbus_bus_register(c, &err)) {
+ dbus_error_free(&err);
+ dbus_connection_close(c);
+ dbus_connection_unref(c);
+ return NULL;
+ }
+ bus = c;
+ return bus;
+}
+
+bool bus_notify(const char *app, const char *title, const char *body)
+{
+ DBusConnection *c = session_bus();
+ if (!c) return false;
+ DBusMessage *msg = dbus_message_new_method_call("org.freedesktop.Notifications",
+ "/org/freedesktop/Notifications", "org.freedesktop.Notifications", "Notify");
+ if (!msg) return false;
+
+ const char *icon = "";
+ dbus_uint32_t replaces = 0;
+ dbus_int32_t expire = -1;
+ DBusMessageIter it, sub, entry, value;
+ dbus_message_iter_init_append(msg, &it);
+ bool ok = dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &app) &&
+ dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &replaces) &&
+ dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &icon) &&
+ dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &title) &&
+ dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &body);
+ /* No actions. */
+ ok = ok && dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "s", &sub) &&
+ dbus_message_iter_close_container(&it, &sub);
+ /* Hints: desktop-entry names the application's .desktop file. */
+ const char *hint = "desktop-entry";
+ ok = ok && dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &sub) &&
+ dbus_message_iter_open_container(&sub, DBUS_TYPE_DICT_ENTRY, NULL, &entry) &&
+ dbus_message_iter_append_basic(&entry, DBUS_TYPE_STRING, &hint) &&
+ dbus_message_iter_open_container(&entry, DBUS_TYPE_VARIANT, "s", &value) &&
+ dbus_message_iter_append_basic(&value, DBUS_TYPE_STRING, &app) &&
+ dbus_message_iter_close_container(&entry, &value) &&
+ dbus_message_iter_close_container(&sub, &entry) &&
+ dbus_message_iter_close_container(&it, &sub);
+ ok = ok && dbus_message_iter_append_basic(&it, DBUS_TYPE_INT32, &expire);
+
+ if (ok) {
+ dbus_message_set_no_reply(msg, TRUE);
+ ok = dbus_connection_send(c, msg, NULL);
+ if (ok) dbus_connection_flush(c);
+ }
+ dbus_message_unref(msg);
+ return ok;
+}
+
+/* Read an integer from it, unwrapping variants: Settings.Read nests one more. */
+static bool iter_int(DBusMessageIter *it, int64_t *out)
+{
+ DBusMessageIter inner[4];
+ int depth = 0;
+ while (dbus_message_iter_get_arg_type(it) == DBUS_TYPE_VARIANT && depth < 4) {
+ dbus_message_iter_recurse(it, &inner[depth]);
+ it = &inner[depth++];
+ }
+ DBusBasicValue v;
+ switch (dbus_message_iter_get_arg_type(it)) {
+ case DBUS_TYPE_BYTE: dbus_message_iter_get_basic(it, &v); *out = v.byt; return true;
+ case DBUS_TYPE_BOOLEAN: dbus_message_iter_get_basic(it, &v); *out = v.bool_val; return true;
+ case DBUS_TYPE_INT16: dbus_message_iter_get_basic(it, &v); *out = v.i16; return true;
+ case DBUS_TYPE_UINT16: dbus_message_iter_get_basic(it, &v); *out = v.u16; return true;
+ case DBUS_TYPE_INT32: dbus_message_iter_get_basic(it, &v); *out = v.i32; return true;
+ case DBUS_TYPE_UINT32: dbus_message_iter_get_basic(it, &v); *out = v.u32; return true;
+ case DBUS_TYPE_INT64: dbus_message_iter_get_basic(it, &v); *out = v.i64; return true;
+ case DBUS_TYPE_UINT64:
+ dbus_message_iter_get_basic(it, &v);
+ if (v.u64 > INT64_MAX) return false;
+ *out = (int64_t)v.u64;
+ return true;
+ default: return false;
+ }
+}
+
+bool portal_read_int(const char *ns, const char *key, int64_t *out)
+{
+ DBusConnection *c = session_bus();
+ if (!c) return false;
+ /* ReadOne is portal version 2; older portals only have Read. */
+ static const char *const methods[] = {"ReadOne", "Read"};
+ for (int i = 0; i < 2; i++) {
+ DBusMessage *msg = dbus_message_new_method_call("org.freedesktop.portal.Desktop",
+ "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Settings", methods[i]);
+ if (!msg) return false;
+ if (!dbus_message_append_args(msg, DBUS_TYPE_STRING, &ns, DBUS_TYPE_STRING, &key,
+ DBUS_TYPE_INVALID)) {
+ dbus_message_unref(msg);
+ return false;
+ }
+ DBusError err;
+ dbus_error_init(&err);
+ DBusMessage *reply = dbus_connection_send_with_reply_and_block(c, msg,
+ PORTAL_TIMEOUT_MS, &err);
+ dbus_message_unref(msg);
+ if (!reply) {
+ bool retry = dbus_error_has_name(&err, DBUS_ERROR_UNKNOWN_METHOD);
+ dbus_error_free(&err);
+ if (retry) continue;
+ return false;
+ }
+ DBusMessageIter it;
+ bool ok = dbus_message_iter_init(reply, &it) && iter_int(&it, out);
+ dbus_message_unref(reply);
+ return ok;
+ }
+ return false;
+}
+
+#else
+
+bool bus_notify(const char *app, const char *title, const char *body)
+{
+ (void)app;
+ (void)title;
+ (void)body;
+ return false;
+}
+
+bool portal_read_int(const char *ns, const char *key, int64_t *out)
+{
+ (void)ns;
+ (void)key;
+ (void)out;
+ return false;
+}
+
+#endif
diff --git a/src/platform/freedesktop/dbus.h b/src/platform/freedesktop/dbus.h
new file mode 100644
index 0000000..3b1662b
--- /dev/null
+++ b/src/platform/freedesktop/dbus.h
@@ -0,0 +1,16 @@
+#ifndef YASL_FREEDESKTOP_DBUS_H
+#define YASL_FREEDESKTOP_DBUS_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/* Session-bus helpers. The first call opens a private connection that later
+ * calls reuse. Each returns false when YASL is built without libdbus-1 or no
+ * session bus is reachable. Call from one thread at a time. */
+
+/* Send org.freedesktop.Notifications.Notify without waiting for a reply. */
+bool bus_notify(const char *app, const char *title, const char *body);
+/* Read an integer setting through the XDG Settings portal, waiting briefly. */
+bool portal_read_int(const char *ns, const char *key, int64_t *out);
+
+#endif
diff --git a/src/platform/freedesktop/desktop.c b/src/platform/freedesktop/desktop.c
new file mode 100644
index 0000000..a8b9bc7
--- /dev/null
+++ b/src/platform/freedesktop/desktop.c
@@ -0,0 +1,150 @@
+#define _GNU_SOURCE
+#include "common/desktop.h"
+#include "platform/freedesktop/dbus.h"
+#include "yasl/desktop.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+extern char **environ;
+
+static size_t copy_str(char *buf, size_t cap, const char *s)
+{
+ size_t len = strlen(s);
+ if (!len || len >= cap) return 0;
+ memcpy(buf, s, len + 1);
+ return len;
+}
+
+/* Find an executable in PATH, like execvp but before forking. */
+static bool find_program(const char *name, char *out, size_t cap)
+{
+ const char *path = getenv("PATH");
+ if (!path || !*path) path = "/usr/local/bin:/usr/bin:/bin";
+ size_t name_len = strlen(name);
+ while (*path) {
+ const char *end = strchr(path, ':');
+ size_t dir_len = end ? (size_t)(end - path) : strlen(path);
+ if (dir_len && dir_len + 1 + name_len < cap) {
+ memcpy(out, path, dir_len);
+ out[dir_len] = '/';
+ memcpy(out + dir_len + 1, name, name_len + 1);
+ if (access(out, X_OK) == 0) return true;
+ }
+ if (!end) break;
+ path = end + 1;
+ }
+ return false;
+}
+
+bool desktop_open_uri(const char *uri)
+{
+ char program[4096];
+ if (!find_program("xdg-open", program, sizeof(program))) return false;
+ char *argv[] = {"xdg-open", (char *)uri, NULL};
+
+ /* Prepare everything before fork: the children of a threaded process may
+ * only make async-signal-safe calls. */
+ struct sigaction dfl = {.sa_handler = SIG_DFL};
+ sigemptyset(&dfl.sa_mask);
+ sigset_t none;
+ sigemptyset(&none);
+ long max_fd = sysconf(_SC_OPEN_MAX);
+ if (max_fd < 0 || max_fd > 65536) max_fd = 65536;
+
+ /* The middle child exits at once, so the opener is reparented away from
+ * the application and never needs reaping here. */
+ pid_t pid = fork();
+ if (pid < 0) return false;
+ if (pid == 0) {
+ setsid();
+ pid_t opener = fork();
+ if (opener != 0) _exit(opener < 0 ? 1 : 0);
+ for (int sig = 1; sig < NSIG; sig++)
+ if (sig != SIGKILL && sig != SIGSTOP) sigaction(sig, &dfl, NULL);
+ sigprocmask(SIG_SETMASK, &none, NULL);
+ int null = open("/dev/null", O_RDONLY);
+ if (null > 0) {
+ dup2(null, STDIN_FILENO);
+ close(null);
+ }
+ /* Keep application descriptors, such as pty masters, out of the opener. */
+#ifdef SYS_close_range
+ if (syscall(SYS_close_range, 3u, ~0u, 0u) != 0)
+#endif
+ for (int fd = 3; fd < max_fd; fd++) close(fd);
+ execve(program, argv, environ);
+ _exit(127);
+ }
+
+ int status;
+ pid_t done;
+ do {
+ done = waitpid(pid, &status, 0);
+ } while (done < 0 && errno == EINTR);
+ /* An application reaper can collect the middle child first. */
+ if (done < 0) return errno == ECHILD;
+ return WIFEXITED(status) && WEXITSTATUS(status) == 0;
+}
+
+bool desktop_notify(const char *app_id, const char *title, const char *body)
+{
+ return bus_notify(app_id ? app_id : "yasl", title, body);
+}
+
+uint32_t desktop_appearance(void)
+{
+ uint32_t flags = 0;
+ bool portal = false;
+ int64_t value;
+ if (portal_read_int("org.freedesktop.appearance", "color-scheme", &value)) {
+ portal = true;
+ if (value == 1) flags |= YASL_APPEARANCE_DARK;
+ else if (value == 2) flags |= YASL_APPEARANCE_LIGHT;
+ }
+ if (portal_read_int("org.freedesktop.appearance", "contrast", &value)) {
+ portal = true;
+ if (value == 1) flags |= YASL_APPEARANCE_HIGH_CONTRAST;
+ }
+ if (portal) return flags;
+
+ /* GTK_THEME is "Name" or "Name:variant", as in "Adwaita:dark". */
+ const char *theme = getenv("GTK_THEME");
+ if (!theme) return 0;
+ if (strstr(theme, "HighContrast")) flags |= YASL_APPEARANCE_HIGH_CONTRAST;
+ if (strstr(theme, ":dark") || strstr(theme, "HighContrastInverse"))
+ flags |= YASL_APPEARANCE_DARK;
+ return flags;
+}
+
+size_t desktop_executable_path(char *buf, size_t cap)
+{
+ /* readlink truncates silently, so a full buffer may hold a cut path. */
+ ssize_t len = readlink("/proc/self/exe", buf, cap);
+ if (len <= 0 || (size_t)len >= cap) return 0;
+ buf[len] = '\0';
+ return (size_t)len;
+}
+
+size_t desktop_resource_dir(char *buf, size_t cap)
+{
+ (void)buf;
+ (void)cap;
+ return 0;
+}
+
+size_t desktop_default_locale(char *buf, size_t cap)
+{
+ static const char *const vars[] = {"LC_ALL", "LC_CTYPE", "LANG"};
+ for (size_t i = 0; i < sizeof(vars) / sizeof(vars[0]); i++) {
+ const char *v = getenv(vars[i]);
+ if (v && *v) return copy_str(buf, cap, v);
+ }
+ return copy_str(buf, cap, "C.UTF-8");
+}
diff --git a/src/platform/freedesktop/meson.build b/src/platform/freedesktop/meson.build
new file mode 100644
index 0000000..6f43895
--- /dev/null
+++ b/src/platform/freedesktop/meson.build
@@ -0,0 +1 @@
+sources += files('desktop.c', 'dbus.c')
diff --git a/src/platform/meson.build b/src/platform/meson.build
index 892969f..593353c 100644
--- a/src/platform/meson.build
+++ b/src/platform/meson.build
@@ -3,6 +3,10 @@
if system == 'linux' or system == 'darwin'
subdir('posix')
endif
+# Desktop services shared by the Linux backends.
+if system == 'linux'
+ subdir('freedesktop')
+endif
if has_wayland
subdir('wayland')
diff --git a/src/platform/wayland/api.h b/src/platform/wayland/api.h
index e5e933e..6c3154c 100644
--- a/src/platform/wayland/api.h
+++ b/src/platform/wayland/api.h
@@ -38,5 +38,6 @@ bool wayland_surface_start_interactive_resize(struct wayland_surface *s, enum ya
bool wayland_surface_show_window_menu(struct wayland_surface *s, struct yasl_vector2i pos);
bool wayland_surface_drag_carry(struct wayland_surface *s, struct yasl_vector2i offset);
bool wayland_surface_place_under_pointer(struct wayland_surface *s, struct yasl_vector2i at);
+bool wayland_surface_bell(struct wayland_surface *s);
#endif
diff --git a/src/platform/wayland/backend.c b/src/platform/wayland/backend.c
index c3c7bed..c6272ce 100644
--- a/src/platform/wayland/backend.c
+++ b/src/platform/wayland/backend.c
@@ -177,6 +177,11 @@ static bool impl_surface_place_under_pointer(struct yasl_surface *s, struct yasl
return wayland_surface_place_under_pointer((struct wayland_surface *)s, at);
}
+static bool impl_surface_bell(struct yasl_surface *s)
+{
+ return wayland_surface_bell((struct wayland_surface *)s);
+}
+
static void *impl_display_get_native_handle(struct yasl_display *d)
{
struct wayland_display *w = (struct wayland_display *)d;
@@ -225,6 +230,7 @@ const struct yasl_backend_ops yasl_wayland_backend = {
.surface_show_window_menu = impl_surface_show_window_menu,
.surface_drag_carry = impl_surface_drag_carry,
.surface_place_under_pointer = impl_surface_place_under_pointer,
+ .surface_bell = impl_surface_bell,
.display_get_native_handle = impl_display_get_native_handle,
.surface_get_native_handle = impl_surface_get_native_handle,
};
diff --git a/src/platform/wayland/internal.h b/src/platform/wayland/internal.h
index c35db7f..84a603d 100644
--- a/src/platform/wayland/internal.h
+++ b/src/platform/wayland/internal.h
@@ -25,6 +25,7 @@
#include "protocols/primary-selection-unstable-v1-client-protocol.h"
#include "protocols/relative-pointer-unstable-v1-client-protocol.h"
#include "protocols/pointer-constraints-unstable-v1-client-protocol.h"
+#include "protocols/xdg-system-bell-v1-client-protocol.h"
#include <fcntl.h>
#include <limits.h>
@@ -217,6 +218,7 @@ struct wayland_display {
struct wp_cursor_shape_manager_v1 *cursor_shape_mgr;
struct zwp_relative_pointer_manager_v1 *relative_pointer_mgr;
struct zwp_pointer_constraints_v1 *pointer_constraints;
+ struct xdg_system_bell_v1 *system_bell;
struct wl_cursor_theme *cursor_theme;
const char *cursor_theme_name;
struct zwp_tablet_manager_v2 *tablet_mgr;
diff --git a/src/platform/wayland/shell.c b/src/platform/wayland/shell.c
index a12bd4d..8795989 100644
--- a/src/platform/wayland/shell.c
+++ b/src/platform/wayland/shell.c
@@ -214,6 +214,8 @@ static void registry_global(void *data, struct wl_registry *registry,
} else if (strcmp(interface, zwp_pointer_constraints_v1_interface.name) == 0) {
c->pointer_constraints = wl_registry_bind(registry, name,
&zwp_pointer_constraints_v1_interface, 1);
+ } else if (strcmp(interface, xdg_system_bell_v1_interface.name) == 0) {
+ c->system_bell = wl_registry_bind(registry, name, &xdg_system_bell_v1_interface, 1);
} else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) {
c->scale_mgr = wl_registry_bind(registry, name,
&wp_fractional_scale_manager_v1_interface, 1);
@@ -403,3 +405,10 @@ int surface_init_shell(struct wayland_surface *w, const char *title)
}
return 0;
}
+
+bool wayland_surface_bell(struct wayland_surface *w)
+{
+ if (!w || w->closed || !w->conn || !w->conn->system_bell || !w->surface) return false;
+ xdg_system_bell_v1_ring(w->conn->system_bell, w->surface);
+ return true;
+}
diff --git a/src/platform/wayland/surface.c b/src/platform/wayland/surface.c
index 0f6d7d7..cf1877d 100644
--- a/src/platform/wayland/surface.c
+++ b/src/platform/wayland/surface.c
@@ -202,6 +202,7 @@ static void display_free(struct wayland_display *c)
if (c->cursor_shape_mgr) wp_cursor_shape_manager_v1_destroy(c->cursor_shape_mgr);
if (c->relative_pointer_mgr) zwp_relative_pointer_manager_v1_destroy(c->relative_pointer_mgr);
if (c->pointer_constraints) zwp_pointer_constraints_v1_destroy(c->pointer_constraints);
+ if (c->system_bell) xdg_system_bell_v1_destroy(c->system_bell);
if (c->cursor_theme) wl_cursor_theme_destroy(c->cursor_theme);
if (c->tablet_mgr) zwp_tablet_manager_v2_destroy(c->tablet_mgr);
if (c->text_input_mgr) zwp_text_input_manager_v3_destroy(c->text_input_mgr);
@@ -246,6 +247,7 @@ struct wayland_display *wayland_display_open(struct yasl_loop *loop, const struc
c->cbs = &c->base.callbacks;
c->ud = spec->ud;
if (spec->app_id) c->app_id = strdup(spec->app_id);
+ c->base.app_id = c->app_id;
}
c->xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
c->wl = c->xkb_ctx ? wl_display_connect(NULL) : NULL;
diff --git a/src/platform/x11/window.c b/src/platform/x11/window.c
index f60a878..be1ff32 100644
--- a/src/platform/x11/window.c
+++ b/src/platform/x11/window.c
@@ -1601,6 +1601,7 @@ static struct yasl_display *display_open(struct yasl_loop *loop,
}
d->base.loop = d->loop;
d->app_id = strdup(spec && spec->app_id ? spec->app_id : "YASL");
+ d->base.app_id = d->app_id;
if (!d->loop || !d->app_id) {
if (d->owns_loop) yasl_loop_destroy(d->loop);
free(d->app_id);
@@ -1803,6 +1804,18 @@ static bool place(struct yasl_surface *base, struct yasl_vector2i at)
return true;
}
+static bool bell(struct yasl_surface *base)
+{
+ struct x11_surface *s = (struct x11_surface *)base;
+ struct x11_display *d = (struct x11_display *)base->display;
+ if (s->destroyed || !d->native) return false;
+ /* The XKB bell names the window, so the window manager can flash it. */
+ if (!XkbBell(d->native, s->window, 0, None))
+ XBell(d->native, 0);
+ XFlush(d->native);
+ return true;
+}
+
static void drag_end(struct x11_display *d, enum yasl_drag_end how)
{
if (!d || !d->drag.active) return;
@@ -2045,6 +2058,7 @@ const struct yasl_backend_ops yasl_x11_backend = {
.surface_set_cursor = cursor,
.surface_drag_carry = drag_carry,
.surface_place_under_pointer = place,
+ .surface_bell = bell,
.surface_start_interactive_move = interactive_move,
.surface_start_interactive_resize = interactive_resize,
.surface_show_window_menu = show_window_menu,