Compare commits

...

103 Commits

Author SHA1 Message Date
Kovid Goyal
dbb97a62bf version 0.26.3 2022-09-22 13:48:20 +05:30
Kovid Goyal
b1fa1c9820 Make test a bit more robust 2022-09-21 23:16:34 +05:30
Kovid Goyal
2dc9488909 switch python autoformatter to isort+black+autoflake 2022-09-21 13:40:37 +05:30
Kovid Goyal
e2a251224d ssh kitten: Better error message when tar is missing 2022-09-21 07:34:15 +05:30
Kovid Goyal
2a2dca8a58 ... 2022-09-18 08:26:40 +05:30
Kovid Goyal
e1f3cb85c0 Window for id should not fail when not called from a callback 2022-09-18 08:24:02 +05:30
Kovid Goyal
13cf38d6c6 Update changelog 2022-09-17 18:21:28 +05:30
Kovid Goyal
5180a41e87 Fix a use-after-free when handling fake mouse clicks and the action causes windows to be removed/re-allocated
Fixes #5506
2022-09-17 18:21:10 +05:30
Kovid Goyal
1c44da2b4a Nicer error message when trying to open the config file and no editor is available 2022-09-13 17:24:11 +05:30
Kovid Goyal
bcd78c3940 Fix a regression that caused kitty not to restore SIGPIPE after python nukes it when launching children. Affects bash which does not sanitize its signal mask.
Fixes #5500
2022-09-13 07:51:32 +05:30
Kovid Goyal
fe2f3d9870 Wayland: Remove the mutter specific ignoring of the first empty surface configure event. It seems to be no longer necessary on current mutter, with the recent Wayland changes 2022-09-11 14:37:50 +05:30
Kovid Goyal
dcf2152a7a Wayland: Generate a XDG_ACTIVATION_TOKEN when opening URLs or running programs in the background via the launch action 2022-09-11 14:25:49 +05:30
Kovid Goyal
58a3baaf0f Wayland: Use XDG_ACTIVATION_TOKEN when present at launch 2022-09-11 13:43:52 +05:30
Kovid Goyal
b247759d30 DRYer 2022-09-11 13:15:42 +05:30
Kovid Goyal
ba8d30896b Try to use the activation protocol to focus windows 2022-09-11 10:39:28 +05:30
Kovid Goyal
4d30ae55f3 Wayland: Mark windows in which a bell as urgent on compositors that support the xdg-activation protocol 2022-09-11 09:33:41 +05:30
Kovid Goyal
983ab3c67d build wayland xdg-activation protocol 2022-09-10 16:08:38 +05:30
Kovid Goyal
0bf228b030 Wayland: Micro-optimization: Assume window will be shown on primary monitor so create it with its scale set to that of the primary monitor 2022-09-10 15:23:56 +05:30
Kovid Goyal
29b3a92ffc Wayland: swap buffers immediately on scale change to ensure attached buffer is a multiple of the new scale
Earlier we were only swapping buffers when ready to draw, but the
Wayland protocol  requires the attached buffer to be a multiple of the
scale. We cannot guarantee an application side swap will be triggered
before the next commit, so instead we blank the new buffer swap it in
the GLFW backend itself.

Fixes #5467
2022-09-10 15:09:46 +05:30
Kovid Goyal
23d73ec933 ... 2022-09-10 12:52:27 +05:30
Kovid Goyal
bc650f6230 Output initial window creation size in debug 2022-09-10 12:28:53 +05:30
Kovid Goyal
5272318d67 Update changelog 2022-09-10 12:10:30 +05:30
Kovid Goyal
5ba887ff68 Wayland: Fix for bug in NVIDIA drivers that prevents transparency working
Fixes #5479
2022-09-10 11:57:18 +05:30
Kovid Goyal
67d70f8b8f Apparently on Wayland we need to set the swap interval on every window not just the first. It's somehow not shared even though the context is shared? Fixes #5495 2022-09-10 10:41:31 +05:30
Kovid Goyal
0fac6e4de9 render frames should be always respected on macOS
Only Wayland suffers from flicker on resize when using render frames.
2022-09-10 10:25:42 +05:30
Kovid Goyal
1e7a11b278 Wayland: When swapping buffers in response to a configure event ensure the window is the current context otherwise the swap fails
The swap failing will mean the buffer is not attached which might cause
issues with some compsitors.
2022-09-10 10:22:09 +05:30
Kovid Goyal
6741ac2087 Sessions: Allow controlling which OS Window is active via the focus_os_window directive 2022-09-10 08:27:36 +05:30
Kovid Goyal
7b2e29a6a8 ... 2022-09-10 07:52:26 +05:30
Kovid Goyal
6f147544d2 Add ! to env var serialization test 2022-09-10 07:21:15 +05:30
Kovid Goyal
89570d3b52 ssh kitten: Fix KITTY_PUBLIC_KEY not being encoded properly when transmitting
Fixes #5496
2022-09-10 07:09:58 +05:30
Kovid Goyal
dd4a65e0c9 ... 2022-09-09 15:41:45 +05:30
Kovid Goyal
79fba9dd9a Use the dynamic property instead of testing identifier serialization 2022-09-09 15:39:00 +05:30
Kovid Goyal
b123c21010 Update changelog
Fixes #5483
2022-09-09 13:13:58 +05:30
Kovid Goyal
9c5d1f9771 Pass null bytes through the clipboard on cocoa 2022-09-09 13:12:12 +05:30
Kovid Goyal
5131224b59 Convenience code for debugging 2022-09-09 12:20:28 +05:30
Kovid Goyal
be89da2271 Handle dyn prefixes 2022-09-09 11:59:49 +05:30
Kovid Goyal
45b408a78b Allow setting arbitrary mime types 2022-09-09 11:14:12 +05:30
Kovid Goyal
0e7da8643e Get cocoa clipboard backend working 2022-09-09 10:59:10 +05:30
Kovid Goyal
410b1816ad More work on cocoa clipboard backend 2022-09-09 07:39:38 +05:30
Kovid Goyal
bcfa2a64e4 Start work on new clipboard api support for macos 2022-09-08 21:45:13 +05:30
Kovid Goyal
91c00fb5ac Implement the new clipboard API for Wayland 2022-09-08 17:29:18 +05:30
Kovid Goyal
c58d217d32 Only warn about cursor size once 2022-09-08 16:28:40 +05:30
Kovid Goyal
a9b6047741 Simplify handling of self offers 2022-09-08 15:34:14 +05:30
Kovid Goyal
6b432bee4f Cleanup sending/receiving mimetype atoms 2022-09-08 12:45:35 +05:30
Kovid Goyal
087f0b449f ... 2022-09-08 12:45:35 +05:30
Kovid Goyal
96160b52b6 Code to get the available mime types in the clipboard 2022-09-08 12:45:34 +05:30
Kovid Goyal
d17a6cd3a3 Get reading from clipboard working 2022-09-08 12:45:34 +05:30
Kovid Goyal
7e1380cc0d Start work on giving GLFW a proper clipboard API 2022-09-08 12:45:34 +05:30
Kovid Goyal
9a99554ed3 Also have confirm() and get_line() return their results on overlay removal 2022-09-08 12:06:25 +05:30
Kovid Goyal
19799bd538 When choosing an option call the callback after the popup window is removed. This ensures the active window is unchanged when the callback is called. Fixes #5488 2022-09-08 12:01:38 +05:30
Kovid Goyal
902e94ceac Fix #5484 2022-09-07 15:09:24 +05:30
Kovid Goyal
5bb2cb06a9 Fix #5477 2022-09-05 21:43:56 +05:30
Kovid Goyal
1559a2e15a Cleanup previous PR 2022-09-05 21:19:58 +05:30
Kovid Goyal
35f94963cd Merge branch 'skip-tests-bash-debug' of https://github.com/jamessan/kitty 2022-09-05 21:18:52 +05:30
James McCoy
f0d497dfe0 Skip bash shell_integration tests if bash is a debug build
Switch to using BASH_VERSINFO rather than BASH_VERSION, since it
provides structured data.

    BASH_VERSINFO
           A readonly array variable whose members hold version information for this instance of bash.  The values assigned to the array members are as follows:
           BASH_VERSINFO[0]        The major version number (the release).
           BASH_VERSINFO[1]        The minor version number (the version).
           BASH_VERSINFO[2]        The patch level.
           BASH_VERSINFO[3]        The build version.
           BASH_VERSINFO[4]        The release status (e.g., beta1).
           BASH_VERSINFO[5]        The value of MACHTYPE.

When release status is not "release", bash builds are in debug mode and
output extra information which disturbs the integration.

Closes #5473
2022-09-05 11:37:37 -04:00
Kovid Goyal
5bdf27fd78 version 0.26.2 2022-09-05 10:49:50 +05:30
Kovid Goyal
f05783e64d Sanitize notifications ids as they are retransmitted over the TTY 2022-09-05 10:41:19 +05:30
Kovid Goyal
c455fea729 Also output screen contents when test fails 2022-09-05 07:52:36 +05:30
Kovid Goyal
5bf5454dbe Nicer fix for #5469 2022-09-04 22:15:17 +05:30
Kovid Goyal
b60d9f73ce Wayland: Fix remembering window size not accurate when client side decorations are present 2022-09-04 14:25:04 +05:30
Kovid Goyal
31fe35cd38 Fix #5469 2022-09-04 13:58:16 +05:30
Kovid Goyal
607bc845fc Allow creating overlay-main windows, which are treated as the active window unlike normal overlays
Fixes #5392
2022-09-03 19:04:31 +05:30
Kovid Goyal
a420d9d19c spacing is a clone safe option 2022-09-02 23:28:58 +05:30
Kovid Goyal
4b81e4936e launch: Allow setting the margin and padding for the newly created window
Fixes #5463
2022-09-02 19:41:05 +05:30
Kovid Goyal
295743a96e Fix ref to launch doc 2022-09-02 16:31:21 +05:30
Kovid Goyal
d3a1848c27 Remove markup from ref_hyperlink text 2022-09-02 16:24:50 +05:30
Kovid Goyal
9cec924f32 hints kitten: Allow using launch as the program to run, to open the result in a new kitty tab/window/etc.
Fixes #5462
2022-09-02 16:23:00 +05:30
Kovid Goyal
67c963ab89 Pass KITTY_LISTEN_ON to programs opened via open_url
Useful if those programs want to talk to kitty via the rc socket
2022-09-02 16:01:30 +05:30
Kovid Goyal
0f7ced25c1 ... 2022-09-01 13:55:48 +05:30
Kovid Goyal
42948ffff0 Merge branch 'docs' of https://github.com/page-down/kitty 2022-09-01 13:55:30 +05:30
pagedown
922ce5bdea Docs: Put example commands in code blocks 2022-09-01 15:35:51 +08:00
pagedown
1b42486026 Docs: Remove the extra empty line in the CLI help
Also keep the example code in a code block, in the HTML documentation.
2022-09-01 15:35:41 +08:00
pagedown
95f1a971b2 Docs: Mention clear_all_shortcuts in the FAQ about show_key kitten 2022-09-01 15:35:17 +08:00
Kovid Goyal
7968c88c67 ... 2022-09-01 07:54:36 +05:30
Kovid Goyal
992e90b0a3 Fix a regression in 0.26.0 that broke mapping of native keys who key codes did not fit in 21 bits
Fixes #5452
2022-08-31 22:19:51 +05:30
Kovid Goyal
afada84f52 Cleanup previous PR
Use wcswidth() for line width calculation. Works with emoji and ignores
escape codes automatically. Add changelog entry.
2022-08-31 08:01:52 +05:30
Kovid Goyal
b978ff8930 Merge branch 'fix/hints-next-line' of https://github.com/trygveaa/kitty 2022-08-31 07:56:38 +05:30
Kovid Goyal
053cede581 Use git rev-parse to get nightly commit hash 2022-08-31 07:22:56 +05:30
Trygve Aaberge
03720402a7 Fix hints sometimes matching next line as part of URL
For URLs where there's fewer characters left to the right edge of the
window than the number of escape characters in the line, the next line
would be included in the URL, as if the URL went all the way to the
right edge.

For example with a 40 chars wide terminal, if you run:

    echo -e '\e[31m1\e[m https://github.com/kovidgoyal/kitty\ntest'

And launch the hints kitten, you'll see that test on the next line will
be included in the URL.

This happened because the calculation for filling the rest of the line
with NUL characters counted the escape characters as well as the visible
characters, so it filled in too few characters.

This is a regression introduced in commit 91966712.
2022-08-30 22:24:58 +02:00
Kovid Goyal
fca0999814 Eureka! Figured out why libedit is breaking in prewarm on macOS via launchd
The prewarm zygote imports the world. shell.py had a top level import
for readline. Which means readline was being imported pre-fork. And of
course as is traditional with Apple libedit is not fork safe. Probably
because it initializes its internal IO routines based on the stdio
handles at time of import which are the handles kitty gets from launchd
2022-08-30 19:35:17 +05:30
Kovid Goyal
78056c659c Another place we cant use readline on macOS 2022-08-30 19:27:55 +05:30
Kovid Goyal
30e1b4680d Only override LANG in default env if it was not set either in the original os env or via the env config directive 2022-08-30 19:17:53 +05:30
Kovid Goyal
89a0c04d19 Fix exec in prewarm forked process sometimes inheriting env vars from grandparent process 2022-08-30 19:03:32 +05:30
Kovid Goyal
1ba027c277 Dont show LC_CTYPE in show_kitty_env_vars when running with system python on macOS 2022-08-30 16:55:17 +05:30
Kovid Goyal
d8e43a3412 macOS: Fix regression in 0.26.0 that caused asking the user for a line of input such as for set_tab_title to not work
Apparently libedit doesn't work in the forked process when the parent
process is run via Launch Services. I cant be bothered to investigate
why, given that libedit is closed source. macOS users will just have to
live without history/completion in the ask kitten until I get around to
writing a replacement for readline/libedit.

And on a personal note, macOS >> Necrotizing fasciitis

Fixes #5447
2022-08-30 16:45:14 +05:30
Kovid Goyal
644e46e91d Reconfigure all stdio streams in the prefork process 2022-08-30 16:22:59 +05:30
Kovid Goyal
e2a1f8dde7 ... 2022-08-30 10:40:59 +05:30
Kovid Goyal
7d09c6b762 Link to start of actual changelog 2022-08-30 10:27:53 +05:30
Kovid Goyal
6604e0d015 Fix regression in 0.26.0 that caused launching kitty without working STDIO handles to result in high CPU usage and prewarming failing
Fixes #5444
2022-08-30 08:15:13 +05:30
Kovid Goyal
bb2ec9cde7 ... 2022-08-30 07:50:36 +05:30
Kovid Goyal
89a2545855 Cleanup previous PR
Allow disabling hyperlinking of rg output. Also parse
--kitten=hyperlink=... form of CLI args.
2022-08-30 07:41:14 +05:30
Kovid Goyal
49f8c0eae6 Merge branch 'hyperlinked-grep-only-matches' of https://github.com/groves/kitty 2022-08-30 06:59:53 +05:30
Kovid Goyal
268d687814 version 0.26.1
Bloody macOS. I have better things to do that play whack-a-mole with all
the bug reports caused macOS not bothering to set LANG.
2022-08-30 01:04:42 +05:30
Charlie Groves
db5b4da388 Add options for linking different parts of ripgrep output 2022-08-29 13:23:42 -04:00
Kovid Goyal
71f8e50460 Update changelog 2022-08-29 21:01:59 +05:30
Kovid Goyal
22fbdbca40 Fix a regression in 0.26.0 that caused kitty to no longer set the LANG environment variable on macOS
Happened because reading the locale uses cocoa APIs and they are not fork
safe, so it was moved to after prewarm forking, but at that point the
default child env had already been set.

Fixes #5439
2022-08-29 20:58:48 +05:30
Kovid Goyal
6253ee2a74 Allow resetting the tab title to default 2022-08-29 20:41:07 +05:30
Kovid Goyal
5b28aed0b1 Allow the set_tab_title action to take an argument 2022-08-29 20:11:18 +05:30
Kovid Goyal
9eabc9ecf1 Merge branch 'bash-inherit_errexit' of https://github.com/grimm26/kovidgoyal-kitty 2022-08-29 19:47:24 +05:30
Mark Keisler
a77852466c bash shell integration: Send STDERR of unsetting inherit_errexit to
/dev/null

Some bash versions may not have that option and rather than checking if
they even have it or if it is set, just quiet the STDERR it may throw.

```
$ kssh myhost
bash: shopt: inherit_errexit: invalid shell option name
myhost:~$ bash --version
GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
```
2022-08-29 08:58:54 -05:00
Kovid Goyal
c68b82e4d0 ssh kitten: Fix executable permission missing from kitty bootstrap script
Fixes #5438
2022-08-29 18:20:09 +05:30
Charlie Groves
8c7968a4d6 Describe --hyperlink-only-matches in the docs 2022-08-28 07:02:39 -04:00
Charlie Groves
f5a70adf0b Add a --hyperlink-only-matches flag to hyperlinked_grep
If it's active, only matching lines get links, not file headers or context lines.
2022-08-26 17:05:50 -04:00
76 changed files with 1806 additions and 781 deletions

View File

@@ -35,6 +35,65 @@ mouse anywhere in the current command to move the cursor there. See
Detailed list of changes
-------------------------------------
0.26.3 [2022-09-22]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Wayland: Mark windows in which a bell occurs as urgent on compositors that support the xdg-activation protocol
- Allow passing null bytes through the system clipboard (:iss:`5483`)
- ssh kitten: Fix :envvar:`KITTY_PUBLIC_KEY` not being encoded properly when transmitting (:iss:`5496`)
- Sessions: Allow controlling which OS Window is active via the ``focus_os_window`` directive
- Wayland: Fix for bug in NVIDIA drivers that prevents transparency working (:iss:`5479`)
- Wayland: Fix for a bug that could cause kitty to become non-responsive when
using multiple OS windows in a single instance on some compositors (:iss:`5495`)
- 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
- Fix a regression that caused kitty not to restore SIGPIPE after python nukes it when launching children. Affects bash which does not sanitize its signal mask. (:iss:`5500`)
- Fix a use-after-free when handling fake mouse clicks and the action causes windows to be removed/re-allocated (:iss:`5506`)
0.26.2 [2022-09-05]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Allow creating :code:`overlay-main` windows, which are treated as the active window unlike normal overlays (:iss:`5392`)
- hints kitten: Allow using :doc:`launch` as the program to run, to open the result in a new kitty tab/window/etc. (:iss:`5462`)
- hyperlinked_grep kitten: Allow control over which parts of ``rg`` output are hyperlinked (:pull:`5428`)
- Fix regression in 0.26.0 that caused launching kitty without working STDIO handles to result in high CPU usage and prewarming failing (:iss:`5444`)
- :doc:`/launch`: Allow setting the margin and padding for newly created windows (:iss:`5463`)
- macOS: Fix regression in 0.26.0 that caused asking the user for a line of input such as for :ac:`set_tab_title` to not work (:iss:`5447`)
- hints kitten: hyperlink matching: Fix hints occasionally matching text on subsequent line as part of hyperlink (:pull:`5450`)
- Fix a regression in 0.26.0 that broke mapping of native keys whose key codes did not fit in 21 bits (:iss:`5452`)
- Wayland: Fix remembering window size not accurate when client side decorations are present
- Fix an issue where notification identifiers were not sanitized leading to
code execution if the user clicked on a notification popup from a malicious
source. Thanks to Carter Sande for discovering this vulnerability.
0.26.1 [2022-08-30]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- ssh kitten: Fix executable permission missing from kitty bootstrap script (:iss:`5438`)
- Fix a regression in 0.26.0 that caused kitty to no longer set the ``LANG`` environment variable on macOS (:iss:`5439`)
- Allow specifying a title when using the :ac:`set_tab_title` action (:iss:`5441`)
0.26.0 [2022-08-29]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -312,7 +312,11 @@ the :sc:`send_text <send_text>` you can use the ``show_key`` kitten. Run::
Then press the key you want to emulate. Note that this kitten will only show
keys that actually reach the terminal program, in particular, keys mapped to
actions in kitty will not be shown. To check those first map them to
:ac:`no_op`.
:ac:`no_op`. You can also start a kitty instance without any shortcuts to
interfere::
kitty -o clear_all_shortcuts=yes kitty +kitten show_key
How do I open a new window or tab with the same working directory as the current window?
--------------------------------------------------------------------------------------------

