diff --git a/docs/changelog.rst b/docs/changelog.rst index caa7ede9d..083f2f6aa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -53,6 +53,8 @@ Detailed list of changes - Wayland: Fix for a bug preventing kitty from starting on Hyprland when using a non-unit scale (:iss:`5467`) +- Wayland: Generate a XDG_ACTIVATION_TOKEN when opening URLs or running programs in the background via the launch action + 0.26.2 [2022-09-05] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/glfw/glfw.py b/glfw/glfw.py index 8558ab90d..3cef8ede9 100755 --- a/glfw/glfw.py +++ b/glfw/glfw.py @@ -246,6 +246,7 @@ def generate_wrappers(glfw_header: str) -> None: int glfwGetNativeKeyForName(const char* key_name, int case_sensitive) void glfwRequestWaylandFrameEvent(GLFWwindow *handle, unsigned long long id, GLFWwaylandframecallbackfunc callback) void glfwWaylandActivateWindow(GLFWwindow *handle, const char *activation_token) + void glfwWaylandRunWithActivationToken(GLFWwindow *handle, GLFWactivationcallback cb, void *cb_data) bool glfwWaylandSetTitlebarColor(GLFWwindow *handle, uint32_t color, bool use_system_color) unsigned long long glfwDBusUserNotify(const char *app_name, const char* icon, const char *summary, const char *body, \ const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callback, void *data) diff --git a/glfw/wl_window.c b/glfw/wl_window.c index eacc86c78..6a1578a26 100644 --- a/glfw/wl_window.c +++ b/glfw/wl_window.c @@ -2223,6 +2223,12 @@ GLFWAPI void glfwWaylandActivateWindow(GLFWwindow* handle, const char *activatio if (activation_token && activation_token[0]) xdg_activation_v1_activate(_glfw.wl.xdg_activation_v1, activation_token, window->wl.surface); } +GLFWAPI void glfwWaylandRunWithActivationToken(GLFWwindow *handle, GLFWactivationcallback cb, void *cb_data) { + _GLFWwindow* window = (_GLFWwindow*) handle; + _GLFW_REQUIRE_INIT(); + get_activation_token(window, _glfw.wl.input_serial, cb, cb_data); +} + GLFWAPI int glfwGetNativeKeyForName(const char* keyName, bool caseSensitive) { return glfw_xkb_keysym_from_name(keyName, caseSensitive); } diff --git a/kitty/boss.py b/kitty/boss.py index 1e543eb4d..7cd41d77a 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -20,7 +20,10 @@ from weakref import WeakValueDictionary from .child import cached_process_data, default_env, set_default_env from .cli import create_opts, parse_args from .cli_stub import CLIOptions -from .clipboard import Clipboard, get_primary_selection, set_primary_selection, get_clipboard_string, set_clipboard_string +from .clipboard import ( + Clipboard, get_clipboard_string, get_primary_selection, + set_clipboard_string, set_primary_selection +) from .conf.utils import BadLine, KeyAction, to_cmdline from .config import common_opts_as_dict, prepare_config_file_for_editing from .constants import ( @@ -38,11 +41,11 @@ from .fast_data_types import ( current_application_quit_request, current_os_window, destroy_global_data, focus_os_window, get_boss, get_options, get_os_window_size, global_font_size, mark_os_window_for_close, os_window_font_size, - patch_global_colors, redirect_mouse_handling, ring_bell, safe_pipe, - send_data_to_peer, set_application_quit_request, set_background_image, - set_boss, set_in_sequence_mode, set_options, set_os_window_size, - set_os_window_title, thread_write, toggle_fullscreen, toggle_maximized, - toggle_secure_input + patch_global_colors, redirect_mouse_handling, ring_bell, + run_with_activation_token, safe_pipe, send_data_to_peer, + set_application_quit_request, set_background_image, set_boss, + set_in_sequence_mode, set_options, set_os_window_size, set_os_window_title, + thread_write, toggle_fullscreen, toggle_maximized, toggle_secure_input ) from .key_encoding import get_name_to_functional_number_map from .keys import get_shortcut, shortcut_matches @@ -1751,7 +1754,16 @@ class Boss: extra_env = {} if self.listening_on: extra_env['KITTY_LISTEN_ON'] = self.listening_on - open_url(url, program or get_options().open_url_with, cwd=cwd, extra_env=extra_env) + + def doit(activation_token: str = '') -> None: + if activation_token: + extra_env['XDG_ACTIVATION_TOKEN'] = activation_token + open_url(url, program or get_options().open_url_with, cwd=cwd, extra_env=extra_env) + + if is_wayland(): + run_with_activation_token(doit) + else: + doit() @ac('misc', 'Click a URL using the keyboard') def open_url_with_hints(self) -> None: @@ -1945,18 +1957,28 @@ class Boss: with suppress(Exception): cwd = cwd_from.cwd_of_child - if stdin: - r, w = safe_pipe(False) - try: - subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd, preexec_fn=clear_handled_signals) - except Exception: - os.close(w) + def doit(activation_token: str = '') -> None: + nonlocal env + if activation_token: + if env is None: + env = default_env().copy() + env['XDG_ACTIVATION_TOKEN'] = activation_token + if stdin: + r, w = safe_pipe(False) + try: + subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd, preexec_fn=clear_handled_signals) + except Exception: + os.close(w) + else: + thread_write(w, stdin) + finally: + os.close(r) else: - thread_write(w, stdin) - finally: - os.close(r) + subprocess.Popen(cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals) + if is_wayland(): + run_with_activation_token(doit) else: - subprocess.Popen(cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals) + doit() def pipe(self, source: str, dest: str, exe: str, *args: str) -> Optional[Window]: cmd = [exe] + list(args) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index f89621576..9d433fa03 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1474,3 +1474,4 @@ def get_docs_ref_map() -> bytes: ... def clearenv() -> None: ... def set_clipboard_data_types(ct: int, mime_types: Tuple[str, ...]) -> None: ... def get_clipboard_mime(ct: int, mime: Optional[str], callback: Callable[[bytes], None]) -> None: ... +def run_with_activation_token(func: Callable[[str], None]) -> None: ... diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index 038099338..8c5578b68 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -452,6 +452,9 @@ load_glfw(const char* path) { *(void **) (&glfwWaylandActivateWindow_impl) = dlsym(handle, "glfwWaylandActivateWindow"); if (glfwWaylandActivateWindow_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwWaylandRunWithActivationToken_impl) = dlsym(handle, "glfwWaylandRunWithActivationToken"); + if (glfwWaylandRunWithActivationToken_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwWaylandSetTitlebarColor_impl) = dlsym(handle, "glfwWaylandSetTitlebarColor"); if (glfwWaylandSetTitlebarColor_impl == NULL) dlerror(); // clear error indicator diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index 33e4bbc0b..0dcd5db13 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -2210,6 +2210,10 @@ typedef void (*glfwWaylandActivateWindow_func)(GLFWwindow*, const char*); GFW_EXTERN glfwWaylandActivateWindow_func glfwWaylandActivateWindow_impl; #define glfwWaylandActivateWindow glfwWaylandActivateWindow_impl +typedef void (*glfwWaylandRunWithActivationToken_func)(GLFWwindow*, GLFWactivationcallback, void*); +GFW_EXTERN glfwWaylandRunWithActivationToken_func glfwWaylandRunWithActivationToken_impl; +#define glfwWaylandRunWithActivationToken glfwWaylandRunWithActivationToken_impl + typedef bool (*glfwWaylandSetTitlebarColor_func)(GLFWwindow*, uint32_t, bool); GFW_EXTERN glfwWaylandSetTitlebarColor_func glfwWaylandSetTitlebarColor_impl; #define glfwWaylandSetTitlebarColor glfwWaylandSetTitlebarColor_impl diff --git a/kitty/glfw.c b/kitty/glfw.c index 82ed7e0ce..be1739ccd 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1317,6 +1317,26 @@ find_os_window(id_type os_window_id) { return NULL; } +static void +activation_token_callback(GLFWwindow *window UNUSED, const char *token, void *data) { + if (!token || !token[0]) { + token = ""; + log_error("Wayland: Did not get activation token from compositor. Use a better compositor."); + } + PyObject *ret = PyObject_CallFunction(data, "s", token); + if (ret == NULL) PyErr_Print(); + else Py_DECREF(ret); + Py_CLEAR(data); +} + +void +run_with_activation_token_in_os_window(OSWindow *w, PyObject *callback) { + if (global_state.is_wayland) { + Py_INCREF(callback); + glfwWaylandRunWithActivationToken(w->handle, activation_token_callback, callback); + } +} + static PyObject* toggle_fullscreen(PyObject UNUSED *self, PyObject *args) { id_type os_window_id = 0; diff --git a/kitty/state.c b/kitty/state.c index 4012803d8..f8fbfd17b 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -831,6 +831,17 @@ PYWRAP1(focus_os_window) { Py_RETURN_FALSE; } +PYWRAP1(run_with_activation_token) { + for (size_t o = 0; o < global_state.num_os_windows; o++) { + OSWindow *os_window = global_state.os_windows + o; + if (os_window->is_focused) { + run_with_activation_token_in_os_window(os_window, args); + break; + } + } + Py_RETURN_NONE; +} + PYWRAP1(set_titlebar_color) { id_type os_window_id; unsigned int color; @@ -1292,6 +1303,7 @@ static PyMethodDef module_methods[] = { MW(set_titlebar_color, METH_VARARGS), MW(focus_os_window, METH_VARARGS), MW(mark_tab_bar_dirty, METH_O), + MW(run_with_activation_token, METH_O), MW(change_background_opacity, METH_VARARGS), MW(background_opacity_of, METH_O), MW(update_window_visibility, METH_VARARGS), diff --git a/kitty/state.h b/kitty/state.h index b4ef7b8b8..895225338 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -271,6 +271,7 @@ void hide_mouse(OSWindow *w); bool is_mouse_hidden(OSWindow *w); void destroy_os_window(OSWindow *w); void focus_os_window(OSWindow *w, bool also_raise, const char *activation_token); +void run_with_activation_token_in_os_window(OSWindow *w, PyObject *callback); void set_os_window_title(OSWindow *w, const char *title); OSWindow* os_window_for_kitty_window(id_type); OSWindow* add_os_window(void);