/* * desktop.c * Copyright (C) 2017 Kovid Goyal * * Distributed under terms of the GPL3 license. */ #include "data-types.h" #include "safe-wrappers.h" #include "cleanup.h" #include "loop-utils.h" #include "threading.h" #include #define FUNC(name, restype, ...) typedef restype (*name##_func)(__VA_ARGS__); static name##_func name = NULL #define LOAD_FUNC(handle, name) {\ *(void **) (&name) = dlsym(handle, #name); \ if (!name) { \ const char* error = dlerror(); \ if (error != NULL) { \ PyErr_Format(PyExc_OSError, "Failed to load the function %s with error: %s", #name, error); dlclose(handle); handle = NULL; return NULL; \ } \ } \ } FUNC(sn_display_new, void*, void*, void*, void*); FUNC(sn_launchee_context_new_from_environment, void*, void*, int); FUNC(sn_launchee_context_new, void*, void*, int, const char*); FUNC(sn_display_unref, void, void*); FUNC(sn_launchee_context_setup_window, void, void*, int32_t); FUNC(sn_launchee_context_complete, void, void*); FUNC(sn_launchee_context_unref, void, void*); static void* libsn_handle = NULL; static PyObject* init_x11_startup_notification(PyObject UNUSED *self, PyObject *args) { static bool done = false; if (!done) { done = true; const char* libnames[] = { #if defined(_KITTY_STARTUP_NOTIFICATION_LIBRARY) _KITTY_STARTUP_NOTIFICATION_LIBRARY, #else "libstartup-notification-1.so", // some installs are missing the .so symlink, so try the full name "libstartup-notification-1.so.0", "libstartup-notification-1.so.0.0.0", #endif NULL }; for (int i = 0; libnames[i]; i++) { libsn_handle = dlopen(libnames[i], RTLD_LAZY); if (libsn_handle) break; } if (libsn_handle == NULL) { PyErr_Format(PyExc_OSError, "Failed to load %s with error: %s", libnames[0], dlerror()); return NULL; } dlerror(); /* Clear any existing error */ #define F(name) LOAD_FUNC(libsn_handle, name) F(sn_display_new); F(sn_launchee_context_new_from_environment); F(sn_launchee_context_new); F(sn_display_unref); F(sn_launchee_context_setup_window); F(sn_launchee_context_complete); F(sn_launchee_context_unref); #undef F } int window_id; PyObject *dp; char *startup_id = NULL; if (!PyArg_ParseTuple(args, "O!i|z", &PyLong_Type, &dp, &window_id, &startup_id)) return NULL; void* display = PyLong_AsVoidPtr(dp); void* sn_display = sn_display_new(display, NULL, NULL); if (!sn_display) { PyErr_SetString(PyExc_OSError, "Failed to create SnDisplay"); return NULL; } void *ctx = startup_id ? sn_launchee_context_new(sn_display, 0, startup_id) : sn_launchee_context_new_from_environment(sn_display, 0); sn_display_unref(sn_display); if (!ctx) { PyErr_SetString(PyExc_OSError, "Failed to create startup-notification context"); return NULL; } sn_launchee_context_setup_window(ctx, window_id); return PyLong_FromVoidPtr(ctx); } static PyObject* end_x11_startup_notification(PyObject UNUSED *self, PyObject *args) { if (!libsn_handle) Py_RETURN_NONE; PyObject *dp; if (!PyArg_ParseTuple(args, "O!", &PyLong_Type, &dp)) return NULL; void *ctx = PyLong_AsVoidPtr(dp); sn_launchee_context_complete(ctx); sn_launchee_context_unref(ctx); Py_RETURN_NONE; } static void* libcanberra_handle = NULL; static void *canberra_ctx = NULL; FUNC(ca_context_create, int, void**); FUNC(ca_context_destroy, int, void*); typedef int (*ca_context_play_func)(void*, uint32_t, ...); static ca_context_play_func ca_context_play = NULL; static PyObject* load_libcanberra_functions(void) { LOAD_FUNC(libcanberra_handle, ca_context_create); LOAD_FUNC(libcanberra_handle, ca_context_play); LOAD_FUNC(libcanberra_handle, ca_context_destroy); return NULL; } static void load_libcanberra(void) { static bool done = false; if (done) return; done = true; const char* libnames[] = { #if defined(_KITTY_CANBERRA_LIBRARY) _KITTY_CANBERRA_LIBRARY, #else "libcanberra.so", // some installs are missing the .so symlink, so try the full name "libcanberra.so.0", "libcanberra.so.0.2.5", #endif NULL }; for (int i = 0; libnames[i]; i++) { libcanberra_handle = dlopen(libnames[i], RTLD_LAZY); if (libcanberra_handle) break; } if (libcanberra_handle == NULL) { fprintf(stderr, "Failed to load %s, cannot play beep sound, with error: %s\n", libnames[0], dlerror()); return; } load_libcanberra_functions(); if (PyErr_Occurred()) { PyErr_Print(); dlclose(libcanberra_handle); libcanberra_handle = NULL; return; } if (ca_context_create(&canberra_ctx) != 0) { fprintf(stderr, "Failed to create libcanberra context, cannot play beep sound\n"); ca_context_destroy(canberra_ctx); canberra_ctx = NULL; dlclose(libcanberra_handle); libcanberra_handle = NULL; } } typedef struct { char *which_sound, *event_id, *media_role; bool is_path; } CanberraEvent; static pthread_t canberra_thread; static int canberra_pipe_r = -1, canberra_pipe_w = -1; static pthread_mutex_t canberra_lock; static CanberraEvent current_sound = {0}; static void free_canberra_event_fields(CanberraEvent *e) { free(e->which_sound); e->which_sound = NULL; free(e->event_id); e->event_id = NULL; free(e->media_role); e->media_role = NULL; } static void play_current_sound(void) { CanberraEvent e; pthread_mutex_lock(&canberra_lock); e = current_sound; current_sound = (const CanberraEvent){ 0 }; pthread_mutex_unlock(&canberra_lock); if (e.which_sound && e.event_id && e.media_role) { const char *which_type = e.is_path ? "media.filename" : "event.id"; ca_context_play( canberra_ctx, 0, which_type, e.which_sound, "event.description", e.event_id, "media.role", e.media_role, "canberra.cache-control", "permanent", NULL ); free_canberra_event_fields(&e); } } static void queue_canberra_sound(const char *which_sound, const char *event_id, bool is_path, const char *media_role) { pthread_mutex_lock(&canberra_lock); current_sound.which_sound = strdup(which_sound); current_sound.event_id = strdup(event_id); current_sound.media_role = strdup(media_role); current_sound.is_path = is_path; pthread_mutex_unlock(&canberra_lock); while (true) { ssize_t ret = write(canberra_pipe_w, "w", 1); if (ret < 0) { if (errno == EINTR) continue; log_error("Failed to write to canberra wakeup fd with error: %s", strerror(errno)); } break; } } static void* canberra_play_loop(void *x UNUSED) { // canberra hangs on misconfigured systems. We dont want kitty to hang so use a thread. // For example: https://github.com/kovidgoyal/kitty/issues/5646 static char buf[16]; set_thread_name("LinuxAudioSucks"); while (true) { int ret = read(canberra_pipe_r, buf, sizeof(buf)); if (ret < 0) { if (errno == EINTR || errno == EAGAIN) continue; break; } play_current_sound(); } safe_close(canberra_pipe_r, __FILE__, __LINE__); return NULL; } void play_canberra_sound(const char *which_sound, const char *event_id, bool is_path, const char *media_role) { load_libcanberra(); if (libcanberra_handle == NULL || canberra_ctx == NULL) return; int ret; if (canberra_pipe_r == -1) { int fds[2]; if ((ret = pthread_mutex_init(&canberra_lock, NULL)) != 0) return; if (!self_pipe(fds, false)) return; canberra_pipe_r = fds[0]; canberra_pipe_w = fds[1]; int flags = fcntl(canberra_pipe_w, F_GETFL); fcntl(canberra_pipe_w, F_SETFL, flags | O_NONBLOCK); if ((ret = pthread_create(&canberra_thread, NULL, canberra_play_loop, NULL)) != 0) return; } queue_canberra_sound(which_sound, event_id, is_path, media_role); } static PyObject* play_desktop_sound(PyObject *self UNUSED, PyObject *args) { const char *which, *event_id = "test sound"; int is_path = 0; if (!PyArg_ParseTuple(args, "s|sp", &which, &event_id, &is_path)) return NULL; play_canberra_sound(which, event_id, is_path, "event"); Py_RETURN_NONE; } static void finalize(void) { if (libsn_handle) dlclose(libsn_handle); libsn_handle = NULL; if (canberra_pipe_w > -1) { pthread_mutex_lock(&canberra_lock); free_canberra_event_fields(¤t_sound); pthread_mutex_unlock(&canberra_lock); safe_close(canberra_pipe_w, __FILE__, __LINE__); } if (canberra_ctx) ca_context_destroy(canberra_ctx); canberra_ctx = NULL; if (libcanberra_handle) dlclose(libcanberra_handle); } static PyMethodDef module_methods[] = { METHODB(init_x11_startup_notification, METH_VARARGS), METHODB(end_x11_startup_notification, METH_VARARGS), METHODB(play_desktop_sound, METH_VARARGS), {NULL, NULL, 0, NULL} /* Sentinel */ }; bool init_desktop(PyObject *m) { if (PyModule_AddFunctions(m, module_methods) != 0) return false; register_at_exit_cleanup_func(DESKTOP_CLEANUP_FUNC, finalize); return true; }