View File

@@ -32,7 +32,12 @@ Glossary
top of an existing kitty window, entirely covering it. Overlays are used
throughout kitty, for example, to display the :ref:`the scrollback buffer <scrollback>`,
to display :doc:`hints </kittens/hints>`, for :doc:`unicode input
</kittens/unicode_input>` etc.
</kittens/unicode_input>` etc. Normal overlays are meant for short
duration popups and so are not considered the :italic:`active window`
when determining the current working directory or getting input text for
kittens, launch commands, etc. To create an overlay considered as a
:italic:`main window` use the :code:`overlay-main` argument to
:doc:`launch`.
hyperlinks
Terminals can have hyperlinks, just like the internet. In kitty you can

View File

@@ -74,6 +74,16 @@ to :program:`rg`. How to do that varies based on the shell:
To learn more about kitty's powerful framework for customizing URL click
actions, see :doc:`here </open_actions>`.
By default, this kitten adds hyperlinks for several parts of ripgrep output:
the per-file header, match context lines, and match lines. You can control
which items are linked with a :command:`--kitten hyperlink` flag. For example,
:command:`--kitten hyperlink=matching_lines` will only add hyperlinks to the
match lines. :command:`--kitten hyperlink=file_headers,context_lines` will link
file headers and context lines but not match lines. :command:`--kitten
hyperlink=none` will cause the command line to be passed to directly to
:command:`rg` so no hyperlinking will be performed. :command:`--kitten
hyperlink` may be specified multiple times.
Hopefully, someday this functionality will make it into some `upstream grep
<https://github.com/BurntSushi/ripgrep/issues/665>`__ program directly removing
the need for this kitten.

View File

@@ -163,8 +163,10 @@ option in :file:`kitty.conf`. For example:
launch sh
# Resize the current window (see the resize_window action for details)
resize_window wider 2
# Make the current window the active (focused) window
# Make the current window the active (focused) window in its tab
focus
# Make the current OS Window the globally active window (not supported on Wayland)
focus_os_window
launch emacs
.. note::

View File

@@ -992,8 +992,6 @@ void _glfwPlatformTerminate(void)
_glfw.ns.appleSettings = nil;
}
free(_glfw.ns.clipboardString);
_glfwTerminateNSGL();
if (global_shortcuts != nil) { [global_shortcuts release]; global_shortcuts = nil; }

View File

@@ -183,7 +183,6 @@ typedef struct _GLFWlibraryNS
char keyName[64];
char text[256];
char* clipboardString;
CGPoint cascadePoint;
// Where to place the cursor when re-enabled
double restoreCursorPosX, restoreCursorPosY;

View File

@@ -30,6 +30,8 @@
#include "../kitty/monotonic.h"
#include <Availability.h>
#import <CoreServices/CoreServices.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include <float.h>
#include <string.h>
@@ -2506,55 +2508,150 @@ bool _glfwPlatformToggleFullscreen(_GLFWwindow* w, unsigned int flags) {
return made_fullscreen;
}
void _glfwPlatformSetClipboardString(const char* string)
{
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
[pasteboard declareTypes:@[NSPasteboardTypeString] owner:nil];
[pasteboard setString:@(string) forType:NSPasteboardTypeString];
// Clipboard {{{
#define UTI_ROUNDTRIP_PREFIX @"uti-is-typical-apple-nih."
static NSString*
mime_to_uti(const char *mime) {
if (strcmp(mime, "text/plain") == 0) return NSPasteboardTypeString;
if (@available(macOS 11.0, *)) {
UTType *t = [UTType typeWithMIMEType:@(mime)]; // auto-released
if (t != nil && !t.dynamic) return t.identifier;
}
return [NSString stringWithFormat:@"%@%s", UTI_ROUNDTRIP_PREFIX, mime]; // auto-released
}
void _glfwPlatformSetPrimarySelectionString(const char* string) {
(void)string;
// Apple doesnt have a primary selection
static const char*
uti_to_mime(NSString *uti) {
if ([uti isEqualToString:NSPasteboardTypeString]) return "text/plain";
if ([uti hasPrefix:UTI_ROUNDTRIP_PREFIX]) return [[uti substringFromIndex:[UTI_ROUNDTRIP_PREFIX length]] UTF8String];
if (@available(macOS 11.0, *)) {
UTType *t = [UTType typeWithIdentifier:uti]; // auto-released
if (t.preferredMIMEType != nil) return [t.preferredMIMEType UTF8String];
}
return "";
}
const char* _glfwPlatformGetPrimarySelectionString(void) { return ""; }
const char* _glfwPlatformGetClipboardString(void)
{
static void
list_clipboard_mimetypes(GLFWclipboardwritedatafun write_data, void *object) {
#define w(x) { if (ok) ok = write_data(object, x, strlen(x)); }
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
free(_glfw.ns.clipboardString); _glfw.ns.clipboardString = NULL;
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
BOOL has_file_urls = [pasteboard canReadObjectForClasses:@[[NSURL class]] options:options];
BOOL has_strings = [pasteboard canReadObjectForClasses:@[[NSString class]] options:nil];
/* NSLog(@"has_file_urls: %d has_strings: %d", has_file_urls, has_strings); */
bool ok = true;
if (has_strings) w("text/plain");
if (has_file_urls) w("text/local-path-list");
for (NSPasteboardItem * item in pasteboard.pasteboardItems) {
for (NSPasteboardType type in item.types) {
/* NSLog(@"%@", type); */
const char *mime = uti_to_mime(type);
if (mime && mime[0] && ![@(mime) hasPrefix:@"text/plain"]) {
/* NSLog(@"ut: %@ mt: %@ tags: %@", ut, ut.preferredMIMEType, ut.tags); */
w(mime);
}
}
}
#undef w
}
static void
get_text_plain(GLFWclipboardwritedatafun write_data, void *object) {
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSDictionary* options = @{NSPasteboardURLReadingFileURLsOnlyKey:@YES};
NSArray* objs = [pasteboard readObjectsForClasses:@[[NSURL class], [NSString class]] options:options];
bool found = false;
if (objs) {
const NSUInteger count = [objs count];
if (count) {
NSMutableString *path_list = [NSMutableString stringWithCapacity:4096]; // auto-released
NSMutableString *text_list = [NSMutableString stringWithCapacity:4096]; // auto-released
NSMutableData *path_list = [NSMutableData dataWithCapacity:4096]; // auto-released
NSMutableData *text_list = [NSMutableData dataWithCapacity:4096]; // auto-released
for (NSUInteger i = 0; i < count; i++) {
id obj = objs[i];
if ([obj isKindOfClass:[NSURL class]]) {
NSURL *url = (NSURL*)obj;
if (url.fileURL && url.fileSystemRepresentation) {
if ([path_list length] > 0) [path_list appendString:@("\n")];
[path_list appendString:@(url.fileSystemRepresentation)];
if ([path_list length] > 0) [path_list appendBytes:"\n" length:1];
[path_list appendBytes:url.fileSystemRepresentation length:strlen(url.fileSystemRepresentation)];
}
} else if ([obj isKindOfClass:[NSString class]]) {
if ([text_list length] > 0) [text_list appendString:@("\n")];
[text_list appendString:obj];
if ([text_list length] > 0) [text_list appendBytes:"\n" length:1];
[text_list appendData:[obj dataUsingEncoding:NSUTF8StringEncoding]];
}
}
const char *text = NULL;
if (path_list.length > 0) text = [path_list UTF8String];
else if (text_list.length > 0) text = [text_list UTF8String];
if (text) _glfw.ns.clipboardString = _glfw_strdup(text);
const NSMutableData *text = nil;
if (path_list.length > 0) text = path_list;
else if (text_list.length > 0) text = text_list;
if (text) {
found = true;
write_data(object, text.mutableBytes, text.length);
}
}
}
if (!_glfw.ns.clipboardString) _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to retrieve object from pasteboard");
return _glfw.ns.clipboardString;
if (!found) _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to retrieve text/plain from pasteboard");
}
void
_glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
if (clipboard_type != GLFW_CLIPBOARD) return;
if (mime_type == NULL) {
list_clipboard_mimetypes(write_data, object);
return;
}
if (strcmp(mime_type, "text/plain") == 0) {
get_text_plain(write_data, object);
return;
}
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
/* NSLog(@"mime: %s uti: %@", mime_type, mime_to_uti(mime_type)); */
NSPasteboardType t = [pasteboard availableTypeFromArray:@[mime_to_uti(mime_type)]];
/* NSLog(@"available type: %@", t); */
if (t != nil) {
NSData *data = [pasteboard dataForType:t]; // auto-released
/* NSLog(@"data: %@", data); */
if (data != nil && data.length > 0) {
write_data(object, data.bytes, data.length);
}
}
}
static NSMutableData*
get_clipboard_data(const _GLFWClipboardData *cd, const char *mime) {
NSMutableData *ans = [NSMutableData dataWithCapacity:8192];
if (ans == nil) return nil;
GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype);
void *iter = chunk.iter;
if (!iter) return ans;
while (true) {
chunk = cd->get_data(mime, iter, cd->ctype);
if (!chunk.sz) break;
[ans appendBytes:chunk.data length:chunk.sz];
if (chunk.free) chunk.free((void*)chunk.free_data);
}
cd->get_data(NULL, iter, cd->ctype);
return ans;
}
void
_glfwPlatformSetClipboard(GLFWClipboardType t) {
if (t != GLFW_CLIPBOARD) return;
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
NSMutableArray<NSPasteboardType> *ptypes = [NSMutableArray arrayWithCapacity:_glfw.clipboard.num_mime_types]; // auto-released
for (size_t i = 0; i < _glfw.clipboard.num_mime_types; i++) {
[ptypes addObject:mime_to_uti(_glfw.clipboard.mime_types[i])];
}
[pasteboard declareTypes:ptypes owner:nil];
for (size_t i = 0; i < _glfw.clipboard.num_mime_types; i++) {
NSMutableData *data = get_clipboard_data(&_glfw.clipboard, _glfw.clipboard.mime_types[i]); // auto-released
/* NSLog(@"putting data: %@ for: %s with UTI: %@", data, _glfw.clipboard.mime_types[i], ptypes[i]); */
if (data != nil) [pasteboard setData:data forType:ptypes[i]];
}
}
// }}}
EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs)
{
if (_glfw.egl.ANGLE_platform_angle)

7
glfw/egl_context.c vendored
View File

@@ -600,8 +600,11 @@ bool _glfwCreateContextEGL(_GLFWwindow* window,
if (_glfw.egl.KHR_gl_colorspace)
setAttrib(EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_SRGB_KHR);
}
if (_glfw.egl.EXT_present_opaque)
setAttrib(EGL_PRESENT_OPAQUE_EXT, !fbconfig->transparent);
// Disabled because it prevents transparency from working on NVIDIA drivers under Wayland
// https://github.com/kovidgoyal/kitty/issues/5479
// We anyway dont use the alpha bits for anything.
/* if (_glfw.egl.EXT_present_opaque) */
/* setAttrib(EGL_PRESENT_OPAQUE_EXT, !fbconfig->transparent); */
setAttrib(EGL_NONE, EGL_NONE);

View File

@@ -111,7 +111,7 @@ def init_env(
elif module == 'cocoa':
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo'.split():
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo UniformTypeIdentifiers'.split():
ans.ldpaths.extend(('-framework', f_))
elif module == 'wayland':
@@ -245,6 +245,8 @@ def generate_wrappers(glfw_header: str) -> None:
const char* glfwGetPrimarySelectionString(GLFWwindow* window, void)
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)

73
glfw/glfw3.h vendored
View File

@@ -1716,8 +1716,21 @@ typedef void (* GLFWjoystickfun)(int,int);
typedef void (* GLFWuserdatafun)(unsigned long long, void*);
typedef void (* GLFWtickcallback)(void*);
typedef void (* GLFWactivationcallback)(GLFWwindow *window, const char *token, void *data);
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);
typedef bool (* GLFWclipboardwritedatafun)(void *object, const char *data, size_t sz);
/*! @brief Video mode type.
*
@@ -5220,64 +5233,8 @@ GLFWAPI const char* glfwGetGamepadName(int jid);
*/
GLFWAPI int glfwGetGamepadState(int jid, GLFWgamepadstate* state);
/*! @brief Sets the clipboard to the specified string.
*
* This function sets the system clipboard to the specified, UTF-8 encoded
* string.
*
* @param[in] window Deprecated. Any valid window or `NULL`.
* @param[in] string A UTF-8 encoded string.
*
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref
* GLFW_PLATFORM_ERROR.
*
* @remark @wayland Clipboard is currently unimplemented.
*
* @pointer_lifetime The specified string is copied before this function
* returns.
*
* @thread_safety This function must only be called from the main thread.
*
* @sa @ref clipboard
* @sa @ref glfwGetClipboardString
*
* @since Added in version 3.0.
*
* @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);
GLFWAPI void glfwGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object);
/*! @brief Returns the GLFW time.
*

2
glfw/init.c vendored
View File

@@ -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);

51
glfw/input.c vendored
View File

@@ -1526,36 +1526,41 @@ 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 glfwGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
_GLFW_REQUIRE_INIT();
_glfwPlatformSetClipboardString(string);
_glfwPlatformGetClipboard(clipboard_type, mime_type, write_data, object);
}
GLFWAPI const char* glfwGetClipboardString(GLFWwindow* handle UNUSED)
{
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
return _glfwPlatformGetClipboardString();
}
#if defined(_GLFW_X11) || defined(_GLFW_WAYLAND) || defined(__APPLE__)
GLFWAPI void glfwSetPrimarySelectionString(GLFWwindow* handle UNUSED, const char* string)
{
assert(string != NULL);
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();
_glfwPlatformSetPrimarySelectionString(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 = 0;
cd->ctype = clipboard_type;
for (size_t i = 0; i < num_mime_types; i++) {
if (mime_types[i]) {
cd->mime_types[cd->num_mime_types++] = _glfw_strdup(mime_types[i]);
}
}
_glfwPlatformSetClipboard(clipboard_type);
}
GLFWAPI const char* glfwGetPrimarySelectionString(GLFWwindow* handle UNUSED)
{
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
return _glfwPlatformGetPrimarySelectionString();
}
#endif
GLFWAPI monotonic_t glfwGetTime(void)
{
_GLFW_REQUIRE_INIT_OR_RETURN(0);

22
glfw/internal.h vendored
View File

@@ -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,8 @@ void _glfwPlatformGetVideoMode(_GLFWmonitor* monitor, GLFWvidmode* mode);
bool _glfwPlatformGetGammaRamp(_GLFWmonitor* monitor, GLFWgammaramp* ramp);
void _glfwPlatformSetGammaRamp(_GLFWmonitor* monitor, const GLFWgammaramp* ramp);
void _glfwPlatformSetClipboardString(const char* string);
const char* _glfwPlatformGetClipboardString(void);
#if defined(_GLFW_X11) || defined(_GLFW_WAYLAND) || defined(__APPLE__)
void _glfwPlatformSetPrimarySelectionString(const char* string);
const char* _glfwPlatformGetPrimarySelectionString(void);
#endif
void _glfwPlatformSetClipboard(GLFWClipboardType t);
void _glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object);
bool _glfwPlatformInitJoysticks(void);
void _glfwPlatformTerminateJoysticks(void);
@@ -855,3 +863,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);

View File

@@ -77,7 +77,8 @@
"unstable/idle-inhibit/idle-inhibit-unstable-v1.xml",
"unstable/xdg-decoration/xdg-decoration-unstable-v1.xml",
"unstable/primary-selection/primary-selection-unstable-v1.xml",
"unstable/text-input/text-input-unstable-v3.xml"
"unstable/text-input/text-input-unstable-v3.xml",
"staging/xdg-activation/xdg-activation-v1.xml"
],
"sources": [
"wl_init.c",

2
glfw/window.c vendored
View File

@@ -71,7 +71,7 @@ void _glfwInputWindowFocus(_GLFWwindow* window, bool focused)
_glfw.focusedWindowId = window->id;
}
_GLFWwindow* _glfwFocusedWindow() {
_GLFWwindow* _glfwFocusedWindow(void) {
if (_glfw.focusedWindowId) {
_GLFWwindow *w = _glfw.windowListHead;
while (w) {

37
glfw/wl_init.c vendored
View File

@@ -99,7 +99,7 @@ static void pointerHandleEnter(void* data UNUSED,
return;
}
window->wl.decorations.focus = focus;
_glfw.wl.serial = serial;
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial;
_glfw.wl.pointerFocus = window;
window->wl.hovered = true;
@@ -144,8 +144,12 @@ static void setCursor(GLFWCursorShape shape, _GLFWwindow* window)
if (!image)
return;
if (image->width % scale || image->height % scale) {
_glfwInputError(GLFW_PLATFORM_ERROR, "WARNING: Cursor image size: %dx%d is not a multiple of window scale: %d. This will"
" cause some compositors such as GNOME to crash. See https://github.com/kovidgoyal/kitty/issues/4878", image->width, image->height, scale);
static uint32_t warned_width = 0, warned_height = 0;
if (warned_width != image->width || warned_height != image->height) {
_glfwInputError(GLFW_PLATFORM_ERROR, "WARNING: Cursor image size: %dx%d is not a multiple of window scale: %d. This will"
" cause some compositors such as GNOME to crash. See https://github.com/kovidgoyal/kitty/issues/4878", image->width, image->height, scale);
warned_width = image->width; warned_height = image->height;
}
}
buffer = wl_cursor_image_get_buffer(image);
@@ -307,7 +311,7 @@ static void pointerHandleButton(void* data UNUSED,
if (window->wl.decorations.focus != CENTRAL_WINDOW)
return;
_glfw.wl.serial = serial;
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial;
/* Makes left, right and middle 0, 1 and 2. Overall order follows evdev
* codes. */
@@ -459,7 +463,7 @@ static void keyboardHandleEnter(void* data UNUSED,
return;
}
_glfw.wl.serial = serial;
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial;
_glfw.wl.keyboardFocusId = window->id;
_glfwInputWindowFocus(window, true);
uint32_t* key;
@@ -512,7 +516,7 @@ static void keyboardHandleKey(void* data UNUSED,
return;
int action = state == WL_KEYBOARD_KEY_STATE_PRESSED ? GLFW_PRESS : GLFW_RELEASE;
_glfw.wl.serial = serial;
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial;
glfw_xkb_handle_key_event(window, &_glfw.wl.xkb, key, action);
if (action == GLFW_PRESS && _glfw.wl.keyboardRepeatRate > 0 && glfw_xkb_should_repeat(&_glfw.wl.xkb, key))
@@ -535,7 +539,7 @@ static void keyboardHandleModifiers(void* data UNUSED,
uint32_t modsLocked,
uint32_t group)
{
_glfw.wl.serial = serial;
_glfw.wl.serial = serial; _glfw.wl.input_serial = serial;
glfw_xkb_update_modifiers(&_glfw.wl.xkb, modsDepressed, modsLatched, modsLocked, 0, 0, group);
}
@@ -715,6 +719,9 @@ static void registryHandleGlobal(void* data UNUSED,
_glfwSetupWaylandPrimarySelectionDevice();
}
}
else if (strstr(interface, "xdg_activation_v1") != 0) {
_glfw.wl.xdg_activation_v1 = wl_registry_bind(registry, name, &xdg_activation_v1_interface, 1);
}
}
@@ -878,6 +885,14 @@ int _glfwPlatformInit(void)
void _glfwPlatformTerminate(void)
{
if (_glfw.wl.activation_requests.array) {
for (size_t i=0; i < _glfw.wl.activation_requests.sz; i++) {
glfw_wl_xdg_activation_request *r = _glfw.wl.activation_requests.array + i;
if (r->callback) r->callback(NULL, NULL, r->callback_data);
xdg_activation_token_v1_destroy(r->token);
}
free(_glfw.wl.activation_requests.array);
}
_glfwTerminateEGL();
if (_glfw.wl.egl.handle)
{
@@ -922,6 +937,8 @@ void _glfwPlatformTerminate(void)
zwp_idle_inhibit_manager_v1_destroy(_glfw.wl.idleInhibitManager);
if (_glfw.wl.dataSourceForClipboard)
wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
if (_glfw.wl.dataSourceForPrimarySelection)
zwp_primary_selection_source_v1_destroy(_glfw.wl.dataSourceForPrimarySelection);
for (size_t doi=0; doi < arraysz(_glfw.wl.dataOffers); doi++) {
if (_glfw.wl.dataOffers[doi].id) {
destroy_data_offer(&_glfw.wl.dataOffers[doi]);
@@ -935,6 +952,8 @@ void _glfwPlatformTerminate(void)
zwp_primary_selection_device_v1_destroy(_glfw.wl.primarySelectionDevice);
if (_glfw.wl.primarySelectionDeviceManager)
zwp_primary_selection_device_manager_v1_destroy(_glfw.wl.primarySelectionDeviceManager);
if (_glfw.wl.xdg_activation_v1)
xdg_activation_v1_destroy(_glfw.wl.xdg_activation_v1);
if (_glfw.wl.registry)
wl_registry_destroy(_glfw.wl.registry);
if (_glfw.wl.display)
@@ -943,10 +962,6 @@ void _glfwPlatformTerminate(void)
wl_display_disconnect(_glfw.wl.display);
}
finalizePollData(&_glfw.wl.eventLoopData);
free(_glfw.wl.clipboardString); _glfw.wl.clipboardString = NULL;
free(_glfw.wl.primarySelectionString); _glfw.wl.primarySelectionString = NULL;
free(_glfw.wl.pasteString); _glfw.wl.pasteString = NULL;
}
const char* _glfwPlatformGetVersionString(void)

21
glfw/wl_platform.h vendored
View File

@@ -58,7 +58,9 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR
#include "wayland-pointer-constraints-unstable-v1-client-protocol.h"
#include "wayland-idle-inhibit-unstable-v1-client-protocol.h"
#include "wayland-primary-selection-unstable-v1-client-protocol.h"
#include "wayland-primary-selection-unstable-v1-client-protocol.h"
#include "wl_text_input.h"
#include "wayland-xdg-activation-v1-client-protocol.h"
#define _glfw_dlopen(name) dlopen(name, RTLD_LAZY | RTLD_LOCAL)
#define _glfw_dlclose(handle) dlclose(handle)
@@ -124,6 +126,14 @@ typedef enum WaylandWindowState {
TOPLEVEL_STATE_TILED_BOTTOM = 128,
} WaylandWindowState;
typedef struct glfw_wl_xdg_activation_request {
GLFWid window_id;
GLFWactivationcallback callback;
void *callback_data;
uintptr_t request_id;
void *token;
} glfw_wl_xdg_activation_request;
static const WaylandWindowState TOPLEVEL_STATE_DOCKED = TOPLEVEL_STATE_MAXIMIZED | TOPLEVEL_STATE_FULLSCREEN | TOPLEVEL_STATE_TILED_TOP | TOPLEVEL_STATE_TILED_LEFT | TOPLEVEL_STATE_TILED_RIGHT | TOPLEVEL_STATE_TILED_BOTTOM;
@@ -276,13 +286,14 @@ typedef struct _GLFWlibraryWayland
struct zwp_primary_selection_device_manager_v1* primarySelectionDeviceManager;
struct zwp_primary_selection_device_v1* primarySelectionDevice;
struct zwp_primary_selection_source_v1* dataSourceForPrimarySelection;
struct xdg_activation_v1* xdg_activation_v1;
int compositorVersion;
int seatVersion;
struct wl_surface* cursorSurface;
GLFWCursorShape cursorPreviousShape;
uint32_t serial;
uint32_t serial, input_serial;
int32_t keyboardRepeatRate;
monotonic_t keyboardRepeatDelay;
@@ -316,12 +327,14 @@ typedef struct _GLFWlibraryWayland
PFN_wl_egl_window_resize window_resize;
} egl;
struct {
glfw_wl_xdg_activation_request *array;
size_t capacity, sz;
} activation_requests;
EventLoopData eventLoopData;
char* pasteString;
char* clipboardString;
size_t dataOffersCounter;
_GLFWWaylandDataOffer dataOffers[8];
char* primarySelectionString;
} _GLFWlibraryWayland;
// Wayland-specific per-monitor data

