diff --git a/glfw/glfw3.h b/glfw/glfw3.h index eee690aeb..f6c5db0c1 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -1718,6 +1718,17 @@ typedef void (* GLFWuserdatafun)(unsigned long long, void*); typedef void (* GLFWtickcallback)(void*); typedef bool (* GLFWdrawtextfun)(GLFWwindow *window, const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, float x_offset, float y_offset, size_t right_margin); typedef char* (* GLFWcurrentselectionfun)(void); +typedef void (* GLFWclipboarddatafreefun)(void* data); +typedef struct GLFWDataChunk { + const char *data; + size_t sz; + GLFWclipboarddatafreefun free; + void *iter, *free_data; +} GLFWDataChunk; +typedef enum { + GLFW_CLIPBOARD, GLFW_PRIMARY_SELECTION +} GLFWClipboardType; +typedef GLFWDataChunk (* GLFWclipboarditerfun)(const char *mime_type, void *iter, GLFWClipboardType ctype); /*! @brief Video mode type. * @@ -5245,39 +5256,7 @@ GLFWAPI int glfwGetGamepadState(int jid, GLFWgamepadstate* state); * * @ingroup input */ -GLFWAPI void glfwSetClipboardString(GLFWwindow* window, const char* string); - -/*! @brief Returns the contents of the clipboard as a string. - * - * This function returns the contents of the system clipboard, if it contains - * or is convertible to a UTF-8 encoded string. If the clipboard is empty or - * if its contents cannot be converted, `NULL` is returned and a @ref - * GLFW_FORMAT_UNAVAILABLE error is generated. - * - * @param[in] window Deprecated. Any valid window or `NULL`. - * @return The contents of the clipboard as a UTF-8 encoded string, or `NULL` - * if an [error](@ref error_handling) occurred. - * - * @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref - * GLFW_PLATFORM_ERROR. - * - * @remark @wayland Clipboard is currently unimplemented. - * - * @pointer_lifetime The returned string is allocated and freed by GLFW. You - * should not free it yourself. It is valid until the next call to @ref - * glfwGetClipboardString or @ref glfwSetClipboardString, or until the library - * is terminated. - * - * @thread_safety This function must only be called from the main thread. - * - * @sa @ref clipboard - * @sa @ref glfwSetClipboardString - * - * @since Added in version 3.0. - * - * @ingroup input - */ -GLFWAPI const char* glfwGetClipboardString(GLFWwindow* window); +GLFWAPI void glfwSetClipboardDataTypes(GLFWClipboardType clipboard_type, const char* const *mime_types, size_t num_mime_types, GLFWclipboarditerfun get_iter); /*! @brief Returns the GLFW time. * diff --git a/glfw/init.c b/glfw/init.c index 13c535635..f85252e67 100644 --- a/glfw/init.c +++ b/glfw/init.c @@ -68,6 +68,8 @@ static void terminate(void) int i; memset(&_glfw.callbacks, 0, sizeof(_glfw.callbacks)); + _glfw_free_clipboard_data(&_glfw.clipboard); + _glfw_free_clipboard_data(&_glfw.primary); while (_glfw.windowListHead) glfwDestroyWindow((GLFWwindow*) _glfw.windowListHead); diff --git a/glfw/input.c b/glfw/input.c index ea2c48073..a26b8d1b8 100644 --- a/glfw/input.c +++ b/glfw/input.c @@ -1526,12 +1526,30 @@ GLFWAPI int glfwGetGamepadState(int jid, GLFWgamepadstate* state) return true; } -GLFWAPI void glfwSetClipboardString(GLFWwindow* handle UNUSED, const char* string) -{ - assert(string != NULL); +void _glfw_free_clipboard_data(_GLFWClipboardData *cd) { + if (cd->mime_types) { + for (size_t i = 0; i < cd->num_mime_types; i++) free((void*)cd->mime_types[i]); + free((void*)cd->mime_types); + } + memset(cd, 0, sizeof(cd[0])); +} +GLFWAPI void glfwSetClipboardDataTypes(GLFWClipboardType clipboard_type, const char* const *mime_types, size_t num_mime_types, GLFWclipboarditerfun get_data) { + assert(mime_types != NULL); + assert(get_data != NULL); _GLFW_REQUIRE_INIT(); - _glfwPlatformSetClipboardString(string); + _GLFWClipboardData *cd = NULL; + switch(clipboard_type) { + case GLFW_CLIPBOARD: cd = &_glfw.clipboard; break; + case GLFW_PRIMARY_SELECTION: cd = &_glfw.primary; break; + } + _glfw_free_clipboard_data(cd); + cd->get_data = get_data; + cd->mime_types = calloc(num_mime_types, sizeof(char*)); + cd->num_mime_types = num_mime_types; + cd->ctype = clipboard_type; + for (size_t i = 0; i < cd->num_mime_types; i++) cd->mime_types[i] = _glfw_strdup(mime_types[i]); + _glfwPlatformSetClipboard(clipboard_type); } GLFWAPI const char* glfwGetClipboardString(GLFWwindow* handle UNUSED) @@ -1541,14 +1559,6 @@ GLFWAPI const char* glfwGetClipboardString(GLFWwindow* handle UNUSED) } #if defined(_GLFW_X11) || defined(_GLFW_WAYLAND) || defined(__APPLE__) -GLFWAPI void glfwSetPrimarySelectionString(GLFWwindow* handle UNUSED, const char* string) -{ - assert(string != NULL); - - _GLFW_REQUIRE_INIT(); - _glfwPlatformSetPrimarySelectionString(string); -} - GLFWAPI const char* glfwGetPrimarySelectionString(GLFWwindow* handle UNUSED) { _GLFW_REQUIRE_INIT_OR_RETURN(NULL); diff --git a/glfw/internal.h b/glfw/internal.h index 101921a25..07efdf367 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -34,6 +34,9 @@ #endif #define arraysz(x) (sizeof(x)/sizeof(x[0])) +#define MAX(x, y) __extension__ ({ \ + __typeof__ (x) a = (x); __typeof__ (y) b = (y); \ + a > b ? a : b;}) #if defined(GLFW_INCLUDE_GLCOREARB) || \ defined(GLFW_INCLUDE_ES1) || \ @@ -561,6 +564,13 @@ struct _GLFWmutex _GLFW_PLATFORM_MUTEX_STATE; }; +typedef struct _GLFWClipboardData { + const char** mime_types; + size_t num_mime_types; + GLFWclipboarditerfun get_data; + GLFWClipboardType ctype; +} _GLFWClipboardData; + // Library global data // struct _GLFWlibrary @@ -575,6 +585,8 @@ struct _GLFWlibrary int refreshRate; } hints; + _GLFWClipboardData primary, clipboard; + _GLFWerror* errorListHead; _GLFWcursor* cursorListHead; _GLFWwindow* windowListHead; @@ -674,12 +686,9 @@ void _glfwPlatformGetVideoMode(_GLFWmonitor* monitor, GLFWvidmode* mode); bool _glfwPlatformGetGammaRamp(_GLFWmonitor* monitor, GLFWgammaramp* ramp); void _glfwPlatformSetGammaRamp(_GLFWmonitor* monitor, const GLFWgammaramp* ramp); -void _glfwPlatformSetClipboardString(const char* string); +void _glfwPlatformSetClipboard(GLFWClipboardType t); const char* _glfwPlatformGetClipboardString(void); -#if defined(_GLFW_X11) || defined(_GLFW_WAYLAND) || defined(__APPLE__) -void _glfwPlatformSetPrimarySelectionString(const char* string); const char* _glfwPlatformGetPrimarySelectionString(void); -#endif bool _glfwPlatformInitJoysticks(void); void _glfwPlatformTerminateJoysticks(void); @@ -855,3 +864,5 @@ void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval, void _glfwPlatformRemoveTimer(unsigned long long timer_id); char* _glfw_strdup(const char* source); + +void _glfw_free_clipboard_data(_GLFWClipboardData *cd); diff --git a/glfw/x11_init.c b/glfw/x11_init.c index 80ab1601e..fc81b042f 100644 --- a/glfw/x11_init.c +++ b/glfw/x11_init.c @@ -680,8 +680,14 @@ void _glfwPlatformTerminate(void) glfw_xkb_release(&_glfw.x11.xkb); glfw_dbus_terminate(&_glfw.x11.dbus); - free(_glfw.x11.primarySelectionString); - free(_glfw.x11.clipboardString); + if (_glfw.x11.mime_atoms.array) { + for (size_t i = 0; i < _glfw.x11.mime_atoms.sz; i++) { + free((void*)_glfw.x11.mime_atoms.array[i].mime); + } + free(_glfw.x11.mime_atoms.array); + } + if (_glfw.x11.clipboard_atoms.array) { free(_glfw.x11.clipboard_atoms.array); } + if (_glfw.x11.primary_atoms.array) { free(_glfw.x11.primary_atoms.array); } if (_glfw.x11.display) { diff --git a/glfw/x11_platform.h b/glfw/x11_platform.h index 5f3f540f3..2a2c87826 100644 --- a/glfw/x11_platform.h +++ b/glfw/x11_platform.h @@ -207,6 +207,16 @@ typedef struct _GLFWwindowX11 } _GLFWwindowX11; +typedef struct MimeAtom { + Atom atom; + const char* mime; +} MimeAtom; + +typedef struct AtomArray { + MimeAtom *array; + size_t sz, capacity; +} AtomArray; + // X11-specific global data // typedef struct _GLFWlibraryX11 @@ -225,10 +235,6 @@ typedef struct _GLFWlibraryX11 XContext context; // Most recent error code received by X error handler int errorCode; - // Primary selection string (while the primary selection is owned) - char* primarySelectionString; - // Clipboard string (while the selection is owned) - char* clipboardString; // Where to place the cursor when re-enabled double restoreCursorPosX, restoreCursorPosY; // The window whose disabled cursor mode is active @@ -291,6 +297,8 @@ typedef struct _GLFWlibraryX11 // XRM database atom Atom RESOURCE_MANAGER; + // Atoms for MIME types + AtomArray mime_atoms, clipboard_atoms, primary_atoms; struct { bool available; diff --git a/glfw/x11_window.c b/glfw/x11_window.c index b9528add3..a12d0b9c7 100644 --- a/glfw/x11_window.c +++ b/glfw/x11_window.c @@ -716,19 +716,60 @@ static bool createNativeWindow(_GLFWwindow* window, return true; } +static size_t +get_clipboard_data(const _GLFWClipboardData *cd, const char *mime, char **data) { + *data = NULL; + GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype); + char *buf = NULL; + size_t sz = 0, cap = 0; + void *iter = chunk.iter; + if (!iter) return 0; + while (true) { + chunk = cd->get_data(mime, iter, cd->ctype); + if (!chunk.sz) break; + if (cap < sz + chunk.sz) { + cap = MAX(cap * 2, sz + 4 * chunk.sz); + buf = realloc(buf, cap); + } + memcpy(buf + sz, chunk.data, chunk.sz); + sz += chunk.sz; + if (chunk.free) chunk.free((void*)chunk.free_data); + } + *data = buf; + cd->get_data(NULL, iter, cd->ctype); + return sz; +} + +static void +get_atom_names(const Atom *atoms, int count, char **atom_names) { + _glfwGrabErrorHandlerX11(); + XGetAtomNames(_glfw.x11.display, (Atom*)atoms, count, atom_names); + _glfwReleaseErrorHandlerX11(); + if (_glfw.x11.errorCode != Success) { + for (int i = 0; i < count; i++) { + _glfwGrabErrorHandlerX11(); + atom_names[i] = XGetAtomName(_glfw.x11.display, atoms[i]); + _glfwReleaseErrorHandlerX11(); + if (_glfw.x11.errorCode != Success) atom_names[i] = NULL; + } + } +} + + // Set the specified property to the selection converted to the requested target // static Atom writeTargetToProperty(const XSelectionRequestEvent* request) { - int i; - char* selectionString = NULL; - const Atom formats[] = { _glfw.x11.UTF8_STRING, XA_STRING }; - const int formatCount = sizeof(formats) / sizeof(formats[0]); + const AtomArray *aa; + const _GLFWClipboardData *cd; - if (request->selection == _glfw.x11.PRIMARY) - selectionString = _glfw.x11.primarySelectionString; - else - selectionString = _glfw.x11.clipboardString; + if (request->selection == _glfw.x11.PRIMARY) { + aa = &_glfw.x11.primary_atoms; + cd = &_glfw.primary; + } else { + aa = &_glfw.x11.clipboard_atoms; + cd = &_glfw.clipboard; + } if (request->property == None) { @@ -741,11 +782,10 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request) { // The list of supported targets was requested - const Atom targets[] = { _glfw.x11.TARGETS, - _glfw.x11.MULTIPLE, - _glfw.x11.UTF8_STRING, - XA_STRING }; - + Atom *targets = calloc(aa->sz + 2, sizeof(Atom)); + targets[0] = _glfw.x11.TARGETS; + targets[1] = _glfw.x11.MULTIPLE; + for (size_t i = 0; i < aa->sz; i++) targets[i+2] = aa->array[i].atom; XChangeProperty(_glfw.x11.display, request->requestor, request->property, @@ -753,8 +793,8 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request) 32, PropModeReplace, (unsigned char*) targets, - sizeof(targets) / sizeof(targets[0])); - + aa->sz + 2); + free(targets); return request->property; } @@ -763,7 +803,7 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request) // Multiple conversions were requested Atom* targets; - unsigned long i, count; + size_t i, j, count; count = _glfwGetWindowPropertyX11(request->requestor, request->property, @@ -772,24 +812,25 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request) for (i = 0; i < count; i += 2) { - int j; - - for (j = 0; j < formatCount; j++) + for (j = 0; j < aa->sz; j++) { - if (targets[i] == formats[j]) + if (targets[i] == aa->array[j].atom) break; } - if (j < formatCount) + if (j < aa->sz) { - if (selectionString) XChangeProperty(_glfw.x11.display, + char *data = NULL; size_t sz = get_clipboard_data(cd, aa->array[j].mime, &data); + + if (data && sz) XChangeProperty(_glfw.x11.display, request->requestor, targets[i + 1], targets[i], 8, PropModeReplace, - (unsigned char *) selectionString, - strlen(selectionString)); + (unsigned char *) data, + sz); + free(data); } else targets[i + 1] = None; @@ -828,20 +869,22 @@ static Atom writeTargetToProperty(const XSelectionRequestEvent* request) // Conversion to a data target was requested - for (i = 0; i < formatCount; i++) + for (size_t i = 0; i < aa->sz; i++) { - if (request->target == formats[i]) + if (request->target == aa->array[i].atom) { // The requested target is one we support - if (selectionString) XChangeProperty(_glfw.x11.display, + char *data = NULL; size_t sz = get_clipboard_data(cd, aa->array[i].mime, &data); + if (data && sz) XChangeProperty(_glfw.x11.display, request->requestor, request->property, request->target, 8, PropModeReplace, - (unsigned char *) selectionString, - strlen(selectionString)); + (unsigned char *) data, + sz); + free(data); return request->property; } @@ -856,13 +899,11 @@ static void handleSelectionClear(XEvent* event) { if (event->xselectionclear.selection == _glfw.x11.PRIMARY) { - free(_glfw.x11.primarySelectionString); - _glfw.x11.primarySelectionString = NULL; + _glfw_free_clipboard_data(&_glfw.primary); } else { - free(_glfw.x11.clipboardString); - _glfw.x11.clipboardString = NULL; + _glfw_free_clipboard_data(&_glfw.clipboard); } } @@ -883,26 +924,20 @@ static void handleSelectionRequest(XEvent* event) static const char* getSelectionString(Atom selection) { - char** selectionString = NULL; + char* selectionString = NULL; const Atom targets[] = { _glfw.x11.UTF8_STRING, XA_STRING }; const size_t targetCount = sizeof(targets) / sizeof(targets[0]); - if (selection == _glfw.x11.PRIMARY) - selectionString = &_glfw.x11.primarySelectionString; - else - selectionString = &_glfw.x11.clipboardString; - if (XGetSelectionOwner(_glfw.x11.display, selection) == _glfw.x11.helperWindowHandle) { // Instead of doing a large number of X round-trips just to put this // string into a window property and then read it back, just return it - return *selectionString; + _GLFWClipboardData *cd = selection == _glfw.x11.PRIMARY ? &_glfw.primary : &_glfw.clipboard; + char *data = NULL; size_t sz = get_clipboard_data(cd, "text/plain", &data); + if (data && sz) return data; } - free(*selectionString); - *selectionString = NULL; - for (size_t i = 0; i < targetCount; i++) { char* data; @@ -998,11 +1033,11 @@ static const char* getSelectionString(Atom selection) { if (targets[i] == XA_STRING) { - *selectionString = convertLatin1toUTF8(string); + selectionString = convertLatin1toUTF8(string); free(string); } else - *selectionString = string; + selectionString = string; break; } @@ -1011,24 +1046,24 @@ static const char* getSelectionString(Atom selection) else if (actualType == targets[i]) { if (targets[i] == XA_STRING) - *selectionString = convertLatin1toUTF8(data); + selectionString = convertLatin1toUTF8(data); else - *selectionString = _glfw_strdup(data); + selectionString = _glfw_strdup(data); } XFree(data); - if (*selectionString) + if (selectionString) break; } - if (!*selectionString) + if (!selectionString) { _glfwInputError(GLFW_FORMAT_UNAVAILABLE, "X11: Failed to convert selection to string"); } - return *selectionString; + return selectionString; } // Make the specified window and its video mode active on its monitor @@ -1098,21 +1133,6 @@ static void onConfigChange(void) } } -static void -get_atom_names(Atom *atoms, int count, char **atom_names) { - _glfwGrabErrorHandlerX11(); - XGetAtomNames(_glfw.x11.display, atoms, count, atom_names); - _glfwReleaseErrorHandlerX11(); - if (_glfw.x11.errorCode != Success) { - for (int i = 0; i < count; i++) { - _glfwGrabErrorHandlerX11(); - atom_names[i] = XGetAtomName(_glfw.x11.display, atoms[i]); - _glfwReleaseErrorHandlerX11(); - if (_glfw.x11.errorCode != Success) atom_names[i] = NULL; - } - } -} - // Process the specified X event // static void processEvent(XEvent *event) @@ -2849,22 +2869,48 @@ void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor UNUSED) } } -void _glfwPlatformSetClipboardString(const char* string) -{ - char* copy = _glfw_strdup(string); - free(_glfw.x11.clipboardString); - _glfw.x11.clipboardString = copy; +static MimeAtom atom_for_mime(const char *mime) { + for (size_t i = 0; i < _glfw.x11.mime_atoms.sz; i++) { + MimeAtom ma = _glfw.x11.mime_atoms.array[i]; + if (strcmp(ma.mime, mime) == 0) { + return ma; + } + } + MimeAtom ma = {.mime=_glfw_strdup(mime), .atom=XInternAtom(_glfw.x11.display, mime, 0)}; + if (_glfw.x11.mime_atoms.capacity < _glfw.x11.mime_atoms.sz + 1) { + _glfw.x11.mime_atoms.capacity += 32; + _glfw.x11.mime_atoms.array = realloc(_glfw.x11.mime_atoms.array, _glfw.x11.mime_atoms.capacity); + } + _glfw.x11.mime_atoms.array[_glfw.x11.mime_atoms.sz++] = ma; + return ma; +} - XSetSelectionOwner(_glfw.x11.display, - _glfw.x11.CLIPBOARD, - _glfw.x11.helperWindowHandle, - CurrentTime); - - if (XGetSelectionOwner(_glfw.x11.display, _glfw.x11.CLIPBOARD) != - _glfw.x11.helperWindowHandle) - { - _glfwInputError(GLFW_PLATFORM_ERROR, - "X11: Failed to become owner of clipboard selection"); +void _glfwPlatformSetClipboard(GLFWClipboardType t) { + Atom which = None; + _GLFWClipboardData *cd = NULL; + AtomArray *aa = NULL; + switch (t) { + case GLFW_CLIPBOARD: which = _glfw.x11.CLIPBOARD; cd = &_glfw.clipboard; aa = &_glfw.x11.clipboard_atoms; break; + case GLFW_PRIMARY_SELECTION: which = _glfw.x11.PRIMARY; cd = &_glfw.primary; aa = &_glfw.x11.primary_atoms; break; + } + XSetSelectionOwner(_glfw.x11.display, which, _glfw.x11.helperWindowHandle, CurrentTime); + if (XGetSelectionOwner(_glfw.x11.display, which) != _glfw.x11.helperWindowHandle) { + _glfwInputError(GLFW_PLATFORM_ERROR, "X11: Failed to become owner of clipboard selection"); + } + if (aa->capacity < cd->num_mime_types + 32) { + aa->capacity = cd->num_mime_types + 32; + aa->array = malloc(sizeof(aa->array[0]) * aa->capacity); + } + aa->sz = 0; + for (size_t i = 0; i < cd->num_mime_types; i++) { + MimeAtom *a = aa->array + aa->sz++; + if (strcmp(cd->mime_types[i], "text/plain") == 0) { + a->atom = XA_ATOM; a->mime = "text/plain"; + a = aa->array + aa->sz++; + a->atom = _glfw.x11.UTF8_STRING; a->mime = "text/plain"; + } else { + *a = atom_for_mime(cd->mime_types[i]); + } } } @@ -2873,24 +2919,6 @@ const char* _glfwPlatformGetClipboardString(void) return getSelectionString(_glfw.x11.CLIPBOARD); } -void _glfwPlatformSetPrimarySelectionString(const char* string) -{ - free(_glfw.x11.primarySelectionString); - _glfw.x11.primarySelectionString = _glfw_strdup(string); - - XSetSelectionOwner(_glfw.x11.display, - _glfw.x11.PRIMARY, - _glfw.x11.helperWindowHandle, - CurrentTime); - - if (XGetSelectionOwner(_glfw.x11.display, _glfw.x11.PRIMARY) != - _glfw.x11.helperWindowHandle) - { - _glfwInputError(GLFW_PLATFORM_ERROR, - "X11: Failed to become owner of primary selection"); - } -} - const char* _glfwPlatformGetPrimarySelectionString(void) { return getSelectionString(_glfw.x11.PRIMARY); diff --git a/kittens/hints/main.py b/kittens/hints/main.py index 405abdcb4..50f1b600c 100644 --- a/kittens/hints/main.py +++ b/kittens/hints/main.py @@ -15,13 +15,14 @@ from typing import ( from kitty.cli import parse_args from kitty.cli_stub import HintsCLIOptions +from kitty.clipboard import set_clipboard_string, set_primary_selection from kitty.constants import website_url -from kitty.fast_data_types import get_options, set_clipboard_string, wcswidth +from kitty.fast_data_types import get_options, wcswidth from kitty.key_encoding import KeyEvent from kitty.typing import BossType, KittyCommonOpts from kitty.utils import ( ScreenSize, kitty_ansi_sanitizer_pat, resolve_custom_file, - screen_size_function, set_primary_selection + screen_size_function ) from ..tui.handler import Handler, result_handler diff --git a/kitty/boss.py b/kitty/boss.py index f6e6c758c..e1bbb6a05 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -20,6 +20,7 @@ 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 .conf.utils import BadLine, KeyAction, to_cmdline from .config import common_opts_as_dict, prepare_config_file_for_editing from .constants import ( @@ -29,19 +30,19 @@ from .constants import ( ) from .fast_data_types import ( CLOSE_BEING_CONFIRMED, GLFW_MOD_ALT, GLFW_MOD_CONTROL, GLFW_MOD_SHIFT, - GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, + GLFW_MOD_SUPER, GLFW_MOUSE_BUTTON_LEFT, GLFW_PRESS, GLFW_PRIMARY_SELECTION, IMPERATIVE_CLOSE_REQUESTED, NO_CLOSE_REQUESTED, ChildMonitor, Color, EllipticCurveKey, KeyEvent, SingleKey, add_timer, apply_options_update, background_opacity_of, change_background_opacity, change_os_window_state, cocoa_set_menubar_title, create_os_window, current_application_quit_request, current_os_window, destroy_global_data, - focus_os_window, get_boss, get_clipboard_string, 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_clipboard_string, set_in_sequence_mode, - set_options, set_os_window_size, set_os_window_title, thread_write, - toggle_fullscreen, toggle_maximized, toggle_secure_input + 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 ) from .key_encoding import get_name_to_functional_number_map from .keys import get_shortcut, shortcut_matches @@ -60,10 +61,9 @@ from .types import _T, AsyncResponse, WindowSystemMouseEvent, ac from .typing import PopenType, TypedDict from .utils import ( cleanup_ssh_control_masters, func_name, get_editor, get_new_os_window_size, - get_primary_selection, is_path_in_temp_dir, less_version, log_error, - macos_version, open_url, parse_address_spec, parse_uri_list, - platform_window_id, remove_socket_file, safe_print, set_primary_selection, - single_instance, startup_notification_handler, which + is_path_in_temp_dir, less_version, log_error, macos_version, open_url, + parse_address_spec, parse_uri_list, platform_window_id, remove_socket_file, + safe_print, single_instance, startup_notification_handler, which ) from .window import CommandOutput, CwdRequest, Window @@ -239,6 +239,8 @@ class Boss: prewarm: PrewarmProcess, ): set_layout_options(opts) + self.clipboard = Clipboard() + self.primary_selection = Clipboard(GLFW_PRIMARY_SELECTION) self.update_check_started = False self.encryption_key = EllipticCurveKey() self.encryption_public_key = f'{RC_ENCRYPTION_PROTOCOL_VERSION}:{base64.b85encode(self.encryption_key.public).decode("ascii")}' diff --git a/kitty/clipboard.py b/kitty/clipboard.py new file mode 100644 index 000000000..56a43cf4b --- /dev/null +++ b/kitty/clipboard.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2022, Kovid Goyal + +import io +from typing import IO, Callable, Dict, Union + +from .constants import supports_primary_selection +from .fast_data_types import GLFW_CLIPBOARD, get_boss, set_clipboard_data_types + +DataType = Union[bytes, 'IO[bytes]'] + + +class Clipboard: + + def __init__(self, clipboard_type: int = GLFW_CLIPBOARD) -> None: + self.data: Dict[str, DataType] = {} + self.clipboard_type = clipboard_type + self.enabled = self.clipboard_type == GLFW_CLIPBOARD or supports_primary_selection + + def set_text(self, x: Union[str, bytes]) -> None: + if self.enabled: + self.data.clear() + if isinstance(x, str): + x = x.encode('utf-8') + self.data['text/plain'] = x + set_clipboard_data_types(self.clipboard_type, tuple(self.data)) + + def get_text(self) -> str: + raise NotImplementedError('TODO: Implement this') + + def __call__(self, mime: str) -> Callable[[], bytes]: + data = self.data.get(mime, b'') + if isinstance(data, bytes): + def chunker() -> bytes: + nonlocal data + assert isinstance(data, bytes) + ans = data + data = b'' + return ans + return chunker + + data.seek(0, 0) + + def io_chunker() -> bytes: + assert not isinstance(data, bytes) + return data.read(io.DEFAULT_BUFFER_SIZE) + return io_chunker + + +def set_clipboard_string(x: Union[str, bytes]) -> None: + get_boss().clipboard.set_text(x) + + +def get_clipboard_string() -> str: + return get_boss().clipboard.get_text() + + +def set_primary_selection(x: Union[str, bytes]) -> None: + get_boss().primary_selection.set_text(x) + + +def get_primary_selection() -> str: + return get_boss().primary_selection.get_text() diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index d5098391e..b874fc96a 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1,7 +1,7 @@ import termios from ctypes import Array, c_ubyte from typing import ( - Any, AnyStr, Callable, Dict, List, NewType, Optional, Tuple, TypedDict, + Any, Callable, Dict, List, NewType, Optional, Tuple, TypedDict, Union, Iterator ) @@ -13,6 +13,8 @@ from kitty.options.types import Options from kitty.types import SignalInfo # Constants {{{ +GLFW_PRIMARY_SELECTION: int +GLFW_CLIPBOARD: int CLD_KILLED: int CLD_STOPPED: int CLD_CONTINUED: int @@ -304,14 +306,6 @@ def log_error_string(s: str) -> None: pass -def set_primary_selection(x: Union[bytes, str]) -> None: - pass - - -def get_primary_selection() -> Optional[bytes]: - pass - - def redirect_std_streams(devnull: str) -> None: pass @@ -597,10 +591,6 @@ def set_in_sequence_mode(yes: bool) -> None: pass -def set_clipboard_string(data: AnyStr) -> None: - pass - - def set_background_image( path: Optional[str], os_window_ids: Tuple[int, ...], @@ -764,10 +754,6 @@ def global_font_size(val: float = -1.) -> float: pass -def get_clipboard_string() -> str: - pass - - def focus_os_window(os_window_id: int, also_raise: bool = True) -> bool: pass @@ -1486,3 +1472,4 @@ class SingleKey: def set_use_os_log(yes: bool) -> None: ... def get_docs_ref_map() -> bytes: ... def clearenv() -> None: ... +def set_clipboard_data_types(ct: int, mime_types: Tuple[str, ...]) -> None: ... diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index df19f72c2..84e8d871a 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -365,11 +365,8 @@ load_glfw(const char* path) { *(void **) (&glfwGetGamepadState_impl) = dlsym(handle, "glfwGetGamepadState"); if (glfwGetGamepadState_impl == NULL) fail("Failed to load glfw function glfwGetGamepadState with error: %s", dlerror()); - *(void **) (&glfwSetClipboardString_impl) = dlsym(handle, "glfwSetClipboardString"); - if (glfwSetClipboardString_impl == NULL) fail("Failed to load glfw function glfwSetClipboardString with error: %s", dlerror()); - - *(void **) (&glfwGetClipboardString_impl) = dlsym(handle, "glfwGetClipboardString"); - if (glfwGetClipboardString_impl == NULL) fail("Failed to load glfw function glfwGetClipboardString with error: %s", dlerror()); + *(void **) (&glfwSetClipboardDataTypes_impl) = dlsym(handle, "glfwSetClipboardDataTypes"); + if (glfwSetClipboardDataTypes_impl == NULL) fail("Failed to load glfw function glfwSetClipboardDataTypes with error: %s", dlerror()); *(void **) (&glfwGetTime_impl) = dlsym(handle, "glfwGetTime"); if (glfwGetTime_impl == NULL) fail("Failed to load glfw function glfwGetTime with error: %s", dlerror()); diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index d2dddd6c0..47c280c10 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -1456,6 +1456,17 @@ typedef void (* GLFWuserdatafun)(unsigned long long, void*); typedef void (* GLFWtickcallback)(void*); typedef bool (* GLFWdrawtextfun)(GLFWwindow *window, const char *text, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, float x_offset, float y_offset, size_t right_margin); typedef char* (* GLFWcurrentselectionfun)(void); +typedef void (* GLFWclipboarddatafreefun)(void* data); +typedef struct GLFWDataChunk { + const char *data; + size_t sz; + GLFWclipboarddatafreefun free; + void *iter, *free_data; +} GLFWDataChunk; +typedef enum { + GLFW_CLIPBOARD, GLFW_PRIMARY_SELECTION +} GLFWClipboardType; +typedef GLFWDataChunk (* GLFWclipboarditerfun)(const char *mime_type, void *iter, GLFWClipboardType ctype); /*! @brief Video mode type. * @@ -2081,13 +2092,9 @@ typedef int (*glfwGetGamepadState_func)(int, GLFWgamepadstate*); GFW_EXTERN glfwGetGamepadState_func glfwGetGamepadState_impl; #define glfwGetGamepadState glfwGetGamepadState_impl -typedef void (*glfwSetClipboardString_func)(GLFWwindow*, const char*); -GFW_EXTERN glfwSetClipboardString_func glfwSetClipboardString_impl; -#define glfwSetClipboardString glfwSetClipboardString_impl - -typedef const char* (*glfwGetClipboardString_func)(GLFWwindow*); -GFW_EXTERN glfwGetClipboardString_func glfwGetClipboardString_impl; -#define glfwGetClipboardString glfwGetClipboardString_impl +typedef void (*glfwSetClipboardDataTypes_func)(GLFWClipboardType, const char* const*, size_t, GLFWclipboarditerfun); +GFW_EXTERN glfwSetClipboardDataTypes_func glfwSetClipboardDataTypes_impl; +#define glfwSetClipboardDataTypes glfwSetClipboardDataTypes_impl typedef monotonic_t (*glfwGetTime_func)(void); GFW_EXTERN glfwGetTime_func glfwGetTime_impl; diff --git a/kitty/glfw.c b/kitty/glfw.c index b64b80073..f5c7a63a4 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1263,13 +1263,6 @@ toggle_secure_input(PYNOARG) { Py_RETURN_NONE; } -static PyObject* -get_clipboard_string(PYNOARG) { - OSWindow *w = current_os_window(); - if (w) return Py_BuildValue("s", glfwGetClipboardString(w->handle)); - return Py_BuildValue("s", ""); -} - static void ring_audio_bell(void) { static monotonic_t last_bell_at = -1; @@ -1298,16 +1291,6 @@ get_content_scale_for_window(PYNOARG) { return Py_BuildValue("ff", xscale, yscale); } -static PyObject* -set_clipboard_string(PyObject UNUSED *self, PyObject *args) { - char *title; - Py_ssize_t sz; - if(!PyArg_ParseTuple(args, "s#", &title, &sz)) return NULL; - OSWindow *w = current_os_window(); - if (w) glfwSetClipboardString(w->handle, title); - Py_RETURN_NONE; -} - static OSWindow* find_os_window(id_type os_window_id) { for (size_t i = 0; i < global_state.num_os_windows; i++) { @@ -1441,28 +1424,6 @@ cocoa_window_id(PyObject UNUSED *self, PyObject *os_wid) { #endif } -static PyObject* -get_primary_selection(PYNOARG) { - if (glfwGetPrimarySelectionString) { - OSWindow *w = current_os_window(); - if (w) return Py_BuildValue("y", glfwGetPrimarySelectionString(w->handle)); - } else log_error("Failed to load glfwGetPrimarySelectionString"); - Py_RETURN_NONE; -} - -static PyObject* -set_primary_selection(PyObject UNUSED *self, PyObject *args) { - char *text; - Py_ssize_t sz; - if (!PyArg_ParseTuple(args, "s#", &text, &sz)) return NULL; - if (glfwSetPrimarySelectionString) { - OSWindow *w = current_os_window(); - if (w) glfwSetPrimarySelectionString(w->handle, text); - } - else log_error("Failed to load glfwSetPrimarySelectionString"); - Py_RETURN_NONE; -} - static PyObject* set_custom_cursor(PyObject *self UNUSED, PyObject *args) { int shape; @@ -1618,26 +1579,73 @@ set_ignore_os_keyboard_processing(bool enabled) { glfwSetIgnoreOSKeyboardProcessing(enabled); } +static void +decref_pyobj(void *x) { + Py_XDECREF(x); +} + +static GLFWDataChunk +get_clipboard_data(const char *mime_type, void *iter, GLFWClipboardType ct) { + GLFWDataChunk ans = {.iter=iter, .free=decref_pyobj}; + if (global_state.boss == NULL) return ans; + if (iter == NULL) { + PyObject *c = PyObject_GetAttrString(global_state.boss, ct == GLFW_PRIMARY_SELECTION ? "primary_selection" : "clipboard"); + if (c == NULL) { + PyErr_Print(); + return ans; + } + PyObject *i = PyObject_CallFunction(c, "s", mime_type); + Py_DECREF(c); + if (!i) { + PyErr_Print(); + return ans; + } + ans.iter = i; + return ans; + } + if (mime_type == NULL) { + Py_XDECREF(iter); + return ans; + } + + PyObject *ret = PyObject_CallFunctionObjArgs(iter, NULL); + if (ret == NULL) return ans; + ans.data = PyBytes_AS_STRING(ret); + ans.sz = PyBytes_GET_SIZE(ret); + ans.free_data = ret; + return ans; +} + +static PyObject* +set_clipboard_data_types(PyObject *self UNUSED, PyObject *args) { + PyObject *mta; + int ctype; + if (!PyArg_ParseTuple(args, "iO!", &ctype, &PyTuple_Type, &mta)) return NULL; + const char **mime_types = calloc(PyTuple_GET_SIZE(mta), sizeof(char*)); + if (!mime_types) return PyErr_NoMemory(); + for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(mta); i++) mime_types[i] = PyUnicode_AsUTF8(PyTuple_GET_ITEM(mta, i)); + glfwSetClipboardDataTypes(ctype, mime_types, PyTuple_GET_SIZE(mta), get_clipboard_data); + free(mime_types); + Py_RETURN_NONE; +} + // Boilerplate {{{ static PyMethodDef module_methods[] = { METHODB(set_custom_cursor, METH_VARARGS), {"create_os_window", (PyCFunction)(void (*) (void))(create_os_window), METH_VARARGS | METH_KEYWORDS, NULL}, METHODB(set_default_window_icon, METH_VARARGS), - METHODB(get_clipboard_string, METH_NOARGS), + METHODB(set_clipboard_data_types, METH_VARARGS), METHODB(toggle_secure_input, METH_NOARGS), METHODB(get_content_scale_for_window, METH_NOARGS), METHODB(ring_bell, METH_NOARGS), - METHODB(set_clipboard_string, METH_VARARGS), METHODB(toggle_fullscreen, METH_VARARGS), METHODB(toggle_maximized, METH_VARARGS), METHODB(change_os_window_state, METH_VARARGS), METHODB(glfw_window_hint, METH_VARARGS), - METHODB(get_primary_selection, METH_NOARGS), METHODB(x11_display, METH_NOARGS), METHODB(get_click_interval, METH_NOARGS), METHODB(x11_window_id, METH_O), - METHODB(set_primary_selection, METH_VARARGS), METHODB(strip_csi, METH_O), #ifndef __APPLE__ METHODB(dbus_send_notification, METH_VARARGS), @@ -1672,6 +1680,7 @@ init_glfw(PyObject *m) { ADDC(GLFW_REPEAT); ADDC(true); ADDC(false); ADDC(GLFW_IBEAM_CURSOR); ADDC(GLFW_HAND_CURSOR); ADDC(GLFW_ARROW_CURSOR); + ADDC(GLFW_PRIMARY_SELECTION); ADDC(GLFW_CLIPBOARD); /* start glfw functional keys (auto generated by gen-key-constants.py do not edit) */ ADDC(GLFW_FKEY_ESCAPE); diff --git a/kitty/launch.py b/kitty/launch.py index 50a9ee17d..28c79d0aa 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -14,17 +14,15 @@ from .boss import Boss from .child import Child from .cli import parse_args from .cli_stub import LaunchCLIOptions +from .clipboard import set_clipboard_string, set_primary_selection from .constants import kitty_exe, shell_path from .fast_data_types import ( - add_timer, get_boss, get_options, get_os_window_title, - patch_color_profiles, set_clipboard_string + add_timer, get_boss, get_options, get_os_window_title, patch_color_profiles ) from .options.utils import env as parse_env from .tabs import Tab, TabManager from .types import OverlayType, run_once -from .utils import ( - get_editor, log_error, resolve_custom_file, set_primary_selection, which -) +from .utils import get_editor, log_error, resolve_custom_file, which from .window import CwdRequest, CwdRequestType, Watchers, Window try: diff --git a/kitty/utils.py b/kitty/utils.py index 79d2ea905..e34cb0489 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -19,8 +19,7 @@ from typing import ( from .constants import ( appname, clear_handled_signals, config_dir, is_macos, is_wayland, - read_kitty_resource, runtime_dir, shell_path, ssh_control_master_template, - supports_primary_selection + read_kitty_resource, runtime_dir, shell_path, ssh_control_master_template ) from .fast_data_types import Color, open_tty from .rgb import to_color @@ -230,20 +229,6 @@ def fit_image(width: int, height: int, pwidth: int, pheight: int) -> Tuple[int, return int(width), int(height) -def set_primary_selection(text: Union[str, bytes]) -> None: - if not supports_primary_selection: - return # There is no primary selection - from kitty.fast_data_types import set_primary_selection as s - s(text) - - -def get_primary_selection() -> str: - if not supports_primary_selection: - return '' # There is no primary selection - from kitty.fast_data_types import get_primary_selection as g - return (g() or b'').decode('utf-8', 'replace') - - def base64_encode( integer: int, chars: str = string.ascii_uppercase + string.ascii_lowercase + string.digits + diff --git a/kitty/window.py b/kitty/window.py index db92dc9cf..4a35ac0d6 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -20,6 +20,10 @@ from typing import ( from .child import ProcessDesc from .cli_stub import CLIOptions +from .clipboard import ( + get_clipboard_string, get_primary_selection, set_clipboard_string, + set_primary_selection +) from .config import build_ansi_color_table from .constants import ( appname, clear_handled_signals, config_dir, is_macos, wakeup_io_loop @@ -34,23 +38,25 @@ from .fast_data_types import ( KeyEvent, Screen, add_timer, add_window, cell_size_for_window, click_mouse_cmd_output, click_mouse_url, compile_program, current_os_window, encode_key_for_tty, get_boss, get_click_interval, - get_clipboard_string, get_options, init_cell_program, mark_os_window_dirty, - mouse_selection, move_cursor_to_mouse_if_in_prompt, pt_to_px, - set_clipboard_string, set_titlebar_color, set_window_logo, - set_window_padding, set_window_render_data, update_ime_position_for_window, - update_window_title, update_window_visibility, wakeup_main_loop + get_options, init_cell_program, mark_os_window_dirty, mouse_selection, + move_cursor_to_mouse_if_in_prompt, pt_to_px, set_titlebar_color, + set_window_logo, set_window_padding, set_window_render_data, + update_ime_position_for_window, update_window_title, + update_window_visibility, wakeup_main_loop ) from .keys import keyboard_mode_name, mod_mask -from .notify import NotificationCommand, handle_notification_cmd, sanitize_identifier_pat +from .notify import ( + NotificationCommand, handle_notification_cmd, sanitize_identifier_pat +) from .options.types import Options from .rgb import to_color from .terminfo import get_capabilities from .types import MouseEvent, OverlayType, WindowGeometry, ac, run_once from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict from .utils import ( - docs_url, get_primary_selection, kitty_ansi_sanitizer_pat, load_shaders, - log_error, open_cmd, open_url, parse_color_set, path_from_osc7_url, - resolve_custom_file, resolved_shell, sanitize_title, set_primary_selection + docs_url, kitty_ansi_sanitizer_pat, load_shaders, log_error, open_cmd, + open_url, parse_color_set, path_from_osc7_url, resolve_custom_file, + resolved_shell, sanitize_title ) MatchPatternType = Union[Pattern[str], Tuple[Pattern[str], Optional[Pattern[str]]]]