505
glfw/wl_window.c vendored
View File

@@ -44,6 +44,59 @@
#define debug(...) if (_glfw.hints.init.debugRendering) fprintf(stderr, __VA_ARGS__);
static void
activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, const char *token) {
for (size_t i = 0; i < _glfw.wl.activation_requests.sz; i++) {
glfw_wl_xdg_activation_request *r = _glfw.wl.activation_requests.array + i;
if (r->request_id == (uintptr_t)data) {
_GLFWwindow *window = _glfwWindowForId(r->window_id);
if (r->callback) r->callback((GLFWwindow*)window, token, r->callback_data);
remove_i_from_array(_glfw.wl.activation_requests.array, i, _glfw.wl.activation_requests.sz);
break;
}
}
xdg_activation_token_v1_destroy(xdg_token);
}
static const struct
xdg_activation_token_v1_listener activation_token_listener = {
.done = &activation_token_done,
};
static bool
get_activation_token(
_GLFWwindow *window, uint32_t serial, GLFWactivationcallback cb, void *cb_data
) {
#define fail(msg) { _glfwInputError(GLFW_PLATFORM_ERROR, msg); if (cb) cb((GLFWwindow*)window, NULL, cb_data); return false; }
if (_glfw.wl.xdg_activation_v1 == NULL) fail("Wayland: activation requests not supported by this Wayland compositor");
struct xdg_activation_token_v1 *token = xdg_activation_v1_get_activation_token(_glfw.wl.xdg_activation_v1);
if (token == NULL) fail("Wayland: failed to create activation request token");
if (_glfw.wl.activation_requests.capacity < _glfw.wl.activation_requests.sz + 1) {
_glfw.wl.activation_requests.capacity = MAX(64u, _glfw.wl.activation_requests.capacity * 2);
_glfw.wl.activation_requests.array = realloc(_glfw.wl.activation_requests.array, _glfw.wl.activation_requests.capacity);
if (!_glfw.wl.activation_requests.array) {
_glfw.wl.activation_requests.capacity = 0;
fail("Wayland: Out of memory while allocation activation request");
}
}
glfw_wl_xdg_activation_request *r = _glfw.wl.activation_requests.array + _glfw.wl.activation_requests.sz++;
memset(r, 0, sizeof(*r));
static uintptr_t rq = 0;
r->window_id = window->id;
r->callback = cb; r->callback_data = cb_data;
r->request_id = ++rq; r->token = token;
if (serial != 0)
xdg_activation_token_v1_set_serial(token, serial, _glfw.wl.seat);
xdg_activation_token_v1_set_surface(token, window->wl.surface);
xdg_activation_token_v1_add_listener(token, &activation_token_listener, (void*)r->request_id);
xdg_activation_token_v1_commit(token);
return true;
#undef fail
}
static struct wl_buffer* createShmBuffer(const GLFWimage* image, bool is_opaque, bool init_data)
{
struct wl_shm_pool* pool;
@@ -199,7 +252,7 @@ static bool checkScaleChange(_GLFWwindow* window)
}
// Makes the surface considered as XRGB instead of ARGB.
static void setOpaqueRegion(_GLFWwindow* window)
static void setOpaqueRegion(_GLFWwindow* window, bool commit_surface)
{
struct wl_region* region;
@@ -209,10 +262,22 @@ static void setOpaqueRegion(_GLFWwindow* window)
wl_region_add(region, 0, 0, window->wl.width, window->wl.height);
wl_surface_set_opaque_region(window->wl.surface, region);
wl_surface_commit(window->wl.surface);
if (commit_surface) wl_surface_commit(window->wl.surface);
wl_region_destroy(region);
}
static void
swap_buffers(_GLFWwindow *window) {
// this will attach the buffer to the surface,
// the client is responsible for clearing the buffer to an appropriate blank
window->swaps_disallowed = false;
GLFWwindow *current = glfwGetCurrentContext();
bool context_is_current = ((_GLFWwindow*)current)->id == window->id;
if (!context_is_current) glfwMakeContextCurrent((GLFWwindow*)window);
window->context.swapBuffers(window);
if (!context_is_current) glfwMakeContextCurrent(current);
}
static void
resizeFramebuffer(_GLFWwindow* window) {
@@ -221,7 +286,7 @@ resizeFramebuffer(_GLFWwindow* window) {
int scaledHeight = window->wl.height * scale;
debug("Resizing framebuffer to: %dx%d at scale: %d\n", window->wl.width, window->wl.height, scale);
wl_egl_window_resize(window->wl.native, scaledWidth, scaledHeight, 0, 0);
if (!window->wl.transparent) setOpaqueRegion(window);
if (!window->wl.transparent) setOpaqueRegion(window, false);
_glfwInputFramebufferSize(window, scaledWidth, scaledHeight);
}
@@ -236,9 +301,9 @@ clipboard_mime(void) {
}
static bool
dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height) {
dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height, bool *scale_changed) {
bool size_changed = width != window->wl.width || height != window->wl.height;
bool scale_changed = checkScaleChange(window);
*scale_changed = checkScaleChange(window);
if (size_changed) {
_glfwInputWindowSize(window, width, height);
@@ -246,7 +311,7 @@ dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height
resizeFramebuffer(window);
}
if (scale_changed) {
if (*scale_changed) {
debug("Scale changed to %d in dispatchChangesAfterConfigure\n", window->wl.scale);
if (!size_changed) resizeFramebuffer(window);
_glfwInputWindowContentScale(window, window->wl.scale, window->wl.scale);
@@ -254,7 +319,7 @@ dispatchChangesAfterConfigure(_GLFWwindow *window, int32_t width, int32_t height
_glfwInputWindowDamage(window);
return size_changed || scale_changed;
return size_changed || *scale_changed;
}
static void
@@ -368,9 +433,22 @@ static bool createSurface(_GLFWwindow* window,
wl_surface_set_user_data(window->wl.surface, window);
window->wl.native = wl_egl_window_create(window->wl.surface,
wndconfig->width,
wndconfig->height);
// If we already have been notified of the primary monitor scale, assume
// the window will be created on it and so avoid a rescale roundtrip in the common
// case of the window being shown on the primary monitor or all monitors having the same scale.
// If you change this also change get_window_content_scale() in the kitty code.
GLFWmonitor* monitor = glfwGetPrimaryMonitor();
float xscale = 1.0, yscale = 1.0;
int scale = 1;
if (monitor) {
glfwGetMonitorContentScale(monitor, &xscale, &yscale);
// see wl_monitor.c xscale is always == yscale
if (xscale <= 0.0001 || xscale != xscale || xscale >= 24) xscale = 1.0;
if (xscale > 1) scale = (int)xscale;
}
debug("Creating window at size: %dx%d and scale %d\n", wndconfig->width, wndconfig->height, scale);
window->wl.native = wl_egl_window_create(window->wl.surface, wndconfig->width * scale, wndconfig->height * scale);
if (!window->wl.native)
return false;
@@ -378,11 +456,13 @@ static bool createSurface(_GLFWwindow* window,
window->wl.height = wndconfig->height;
window->wl.user_requested_content_size.width = wndconfig->width;
window->wl.user_requested_content_size.height = wndconfig->height;
window->wl.scale = 1;
window->wl.scale = scale;
if (!window->wl.transparent)
setOpaqueRegion(window);
setOpaqueRegion(window, false);
wl_surface_set_buffer_scale(window->wl.surface, scale);
return true;
}
@@ -496,15 +576,7 @@ static void xdgSurfaceHandleConfigure(void* data,
int height = window->wl.pending.height;
if (!window->wl.surface_configured_once) {
window->wl.surface_configured_once = true;
// this will attach the buffer to the surface, the client is responsible for clearing the buffer to an appropriate blank
window->swaps_disallowed = false;
window->context.swapBuffers(window);
if (!width && !height && !new_states && !window->wl.decorations.serverSide && getenv("XAUTHORITY") && strstr(getenv("XAUTHORITY"), "mutter")) {
// https://github.com/kovidgoyal/kitty/issues/4802
debug("Ignoring first empty surface configure event on mutter.\n");
return;
}
swap_buffers(window);
}
if (new_states != window->wl.current.toplevel_states ||
@@ -528,10 +600,11 @@ static void xdgSurfaceHandleConfigure(void* data,
}
bool resized = false;
bool scale_changed = false;
if (window->wl.pending_state) {
int width = window->wl.pending.width, height = window->wl.pending.height;
set_csd_window_geometry(window, &width, &height);
resized = dispatchChangesAfterConfigure(window, width, height);
resized = dispatchChangesAfterConfigure(window, width, height, &scale_changed);
if (window->wl.decorations.serverSide) {
free_csd_surfaces(window);
} else {
@@ -542,6 +615,10 @@ static void xdgSurfaceHandleConfigure(void* data,
inform_compositor_of_window_geometry(window, "configure");
// we need to swap buffers here to ensure the buffer attached to the surface is a multiple
// of the new scale. See https://github.com/kovidgoyal/kitty/issues/5467
if (scale_changed) swap_buffers(window);
// if a resize happened there will be a commit at the next render frame so
// dont commit here, GNOME doesnt like it and its not really needed anyway
if (!resized) wl_surface_commit(window->wl.surface);
@@ -998,7 +1075,7 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
if (window->decorated && !window->monitor && !window->wl.decorations.serverSide)
{
if (top)
*top = window->wl.decorations.metrics.top;
*top = window->wl.decorations.metrics.top - window->wl.decorations.metrics.visible_titlebar_height;
if (left)
*left = window->wl.decorations.metrics.width;
if (right)
@@ -1073,34 +1150,45 @@ void _glfwPlatformHideWindow(_GLFWwindow* window)
window->wl.visible = false;
}
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window UNUSED)
{
// TODO
static bool notified = false;
if (!notified) {
_glfwInputError(GLFW_FEATURE_UNIMPLEMENTED,
"Wayland: Window attention request not implemented yet");
notified = true;
static void
request_attention(GLFWwindow *window, const char *token, void *data UNUSED) {
if (window && token && token[0]) xdg_activation_v1_activate(_glfw.wl.xdg_activation_v1, token, ((_GLFWwindow*)window)->wl.surface);
}
static bool
has_activation_in_flight(_GLFWwindow* window, GLFWactivationcallback callback) {
for (size_t i = 0; i < _glfw.wl.activation_requests.sz; i++) {
glfw_wl_xdg_activation_request *r = _glfw.wl.activation_requests.array + i;
if (r->window_id == window->id && r->callback == callback) return true;
}
return false;
}
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window) {
if (!has_activation_in_flight(window, request_attention)) get_activation_token(window, 0, request_attention, NULL);
}
int _glfwPlatformWindowBell(_GLFWwindow* window UNUSED)
{
// TODO: Use an actual Wayland API to implement this when one becomes available
static char tty[L_ctermid + 1];
int fd = open(ctermid(tty), O_WRONLY | O_CLOEXEC);
if (fd > -1) {
int ret = write(fd, "\x07", 1) == 1 ? true : false;
close(fd);
return ret;
}
return false;
}
static void
focus_window(GLFWwindow *window, const char *token, void *data UNUSED) {
if (!window) return;
if (token && token[0]) xdg_activation_v1_activate(_glfw.wl.xdg_activation_v1, token, ((_GLFWwindow*)window)->wl.surface);
else {
_glfwInputError(GLFW_PLATFORM_ERROR, "Wayland: Window focus request via xdg-activation protocol was denied by the compositor. Use a better compositor.");
}
}
void _glfwPlatformFocusWindow(_GLFWwindow* window UNUSED)
{
_glfwInputError(GLFW_FEATURE_UNAVAILABLE,
"Wayland: The platform does not support setting the input focus");
// Attempt to focus the window by using the activation protocol, whether it works
// is entirely compositor dependent and as we all know Wayland and its ecosystem is
// the product of morons.
if (_glfw.wl.input_serial && !has_activation_in_flight(window, focus_window)) get_activation_token(window, _glfw.wl.input_serial, focus_window, NULL);
}
void _glfwPlatformSetWindowMonitor(_GLFWwindow* window,
@@ -1447,53 +1535,68 @@ void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor)
}
}
static void send_text(char *text, int fd)
{
if (text) {
size_t len = strlen(text), pos = 0;
monotonic_t start = glfwGetTime();
while (pos < len && glfwGetTime() - start < s_to_monotonic_t(2ll)) {
ssize_t ret = write(fd, text + pos, len - pos);
if (ret < 0) {
if (errno == EAGAIN || errno == EINTR) continue;
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Could not copy writing to destination fd failed with error: %s", strerror(errno));
break;
}
if (ret > 0) {
start = glfwGetTime();
pos += ret;
}
static bool
write_all(int fd, const char *data, size_t sz) {
monotonic_t start = glfwGetTime();
size_t pos = 0;
while (pos < sz && glfwGetTime() - start < s_to_monotonic_t(2ll)) {
ssize_t ret = write(fd, data + pos, sz - pos);
if (ret < 0) {
if (errno == EAGAIN || errno == EINTR) continue;
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Could not copy writing to destination fd failed with error: %s", strerror(errno));
return false;
}
if (ret > 0) {
start = glfwGetTime();
pos += ret;
}
}
return pos >= sz;
}
static void
send_clipboard_data(const _GLFWClipboardData *cd, const char *mime, int fd) {
if (strcmp(mime, "text/plain;charset=utf-8") == 0 || strcmp(mime, "UTF8_STRING") == 0 || strcmp(mime, "TEXT") == 0 || strcmp(mime, "STRING") == 0) mime = "text/plain";
GLFWDataChunk chunk = cd->get_data(mime, NULL, cd->ctype);
void *iter = chunk.iter;
if (!iter) return;
bool keep_going = true;
while (keep_going) {
chunk = cd->get_data(mime, iter, cd->ctype);
if (!chunk.sz) break;
if (!write_all(fd, chunk.data, chunk.sz)) keep_going = false;
if (chunk.free) chunk.free((void*)chunk.free_data);
}
cd->get_data(NULL, iter, cd->ctype);
}
static void _glfwSendClipboardText(void *data UNUSED, struct wl_data_source *data_source UNUSED, const char *mime_type, int fd) {
send_clipboard_data(&_glfw.clipboard, mime_type, fd);
close(fd);
}
static void _glfwSendClipboardText(void *data UNUSED, struct wl_data_source *data_source UNUSED, const char *mime_type UNUSED, int fd) {
send_text(_glfw.wl.clipboardString, fd);
}
static void _glfwSendPrimarySelectionText(void *data UNUSED, struct zwp_primary_selection_source_v1 *primary_selection_source UNUSED,
const char *mime_type UNUSED, int fd) {
send_text(_glfw.wl.primarySelectionString, fd);
const char *mime_type, int fd) {
send_clipboard_data(&_glfw.primary, mime_type, fd);
close(fd);
}
static char* read_offer_string(int data_pipe, size_t *data_sz) {
static void
read_offer(int data_pipe, GLFWclipboardwritedatafun write_data, void *object) {
wl_display_flush(_glfw.wl.display);
size_t capacity = 0;
char *buf = NULL;
*data_sz = 0;
struct pollfd fds;
fds.fd = data_pipe;
fds.events = POLLIN;
monotonic_t start = glfwGetTime();
#define bail(...) { \
_glfwInputError(GLFW_PLATFORM_ERROR, __VA_ARGS__); \
free(buf); buf = NULL; \
close(data_pipe); \
return NULL; \
return; \
}
char buf[8192];
while (glfwGetTime() - start < s_to_monotonic_t(2ll)) {
int ret = poll(&fds, 1, 2000);
if (ret == -1) {
@@ -1503,34 +1606,65 @@ static char* read_offer_string(int data_pipe, size_t *data_sz) {
if (!ret) {
bail("Wayland: Failed to read clipboard data from pipe (timed out)");
}
if (capacity <= *data_sz || capacity - *data_sz <= 64) {
capacity += 4096;
buf = realloc(buf, capacity);
if (!buf) {
bail("Wayland: Failed to allocate memory to read clipboard data");
}
}
ret = read(data_pipe, buf + *data_sz, capacity - *data_sz - 1);
ret = read(data_pipe, buf, sizeof(buf));
if (ret == -1) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue;
bail("Wayland: Failed to read clipboard data from pipe with error: %s", strerror(errno));
}
if (ret == 0) { close(data_pipe); buf[*data_sz] = 0; return buf; }
*data_sz += ret;
if (ret == 0) { close(data_pipe); return; }
if (!write_data(object, buf, ret)) bail("Wayland: call to write_data() failed with data from data offer");
start = glfwGetTime();
}
bail("Wayland: Failed to read clipboard data from pipe (timed out)");
#undef bail
}
static char* read_primary_selection_offer(struct zwp_primary_selection_offer_v1 *primary_selection_offer, const char *mime) {
typedef struct chunked_writer {
char *buf; size_t sz, cap;
} chunked_writer;
static bool
write_chunk(void *object, const char *data, size_t sz) {
chunked_writer *cw = object;
if (cw->cap < cw->sz + sz) {
cw->cap = MAX(cw->cap * 2, cw->sz + 8*sz);
cw->buf = realloc(cw->buf, cw->cap);
}
memcpy(cw->buf + cw->sz, data, sz);
cw->sz += sz;
return true;
}
static char*
read_offer_string(int data_pipe, size_t *sz) {
chunked_writer cw = {0};
read_offer(data_pipe, write_chunk, &cw);
if (cw.buf) {
*sz = cw.sz;
return cw.buf;
}
*sz = 0;
return NULL;
}
static void
read_clipboard_data_offer(struct wl_data_offer *data_offer, const char *mime, GLFWclipboardwritedatafun write_data, void *object) {
int pipefd[2];
if (pipe2(pipefd, O_CLOEXEC) != 0) return NULL;
if (pipe2(pipefd, O_CLOEXEC) != 0) return;
wl_data_offer_receive(data_offer, mime, pipefd[1]);
close(pipefd[1]);
read_offer(pipefd[0], write_data, object);
}
static void
read_primary_selection_offer(struct zwp_primary_selection_offer_v1 *primary_selection_offer, const char *mime, GLFWclipboardwritedatafun write_data, void *object) {
int pipefd[2];
if (pipe2(pipefd, O_CLOEXEC) != 0) return;
zwp_primary_selection_offer_v1_receive(primary_selection_offer, mime, pipefd[1]);
close(pipefd[1]);
size_t sz = 0;
return read_offer_string(pipefd[0], &sz);
read_offer(pipefd[0], write_data, object);
}
static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime, size_t *sz) {
@@ -1542,24 +1676,24 @@ static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime,
}
static void data_source_canceled(void *data UNUSED, struct wl_data_source *wl_data_source) {
if (_glfw.wl.dataSourceForClipboard == wl_data_source)
if (_glfw.wl.dataSourceForClipboard == wl_data_source) {
_glfw.wl.dataSourceForClipboard = NULL;
_glfw_free_clipboard_data(&_glfw.clipboard);
}
wl_data_source_destroy(wl_data_source);
}
static void primary_selection_source_canceled(void *data UNUSED, struct zwp_primary_selection_source_v1 *primary_selection_source) {
if (_glfw.wl.dataSourceForPrimarySelection == primary_selection_source)
if (_glfw.wl.dataSourceForPrimarySelection == primary_selection_source) {
_glfw.wl.dataSourceForPrimarySelection = NULL;
_glfw_free_clipboard_data(&_glfw.primary);
}
zwp_primary_selection_source_v1_destroy(primary_selection_source);
}
static void data_source_target(void *data UNUSED, struct wl_data_source *wl_data_source UNUSED, const char* mime UNUSED) {
}
static const struct wl_data_source_listener data_source_listener = {
.send = _glfwSendClipboardText,
.cancelled = data_source_canceled,
.target = data_source_target,
};
static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = {
@@ -1836,30 +1970,69 @@ static bool _glfwEnsureDataDevice(void) {
return true;
}
void _glfwPlatformSetClipboardString(const char* string)
{
if (!_glfwEnsureDataDevice()) return;
free(_glfw.wl.clipboardString);
_glfw.wl.clipboardString = _glfw_strdup(string);
if (_glfw.wl.dataSourceForClipboard)
wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
_glfw.wl.dataSourceForClipboard = wl_data_device_manager_create_data_source(_glfw.wl.dataDeviceManager);
if (!_glfw.wl.dataSourceForClipboard)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Cannot copy failed to create data source");
return;
typedef void(*add_offer_func)(void*, const char *mime);
void
_glfwPlatformSetClipboard(GLFWClipboardType t) {
_GLFWClipboardData *cd = NULL;
void *data_source;
add_offer_func f;
if (t == GLFW_CLIPBOARD) {
if (!_glfwEnsureDataDevice()) return;
cd = &_glfw.clipboard;
f = (add_offer_func)wl_data_source_offer;
if (_glfw.wl.dataSourceForClipboard) wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
_glfw.wl.dataSourceForClipboard = wl_data_device_manager_create_data_source(_glfw.wl.dataDeviceManager);
if (!_glfw.wl.dataSourceForClipboard)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Cannot copy failed to create data source");
return;
}
wl_data_source_add_listener(_glfw.wl.dataSourceForClipboard, &data_source_listener, NULL);
data_source = _glfw.wl.dataSourceForClipboard;
} else {
if (!_glfw.wl.primarySelectionDevice) {
static bool warned_about_primary_selection_device = false;
if (!warned_about_primary_selection_device) {
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Cannot copy no primary selection device available");
warned_about_primary_selection_device = true;
}
return;
}
cd = &_glfw.primary;
f = (add_offer_func)zwp_primary_selection_source_v1_offer;
if (_glfw.wl.dataSourceForPrimarySelection) zwp_primary_selection_source_v1_destroy(_glfw.wl.dataSourceForPrimarySelection);
_glfw.wl.dataSourceForPrimarySelection = zwp_primary_selection_device_manager_v1_create_source(_glfw.wl.primarySelectionDeviceManager);
if (!_glfw.wl.dataSourceForPrimarySelection)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Cannot copy failed to create primary selection source");
return;
}
zwp_primary_selection_source_v1_add_listener(_glfw.wl.dataSourceForPrimarySelection, &primary_selection_source_listener, NULL);
data_source = _glfw.wl.dataSourceForPrimarySelection;
}
f(data_source, clipboard_mime());
for (size_t i = 0; i < cd->num_mime_types; i++) {
if (strcmp(cd->mime_types[i], "text/plain") == 0) {
f(data_source, "TEXT");
f(data_source, "STRING");
f(data_source, "UTF8_STRING");
f(data_source, "text/plain;charset=utf-8");
}
f(data_source, cd->mime_types[i]);
}
wl_data_source_add_listener(_glfw.wl.dataSourceForClipboard, &data_source_listener, NULL);
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, clipboard_mime());
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "text/plain");
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "text/plain;charset=utf-8");
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "TEXT");
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "STRING");
wl_data_source_offer(_glfw.wl.dataSourceForClipboard, "UTF8_STRING");
struct wl_callback *callback = wl_display_sync(_glfw.wl.display);
static const struct wl_callback_listener clipboard_copy_callback_listener = {.done = clipboard_copy_callback_done};
wl_callback_add_listener(callback, &clipboard_copy_callback_listener, _glfw.wl.dataSourceForClipboard);
if (t == GLFW_CLIPBOARD) {
static const struct wl_callback_listener clipboard_copy_callback_listener = {.done = clipboard_copy_callback_done};
wl_callback_add_listener(callback, &clipboard_copy_callback_listener, _glfw.wl.dataSourceForClipboard);
} else {
static const struct wl_callback_listener primary_selection_copy_callback_listener = {.done = primary_selection_copy_callback_done};
wl_callback_add_listener(callback, &primary_selection_copy_callback_listener, _glfw.wl.dataSourceForPrimarySelection);
}
}
static bool
@@ -1877,81 +2050,47 @@ plain_text_mime_for_offer(const _GLFWWaylandDataOffer *d) {
A("text/plain");
A("UTF8_STRING");
A("STRING");
A("TEXT");
#undef A
return NULL;
}
const char* _glfwPlatformGetClipboardString(void)
{
void
_glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
_GLFWWaylandOfferType offer_type = clipboard_type == GLFW_PRIMARY_SELECTION ? PRIMARY_SELECTION : CLIPBOARD;
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
_GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i;
if (d->id && d->offer_type == CLIPBOARD) {
if (d->is_self_offer) return _glfw.wl.clipboardString;
const char *mime = plain_text_mime_for_offer(d);
if (mime) {
free(_glfw.wl.pasteString);
size_t sz = 0;
_glfw.wl.pasteString = read_data_offer(d->id, mime, &sz);
return _glfw.wl.pasteString;
if (d->id && d->offer_type == offer_type) {
if (d->is_self_offer) {
write_data(object, NULL, 1);
return;
}
}
}
return NULL;
}
void _glfwPlatformSetPrimarySelectionString(const char* string)
{
if (!_glfw.wl.primarySelectionDevice) {
static bool warned_about_primary_selection_device = false;
if (!warned_about_primary_selection_device) {
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Cannot copy no primary selection device available");
warned_about_primary_selection_device = true;
}
return;
}
if (_glfw.wl.primarySelectionString == string) return;
free(_glfw.wl.primarySelectionString);
_glfw.wl.primarySelectionString = _glfw_strdup(string);
if (_glfw.wl.dataSourceForPrimarySelection)
zwp_primary_selection_source_v1_destroy(_glfw.wl.dataSourceForPrimarySelection);
_glfw.wl.dataSourceForPrimarySelection = zwp_primary_selection_device_manager_v1_create_source(_glfw.wl.primarySelectionDeviceManager);
if (!_glfw.wl.dataSourceForPrimarySelection)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Cannot copy failed to create primary selection source");
return;
}
zwp_primary_selection_source_v1_add_listener(_glfw.wl.dataSourceForPrimarySelection, &primary_selection_source_listener, NULL);
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, clipboard_mime());
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "text/plain");
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "text/plain;charset=utf-8");
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "TEXT");
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "STRING");
zwp_primary_selection_source_v1_offer(_glfw.wl.dataSourceForPrimarySelection, "UTF8_STRING");
struct wl_callback *callback = wl_display_sync(_glfw.wl.display);
static const struct wl_callback_listener primary_selection_copy_callback_listener = {.done = primary_selection_copy_callback_done};
wl_callback_add_listener(callback, &primary_selection_copy_callback_listener, _glfw.wl.dataSourceForPrimarySelection);
}
const char* _glfwPlatformGetPrimarySelectionString(void)
{
if (_glfw.wl.dataSourceForPrimarySelection != NULL) return _glfw.wl.primarySelectionString;
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
_GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i;
if (d->id && d->is_primary && d->offer_type == PRIMARY_SELECTION) {
if (d->is_self_offer) return _glfw.wl.primarySelectionString;
const char *mime = plain_text_mime_for_offer(d);
if (mime) {
free(_glfw.wl.pasteString);
_glfw.wl.pasteString = read_primary_selection_offer(d->id, mime);
return _glfw.wl.pasteString;
if (mime_type == NULL) {
bool ok = true;
for (size_t o = 0; o < d->mimes_count; o++) {
const char *q = d->mimes[o];
if (strchr(d->mimes[0], '/')) {
if (strcmp(q, clipboard_mime()) == 0) continue;
if (strcmp(q, "text/plain;charset=utf-8") == 0) q = "text/plain";
} else {
if (strcmp(q, "UTF8_STRING") == 0 || strcmp(q, "STRING") == 0 || strcmp(q, "TEXT") == 0) q = "text/plain";
}
if (ok) ok = write_data(object, q, strlen(q));
}
return;
}
if (strcmp(mime_type, "text/plain") == 0) {
mime_type = plain_text_mime_for_offer(d);
if (!mime_type) return;
}
if (d->is_primary) {
read_primary_selection_offer(d->id, mime_type, write_data, object);
} else {
read_clipboard_data_offer(d->id, mime_type, write_data, object);
}
break;
}
}
return NULL;
}
EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs UNUSED)
@@ -2073,6 +2212,18 @@ GLFWAPI struct wl_surface* glfwGetWaylandWindow(GLFWwindow* handle)
return window->wl.surface;
}
GLFWAPI void glfwWaylandActivateWindow(GLFWwindow* handle, const char *activation_token) {
_GLFWwindow* window = (_GLFWwindow*) handle;
_GLFW_REQUIRE_INIT();
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);
}

10
glfw/x11_init.c vendored
View File

@@ -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)
{

16
glfw/x11_platform.h vendored
View File

@@ -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;

345
glfw/x11_window.c vendored
View File

@@ -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]));
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) 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) 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);
}
}
@@ -881,34 +922,21 @@ static void handleSelectionRequest(XEvent* event)
XSendEvent(_glfw.x11.display, request->requestor, False, 0, &reply);
}
static const char* getSelectionString(Atom selection)
static void
getSelectionString(Atom selection, Atom *targets, size_t num_targets, GLFWclipboardwritedatafun write_data, void *object, bool report_not_found)
{
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;
#define XFREE(x) { if (x) XFree(x); x = NULL; }
if (XGetSelectionOwner(_glfw.x11.display, selection) == _glfw.x11.helperWindowHandle) {
write_data(object, NULL, 1);
return;
}
free(*selectionString);
*selectionString = NULL;
for (size_t i = 0; i < targetCount; i++)
bool found = false;
for (size_t i = 0; !found && i < num_targets; i++)
{
char* data;
Atom actualType;
int actualFormat;
unsigned long itemCount, bytesAfter;
char* data = NULL;
Atom actualType = None;
int actualFormat = 0;
unsigned long itemCount = 0, bytesAfter = 0;
monotonic_t start = glfwGetTime();
XEvent notification, dummy;
@@ -925,8 +953,7 @@ static const char* getSelectionString(Atom selection)
&notification))
{
monotonic_t time = glfwGetTime();
if (time - start > s_to_monotonic_t(2ll))
return "";
if (time - start > s_to_monotonic_t(2ll)) return;
waitForX11Event(s_to_monotonic_t(2ll) - (time - start));
}
@@ -953,9 +980,6 @@ static const char* getSelectionString(Atom selection)
if (actualType == _glfw.x11.INCR)
{
size_t size = 1;
char* string = NULL;
for (;;)
{
start = glfwGetTime();
@@ -966,13 +990,12 @@ static const char* getSelectionString(Atom selection)
{
monotonic_t time = glfwGetTime();
if (time - start > s_to_monotonic_t(2ll)) {
free(string);
return "";
return;
}
waitForX11Event(s_to_monotonic_t(2ll) - (time - start));
}
XFree(data);
XFREE(data);
XGetWindowProperty(_glfw.x11.display,
notification.xselection.requestor,
notification.xselection.property,
@@ -988,47 +1011,41 @@ static const char* getSelectionString(Atom selection)
if (itemCount)
{
size += itemCount;
string = realloc(string, size);
string[size - itemCount - 1] = '\0';
strcat(string, data);
}
if (!itemCount)
{
if (targets[i] == XA_STRING)
{
*selectionString = convertLatin1toUTF8(string);
free(string);
const char *string = data;
if (targets[i] == XA_STRING) {
string = convertLatin1toUTF8(data);
itemCount = strlen(string);
}
else
*selectionString = string;
bool ok = write_data(object, string, itemCount);
if (string != data) free((void*)string);
if (!ok) { XFREE(data); break; }
} else { found = true; break; }
break;
}
}
}
else if (actualType == targets[i])
{
if (targets[i] == XA_STRING)
*selectionString = convertLatin1toUTF8(data);
else
*selectionString = _glfw_strdup(data);
if (targets[i] == XA_STRING) {
const char *string = convertLatin1toUTF8(data);
write_data(object, string, strlen(string)); free((void*)string);
} else write_data(object, data, itemCount);
found = true;
}
else if (actualType == XA_ATOM && targets[i] == _glfw.x11.TARGETS) {
found = true;
write_data(object, data, itemCount);
}
XFree(data);
XFREE(data);
if (*selectionString)
break;
}
if (!*selectionString)
if (!found && report_not_found)
{
_glfwInputError(GLFW_FORMAT_UNAVAILABLE,
"X11: Failed to convert selection to string");
"X11: Failed to convert selection to data from clipboard");
}
return *selectionString;
#undef XFREE
}
// Make the specified window and its video mode active on its monitor
@@ -1098,21 +1115,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,51 +2851,106 @@ 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++;
*a = atom_for_mime(cd->mime_types[i]);
if (strcmp(cd->mime_types[i], "text/plain") == 0) {
a = aa->array + aa->sz++;
a->atom = _glfw.x11.UTF8_STRING;
a->mime = "text/plain";
}
}
}
const char* _glfwPlatformGetClipboardString(void)
{
return getSelectionString(_glfw.x11.CLIPBOARD);
typedef struct chunked_writer {
char *buf; size_t sz, cap;
} chunked_writer;
static bool
write_chunk(void *object, const char *data, size_t sz) {
chunked_writer *cw = object;
if (cw->cap < cw->sz + sz) {
cw->cap = MAX(cw->cap * 2, cw->sz + 8*sz);
cw->buf = realloc(cw->buf, cw->cap);
}
memcpy(cw->buf + cw->sz, data, sz);
cw->sz += sz;
return true;
}
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");
static void
get_available_mime_types(Atom which_clipboard, GLFWclipboardwritedatafun write_data, void *object) {
chunked_writer cw = {0};
getSelectionString(which_clipboard, &_glfw.x11.TARGETS, 1, write_chunk, &cw, false);
size_t count = 0;
bool ok = true;
if (cw.buf) {
Atom *atoms = (Atom*)cw.buf;
count = cw.sz / sizeof(Atom);
char **names = calloc(count, sizeof(char*));
get_atom_names(atoms, count, names);
for (size_t i = 0; i < count; i++) {
if (strchr(names[i], '/')) {
if (ok) ok = write_data(object, names[i], strlen(names[i]));
} else {
if (atoms[i] == _glfw.x11.UTF8_STRING || atoms[i] == XA_STRING) {
if (ok) ok = write_data(object, "text/plain", strlen("text/plain"));
}
}
XFree(names[i]);
}
free(cw.buf);
free(names);
}
}
const char* _glfwPlatformGetPrimarySelectionString(void)
{
return getSelectionString(_glfw.x11.PRIMARY);
void
_glfwPlatformGetClipboard(GLFWClipboardType clipboard_type, const char* mime_type, GLFWclipboardwritedatafun write_data, void *object) {
Atom atoms[4], which = clipboard_type == GLFW_PRIMARY_SELECTION ? _glfw.x11.PRIMARY : _glfw.x11.CLIPBOARD;
if (mime_type == NULL) {
get_available_mime_types(which, write_data, object);
return;
}
size_t count = 0;
atoms[count++] = atom_for_mime(mime_type).atom;
if (strcmp(mime_type, "text/plain") == 0) {
atoms[count++] = _glfw.x11.UTF8_STRING;
atoms[count++] = XA_STRING;
}
getSelectionString(which, atoms, count, write_data, object, true);
}
EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs)

View File

@@ -158,14 +158,17 @@ def extra_for(width: int, screen_width: int) -> int:
class Password(Handler):
def __init__(self, cli_opts: AskCLIOptions, prompt: str) -> None:
def __init__(self, cli_opts: AskCLIOptions, prompt: str, is_password: bool = True, initial_text: str = '') -> None:
self.cli_opts = cli_opts
self.prompt = prompt
self.initial_text = initial_text
from kittens.tui.line_edit import LineEdit
self.line_edit = LineEdit(is_password=True)
self.line_edit = LineEdit(is_password=is_password)
def initialize(self) -> None:
self.cmd.set_cursor_shape('beam')
if self.initial_text:
self.line_edit.on_text(self.initial_text, True)
self.draw_screen()
@Handler.atomic_update
@@ -205,7 +208,7 @@ class Password(Handler):
return ''
class Choose(Handler):
class Choose(Handler): # {{{
mouse_tracking = MouseTracking.buttons_only
def __init__(self, cli_opts: AskCLIOptions) -> None:
@@ -441,6 +444,7 @@ class Choose(Handler):
def on_interrupt(self) -> None:
self.quit_loop(1)
on_eot = on_interrupt
# }}}
def main(args: List[str]) -> Response:

View File

@@ -2,7 +2,6 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import re
from contextlib import suppress
from fnmatch import fnmatch
from functools import lru_cache
@@ -12,6 +11,7 @@ from typing import (
)
from kitty.guess_mime_type import guess_type
from kitty.utils import control_codes_pat
if TYPE_CHECKING:
from .highlight import DiffHighlight # noqa
@@ -161,12 +161,9 @@ def collect_files(collection: Collection, left: str, right: str) -> None:
collection.add_add(right_path_map[name])
sanitize_pat = re.compile('[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]')
def sanitize(text: str) -> str:
ntext = text.replace('\r\n', '\n')
return sanitize_pat.sub('', ntext)
return control_codes_pat().sub('', ntext)
@lru_cache(maxsize=1024)

View File

@@ -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
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
@@ -411,7 +412,10 @@ def convert_text(text: str, cols: int) -> str:
appended = False
for line in full_line.split('\r'):
if line:
lines.append(line.ljust(cols, '\0'))
line_sz = wcswidth(line)
if line_sz < cols:
line += '\0' * (cols - line_sz)
lines.append(line)
lines.append('\r')
appended = True
if appended:
@@ -536,12 +540,27 @@ OPTIONS = r'''
--program
type=list
What program to use to open matched text. Defaults to the default open program
for the operating system. Use a value of :code:`-` to paste the match into the
terminal window instead. A value of :code:`@` will copy the match to the
clipboard. A value of :code:`*` will copy the match to the primary selection
(on systems that support primary selections). A value of :code:`default` will
run the default open program. Can be specified multiple times to run multiple
programs.
for the operating system. Various special values are supported:
:code:`-`
paste the match into the terminal window.
:code:`@`
copy the match to the clipboard
:code:`*`
copy the match to the primary selection (on systems that support primary selections)
:code:`default`
run the default open program.
:code:`launch`
run :doc:`/launch` to open the program in a new kitty tab, window, overlay, etc.
For example::
--program "launch --type=tab vim"
Can be specified multiple times to run multiple programs.
--type
@@ -803,6 +822,7 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
elif program == '*':
set_primary_selection(joined_text())
else:
from kitty.conf.utils import to_cmdline
cwd = data['cwd']
program = get_options().open_url_with if program == 'default' else program
if text_type == 'hyperlink':
@@ -811,12 +831,20 @@ def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int,
if w is not None:
w.open_url(m, hyperlink_id=1, cwd=cwd)
else:
launch_args = []
if isinstance(program, str) and program.startswith('launch '):
launch_args = to_cmdline(program)
launch_args.insert(1, '--cwd=' + cwd)
for m, groupdict in zip(matches, groupdicts):
if groupdict:
m = []
for k, v in groupdict.items():
m.append('{}={}'.format(k, v or ''))
boss.open_url(m, program, cwd=cwd)
if launch_args:
w = boss.window_id_map.get(target_window_id)
boss.call_remote_control(active_window=w, args=tuple(launch_args + ([m] if isinstance(m, str) else m)))
else:
boss.open_url(m, program, cwd=cwd)
if __name__ == '__main__':

64
kittens/hyperlinked_grep/main.py Normal file → Executable file
View File

@@ -20,7 +20,49 @@ def write_hyperlink(write: Callable[[bytes], None], url: bytes, line: bytes, fra
def main() -> None:
if not sys.stdout.isatty() and '--pretty' not in sys.argv and '-p' not in sys.argv:
i = 1
all_link_options = {'matching_lines', 'context_lines', 'file_headers'}
link_options = set()
delegate_to_rg = False
def parse_link_options(raw: str) -> None:
nonlocal delegate_to_rg
if not raw:
raise SystemExit('Must specify an argument for --kitten option')
p, _, s = raw.partition('=')
if p != 'hyperlink':
raise SystemExit(f'Unknown argument for --kitten: {raw}')
for option in s.split(','):
if option == 'all':
link_options.update(all_link_options)
delegate_to_rg = False
elif option == 'none':
delegate_to_rg = True
link_options.clear()
elif option not in all_link_options:
a = ', '.join(sorted(all_link_options))
raise SystemExit(f"hyperlink option must be one of all, none, {a}, not '{option}'")
else:
link_options.add(option)
delegate_to_rg = False
while i < len(sys.argv):
if sys.argv[i] == '--kitten':
next_item = '' if i + 1 >= len(sys.argv) else sys.argv[i + 1]
parse_link_options(next_item)
del sys.argv[i:i+2]
elif sys.argv[i].startswith('--kitten='):
parse_link_options(sys.argv[i][len('--kitten='):])
del sys.argv[i]
else:
i += 1
if not link_options: # Default to linking everything if no options given
link_options.update(all_link_options)
link_file_headers = 'file_headers' in link_options
link_context_lines = 'context_lines' in link_options
link_matching_lines = 'matching_lines' in link_options
if delegate_to_rg or (not sys.stdout.isatty() and '--pretty' not in sys.argv and '-p' not in sys.argv):
os.execlp('rg', 'rg', *sys.argv[1:])
cmdline = ['rg', '--pretty', '--with-filename'] + sys.argv[1:]
try:
@@ -31,7 +73,7 @@ def main() -> None:
write: Callable[[bytes], None] = cast(Callable[[bytes], None], sys.stdout.buffer.write)
sgr_pat = re.compile(br'\x1b\[.*?m')
osc_pat = re.compile(b'\x1b\\].*?\x1b\\\\')
num_pat = re.compile(br'^(\d+)[:-]')
num_pat = re.compile(br'^(\d+)([:-])')
in_result: bytes = b''
hostname = socket.gethostname().encode('utf-8')
@@ -43,20 +85,22 @@ def main() -> None:
if not clean_line:
in_result = b''
write(b'\n')
continue
if in_result:
elif in_result:
m = num_pat.match(clean_line)
if m is not None:
write_hyperlink(write, in_result, line, frag=m.group(1))
else:
write(line)
is_match_line = m.group(2) == b':'
if (is_match_line and link_matching_lines) or (not is_match_line and link_context_lines):
write_hyperlink(write, in_result, line, frag=m.group(1))
continue
write(line)
else:
if line.strip():
path = quote_from_bytes(os.path.abspath(clean_line)).encode('utf-8')
in_result = b'file://' + hostname + path
write_hyperlink(write, in_result, line)
else:
write(line)
if link_file_headers:
write_hyperlink(write, in_result, line)
continue
write(line)
except KeyboardInterrupt:
p.send_signal(signal.SIGINT)
except (EOFError, BrokenPipeError):

View File

@@ -273,9 +273,9 @@ def save_as(conn_data: SSHConnectionData, remote_path: str, cli_opts: RemoteFile
)
print('Relative paths will be resolved from:', styled(os.getcwd(), fg_intense=True, bold=True))
print()
from ..tui.path_completer import PathCompleter
from ..tui.path_completer import get_path
try:
dest = PathCompleter().input()
dest = get_path()
except (KeyboardInterrupt, EOFError):
return
if dest:

View File

@@ -172,7 +172,8 @@ def make_tarfile(ssh_opts: SSHOptions, base_env: Dict[str, str], compression: st
if ssh_opts.remote_kitty != 'no':
env['KITTY_REMOTE'] = ssh_opts.remote_kitty
if os.environ.get('KITTY_PUBLIC_KEY'):
env['KITTY_PUBLIC_KEY'] = os.environ['KITTY_PUBLIC_KEY']
env.pop('KITTY_PUBLIC_KEY', None)
literal_env['KITTY_PUBLIC_KEY'] = os.environ['KITTY_PUBLIC_KEY']
env_script = serialize_env(literal_env, env, base_env, for_python=compression != 'gz')
buf = io.BytesIO()
with tarfile.open(mode=f'w:{compression}', fileobj=buf, encoding='utf-8') as tf:

View File

@@ -121,11 +121,22 @@ class Handler:
self._tui_loop.quit(1)
def on_key_event(self, key_event: KeyEventType, in_bracketed_paste: bool = False) -> None:
' Override this method and perform_default_key_action() to handle all key events '
if key_event.text:
self.on_text(key_event.text, in_bracketed_paste)
else:
self.on_key(key_event)
def perform_default_key_action(self, key_event: KeyEventType) -> bool:
' Override in sub-class if you want to handle these key events yourself '
if key_event.matches('ctrl+c'):
self.on_interrupt()
return True
if key_event.matches('ctrl+d'):
self.on_eot()
return True
return False
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
pass

View File

@@ -330,13 +330,8 @@ class Loop:
except Exception:
pass
else:
if k.matches('ctrl+c'):
self.handler.on_interrupt()
return
if k.matches('ctrl+d'):
self.handler.on_eot()
return
self.handler.on_key_event(k)
if not self.handler.perform_default_key_action(k):
self.handler.on_key_event(k)
def _on_pm(self, pm: str) -> None:
pass

View File

@@ -147,5 +147,9 @@ class PathCompleter:
return ''
def get_path(prompt: str = '> ') -> str:
return PathCompleter(prompt).input()
def develop() -> None:
PathCompleter().input()

View File

@@ -20,6 +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_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 (
@@ -29,19 +33,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,
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
@@ -60,10 +64,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 +242,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")}'
@@ -294,14 +299,22 @@ class Boss:
def startup_first_child(self, os_window_id: Optional[int], startup_sessions: Iterable[Session] = ()) -> None:
si = startup_sessions or create_sessions(get_options(), self.args, default_session=get_options().startup_session)
focused_os_window = wid = 0
token = os.environ.pop('XDG_ACTIVATION_TOKEN', '')
for startup_session in si:
self.add_os_window(startup_session, os_window_id=os_window_id)
wid = self.add_os_window(startup_session, os_window_id=os_window_id)
if startup_session.focus_os_window:
focused_os_window = wid
os_window_id = None
if self.args.start_as != 'normal':
if self.args.start_as == 'fullscreen':
self.toggle_fullscreen()
else:
change_os_window_state(self.args.start_as)
if focused_os_window > 0:
focus_os_window(focused_os_window, True, token)
elif token and is_wayland() and wid:
focus_os_window(wid, True, token)
def add_os_window(
self,
@@ -623,6 +636,7 @@ class Boss:
if isinstance(data, dict) and data.get('cmd') == 'new_instance':
from .cli_stub import CLIOptions
startup_id = data.get('startup_id')
activation_token = data.get('activation_token', '')
args, rest = parse_args(data['args'][1:], result_class=CLIOptions)
cmdline_args_for_open = data.get('cmdline_args_for_open')
if cmdline_args_for_open:
@@ -635,14 +649,21 @@ class Boss:
args.session = PreReadSession(data['stdin'])
if not os.path.isabs(args.directory):
args.directory = os.path.join(data['cwd'], args.directory)
focused_os_window = os_window_id = 0
for session in create_sessions(opts, args, respect_cwd=True):
os_window_id = self.add_os_window(
session, wclass=args.cls, wname=args.name, opts_for_size=opts, startup_id=startup_id,
override_title=args.title or None)
if session.focus_os_window:
focused_os_window = os_window_id
if opts.background_opacity != get_options().background_opacity:
self._set_os_window_background_opacity(os_window_id, opts.background_opacity)
if data.get('notify_on_os_window_death'):
self.os_window_death_actions[os_window_id] = partial(self.notify_on_os_window_death, data['notify_on_os_window_death'])
if focused_os_window > 0:
focus_os_window(focused_os_window, True, activation_token or '')
elif activation_token and is_wayland() and os_window_id:
focus_os_window(os_window_id, True, activation_token or '')
else:
log_error('Unknown message received from peer, ignoring')
return None
@@ -766,11 +787,19 @@ class Boss:
confirm_on_cancel: bool = False, # on closing window
confirm_on_accept: bool = True, # on pressing enter
) -> None:
result: bool = False
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
callback(res.get('response') == 'y', *args)
nonlocal result
result = res.get('response') == 'y'
def on_popup_overlay_removal(wid: int, boss: Boss) -> None:
callback(result, *args)
self.run_kitten_with_metadata(
'ask', ['--type=yesno', '--message', msg, '--default', 'y' if confirm_on_accept else 'n'],
window=window, custom_callback=callback_, default_data={'response': 'y' if confirm_on_cancel else 'n'})
window=window, custom_callback=callback_, action_on_removal=on_popup_overlay_removal,
default_data={'response': 'y' if confirm_on_cancel else 'n'})
def choose(
self, msg: str, # can contain newlines and ANSI formatting
@@ -782,8 +811,12 @@ class Boss:
hidden_text_placeholder: str = 'HIDDEN_TEXT_PLACEHOLDER', # placeholder text to insert in to message
unhide_key: str = 'u', # key to press to unhide hidden text
) -> Optional[Window]:
result: str = ''
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
callback(res.get('response') or '')
nonlocal result
result = res.get('response') or ''
if hidden_text:
msg = msg.replace(hidden_text, hidden_text_placeholder)
cmd = ['--type=choices', '--message', msg]
@@ -796,7 +829,14 @@ class Boss:
input_data = hidden_text
else:
input_data = None
ans = self.run_kitten_with_metadata('ask', cmd, window=window, custom_callback=callback_, input_data=input_data, default_data={'response': ''})
def on_popup_overlay_removal(wid: int, boss: Boss) -> None:
callback(result)
ans = self.run_kitten_with_metadata(
'ask', cmd, window=window, custom_callback=callback_, input_data=input_data, default_data={'response': ''},
action_on_removal=on_popup_overlay_removal
)
if isinstance(ans, Window):
return ans
return None
@@ -808,10 +848,19 @@ class Boss:
prompt: str = '> ',
is_password: bool = False
) -> None:
result: str = ''
def callback_(res: Dict[str, Any], x: int, boss: Boss) -> None:
callback(res.get('response') or '')
nonlocal result
result = res.get('response') or ''
def on_popup_overlay_removal(wid: int, boss: Boss) -> None:
callback(result)
cmd = ['--type', 'password' if is_password else 'line', '--message', msg, '--prompt', prompt]
self.run_kitten_with_metadata('ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''})
self.run_kitten_with_metadata(
'ask', cmd, window=window, custom_callback=callback_, default_data={'response': ''}, action_on_removal=on_popup_overlay_removal
)
def confirm_tab_close(self, tab: Tab) -> None:
x = get_options().confirm_os_window_close
@@ -1473,9 +1522,7 @@ class Boss:
@ac('misc', 'Edit the kitty.conf config file in your favorite text editor')
def edit_config_file(self, *a: Any) -> None:
confpath = prepare_config_file_for_editing()
# On macOS vim fails to handle SIGWINCH if it occurs early, so add a
# small delay.
cmd = [kitty_exe(), '+runpy', 'import os, sys, time; time.sleep(0.05); os.execvp(sys.argv[1], sys.argv[1:])'] + get_editor(get_options()) + [confpath]
cmd = [kitty_exe(), '+edit'] + get_editor(get_options()) + [confpath]
self.new_os_window(*cmd)
def run_kitten_with_metadata(
@@ -1579,10 +1626,28 @@ class Boss:
def input_unicode_character(self) -> None:
self.run_kitten_with_metadata('unicode_input')
@ac('tab', 'Change the title of the active tab')
def set_tab_title(self) -> None:
@ac(
'tab', '''
Change the title of the active tab interactively, by typing in the new title.
If you specify an argument to this action then that is used as the title instead of asking for it.
Use the empty string ("") to reset the title to default. For example::
# interactive usage
map f1 set_tab_title
# set a specific title
map f2 set_tab_title some title
# reset to default
map f3 set_tab_title ""
'''
)
def set_tab_title(self, title: Optional[str] = None) -> None:
tab = self.active_tab
if tab:
if title is not None:
if title in ('""', "''"):
title = ''
tab.set_title(title)
return
args = [
'--name=tab-title', '--message', _('Enter the new title for this tab below.'),
'--default', tab.name or tab.title, 'do_set_tab_title', str(tab.id)]
@@ -1684,7 +1749,19 @@ class Boss:
if actions:
self.drain_actions(actions)
if not found_action:
open_url(url, program or get_options().open_url_with, cwd=cwd)
extra_env = {}
if self.listening_on:
extra_env['KITTY_LISTEN_ON'] = self.listening_on
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:
@@ -1878,18 +1955,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)
@@ -2396,8 +2483,11 @@ class Boss:
@ac('debug', 'Show the environment variables that the kitty process sees')
def show_kitty_env_vars(self) -> None:
w = self.active_window
env = os.environ.copy()
if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not getattr(sys, 'kitty_run_data').get('lc_ctype_before_python'):
del env['LC_CTYPE']
if w:
output = '\n'.join(f'{k}={v}' for k, v in os.environ.items())
output = '\n'.join(f'{k}={v}' for k, v in env.items())
self.display_scrollback(w, output, title=_('Current kitty env vars'), report_cursor=False)
@ac('debug', '''

View File

@@ -768,7 +768,7 @@ render(monotonic_t now, bool input_read) {
if (w->render_state == RENDER_FRAME_NOT_REQUESTED || no_render_frame_received_recently(w, now, ms_to_monotonic_t(250ll))) request_frame_render(w);
// dont respect render frames soon after a resize on Wayland as they cause flicker because
// we want to fill the newly resized buffer ASAP, not at compositors convenience
if (global_state.is_wayland && (monotonic() - w->viewport_resized_at) > s_double_to_monotonic_t(1)) continue;
if (!global_state.is_wayland || (monotonic() - w->viewport_resized_at) > s_double_to_monotonic_t(1)) continue;
}
w->render_calls++;
make_os_window_context_current(w);
@@ -974,7 +974,7 @@ process_pending_resizes(monotonic_t now) {
static void
close_os_window(ChildMonitor *self, OSWindow *os_window) {
int w = os_window->window_width, h = os_window->window_height;
int w = os_window->content_area_width, h = os_window->content_area_height;
if (os_window->before_fullscreen.is_set && is_os_window_fullscreen(os_window)) {
w = os_window->before_fullscreen.w; h = os_window->before_fullscreen.h;
}

View File

@@ -100,11 +100,14 @@ spawn(PyObject *self UNUSED, PyObject *args) {
#if PY_VERSION_HEX >= 0x03070000
PyOS_AfterFork_Child();
#endif
// See _Py_RestoreSignals in signalmodule.c for a list of signals python nukes
const struct sigaction act = {.sa_handler=SIG_DFL};
#define SA(which) if (sigaction(which, &act, NULL) != 0) exit_on_err("sigaction() in child process failed");
for (int si = 0; si < num_handled_signals; si++) { SA(handled_signals[si]); }
// See _Py_RestoreSignals in signalmodule.c for a list of signals python nukes
#ifdef SIGPIPE
SA(SIGPIPE)
#endif
#ifdef SIGXFSZ
SA(SIGXFSZ);
#endif
@@ -181,8 +184,26 @@ spawn(PyObject *self UNUSED, PyObject *args) {
return PyLong_FromLong(pid);
}
#ifdef __APPLE__
#include <crt_externs.h>
#else
extern char **environ;
#endif
static PyObject*
clearenv_py(PyObject *self UNUSED, PyObject *args UNUSED) {
#ifdef __APPLE__
char **e = *_NSGetEnviron();
if (e) *e = NULL;
#else
if (environ) *environ = NULL;
#endif
Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = {
METHODB(spawn, METH_VARARGS),
{"clearenv", clearenv_py, METH_NOARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};

View File

@@ -148,6 +148,7 @@ def process_env() -> Dict[str, str]:
ssl_env_var = getattr(sys, 'kitty_ssl_env_var', None)
if ssl_env_var is not None:
ans.pop(ssl_env_var, None)
ans.pop('XDG_ACTIVATION_TOKEN', None)
return ans
@@ -168,6 +169,10 @@ def set_default_env(val: Optional[Dict[str, str]] = None) -> None:
setattr(default_env, 'lc_ctype_set_by_user', has_lctype)
def set_LANG_in_default_env(val: str) -> None:
default_env().setdefault('LANG', val)
def openpty() -> Tuple[int, int]:
master, slave = os.openpty() # Note that master and slave are in blocking mode
os.set_inheritable(slave, True)

View File

@@ -187,6 +187,7 @@ def hostname() -> str:
def ref_hyperlink(x: str, prefix: str = '') -> str:
t, q = text_and_target(x)
url = f'kitty+doc://{hostname()}/#ref={prefix}{q}'
t = re.sub(r':([a-z]+):`([^`]+)`', r'\2', t)
return hyperlink_for_url(url, t)
@@ -293,7 +294,7 @@ def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, Option
else:
prev_indent = 0
if prev_line:
current_cmd['help'] += '\n\n'
current_cmd['help'] += '\n' if current_cmd['help'].endswith('::') else '\n\n'
else:
state = NORMAL
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
@@ -429,6 +430,8 @@ class PrintHelpForSeq:
a('{}: {} {}{}'.format(title('Usage'), bold(yellow(appname)), optstring, usage))
a('')
message = message or default_msg
# replace rst literal code block syntax
message = message.replace('::\n\n', ':\n\n')
wa(prettify(message))
a('')
if seq:
@@ -448,6 +451,8 @@ class PrintHelpForSeq:
blocks[-1] += dt
if opt.get('help'):
t = help_text.replace('%default', str(defval)).strip()
# replace rst literal code block syntax
t = t.replace('::\n\n', ':\n\n')
t = t.replace('#placeholder_for_formatting#', '')
wa(prettify(t), indent=4)
if opt.get('choices'):
@@ -801,6 +806,7 @@ Output commands received from child process to STDOUT.
Replay previously dumped commands. Specify the path to a dump file previously
created by :option:`{appname} --dump-commands`. You
can open a new kitty window to replay the commands with::
{appname} sh -c "{appname} --replay-commands /path/to/dump/file; read"

121
kitty/clipboard.py Normal file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
import io
from typing import IO, Callable, Dict, List, Tuple, Union
from .conf.utils import uniq
from .constants import supports_primary_selection
from .fast_data_types import (
GLFW_CLIPBOARD, GLFW_PRIMARY_SELECTION, get_boss, get_clipboard_mime,
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 isinstance(x, str):
x = x.encode('utf-8')
self.set_mime({'text/plain': x})
def set_mime(self, data: Dict[str, DataType]) -> None:
if self.enabled and isinstance(data, dict):
self.data = data
set_clipboard_data_types(self.clipboard_type, tuple(self.data))
def get_text(self) -> str:
parts: List[bytes] = []
self.get_mime("text/plain", parts.append)
return b''.join(parts).decode('utf-8', 'replace')
def get_mime(self, mime: str, output: Callable[[bytes], None]) -> None:
if self.enabled:
try:
get_clipboard_mime(self.clipboard_type, mime, output)
except RuntimeError as err:
if str(err) != 'is_self_offer':
raise
data = self.data.get(mime, b'')
if isinstance(data, bytes):
output(data)
else:
data.seek(0, 0)
q = b' '
while q:
q = data.read(io.DEFAULT_BUFFER_SIZE)
output(q)
def get_mime_data(self, mime: str) -> bytes:
ans: List[bytes] = []
self.get_mime(mime, ans.append)
return b''.join(ans)
def get_available_mime_types_for_paste(self) -> Tuple[str, ...]:
if self.enabled:
parts: List[bytes] = []
try:
get_clipboard_mime(self.clipboard_type, None, parts.append)
except RuntimeError as err:
if str(err) != 'is_self_offer':
raise
return tuple(self.data)
return tuple(x.decode('utf-8', 'replace') for x in uniq(parts))
return ()
def __call__(self, mime: str) -> Callable[[], bytes]:
data = self.data.get(mime, b'')
if isinstance(data, str): # type: ignore
data = data.encode('utf-8') # type: ignore
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()
def develop() -> Tuple[Clipboard, Clipboard]:
from .constants import is_macos, detect_if_wayland_ok
from .fast_data_types import set_boss
from .main import init_glfw_module
glfw_module = 'cocoa' if is_macos else ('wayland' if detect_if_wayland_ok() else 'x11')
class Boss:
clipboard = Clipboard()
primary_selection = Clipboard(GLFW_PRIMARY_SELECTION)
init_glfw_module(glfw_module)
set_boss(Boss()) # type: ignore
return Boss.clipboard, Boss.primary_selection

View File

@@ -22,7 +22,7 @@ class Version(NamedTuple):
appname: str = 'kitty'
kitty_face = '🐱'
version: Version = Version(0, 26, 0)
version: Version = Version(0, 26, 3)
str_version: str = '.'.join(map(str, version))
_plat = sys.platform.lower()
is_macos: bool = 'darwin' in _plat

View File

@@ -81,6 +81,25 @@ def launch(args: List[str]) -> None:
runpy.run_path(exe, run_name='__main__')
def edit(args: List[str]) -> None:
import shutil
from .constants import is_macos
if is_macos:
# On macOS vim fails to handle SIGWINCH if it occurs early, so add a small delay.
import time
time.sleep(0.05)
exe = args[1]
if not os.path.isabs(exe):
exe = shutil.which(exe) or ''
if not exe or not os.access(exe, os.X_OK):
print('Cannot find an editor on your system. Set the \x1b[33meditor\x1b[39m value in kitty.conf'
' to the absolute path of your editor of choice.', file=sys.stderr)
from kitty.utils import hold_till_enter
hold_till_enter()
raise SystemExit(1)
os.execv(exe, args[1:])
def shebang(args: List[str]) -> None:
script_path = args[1]
cmd = args[2:]
@@ -152,6 +171,7 @@ namespaced_entry_points['open'] = open_urls
namespaced_entry_points['kitten'] = run_kitten
namespaced_entry_points['edit-config'] = edit_config_file
namespaced_entry_points['shebang'] = shebang
namespaced_entry_points['edit'] = edit
def setup_openssl_environment(ext_dir: str) -> None:

View File

@@ -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,11 +754,7 @@ 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:
def focus_os_window(os_window_id: int, also_raise: bool = True, activation_token: Optional[str] = None) -> bool:
pass
@@ -1485,3 +1471,7 @@ 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: ...
def get_clipboard_mime(ct: int, mime: Optional[str], callback: Callable[[bytes], None]) -> None: ...
def run_with_activation_token(func: Callable[[str], None]) -> None: ...

14
kitty/glfw-wrapper.c generated
View File

@@ -365,11 +365,11 @@ 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 **) (&glfwSetClipboardDataTypes_impl) = dlsym(handle, "glfwSetClipboardDataTypes");
if (glfwSetClipboardDataTypes_impl == NULL) fail("Failed to load glfw function glfwSetClipboardDataTypes 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 **) (&glfwGetClipboard_impl) = dlsym(handle, "glfwGetClipboard");
if (glfwGetClipboard_impl == NULL) fail("Failed to load glfw function glfwGetClipboard 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());
@@ -449,6 +449,12 @@ load_glfw(const char* path) {
*(void **) (&glfwRequestWaylandFrameEvent_impl) = dlsym(handle, "glfwRequestWaylandFrameEvent");
if (glfwRequestWaylandFrameEvent_impl == NULL) dlerror(); // clear error indicator
*(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

33
kitty/glfw-wrapper.h generated
View File

@@ -1454,8 +1454,21 @@ typedef void (* GLFWjoystickfun)(int,int);
typedef void (* GLFWuserdatafun)(unsigned long long, void*);
typedef void (* GLFWtickcallback)(void*);
typedef void (* GLFWactivationcallback)(GLFWwindow *window, const char *token, void *data);
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);
typedef bool (* GLFWclipboardwritedatafun)(void *object, const char *data, size_t sz);
/*! @brief Video mode type.
*
@@ -2081,13 +2094,13 @@ 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 void (*glfwSetClipboardDataTypes_func)(GLFWClipboardType, const char* const*, size_t, GLFWclipboarditerfun);
GFW_EXTERN glfwSetClipboardDataTypes_func glfwSetClipboardDataTypes_impl;
#define glfwSetClipboardDataTypes glfwSetClipboardDataTypes_impl
typedef const char* (*glfwGetClipboardString_func)(GLFWwindow*);
GFW_EXTERN glfwGetClipboardString_func glfwGetClipboardString_impl;
#define glfwGetClipboardString glfwGetClipboardString_impl
typedef void (*glfwGetClipboard_func)(GLFWClipboardType, const char*, GLFWclipboardwritedatafun, void*);
GFW_EXTERN glfwGetClipboard_func glfwGetClipboard_impl;
#define glfwGetClipboard glfwGetClipboard_impl
typedef monotonic_t (*glfwGetTime_func)(void);
GFW_EXTERN glfwGetTime_func glfwGetTime_impl;
@@ -2193,6 +2206,14 @@ typedef void (*glfwRequestWaylandFrameEvent_func)(GLFWwindow*, unsigned long lon
GFW_EXTERN glfwRequestWaylandFrameEvent_func glfwRequestWaylandFrameEvent_impl;
#define glfwRequestWaylandFrameEvent glfwRequestWaylandFrameEvent_impl
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

View File

@@ -97,6 +97,22 @@ min_size_for_os_window(OSWindow *window, int *min_width, int *min_height) {
}
static void
adjust_window_size_for_csd(OSWindow *w, int width, int height, int *adjusted_width, int *adjusted_height) {
*adjusted_width = width; *adjusted_height = height;
if (global_state.is_wayland) {
int left = -1, top, right, bottom;
glfwGetWindowFrameSize(w->handle, &left, &top, &right, &bottom);
if (left > -1) {
*adjusted_width -= left + right;
*adjusted_height -= top + bottom;
*adjusted_width = MAX(0, *adjusted_width);
*adjusted_height = MAX(0, *adjusted_height);
}
}
}
void
update_os_window_viewport(OSWindow *window, bool notify_boss) {
int w, h, fw, fh;
@@ -135,6 +151,7 @@ update_os_window_viewport(OSWindow *window, bool notify_boss) {
window->viewport_height = MAX(window->viewport_height, min_height);
window->window_width = MAX(w, min_width);
window->window_height = MAX(h, min_height);
adjust_window_size_for_csd(window, window->window_width, window->window_height, &window->content_area_width, &window->content_area_height);
if (notify_boss) {
call_boss(on_window_resize, "KiiO", window->id, window->viewport_width, window->viewport_height, dpi_changed ? Py_True : Py_False);
}
@@ -295,6 +312,14 @@ dpi_change_callback(GLFWwindow *w, float x_scale UNUSED, float y_scale UNUSED) {
window->live_resize.last_resize_event_at = monotonic();
global_state.callback_os_window = NULL;
request_tick_callback();
if (global_state.is_wayland) {
// because of the stupidity of Wayland design, the GLFW wayland backend
// will need to swap buffers immediately after a scale change to ensure
// the buffer size is a multiple of the scale. So blank the new buffer to
// ensure we dont leak any unintialized pixels to the screen. The OpenGL viewport
// will already have been resized to its new size in framebuffer_size_callback
blank_os_window(window);
}
}
static void
@@ -616,6 +641,7 @@ set_os_window_size(OSWindow *os_window, int x, int y) {
static void
get_window_content_scale(GLFWwindow *w, float *xscale, float *yscale, double *xdpi, double *ydpi) {
// if you change this function also change createSurface() in wl_window.c
*xscale = 1; *yscale = 1;
if (w) glfwGetWindowContentScale(w, xscale, yscale);
else {
@@ -652,13 +678,14 @@ set_os_window_dpi(OSWindow *w) {
static bool
do_toggle_fullscreen(OSWindow *w, unsigned int flags, bool restore_sizes) {
int width, height, x, y;
int width, height, x, y, content_area_width, content_area_height;
glfwGetWindowSize(w->handle, &width, &height);
glfwGetWindowPos(w->handle, &x, &y);
adjust_window_size_for_csd(w, width, height, &content_area_width, &content_area_height);
bool was_maximized = glfwGetWindowAttrib(w->handle, GLFW_MAXIMIZED);
if (glfwToggleFullscreen(w->handle, flags)) {
w->before_fullscreen.is_set = true;
w->before_fullscreen.w = width; w->before_fullscreen.h = height; w->before_fullscreen.x = x; w->before_fullscreen.y = y;
w->before_fullscreen.w = content_area_width; w->before_fullscreen.h = content_area_height; w->before_fullscreen.x = x; w->before_fullscreen.y = y;
w->before_fullscreen.was_maximized = was_maximized;
return true;
}
@@ -815,6 +842,10 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
if (!global_state.is_wayland) {
// On Wayland windows dont get a content scale until they receive an enterEvent anyway
// which won't happen until the event loop ticks, so using a temp window is useless.
// The glfw backend assigns new windows a scale matching the primary monitor or 1 if
// no monitor has yet been reported. This is mimiced by get_window_content_scale() when
// using a null window, so we will match. Lets hope neither of these two change their algorithms
// in the future.
temp_window = glfwCreateWindow(640, 480, "temp", NULL, common_context);
if (temp_window == NULL) { fatal("Failed to create GLFW temp window! This usually happens because of old/broken OpenGL drivers. kitty requires working OpenGL 3.3 drivers."); }
}
@@ -838,7 +869,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) {
// changing, in case the background color is not black
blank_canvas(is_semi_transparent ? OPT(background_opacity) : 1.0f, OPT(background));
#ifndef __APPLE__
if (is_first_window) glfwSwapInterval(OPT(sync_to_monitor) && !global_state.is_wayland ? 1 : 0);
glfwSwapInterval(OPT(sync_to_monitor) && !global_state.is_wayland ? 1 : 0);
#endif
// On Wayland the initial swap is allowed only after the first XDG configure event
if (glfwAreSwapsAllowed(glfw_window)) glfwSwapBuffers(glfw_window);
@@ -1006,13 +1037,17 @@ destroy_os_window(OSWindow *w) {
}
void
focus_os_window(OSWindow *w, bool also_raise) {
focus_os_window(OSWindow *w, bool also_raise, const char *activation_token) {
if (w->handle) {
#ifdef __APPLE__
if (!also_raise) cocoa_focus_window(glfwGetCocoaWindow(w->handle));
else glfwFocusWindow(w->handle);
(void)activation_token;
#else
(void)also_raise;
if (global_state.is_wayland && activation_token && activation_token[0] && also_raise) {
glfwWaylandActivateWindow(w->handle, activation_token);
return;
}
glfwFocusWindow(w->handle);
#endif
}
@@ -1245,13 +1280,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;
@@ -1280,16 +1308,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++) {
@@ -1299,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;
@@ -1372,10 +1410,10 @@ wakeup_main_loop() {
bool
should_os_window_be_rendered(OSWindow* w) {
return (
glfwGetWindowAttrib(w->handle, GLFW_ICONIFIED) ||
!glfwGetWindowAttrib(w->handle, GLFW_VISIBLE) ||
glfwGetWindowAttrib(w->handle, GLFW_OCCLUDED) ||
!glfwAreSwapsAllowed(w->handle)
glfwGetWindowAttrib(w->handle, GLFW_ICONIFIED)
|| !glfwGetWindowAttrib(w->handle, GLFW_VISIBLE)
|| glfwGetWindowAttrib(w->handle, GLFW_OCCLUDED)
|| !glfwAreSwapsAllowed(w->handle)
) ? false : true;
}
@@ -1423,28 +1461,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;
@@ -1600,26 +1616,94 @@ 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) { return ans; }
PyObject *i = PyObject_CallFunction(c, "s", mime_type);
Py_DECREF(c);
if (!i) { 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;
if (glfwSetClipboardDataTypes) {
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);
} else log_error("GLFW not initialized cannot set clipboard data");
if (PyErr_Occurred()) return NULL;
Py_RETURN_NONE;
}
static bool
write_clipboard_data(void *callback, const char *data, size_t sz) {
Py_ssize_t z = sz;
if (data == NULL) {
PyErr_SetString(PyExc_RuntimeError, "is_self_offer");
return false;
}
PyObject *ret = PyObject_CallFunction(callback, "y#", data, z);
bool ok = false;
if (ret != NULL) { ok = true; Py_DECREF(ret); }
return ok;
}
static PyObject*
get_clipboard_mime(PyObject *self UNUSED, PyObject *args) {
int ctype;
const char *mime;
PyObject *callback;
if (!PyArg_ParseTuple(args, "izO", &ctype, &mime, &callback)) return NULL;
glfwGetClipboard(ctype, mime, write_clipboard_data, callback);
if (PyErr_Occurred()) return NULL;
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(get_clipboard_mime, 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),
@@ -1654,6 +1738,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);

View File

@@ -284,9 +284,9 @@ static PyMethodDef module_methods[] = {
};
// SingleKey {{{
typedef uint32_t keybitfield;
#define KEY_BITS 21
#define MOD_BITS 10
typedef uint64_t keybitfield;
#define KEY_BITS 51
#define MOD_BITS 12
#if 1 << (MOD_BITS-1) < GLFW_MOD_KITTY
#error "Not enough mod bits"
#endif
@@ -309,19 +309,19 @@ typedef struct {
} SingleKey;
static inline void
SingleKey_set_vals(SingleKey *self, long key, unsigned short mods, int is_native) {
if (key >= 0 && (unsigned long)key <= BIT_MASK(keybitfield, KEY_BITS)) {
keybitfield k = (keybitfield)(unsigned long)key;
SingleKey_set_vals(SingleKey *self, long long key, unsigned short mods, int is_native) {
if (key >= 0 && (unsigned long long)key <= BIT_MASK(keybitfield, KEY_BITS)) {
keybitfield k = (keybitfield)(unsigned long long)key;
self->key.key = k & BIT_MASK(keybitfield, KEY_BITS);
}
if (!(mods & 1 << (MOD_BITS + 1))) self->key.mods = mods & BIT_MASK(u_int32_t, MOD_BITS);
if (!(mods & 1 << (MOD_BITS + 1))) self->key.mods = mods & BIT_MASK(uint32_t, MOD_BITS);
if (is_native > -1) self->key.is_native = is_native ? 1 : 0;
}
static PyObject *
SingleKey_new(PyTypeObject *type, PyObject *args, PyObject *kw) {
long key = -1; unsigned short mods = 1 << (MOD_BITS + 1); int is_native = -1;
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Hpl", SingleKey_kwds, &mods, &is_native, &key)) return NULL;
long long key = -1; unsigned short mods = 1 << (MOD_BITS + 1); int is_native = -1;
if (!PyArg_ParseTupleAndKeywords(args, kw, "|HpL", SingleKey_kwds, &mods, &is_native, &key)) return NULL;
SingleKey *self = (SingleKey *)type->tp_alloc(type, 0);
if (self) SingleKey_set_vals(self, key, mods, is_native);
return (PyObject*)self;
@@ -341,8 +341,8 @@ SingleKey_repr(PyObject *s) {
unsigned int mods = self->key.mods;
if (mods) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "mods=%u, ", mods);
if (self->key.is_native) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "is_native=True, ");
unsigned long key = self->key.key;
if (key) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "key=%lu, ", key);
unsigned long long key = self->key.key;
if (key) pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, "key=%llu, ", key);
if (buf[pos-1] == ' ') pos -= 2;
pos += PyOS_snprintf(buf + pos, sizeof(buf) - pos, ")");
return PyUnicode_FromString(buf);
@@ -350,8 +350,8 @@ SingleKey_repr(PyObject *s) {
static PyObject*
SingleKey_get_key(SingleKey *self, void UNUSED *closure) {
const unsigned long val = self->key.key;
return PyLong_FromUnsignedLong(val);
const unsigned long long val = self->key.key;
return PyLong_FromUnsignedLongLong(val);
}
static PyObject*
@@ -436,8 +436,8 @@ SingleKey_resolve_kitty_mod(SingleKey *self, PyObject *km) {
static PyObject*
SingleKey_replace(SingleKey *self, PyObject *args, PyObject *kw) {
long key = -2; unsigned short mods = 1 << (MOD_BITS + 1); int is_native = -1;
if (!PyArg_ParseTupleAndKeywords(args, kw, "|Hpl", SingleKey_kwds, &mods, &is_native, &key)) return NULL;
long long key = -2; unsigned short mods = 1 << (MOD_BITS + 1); int is_native = -1;
if (!PyArg_ParseTupleAndKeywords(args, kw, "|HpL", SingleKey_kwds, &mods, &is_native, &key)) return NULL;
SingleKey *ans = (SingleKey*)SingleKey_Type.tp_alloc(&SingleKey_Type, 0);
if (ans) {
if (key == -1) key = 0;

View File

@@ -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 run_once
from .utils import (
get_editor, log_error, resolve_custom_file, set_primary_selection, which
)
from .types import OverlayType, run_once
from .utils import get_editor, log_error, resolve_custom_file, which
from .window import CwdRequest, CwdRequestType, Watchers, Window
try:
@@ -56,7 +54,7 @@ of the active window in the tab is used as the tab title. The special value
--type
type=choices
default=window
choices=window,tab,os-window,overlay,background,clipboard,primary
choices=window,tab,os-window,overlay,overlay-main,background,clipboard,primary
Where to launch the child process:
:code:`window`
@@ -71,6 +69,13 @@ Where to launch the child process:
:code:`overlay`
An :term:`overlay window <overlay>` covering the current active kitty window
:code:`overlay-main`
An :term:`overlay window <overlay>` covering the current active kitty window.
Unlike a plain overlay window, this window is considered as a :italic:`main`
window which means it is used as the active window for getting the current working
directory, the input text for kittens, launch commands, etc. Useful if this overlay is
intended to run for a long time as a primary window.
:code:`background`
The process will be run in the :italic:`background`, without a kitty window.
@@ -165,12 +170,14 @@ type=list
Restrict the actions remote control is allowed to take. This works like
:opt:`remote_control_password`. You can specify a password and list of actions
just as for :opt:`remote_control_password`. For example::
--remote-control-password '"my passphrase" get-* set-colors'
This password will be in effect for this window only.
Note that any passwords you have defined for :opt:`remote_control_password`
in :file:`kitty.conf` are also in effect. You can override them by using the same password here.
You can also disable all :opt:`remote_control_password` global passwords for this window, by using::
--remote-control-password '!'
This option only takes effect if :option:`--allow-remote-control`
@@ -271,8 +278,17 @@ effect if :option:`--logo` is specified. See :opt:`window_logo_alpha`.
type=list
Change colors in the newly launched window. You can either specify a path to a
:file:`.conf` file with the same syntax as :file:`kitty.conf` to read the colors
from, or specify them individually, for example: :code:`--color background=white
--color foreground=red`.
from, or specify them individually, for example::
--color background=white --color foreground=red
--spacing
type=list
Set the margin and padding for the newly created window.
For example: :code:`margin=20` or :code:`padding-left=10` or :code:`margin-h=30`. The shorthand form sets
all values, the :code:`*-h` and :code:`*-v` variants set horizontal and vertical values.
Can be specified multiple times.
--watcher -w
@@ -454,6 +470,10 @@ def launch(
'overlay_for': None,
'stdin': None
}
spacing = {}
if opts.spacing:
from .rc.set_spacing import parse_spacing_settings, patch_window_edges
spacing = parse_spacing_settings(opts.spacing)
if opts.cwd:
if opts.cwd == 'current':
if active:
@@ -524,7 +544,7 @@ def launch(
kw['cmd'] = final_cmd
if force_window_launch and opts.type not in non_window_launch_types:
opts.type = 'window'
if opts.type == 'overlay' and active:
if opts.type in ('overlay', 'overlay-main') and active:
kw['overlay_for'] = active.id
if opts.type == 'background':
cmd = kw['cmd']
@@ -549,12 +569,17 @@ def launch(
if tab is not None:
watchers = load_watch_modules(opts.watcher)
new_window: Window = tab.new_window(env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, **kw)
if spacing:
patch_window_edges(new_window, spacing)
tab.relayout()
if opts.color:
apply_colors(new_window, opts.color)
if opts.keep_focus and active:
boss.set_active_window(active, switch_os_window_if_needed=True, for_keep_focus=True)
if opts.logo:
new_window.set_logo(opts.logo, opts.logo_position or '', opts.logo_alpha)
if opts.type == 'overlay-main':
new_window.overlay_type = OverlayType.main
return new_window
return None
@@ -564,7 +589,7 @@ def clone_safe_opts() -> FrozenSet[str]:
return frozenset((
'window_title', 'tab_title', 'type', 'keep_focus', 'cwd', 'env', 'hold',
'location', 'os_window_class', 'os_window_name', 'os_window_title',
'logo', 'logo_position', 'logo_alpha', 'color'
'logo', 'logo_position', 'logo_alpha', 'color', 'spacing',
))

View File

@@ -15,8 +15,10 @@
#endif
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <wchar.h>
#include <Python.h>
#include <fcntl.h>
#ifndef KITTY_LIB_PATH
#define KITTY_LIB_PATH "../.."
@@ -268,9 +270,55 @@ read_exe_path(char *exe, size_t buf_sz) {
}
#endif // }}}
static bool
is_valid_fd(int fd)
{
// This is copied from the python source code as we need the exact same semantics
// to prevent python from giving us None for sys.stdout and friends.
#if defined(F_GETFD) && ( \
defined(__linux__) || \
defined(__APPLE__) || \
defined(__wasm__))
return fcntl(fd, F_GETFD) >= 0;
#elif defined(__linux__)
int fd2 = dup(fd);
if (fd2 >= 0) {
close(fd2);
}
return (fd2 >= 0);
#else
struct stat st;
return (fstat(fd, &st) == 0);
#endif
}
static bool
reopen_to_null(const char *mode, FILE *stream) {
errno = 0;
while (true) {
if (freopen("/dev/null", mode, stream) != NULL) return true;
if (errno == EINTR) continue;
perror("Failed to re-open STDIO handle to /dev/null");
return false;
}
}
static bool
ensure_working_stdio(void) {
#define C(which, mode) { \
int fd = fileno(which); \
if (fd < 0) { if (!reopen_to_null(mode, which)) return false; } \
else if (!is_valid_fd(fd)) { \
close(fd); if (!reopen_to_null(mode, which)) return false; \
}}
C(stdin, "r") C(stdout, "w") C(stderr, "w")
return true;
#undef C
}
int main(int argc, char *argv[], char* envp[]) {
if (argc < 1 || !argv) { fprintf(stderr, "Invalid argc/argv\n"); return 1; }
if (!ensure_working_stdio()) return 1;
char exe[PATH_MAX+1] = {0};
char exe_dir_buf[PATH_MAX+1] = {0};
FREE_AFTER_FUNCTION const char *lc_ctype = NULL;

View File

@@ -10,7 +10,7 @@ from typing import Dict, Generator, List, Optional, Sequence, Tuple
from .borders import load_borders_program
from .boss import Boss
from .child import set_default_env
from .child import set_default_env, set_LANG_in_default_env
from .cli import create_opts, parse_args
from .cli_stub import CLIOptions
from .conf.utils import BadLine
@@ -61,7 +61,7 @@ def talk_to_instance(args: CLIOptions) -> None:
if args.session == '-':
stdin = sys.stdin.read()
data = {'cmd': 'new_instance', 'args': tuple(sys.argv), 'cmdline_args_for_open': getattr(sys, 'cmdline_args_for_open', []),
'startup_id': os.environ.get('DESKTOP_STARTUP_ID'),
'startup_id': os.environ.get('DESKTOP_STARTUP_ID'), 'activation_token': os.environ.get('XDG_ACTIVATION_TOKEN'),
'cwd': os.getcwd(), 'stdin': stdin}
notify_socket = None
if args.wait_for_single_instance_window_close:
@@ -229,6 +229,7 @@ def ensure_macos_locale() -> None:
else:
log_error(f'Could not set LANG Cocoa returns language as: {lang}')
os.environ['LANG'] = f'{lang}.UTF-8'
set_LANG_in_default_env(os.environ['LANG'])
@contextmanager
@@ -368,6 +369,7 @@ def set_locale() -> None:
except Exception:
log_error('Failed to set locale with no LANG')
os.environ['LANG'] = old_lang
set_LANG_in_default_env(old_lang)
def _main() -> None:
@@ -377,6 +379,7 @@ def _main() -> None:
if is_macos and os.environ.pop('KITTY_LAUNCHED_BY_LAUNCH_SERVICES', None) == '1':
os.chdir(os.path.expanduser('~'))
args = macos_cmdline(args)
getattr(sys, 'kitty_run_data')['launched_by_launch_services'] = True
try:
cwd_ok = os.path.isdir(os.getcwd())
except Exception:

View File

@@ -141,6 +141,19 @@ encode_mouse_scroll(Window *w, int button, int mods) {
// }}}
static Window*
window_for_id(id_type window_id) {
if (global_state.callback_os_window && global_state.callback_os_window->num_tabs) {
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
for (unsigned int i = 0; i < t->num_windows; i++) {
Window *w = t->windows + i;
if (w->id == window_id) return w;
}
}
return window_for_window_id(window_id);
}
static bool
dispatch_mouse_event(Window *w, int button, int count, int modifiers, bool grabbed) {
bool handled = false;
@@ -492,8 +505,10 @@ send_pending_click_to_window(Window *w, void *data) {
) {
MousePosition current_pos = w->mouse_pos;
w->mouse_pos = pc->mouse_pos;
id_type wid = w->id;
dispatch_mouse_event(w, pc->button, pc->count, pc->modifiers, pc->grabbed);
w->mouse_pos = current_pos;
w = window_for_id(wid);
if (w) w->mouse_pos = current_pos;
}
#undef press
}
@@ -581,16 +596,6 @@ mouse_in_region(Region *r) {
return true;
}
static Window*
window_for_id(id_type window_id) {
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
for (unsigned int i = 0; i < t->num_windows; i++) {
Window *w = t->windows + i;
if (w->id == window_id) return w;
}
return NULL;
}
static Window*
window_for_event(unsigned int *window_idx, bool *in_tab_bar) {
Region central, tab_bar;
@@ -653,7 +658,7 @@ enter_event() {
// On cocoa there is no way to configure the window manager to
// focus windows on mouse enter, so we do it ourselves
if (OPT(focus_follows_mouse) && !global_state.callback_os_window->is_focused) {
focus_os_window(global_state.callback_os_window, false);
focus_os_window(global_state.callback_os_window, false, NULL);
}
#endif
}

View File

@@ -1,13 +1,15 @@
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
import re
from base64 import standard_b64decode
from collections import OrderedDict
from itertools import count
from typing import Dict, Optional, Callable
from typing import Callable, Dict, Optional
from .constants import is_macos, logo_png_file
from .fast_data_types import get_boss
from .types import run_once
from .utils import log_error
NotifyImplementation = Callable[[str, str, str], None]
@@ -92,6 +94,11 @@ def parse_osc_777(raw: str) -> NotificationCommand:
return ans
@run_once
def sanitize_identifier_pat() -> 're.Pattern[str]':
return re.compile(r'[^a-zA-Z0-9-_+.]+')
def parse_osc_99(raw: str) -> NotificationCommand:
cmd = NotificationCommand()
metadata, payload = raw.partition(';')[::2]
@@ -107,7 +114,7 @@ def parse_osc_99(raw: str) -> NotificationCommand:
if k == 'p':
payload_type = v
elif k == 'i':
cmd.identifier = v
cmd.identifier = sanitize_identifier_pat().sub('', v)
elif k == 'e':
payload_is_encoded = v == '1'
elif k == 'd':

View File

@@ -136,7 +136,7 @@ def detach_tab_parse(func: str, rest: str) -> FuncArgsType:
return func, (rest,)
@func_with_args('set_background_opacity', 'goto_layout', 'toggle_layout', 'kitty_shell', 'show_kitty_doc')
@func_with_args('set_background_opacity', 'goto_layout', 'toggle_layout', 'kitty_shell', 'show_kitty_doc', 'set_tab_title')
def simple_parse(func: str, rest: str) -> FuncArgsType:
return func, [rest]

View File

@@ -24,9 +24,9 @@ from typing import (
from kitty.constants import kitty_exe, running_in_kitty
from kitty.entry_points import main as main_entry_point
from kitty.fast_data_types import (
CLD_EXITED, CLD_KILLED, CLD_STOPPED, get_options, install_signal_handlers,
read_signals, remove_signal_handlers, safe_pipe, set_options,
set_use_os_log
CLD_EXITED, CLD_KILLED, CLD_STOPPED, clearenv, get_options,
install_signal_handlers, read_signals, remove_signal_handlers, safe_pipe,
set_options, set_use_os_log
)
from kitty.options.types import Options
from kitty.shm import SharedMemory
@@ -293,6 +293,13 @@ def child_main(cmd: Dict[str, Any], ready_fd: int = -1, prewarm_type: str = 'dir
env = cmd.get('env')
if env is not None:
os.environ.clear()
# os.environ.clear() does not delete all existing env vars from the
# libc environ pointer in some circumstances, I havent figured out
# which exactly. Presumably there is something that alters the
# libc environ pointer?? The environ pointer is used by os.exec and
# therefore by subprocess and friends, so we need to ensure it is
# cleared.
clearenv()
os.environ.update(env)
argv = cmd.get('argv')
if argv:
@@ -567,8 +574,9 @@ def exec_main(stdin_read: int, stdout_write: int, death_notify_write: int) -> No
os.set_inheritable(stdout_write, False)
os.set_inheritable(death_notify_write, False)
running_in_kitty(False)
if not sys.stdout.line_buffering: # happens if the parent kitty instance has stdout not pointing to a terminal
sys.stdout.reconfigure(line_buffering=True) # type: ignore
for x in (sys.stdout, sys.stdin, sys.stderr):
if not x.line_buffering: # happens if the parent kitty instance has stdout not pointing to a terminal
x.reconfigure(line_buffering=True) # type: ignore
try:
main(stdin_read, stdout_write, death_notify_write)
finally:

View File

@@ -41,6 +41,7 @@ class Launch(RemoteCommand):
@first_cmd_output_on_screen.@last_cmd_output.@last_visited_cmd_output: Where to get stdin for the process from
stdin_add_formatting/bool: Boolean indicating whether to add formatting codes to stdin
stdin_add_line_wrap_markers/bool: Boolean indicating whether to add line wrap markers to stdin
spacing/list.str: A list of spacing specifications, see the docs for the set-spacing command
no_response/bool: Boolean indicating whether to send back the window id
marker/str: Specification for marker for new window, for example: "text 1 ERROR"
logo/str: Path to window logo
@@ -53,8 +54,8 @@ class Launch(RemoteCommand):
desc = (
'Prints out the id of the newly opened window. Any command line arguments'
' are assumed to be the command line used to run in the new window, if none'
' are provided, the default shell is run. For example:\n'
':code:`kitty @ launch --title=Email mutt`'
' are provided, the default shell is run. For example::\n\n'
' kitty @ launch --title=Email mutt'
)
options_spec = MATCH_TAB_OPTION + '\n\n' + '''\
--no-response

View File

@@ -34,8 +34,8 @@ class NewWindow(RemoteCommand):
' Prints out the id of the newly opened window'
' (unless :option:`--no-response` is used). Any command line arguments'
' are assumed to be the command line used to run in the new window, if none'
' are provided, the default shell is run. For example:\n'
':code:`kitty @ new-window --title Email mutt`'
' are provided, the default shell is run. For example::\n\n'
' kitty @ new-window --title Email mutt'
)
options_spec = MATCH_TAB_OPTION + '''\n
--title

View File

@@ -27,7 +27,8 @@ class SetBackgroundOpacity(RemoteCommand):
desc = (
'Set the background opacity for the specified windows. This will only work if you have turned on'
' :opt:`dynamic_background_opacity` in :file:`kitty.conf`. The background opacity affects all kitty windows in a'
' single OS window. For example: :code:`kitty @ set-background-opacity 0.5`'
' single OS window. For example::\n\n'
' kitty @ set-background-opacity 0.5'
)
options_spec = '''\
--all -a

View File

@@ -68,7 +68,8 @@ class SetColors(RemoteCommand):
'Set the terminal colors for the specified windows/tabs (defaults to active window).'
' You can either specify the path to a conf file'
' (in the same format as :file:`kitty.conf`) to read the colors from or you can specify individual colors,'
' for example: :code:`kitty @ set-colors foreground=red background=white`'
' for example::\n\n'
' kitty @ set-colors foreground=red background=white'
)
options_spec = '''\
--all -a

View File

@@ -2,7 +2,7 @@
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import TYPE_CHECKING, Dict, Optional, List
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional
from .base import (
MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType,
@@ -37,6 +37,34 @@ def patch_configured_edges(opts: 'Options', s: Dict[str, Optional[float]]) -> No
setattr(opts, q, new_edges)
def parse_spacing_settings(args: Iterable[str]) -> Dict[str, Optional[float]]:
mapper: Dict[str, List[str]] = {}
for q in ('margin', 'padding'):
mapper[q] = f'{q}-left {q}-top {q}-right {q}-bottom'.split()
mapper[f'{q}-h'] = mapper[f'{q}-horizontal'] = f'{q}-left {q}-right'.split()
mapper[f'{q}-v'] = mapper[f'{q}-vertical'] = f'{q}-top {q}-bottom'.split()
for edge in ('left', 'top', 'right', 'bottom'):
mapper[f'{q}-{edge}'] = [f'{q}-{edge}']
settings: Dict[str, Optional[float]] = {}
for spec in args:
parts = spec.split('=', 1)
if len(parts) != 2:
raise ValueError(f'{spec} is not a valid setting')
which = mapper.get(parts[0].lower())
if not which:
raise ValueError(f'{parts[0]} is not a valid edge specification')
if parts[1].lower() == 'default':
val = None
else:
try:
val = float(parts[1])
except Exception:
raise ValueError(f'{parts[1]} is not a number')
for q in which:
settings[q] = val
return settings
class SetSpacing(RemoteCommand):
'''
@@ -70,32 +98,12 @@ windows).
argspec = 'MARGIN_OR_PADDING ...'
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
settings: Dict[str, Optional[float]] = {}
mapper: Dict[str, List[str]] = {}
for q in ('margin', 'padding'):
mapper[q] = f'{q}-left {q}-top {q}-right {q}-bottom'.split()
mapper[f'{q}-h'] = mapper[f'{q}-horizontal'] = f'{q}-left {q}-right'.split()
mapper[f'{q}-v'] = mapper[f'{q}-vertical'] = f'{q}-top {q}-bottom'.split()
for edge in ('left', 'top', 'right', 'bottom'):
mapper[f'{q}-{edge}'] = [f'{q}-{edge}']
if not args:
self.fatal('At least one setting must be specified')
for spec in args:
parts = spec.split('=', 1)
if len(parts) != 2:
self.fatal(f'{spec} is not a valid setting')
which = mapper.get(parts[0].lower())
if not which:
self.fatal(f'{parts[0]} is not a valid edge specification')
if parts[1].lower() == 'default':
val = None
else:
try:
val = float(parts[1])
except Exception:
self.fatal(f'{parts[1]} is not a number')
for q in which:
settings[q] = val
try:
settings = parse_spacing_settings(args)
except Exception as e:
self.fatal(str(e))
ans = {
'match_window': opts.match, 'match_tab': opts.match_tab,
'all': opts.all, 'configured': opts.configured,

View File

@@ -60,6 +60,7 @@ class Session:
self.default_title = default_title
self.os_window_size: Optional[WindowSizes] = None
self.os_window_class: Optional[str] = None
self.focus_os_window: bool = False
def add_tab(self, opts: Options, name: str = '') -> None:
if self.tabs and not self.tabs[-1].windows:
@@ -146,6 +147,8 @@ def parse_session(raw: str, opts: Options) -> Generator[Session, None, None]:
ans.add_window(rest)
elif cmd == 'focus':
ans.focus()
elif cmd == 'focus_os_window':
ans.focus_os_window = True
elif cmd == 'enabled_layouts':
ans.set_enabled_layouts(rest)
elif cmd == 'cd':

View File

@@ -2,7 +2,6 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import readline
import shlex
import sys
import traceback
@@ -39,6 +38,7 @@ def match_commands() -> Tuple[str, ...]:
@run_once
def init_readline() -> None:
import readline
global is_libedit
with suppress(OSError):
readline.read_init_file()
@@ -90,6 +90,7 @@ class Completer:
self.history_path = os.path.join(ddir, 'shell.history')
def complete(self, text: str, state: int) -> Optional[str]:
import readline
if state == 0:
line = readline.get_line_buffer()
cmdline = shlex.split(line)
@@ -102,6 +103,7 @@ class Completer:
return None
def __enter__(self) -> 'Completer':
import readline
with suppress(Exception):
readline.read_history_file(self.history_path)
readline.set_completer(self.complete)
@@ -110,6 +112,7 @@ class Completer:
return self
def __exit__(self, *a: Any) -> None:
import readline
readline.write_history_file(self.history_path)

View File

@@ -822,14 +822,26 @@ PYWRAP0(current_application_quit_request) {
PYWRAP1(focus_os_window) {
id_type os_window_id;
int also_raise = 1;
PA("K|p", &os_window_id, &also_raise);
const char *activation_token = NULL;
PA("K|pz", &os_window_id, &also_raise, &activation_token);
WITH_OS_WINDOW(os_window_id)
if (!os_window->is_focused) focus_os_window(os_window, also_raise);
if (!os_window->is_focused || (activation_token && activation_token[0])) focus_os_window(os_window, also_raise, activation_token);
Py_RETURN_TRUE;
END_WITH_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;
@@ -1291,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),

View File

@@ -188,7 +188,7 @@ typedef struct {
int x, y, w, h;
bool is_set, was_maximized;
} before_fullscreen;
int viewport_width, viewport_height, window_width, window_height;
int viewport_width, viewport_height, window_width, window_height, content_area_width, content_area_height;
double viewport_x_ratio, viewport_y_ratio;
Tab *tabs;
BackgroundImage *bgimage;
@@ -262,7 +262,7 @@ void get_os_window_size(OSWindow *os_window, int *w, int *h, int *fw, int *fh);
void get_os_window_content_scale(OSWindow *os_window, double *xdpi, double *ydpi, float *xscale, float *yscale);
void update_os_window_references(void);
void mark_os_window_for_close(OSWindow* w, CloseRequest cr);
void update_os_window_viewport(OSWindow *window, bool);
void update_os_window_viewport(OSWindow *window, bool notify_boss);
bool should_os_window_be_rendered(OSWindow* w);
void wakeup_main_loop(void);
void swap_window_buffers(OSWindow *w);
@@ -270,7 +270,8 @@ bool make_window_context_current(id_type);
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);
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);

View File

@@ -211,7 +211,7 @@ class Tab: # {{{
@property
def active_window_for_cwd(self) -> Optional[Window]:
return self.windows.active_group_base
return self.windows.active_group_main
@property
def title(self) -> str:

View File

@@ -1,16 +1,24 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
from enum import Enum
from functools import update_wrapper
from typing import (
TYPE_CHECKING, Any, Callable, Generic, NamedTuple, Tuple, TypeVar, Union, Iterator, Dict
TYPE_CHECKING, Any, Callable, Dict, Generic, Iterator, NamedTuple, Tuple,
TypeVar, Union
)
if TYPE_CHECKING:
from kitty.fast_data_types import SingleKey
_T = TypeVar('_T')
class OverlayType(Enum):
transient: str = 'transient'
main: str = 'main'
class ParsedShortcut(NamedTuple):
mods: int
key_name: str
@@ -160,11 +168,11 @@ def run_once(f: Callable[[], _T]) -> 'RunOnce[_T]':
@run_once
def modmap() -> Dict[str, int]:
from .constants import is_macos
from .fast_data_types import (
GLFW_MOD_ALT, GLFW_MOD_CAPS_LOCK, GLFW_MOD_CONTROL, GLFW_MOD_HYPER,
GLFW_MOD_META, GLFW_MOD_NUM_LOCK, GLFW_MOD_SHIFT, GLFW_MOD_SUPER
)
from .constants import is_macos
return {'ctrl': GLFW_MOD_CONTROL, 'shift': GLFW_MOD_SHIFT, ('opt' if is_macos else 'alt'): GLFW_MOD_ALT,
('cmd' if is_macos else 'super'): GLFW_MOD_SUPER, 'hyper': GLFW_MOD_HYPER, 'meta': GLFW_MOD_META,

View File

@@ -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 +
@@ -269,7 +254,8 @@ def command_for_open(program: Union[str, List[str]] = 'default') -> List[str]:
return cmd
def open_cmd(cmd: Union[Iterable[str], List[str]], arg: Union[None, Iterable[str], str] = None, cwd: Optional[str] = None) -> 'PopenType[bytes]':
def open_cmd(cmd: Union[Iterable[str], List[str]], arg: Union[None, Iterable[str], str] = None,
cwd: Optional[str] = None, extra_env: Optional[Dict[str, str]] = None) -> 'PopenType[bytes]':
import subprocess
if arg is not None:
cmd = list(cmd)
@@ -277,13 +263,17 @@ def open_cmd(cmd: Union[Iterable[str], List[str]], arg: Union[None, Iterable[str
cmd.append(arg)
else:
cmd.extend(arg)
env: Optional[Dict[str, str]] = None
if extra_env:
env = os.environ.copy()
env.update(extra_env)
return subprocess.Popen(
tuple(cmd), stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=cwd or None,
preexec_fn=clear_handled_signals)
preexec_fn=clear_handled_signals, env=env)
def open_url(url: str, program: Union[str, List[str]] = 'default', cwd: Optional[str] = None) -> 'PopenType[bytes]':
return open_cmd(command_for_open(program), url, cwd=cwd)
def open_url(url: str, program: Union[str, List[str]] = 'default', cwd: Optional[str] = None, extra_env: Optional[Dict[str, str]] = None) -> 'PopenType[bytes]':
return open_cmd(command_for_open(program), url, cwd=cwd, extra_env=extra_env)
def detach(fork: bool = True, setsid: bool = True, redirect: bool = True) -> None:

View File

@@ -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
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, WindowGeometry, ac, run_once
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]]]]
@@ -462,6 +468,7 @@ global_watchers = GlobalWatchers()
class Window:
window_custom_type: str = ''
overlay_type = OverlayType.transient
def __init__(
self,
@@ -642,6 +649,8 @@ class Window:
}
if self.window_custom_type:
ans['window_custom_type'] = self.window_custom_type
if self.overlay_type is not OverlayType.transient:
ans['overlay_type'] = self.overlay_type.value
return ans
@property
@@ -998,6 +1007,7 @@ class Window:
self.screen.send_escape_code_to_child(OSC, f'{code};rgb:{r:04x}/{g:04x}/{b:04x}')
def report_notification_activated(self, identifier: str) -> None:
identifier = sanitize_identifier_pat().sub('', identifier)
self.screen.send_escape_code_to_child(OSC, f'99;i={identifier};')
def set_dynamic_color(self, code: int, value: Union[str, bytes]) -> None:

View File

@@ -7,7 +7,7 @@ from contextlib import suppress
from itertools import count
from typing import Any, Deque, Dict, Iterator, List, Optional, Tuple, Union
from .types import WindowGeometry
from .types import OverlayType, WindowGeometry
from .typing import EdgeLiteral, TabType, WindowType
WindowOrId = Union[WindowType, int]
@@ -54,7 +54,10 @@ class WindowGroup:
return False
@property
def base_window_id(self) -> int:
def main_window_id(self) -> int:
for w in reversed(self.windows):
if w.overlay_type is OverlayType.main:
return w.id
return self.windows[0].id if self.windows else 0
@property
@@ -298,9 +301,9 @@ class WindowList:
return None
@property
def active_group_base(self) -> Optional[WindowType]:
def active_group_main(self) -> Optional[WindowType]:
with suppress(Exception):
return self.id_map[self.groups[self.active_group_idx].base_window_id]
return self.id_map[self.groups[self.active_group_idx].main_window_id]
return None
def set_active_window_group_for(self, x: WindowOrId, for_keep_focus: Optional[WindowType] = None) -> None:

View File

@@ -3,6 +3,7 @@
import os
import stat
import sys
import unittest
from functools import partial
@@ -58,7 +59,15 @@ class TestBuild(BaseTest):
self.assertTrue(os.path.isdir(terminfo_dir), f'Terminfo dir: {terminfo_dir}')
self.assertTrue(os.path.exists(logo_png_file), f'Logo file: {logo_png_file}')
self.assertTrue(os.path.exists(zsh), f'Shell integration: {zsh}')
self.assertTrue(os.access(os.path.join(shell_integration_dir, 'ssh', 'askpass.py'), os.X_OK))
def is_executable(x):
mode = os.stat(x).st_mode
q = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
return mode & q == q
for x in ('kitty', 'askpass.py'):
x = os.path.join(shell_integration_dir, 'ssh', x)
self.assertTrue(is_executable(x), f'{x} is not executable')
if getattr(sys, 'frozen', False):
self.assertTrue(os.path.isdir(local_docs()), f'Local docs: {local_docs()}')
@@ -76,8 +85,8 @@ class TestBuild(BaseTest):
del pygments
def test_docs_url(self):
from kitty.utils import docs_url
from kitty.constants import website_url
from kitty.utils import docs_url
def run_tests(p, base, suffix='.html'):
def t(x, e):
@@ -98,6 +107,22 @@ class TestBuild(BaseTest):
run_tests(partial(docs_url, local_docs_root=None), w, '/')
self.ae(docs_url('#ref=issues-123'), 'https://github.com/kovidgoyal/kitty/issues/123')
def test_launcher_ensures_stdio(self):
from kitty.constants import kitty_exe
import subprocess
exe = kitty_exe()
cp = subprocess.run([exe, '+runpy', f'''\
import os, sys
if sys.stdin:
os.close(sys.stdin.fileno())
if sys.stdout:
os.close(sys.stdout.fileno())
if sys.stderr:
os.close(sys.stderr.fileno())
os.execlp({exe!r}, 'kitty', '+runpy', 'import sys; raise SystemExit(1 if sys.stdout is None or sys.stdin is None or sys.stderr is None else 0)')
'''])
self.assertEqual(cp.returncode, 0)
def main() -> None:
tests = unittest.defaultTestLoader.loadTestsFromTestCase(TestBuild)

View File

@@ -535,7 +535,7 @@ class TestDataTypes(BaseTest):
q('a\x1b[bc', 'ac')
q('a\x1b[12;34:43mbc', 'abc')
def test_SingleKey(self):
def test_single_key(self):
from kitty.fast_data_types import (
GLFW_MOD_KITTY, GLFW_MOD_SHIFT, SingleKey
)
@@ -548,9 +548,14 @@ class TestDataTypes(BaseTest):
self.ae(repr(SingleKey(key=23, mods=2, is_native=True)), 'SingleKey(mods=2, is_native=True, key=23)')
self.ae(repr(SingleKey(key=23, mods=2)), 'SingleKey(mods=2, key=23)')
self.ae(repr(SingleKey(key=23)), 'SingleKey(key=23)')
self.ae(repr(SingleKey(key=0x1008ff57)), 'SingleKey(key=269025111)')
self.ae(repr(SingleKey(key=23)._replace(mods=2)), 'SingleKey(mods=2, key=23)')
self.ae(repr(SingleKey(key=23)._replace(key=-1, mods=GLFW_MOD_KITTY)), f'SingleKey(mods={GLFW_MOD_KITTY})')
self.assertEqual(SingleKey(key=1), SingleKey(key=1))
self.assertEqual(hash(SingleKey(key=1)), hash(SingleKey(key=1)))
self.assertNotEqual(hash(SingleKey(key=1, mods=2)), hash(SingleKey(key=1)))
self.assertNotEqual(SingleKey(key=1, mods=2), SingleKey(key=1))
def test_notify_identifier_sanitization(self):
from kitty.notify import sanitize_identifier_pat
self.ae(sanitize_identifier_pat().sub('', '\x1b\nabc\n[*'), 'abc')

View File

@@ -10,13 +10,14 @@ class TestHints(BaseTest):
def test_url_hints(self):
from kittens.hints.main import (
Mark, convert_text, functions_for, linenum_marks,
linenum_process_result, mark, parse_hints_args
linenum_process_result, mark, parse_hints_args, process_escape_codes
)
args = parse_hints_args([])[0]
pattern, post_processors = functions_for(args)
def create_marks(text, cols=20, mark=mark):
text = convert_text(text, cols)
text, _ = process_escape_codes(text)
return tuple(mark(pattern, post_processors, text, args))
def t(text, url, cols=20):
@@ -32,6 +33,8 @@ class TestHints(BaseTest):
t(f'link:{u}[xxx]', u)
t(f'`xyz <{u}>`_.', u)
t(f'<a href="{u}">moo', u)
t('\x1b[mhttp://test.me/1234\n\x1b[mx', 'http://test.me/1234')
t('\x1b[mhttp://test.me/12345\r\x1b[m6\n\x1b[mx', 'http://test.me/123456')
def m(text, path, line, cols=20):

View File

@@ -28,13 +28,15 @@ class Prewarm(BaseTest):
cwd = tempfile.gettempdir()
env = {'TEST_ENV_PASS': 'xyz'}
cols = 117
cols = 317
stdin_data = 'from_stdin'
pty = self.create_pty(cols=cols)
ttyname = os.ttyname(pty.slave_fd)
opts = get_options()
opts.config_overrides = 'font_family prewarm',
os.environ['SHOULD_NOT_BE_PRESENT'] = '1'
p = fork_prewarm_process(opts, use_exec=True)
del os.environ['SHOULD_NOT_BE_PRESENT']
if p is None:
return
p.take_from_worker_fd(create_file=True)
@@ -44,22 +46,21 @@ import os, json; from kitty.utils import *; from kitty.fast_data_types import ge
'ttyname': os.ttyname(sys.stdout.fileno()),
'cols': read_screen_size().cols,
'cwd': os.getcwd(),
'env': os.environ.get('TEST_ENV_PASS'),
'env': os.environ.copy(),
'pid': os.getpid(),
'font_family': get_options().font_family,
'stdin': sys.stdin.read(),
'done': 'hello',
}, indent=2))"""], cwd=cwd, env=env, stdin_data=stdin_data)
}, indent=2), "ALL_OUTPUT_PRESENT", sep="")"""], cwd=cwd, env=env, stdin_data=stdin_data)
self.assertFalse(pty.screen_contents().strip())
p.mark_child_as_ready(child.child_id)
pty.wait_till(lambda: 'hello' in pty.screen_contents())
data = json.loads(pty.screen_contents())
pty.wait_till(lambda: 'ALL_OUTPUT_PRESENT' in pty.screen_contents())
data = json.JSONDecoder().raw_decode(pty.screen_contents())[0]
self.ae(data['cols'], cols)
self.assertTrue(data['cterm'])
self.ae(data['ttyname'], ttyname)
self.ae(os.path.realpath(data['cwd']), os.path.realpath(cwd))
self.ae(data['env'], env['TEST_ENV_PASS'])
self.ae(data['env']['TEST_ENV_PASS'], env['TEST_ENV_PASS'])
self.assertNotIn('SHOULD_NOT_BE_PRESENT', data['env'])
self.ae(data['font_family'], 'prewarm')
self.ae(int(p.from_worker.readline()), data['pid'])

View File

@@ -25,10 +25,11 @@ def bash_ok():
v = shutil.which('bash')
if not v:
return False
o = subprocess.check_output([v, '-c', 'echo "${BASH_VERSION}"']).decode('utf-8').strip()
if not o or int(o[0]) < 5:
o = subprocess.check_output([v, '-c', 'echo "${BASH_VERSINFO[0]}\n${BASH_VERSINFO[4]}"']).decode('utf-8').splitlines()
if not o:
return False
return True
major_ver, relstatus = o[0], o[-1]
return int(major_ver) >= 5 and relstatus == 'release'
def basic_shell_env(home_dir):
@@ -240,7 +241,7 @@ function _set_status_prompt; function fish_prompt; echo -n "$pipestatus $status
pty.send_cmd_to_child('exit')
@unittest.skipUnless(bash_ok(), 'bash not installed or too old')
@unittest.skipUnless(bash_ok(), 'bash not installed, too old, or debug build')
def test_bash_integration(self):
ps1 = 'prompt> '
with self.run_shell(
@@ -368,4 +369,4 @@ PS1="{ps1}"
pty.send_cmd_to_child('clone-in-kitty')
pty.wait_till(lambda: len(pty.callbacks.clone_cmds) == 1)
env = pty.callbacks.clone_cmds[0].env
self.ae(env.get('ES'), 'a\n `b` c\n$d')
self.ae(env.get('ES'), 'a\n `b` c\n$d', f'Screen contents: {pty.screen_contents()!r}')

View File

@@ -149,7 +149,7 @@ copy --exclude */w.* d1
self.ae(len(glob.glob(f'{remote_home}/{tname}/*/xterm-kitty')), 2)
def test_ssh_env_vars(self):
tset = '$A-$(echo no)-`echo no2` "something\nelse"'
tset = '$A-$(echo no)-`echo no2` !Q5 "something\nelse"'
for sh in self.all_possible_sh:
with self.subTest(sh=sh), tempfile.TemporaryDirectory() as tdir:
os.mkdir(os.path.join(tdir, 'cwd'))

View File

@@ -269,13 +269,7 @@ class GitHub(Base): # {{{
def update_nightly_description(self, release_id: int) -> None:
url = f'{self.url_base}/{release_id}'
now = str(datetime.datetime.utcnow()).split('.')[0] + ' UTC'
try:
with open('.git/refs/heads/master') as f:
commit = f.read().strip()
except FileNotFoundError:
time.sleep(1)
with open('.git/refs/heads/master') as f:
commit = f.read().strip()
commit = subprocess.check_output(['git', 'rev-parse', '--verify', '--end-of-options', 'master^{commit}']).decode('utf-8').strip()
self.patch(
url, 'Failed to update nightly release description',
body=f'Nightly release, generated on: {now} from commit: {commit}.'
@@ -368,7 +362,7 @@ class GitHub(Base): # {{{
'target_commitish': 'master',
'name': f'version {self.version}',
'body': f'Release version {self.version}.'
' For changelog, see https://sw.kovidgoyal.net/kitty/changelog/'
' For changelog, see https://sw.kovidgoyal.net/kitty/changelog/#detailed-list-of-changes'
' GPG key used for signing tarballs is: https://calibre-ebook.com/signatures/kovid.gpg',
'draft': False,
'prerelease': False

View File

@@ -1,15 +1,10 @@
[flake8]
max-line-length = 160
exclude==template.py,linux-package
[yapf]
based_on_style = pep8
split_penalty_import_names = 100
dedent_closing_brackets = True
coalesce_brackets = True
blank_line_before_nested_class_or_def = True
extend_ignore=W503,E203
[isort]
profile = black
combine_as_imports = True
multi_line_output = 5

View File

@@ -1363,10 +1363,19 @@ def package(args: Options, bundle_type: str) -> None:
f.seek(0), f.truncate(), f.write(raw)
compile_python(libdir)
def should_be_executable(path: str) -> bool:
if path.endswith('.so'):
return True
q = path.split(os.sep)[-2:]
if len(q) == 2 and q[0] == 'ssh' and q[1] in ('askpass.py', 'kitty'):
return True
return False
for root, dirs, files in os.walk(libdir):
for f_ in files:
path = os.path.join(root, f_)
os.chmod(path, 0o755 if f_.endswith('.so') or os.path.basename(f_) == 'askpass.py' else 0o644)
os.chmod(path, 0o755 if should_be_executable(path) else 0o644)
if not is_macos:
create_linux_bundle_gunk(ddir, args.libdir_name)

View File

@@ -23,7 +23,7 @@ if [[ -n "$KITTY_BASH_INJECT" ]]; then
}
else
builtin set +o posix
builtin shopt -u inherit_errexit # resetting posix does not clear this
builtin shopt -u inherit_errexit 2>/dev/null # resetting posix does not clear this
if [[ -n "$KITTY_BASH_UNEXPORT_HISTFILE" ]]; then
builtin export -n HISTFILE
builtin unset KITTY_BASH_UNEXPORT_HISTFILE

View File

@@ -92,6 +92,7 @@ read_base64_from_tty() {
untar_and_read_env() {
# extract the tar file atomically, in the sense that any file from the
# tarfile is only put into place after it has been fully written to disk
command -v tar > /dev/null 2> /dev/null || die "tar is not availiable on this server. The ssh kitten requires tar."
tdir=$(command mktemp -d "$HOME/.kitty-ssh-kitten-untar-XXXXXXXXXXXX")
[ $? = 0 ] || die "Creating temp directory failed"
# suppress STDERR for tar as tar prints various warnings if for instance, timestamps are in the future