Compare commits

...

143 Commits

Author SHA1 Message Date
Kovid Goyal
55b53bcac6 version 0.26.4 2022-10-17 08:18:51 +05:30
Kovid Goyal
57d044861f Merge branch 'patch-1' of https://github.com/ms-test/kitty 2022-10-16 13:28:06 +05:30
ms-test
aab149366d Check correct pair member
I haven't looked deeply, but this looked to be a simple typo unless it's checking one both times because of a type invariant. If that is the case, and the check is correct, then a comment and/or code refactoring to call attention to this would likely be a good idea.
2022-10-16 03:53:16 -04:00
Kovid Goyal
896f93822d kitty shell: Also output active tab id
Fixes #5590
2022-10-16 08:52:35 +05:30
Kovid Goyal
6890e265b6 GNOME Wayland: Fix a memory leak in gnome-shell when using client side decorations
destroy CSD buffers that were never attached explicitly, as mutter does
not send release events for these even when the pool is destroyed.
2022-10-12 17:52:47 +05:30
Kovid Goyal
48a4edc199 Merge branch 'action_cwd_doc' of https://github.com/cbosdo/kitty 2022-10-10 14:04:29 +05:30
Cédric Bosdonnat
d63be90a7f Recommend using --cwd=current in open-actions.conf
Editors like Vim are basing path-related commands on the current working
directory. Launching the action with the current CWD in hyperlink opens
makes life easier and would work with every editor.
2022-10-10 10:12:20 +02:00
Kovid Goyal
6af020e4ef Merge branch 'marksisson/simple-docs-change' of https://github.com/marksisson/kitty 2022-10-09 12:18:25 +05:30
Mark Sisson
ca24e9ca67 change linux_package build instructions to use make target 2022-10-09 00:02:38 -05:00
Kovid Goyal
fbfaefd72a Wayland: Fix an abort when changing background colors with wayland_titlebar_color set to background
Fixes #5562
2022-10-07 14:13:12 +05:30
Kovid Goyal
94b3776066 Merge branch 'master' of https://github.com/Diff-fusion/kitty 2022-10-05 09:09:23 +05:30
Felix
15e4be9051 Fix clipboard target properties and memory leak 2022-10-04 17:22:15 +02:00
Kovid Goyal
ae8f3de070 Allow centering the background image
Fixes #5525
2022-10-04 08:26:28 +05:30
Kovid Goyal
c8d18ffe26 Add a note about yet another gnome bug 2022-10-03 22:06:00 +05:30
Kovid Goyal
4d80427908 Wayland: Fix background image scaling using tiled mode on high DPI screens 2022-10-03 21:49:52 +05:30
Kovid Goyal
0e69324469 Fix resize draw strategy == static causing background image/borders to not be drawn 2022-10-03 21:40:00 +05:30
Kovid Goyal
cf22729dfa Fix background_tint not applying to window margins and padding
Fixes #3933
2022-10-03 21:39:57 +05:30
Kovid Goyal
1402c5bbfa Forgot to change check for space for next tab to use its max tab length rather than the current tabs max tab length, since they can now be different 2022-10-03 16:21:50 +05:30
Kovid Goyal
f0f0c8f4fe max_title_length -> max_tab_length 2022-10-03 16:15:24 +05:30
Kovid Goyal
96faac95db DRYer 2022-10-03 15:32:42 +05:30
Kovid Goyal
08041415d1 Document the purpose of for_layout 2022-10-03 13:26:55 +05:30
Kovid Goyal
647b18d345 Tab bar: Improve empty space management when some tabs have short titles, allocate the saved space to the active tab
Fixes #5548
2022-10-03 13:24:21 +05:30
Kovid Goyal
a116e3cadd Merge branch 'patch-1' of https://github.com/sorrycc/kitty 2022-09-30 07:46:32 +05:30
chencheng (云谦)
52c9b5f5b8 Update custom.css 2022-09-30 08:44:09 +08:00
Kovid Goyal
bbc9f588f1 Merge branch 'unicode-15' of https://github.com/page-down/kitty 2022-09-29 09:17:02 +05:30
pagedown
13a3c6b5b2 Update to Unicode 15.0 2022-09-29 10:13:21 +08:00
pagedown
fc15b20f24 Update to Nerd Fonts 2.2.2 2022-09-29 10:13:10 +08:00
Kovid Goyal
cd8f2e5cc6 another mypy update another round of breakage 2022-09-27 07:19:03 +05:30
Kovid Goyal
267798a277 Update the docs of open_url_with to indicate that open-actions.conf is read by the "default" handler 2022-09-26 20:21:44 +05:30
Kovid Goyal
921ac12e90 Clear both actions caches when config file is reloaded 2022-09-26 20:15:08 +05:30
Kovid Goyal
901e00cab1 Cleanup previous PR
1) Dont use deprecated code
2) Always set the dock icon on startup as the dock icon doesnt change
   till the dock is restarted
3) Update the app icon automatically if the mtime on the custom icon in
   the config dir is newer than the mtime of the sentinel file apple
   puts inside the application bundle to indicate it has a custom icon
2022-09-25 18:11:17 +05:30
Kovid Goyal
d0769f3979 ... 2022-09-25 17:36:32 +05:30
Kovid Goyal
7e4dd8cf1e Merge branch 'macos-set-icon' of https://github.com/page-down/kitty 2022-09-25 14:40:36 +05:30
Kovid Goyal
5ea0519f62 Pass through python used for type check to the test code from before the env is sanitized 2022-09-25 14:36:12 +05:30
pagedown
a1029418f8 macOS: Allow to set custom app icon automatically 2022-09-25 15:25:43 +08:00
Kovid Goyal
98eacb2067 panel kitten: Do not depend on xprop 2022-09-24 15:44:54 +05:30
Kovid Goyal
2cef9abbd3 Get rid of one call to xprop in the panel kitten 2022-09-24 15:17:11 +05:30
Kovid Goyal
0fa158c809 Merge branch 'docs' of https://github.com/page-down/kitty 2022-09-24 14:38:42 +05:30
pagedown
c54a6b398b Add some missing no_response for rc commands 2022-09-24 12:43:13 +08:00
pagedown
27ea367123 Docs: Remove the extra backslashes from the install commands 2022-09-24 12:39:05 +08:00
Kovid Goyal
42178a4570 X11: Fix a regression in the previous release that caused pasting from GTK based applications to have extra newlines
Fixes #5528

And let me just re-iterate, GNOME/GTK is developed by morons.
2022-09-24 08:56:50 +05:30
Kovid Goyal
e98ecc1c4f Another auto-indent fix 2022-09-24 08:34:07 +05:30
Kovid Goyal
0d64246209 fix vim autoindent 2022-09-24 08:31:14 +05:30
Kovid Goyal
a0495219ab ... 2022-09-24 08:30:40 +05:30
Kovid Goyal
9d98aa3159 Merge branch 'patch-2' of https://github.com/sashashura/kitty 2022-09-24 07:29:42 +05:30
Alex
5c82f4beca build: harden ci.yml permissions
Signed-off-by: Alex <aleksandrosansan@gmail.com>
2022-09-23 22:14:57 +02:00
Alex
13539bd8c6 build: harden codeql-analysis.yml permissions
Signed-off-by: Alex <aleksandrosansan@gmail.com>
2022-09-23 22:14:16 +02:00
Kovid Goyal
26b8ab9adf Use a regex for bracketed paste sanitization 2022-09-23 22:18:03 +05:30
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
Charlie Groves
db5b4da388 Add options for linking different parts of ripgrep output 2022-08-29 13:23:42 -04:00
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
107 changed files with 35667 additions and 32779 deletions

View File

@@ -6,6 +6,9 @@ env:
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
linux:
name: Linux (python=${{ matrix.pyver }} cc=${{ matrix.cc }} sanitize=${{ matrix.sanitize }})

View File

@@ -9,9 +9,16 @@ on:
schedule:
- cron: '0 22 * * 5'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
CodeQL-Build:
permissions:
contents: read # to fetch code (actions/checkout)
security-events: write # to upload SARIF results (github/codeql-action/analyze)
runs-on: ubuntu-latest
steps:

View File

@@ -10,7 +10,7 @@
}
.sidebar-logo {
max-height: 128px;
height: 128px;
}
.major-features li {

View File

@@ -85,7 +85,7 @@ Customizing the installation
.. code-block:: sh
_kitty_install_cmd \\
_kitty_install_cmd \
installer=nightly
If you want to install it in parallel to the released kitty specify a
@@ -93,14 +93,14 @@ Customizing the installation
.. code-block:: sh
_kitty_install_cmd \\
_kitty_install_cmd \
installer=nightly dest=/some/other/location
* You can specify a different install location, with ``dest``:
.. code-block:: sh
_kitty_install_cmd \\
_kitty_install_cmd \
dest=/some/other/location
* You can tell the installer not to launch |kitty| after installing it with
@@ -108,14 +108,14 @@ Customizing the installation
.. code-block:: sh
_kitty_install_cmd \\
_kitty_install_cmd \
launch=n
* You can use a previously downloaded dmg/tarball, with ``installer``:
.. code-block:: sh
_kitty_install_cmd \\
_kitty_install_cmd \
installer=/path/to/dmg or tarball

View File

@@ -160,7 +160,7 @@ While |kitty| does use Python, it is not a traditional Python package, so please
do not install it in site-packages.
Instead run::
python3 setup.py linux-package
make linux-package
This will install |kitty| into the directory :file:`linux-package`. You can run
|kitty| with :file:`linux-package/bin/kitty`. All the files needed to run kitty

View File

@@ -35,6 +35,78 @@ mouse anywhere in the current command to move the cursor there. See
Detailed list of changes
-------------------------------------
0.26.4 [2022-10-17]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- macOS: Allow changing the kitty icon by placing a custom icon in the kitty config folder (:pull:`5464`)
- Allow centering the :opt:`background_image` (:iss:`5525`)
- X11: Fix a regression in the previous release that caused pasting from GTK based applications to have extra newlines (:iss:`5528`)
- Tab bar: Improve empty space management when some tabs have short titles, allocate the saved space to the active tab (:iss:`5548`)
- Fix :opt:`background_tint` not applying to window margins and padding (:iss:`3933`)
- Wayland: Fix background image scaling using tiled mode on high DPI screens
- Wayland: Fix an abort when changing background colors with :opt:`wayland_titlebar_color` set to ``background`` (:iss:`5562`)
- Update to Unicode 15.0 (:pull:`5542`)
- GNOME Wayland: Fix a memory leak in gnome-shell when using client side decorations
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]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -205,13 +205,15 @@ fonts to be freely resizable, so it does not support bitmapped fonts.
.. note::
If you are trying to use a font patched with `Nerd Fonts
<https://nerdfonts.com/>`__ symbols, don't do that as patching destroys
fonts. There is no need, simply install the standalone ``Symbols Nerd Font``
fonts. There is no need, simply install the standalone ``Symbols Nerd Font Mono``
(the file :file:`NerdFontsSymbolsOnly.zip` from the `Nerd Fonts releases page
<https://github.com/ryanoasis/nerd-fonts/releases>`__). kitty should pick up
symbols from it automatically, and you can tell it to do so explicitly in
case it doesn't with the :opt:`symbol_map` directive::
symbol_map U+23FB-U+23FE,U+2665,U+26A1,U+2B58,U+E000-U+E00A,U+E0A0-U+E0A3,U+E0B0-U+E0C8,U+E0CA,U+E0CC-U+E0D2,U+E0D4,U+E200-U+E2A9,U+E300-U+E3E3,U+E5FA-U+E62F,U+E700-U+E7C5,U+F000-U+F2E0,U+F300-U+F31C,U+F400-U+F4A9,U+F500-U+F8FF Symbols Nerd Font
# Nerd Fonts v2.2.2
symbol_map U+23FB-U+23FE,U+2665,U+26A1,U+2B58,U+E000-U+E00A,U+E0A0-U+E0A3,U+E0B0-U+E0C8,U+E0CA,U+E0CC-U+E0D2,U+E0D4,U+E200-U+E2A9,U+E300-U+E3E3,U+E5FA-U+E634,U+E700-U+E7C5,U+EA60-U+EBEB,U+F000-U+F2E0,U+F300-U+F32F,U+F400-U+F4A9,U+F500-U+F8FF Symbols Nerd Font Mono
Those Unicode symbols beyond the ``E000-F8FF`` Unicode private use area are
not included.
@@ -287,13 +289,36 @@ homepage:
:target: https://github.com/samholmes/whiskers
:width: 256
On macOS you can change the icon by following the steps:
On macOS you can put :file:`kitty.app.icns` or :file:`kitty.app.png` in the
:ref:`kitty configuration directory <confloc>`, and this icon will be applied
automatically at startup. Unfortunately, Apple's Dock does not change its
cached icon so the custom icon will revert when kitty is quit. Run the
following to force the Dock to update its cached icons:
.. code-block:: sh
rm /var/folders/*/*/*/com.apple.dock.iconcache; killall Dock
If you prefer not to keep a custom icon in the kitty config folder, you can
also set it with the following command:
.. code-block:: sh
# Set kitty.icns as the icon for currently running kitty
kitty +runpy 'from kitty.fast_data_types import cocoa_set_app_icon; import sys; cocoa_set_app_icon(*sys.argv[1:]); print("OK")' kitty.icns
# Set the icon for app bundle specified by the path
kitty +runpy 'from kitty.fast_data_types import cocoa_set_app_icon; import sys; cocoa_set_app_icon(*sys.argv[1:]); print("OK")' /path/to/icon.png /Applications/kitty.app
You can also change the icon manually by following the steps:
#. Find :file:`kitty.app` in the Applications folder, select it and press :kbd:`⌘+I`
#. Drag :file:`kitty.icns` onto the application icon in the kitty info pane
#. Delete the icon cache and restart Dock::
#. Delete the icon cache and restart Dock:
$ rm /var/folders/*/*/*/com.apple.dock.iconcache; killall Dock
.. code-block:: sh
rm /var/folders/*/*/*/com.apple.dock.iconcache; killall Dock
How do I map key presses in kitty to different keys in the terminal program?
@@ -312,7 +337,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

@@ -17,12 +17,12 @@ following contents:
# by the hyperlink_grep kitten and nothing else so far.
protocol file
fragment_matches [0-9]+
action launch --type=overlay vim +${FRAGMENT} ${FILE_PATH}
action launch --type=overlay --cwd=current vim +${FRAGMENT} ${FILE_PATH}
# Open text files without fragments in the editor
protocol file
mime text/*
action launch --type=overlay ${EDITOR} ${FILE_PATH}
action launch --type=overlay --cwd=current ${EDITOR} ${FILE_PATH}
Now, run a search with::
@@ -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':
@@ -172,6 +172,8 @@ class Arg:
while self.name.startswith('*'):
self.name = self.name[1:]
self.type = self.type + '*'
if '[' in self.name:
self.type += '[' + self.name.partition('[')[-1]
def __repr__(self) -> str:
return f'Arg({self.type}, {self.name})'
@@ -245,11 +247,15 @@ 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)
void glfwDBusSetUserNotificationHandler(GLFWDBusnotificationactivatedfun handler)
int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc)
void glfwSetX11WindowAsDock(int32_t x11_window_id)
void glfwSetX11WindowStrut(int32_t x11_window_id, uint32_t dimensions[12])
'''.splitlines():
if line:
functions.append(Function(line.strip(), check_fail=False))

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

View File

@@ -104,9 +104,10 @@ init_buffer_pair(_GLFWWaylandBufferPair *pair, size_t width, size_t height, unsi
static bool
window_has_buffer(_GLFWwindow *window, struct wl_buffer *q) {
#define Q(which) decs.which.buffer.a == q || decs.which.buffer.b == q
return Q(left) || Q(top) || Q(right) || Q(bottom);
#define Q(which) if (decs.which.buffer.a == q) { decs.which.buffer.a_needs_to_be_destroyed = false; return true; } if (decs.which.buffer.b == q) { decs.which.buffer.b_needs_to_be_destroyed = false; return true; }
Q(left); Q(top); Q(right); Q(bottom);
#undef Q
return false;
}
static void
@@ -122,10 +123,12 @@ static void
alloc_buffer_pair(uintptr_t window_id, _GLFWWaylandBufferPair *pair, struct wl_shm_pool *pool, uint8_t *data, size_t *offset) {
pair->data.a = data + *offset;
pair->a = wl_shm_pool_create_buffer(pool, *offset, pair->width, pair->height, pair->stride, WL_SHM_FORMAT_ARGB8888);
pair->a_needs_to_be_destroyed = true;
wl_buffer_add_listener(pair->a, &handle_buffer_events, (void*)window_id);
*offset += pair->size_in_bytes;
pair->data.b = data + *offset;
pair->b = wl_shm_pool_create_buffer(pool, *offset, pair->width, pair->height, pair->stride, WL_SHM_FORMAT_ARGB8888);
pair->b_needs_to_be_destroyed = true;
wl_buffer_add_listener(pair->b, &handle_buffer_events, (void*)window_id);
*offset += pair->size_in_bytes;
pair->front = pair->a; pair->back = pair->b;
@@ -331,6 +334,8 @@ free_csd_surfaces(_GLFWwindow *window) {
static void
free_csd_buffers(_GLFWwindow *window) {
#define d(which) { \
if (decs.which.buffer.a_needs_to_be_destroyed && decs.which.buffer.a) wl_buffer_destroy(decs.which.buffer.a); \
if (decs.which.buffer.b_needs_to_be_destroyed && decs.which.buffer.b) wl_buffer_destroy(decs.which.buffer.b); \
memset(&decs.which.buffer, 0, sizeof(_GLFWWaylandBufferPair)); \
}
d(left); d(top); d(right); d(bottom);
@@ -353,9 +358,10 @@ create_csd_surfaces(_GLFWwindow *window, _GLFWWaylandCSDEdge *s) {
}
#define damage_csd(which, xbuffer) \
wl_surface_attach(decs.which.surface, xbuffer, 0, 0); \
wl_surface_attach(decs.which.surface, (xbuffer), 0, 0); \
wl_surface_damage(decs.which.surface, 0, 0, decs.which.buffer.width, decs.which.buffer.height); \
wl_surface_commit(decs.which.surface)
wl_surface_commit(decs.which.surface); \
if (decs.which.buffer.a == (xbuffer)) { decs.which.buffer.a_needs_to_be_destroyed = false; } else { decs.which.buffer.b_needs_to_be_destroyed = false; }
bool
ensure_csd_resources(_GLFWwindow *window) {
@@ -450,8 +456,5 @@ set_titlebar_color(_GLFWwindow *window, uint32_t color, bool use_system_color) {
decs.use_custom_titlebar_color = use_custom_color;
decs.titlebar_color = color;
}
if (window->decorated && decs.top.surface) {
update_title_bar(window);
damage_csd(top, decs.top.buffer.front);
}
change_csd_title(window);
}

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)

22
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)
@@ -102,6 +104,7 @@ typedef struct _GLFWWaylandBufferPair {
struct { uint8_t *a, *b, *front, *back; } data;
bool has_pending_update;
size_t size_in_bytes, width, height, stride;
bool a_needs_to_be_destroyed, b_needs_to_be_destroyed;
} _GLFWWaylandBufferPair;
typedef struct _GLFWWaylandCSDEdge {
@@ -124,6 +127,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 +287,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 +328,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);
}

14
glfw/x11_init.c vendored
View File

@@ -137,6 +137,8 @@ static void detectEWMH(void)
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_WINDOW_TYPE");
_glfw.x11.NET_WM_WINDOW_TYPE_NORMAL =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_WINDOW_TYPE_NORMAL");
_glfw.x11.NET_WM_WINDOW_TYPE_DOCK =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_WINDOW_TYPE_DOCK");
_glfw.x11.NET_WORKAREA =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WORKAREA");
_glfw.x11.NET_CURRENT_DESKTOP =
@@ -147,6 +149,8 @@ static void detectEWMH(void)
getAtomIfSupported(supportedAtoms, atomCount, "_NET_FRAME_EXTENTS");
_glfw.x11.NET_REQUEST_FRAME_EXTENTS =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_REQUEST_FRAME_EXTENTS");
_glfw.x11.NET_WM_STRUT_PARTIAL =
getAtomIfSupported(supportedAtoms, atomCount, "_NET_WM_STRUT_PARTIAL");
XFree(supportedAtoms);
}
@@ -680,8 +684,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)
{

18
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
@@ -247,6 +253,7 @@ typedef struct _GLFWlibraryX11
Atom NET_WM_PING;
Atom NET_WM_WINDOW_TYPE;
Atom NET_WM_WINDOW_TYPE_NORMAL;
Atom NET_WM_WINDOW_TYPE_DOCK;
Atom NET_WM_STATE;
Atom NET_WM_STATE_ABOVE;
Atom NET_WM_STATE_FULLSCREEN;
@@ -262,6 +269,7 @@ typedef struct _GLFWlibraryX11
Atom NET_ACTIVE_WINDOW;
Atom NET_FRAME_EXTENTS;
Atom NET_REQUEST_FRAME_EXTENTS;
Atom NET_WM_STRUT_PARTIAL;
Atom MOTIF_WM_HINTS;
// Xdnd (drag and drop) atoms
@@ -291,6 +299,8 @@ typedef struct _GLFWlibraryX11
// XRM database atom
Atom RESOURCE_MANAGER;
// Atoms for MIME types
AtomArray mime_atoms, clipboard_atoms, primary_atoms;
struct {
bool available;

366
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]));
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,112 @@ 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 = reallocarray(aa->array, aa->capacity, sizeof(aa->array[0]));
}
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;
if (strcmp(mime_type, "text/plain") == 0) {
// we need to do this because GTK/GNOME is developed by morons
// they convert text/plain to DOS line endings
// https://gitlab.gnome.org/GNOME/gtk/-/issues/2307
atoms[count++] = atom_for_mime("text/plain;charset=utf-8").atom;
atoms[count++] = _glfw.x11.UTF8_STRING;
atoms[count++] = atom_for_mime("text/plain").atom;
atoms[count++] = XA_STRING;
} else {
atoms[count++] = atom_for_mime(mime_type).atom;
}
getSelectionString(which, atoms, count, write_data, object, true);
}
EGLenum _glfwPlatformGetEGLPlatform(EGLint** attribs)
@@ -3134,3 +3197,18 @@ GLFWAPI int glfwSetX11LaunchCommand(GLFWwindow *handle, char **argv, int argc)
_GLFWwindow* window = (_GLFWwindow*) handle;
return XSetCommand(_glfw.x11.display, window->x11.handle, argv, argc);
}
GLFWAPI void glfwSetX11WindowAsDock(int32_t x11_window_id) {
_GLFW_REQUIRE_INIT();
Atom type = _glfw.x11.NET_WM_WINDOW_TYPE_DOCK;
XChangeProperty(_glfw.x11.display, x11_window_id,
_glfw.x11.NET_WM_WINDOW_TYPE, XA_ATOM, 32,
PropModeReplace, (unsigned char*) &type, 1);
}
GLFWAPI void glfwSetX11WindowStrut(int32_t x11_window_id, uint32_t dimensions[12]) {
_GLFW_REQUIRE_INIT();
XChangeProperty(_glfw.x11.display, x11_window_id,
_glfw.x11.NET_WM_STRUT_PARTIAL, XA_CARDINAL, 32,
PropModeReplace, (unsigned char*) dimensions, 12);
}

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__':

View File

@@ -1,3 +1,3 @@
# generated by gen-wcwidth.py, do not edit
url_delimiters = '\x00-\x09\x0b-\x0c\x0e-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u0890-\u0891\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U000110cd\U00013430-\U00013438\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd' # noqa
url_delimiters = '\x00-\x09\x0b-\x0c\x0e-\x20\x7f-\xa0\xad\u0600-\u0605\u061c\u06dd\u070f\u0890-\u0891\u08e2\u1680\u180e\u2000-\u200f\u2028-\u202f\u205f-\u2064\u2066-\u206f\u3000\ud800-\uf8ff\ufeff\ufff9-\ufffb\U000110bd\U000110cd\U00013430-\U0001343f\U0001bca0-\U0001bca3\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f\U000f0000-\U000ffffd\U00100000-\U0010fffd' # noqa

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

@@ -2,8 +2,6 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
import shutil
import subprocess
import sys
from typing import Any, Callable, Dict, List, Tuple
@@ -11,6 +9,7 @@ from kitty.cli import parse_args
from kitty.cli_stub import PanelCLIOptions
from kitty.constants import appname, is_macos
from kitty.os_window_size import WindowSizeData
from kitty.fast_data_types import make_x11_window_a_dock_window
OPTIONS = r'''
--lines
@@ -65,14 +64,7 @@ def parse_panel_args(args: List[str]) -> Tuple[PanelCLIOptions, List[str]]:
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten panel', result_class=PanelCLIOptions)
def call_xprop(*cmd: str, silent: bool = False) -> None:
cmd_ = ['xprop'] + list(cmd)
try:
cp = subprocess.run(cmd_, stdout=subprocess.DEVNULL if silent else None)
except FileNotFoundError:
raise SystemExit('You must have the xprop program installed')
if cp.returncode != 0:
raise SystemExit(cp.returncode)
Strut = Tuple[int, int, int, int, int, int, int, int, int, int, int, int]
def create_strut(
@@ -80,43 +72,33 @@ def create_strut(
left: int = 0, right: int = 0, top: int = 0, bottom: int = 0, left_start_y: int = 0, left_end_y: int = 0,
right_start_y: int = 0, right_end_y: int = 0, top_start_x: int = 0, top_end_x: int = 0,
bottom_start_x: int = 0, bottom_end_x: int = 0
) -> None:
call_xprop(
'-id',
str(int(win_id)), '-format', '_NET_WM_STRUT_PARTIAL', '32cccccccccccc',
'-set', '_NET_WM_STRUT_PARTIAL',
f'{left},{right},{top},{bottom},'
f'{left_start_y},{left_end_y},{right_start_y},{right_end_y},'
f'{top_start_x},{top_end_x},{bottom_start_x},{bottom_end_x}'
)
) -> Strut:
return left, right, top, bottom, left_start_y, left_end_y, right_start_y, right_end_y, top_start_x, top_end_x, bottom_start_x, bottom_end_x
def create_top_strut(win_id: int, width: int, height: int) -> None:
create_strut(win_id, top=height, top_end_x=width)
def create_top_strut(win_id: int, width: int, height: int) -> Strut:
return create_strut(win_id, top=height, top_end_x=width)
def create_bottom_strut(win_id: int, width: int, height: int) -> None:
create_strut(win_id, bottom=height, bottom_end_x=width)
def create_bottom_strut(win_id: int, width: int, height: int) -> Strut:
return create_strut(win_id, bottom=height, bottom_end_x=width)
def create_left_strut(win_id: int, width: int, height: int) -> None:
create_strut(win_id, left=width, left_end_y=height)
def create_left_strut(win_id: int, width: int, height: int) -> Strut:
return create_strut(win_id, left=width, left_end_y=height)
def create_right_strut(win_id: int, width: int, height: int) -> None:
create_strut(win_id, right=width, right_end_y=height)
def create_right_strut(win_id: int, width: int, height: int) -> Strut:
return create_strut(win_id, right=width, right_end_y=height)
window_width = window_height = 0
def setup_x11_window(win_id: int) -> None:
call_xprop(
'-id', str(win_id), '-format', '_NET_WM_WINDOW_TYPE', '32a',
'-set', '_NET_WM_WINDOW_TYPE', '_NET_WM_WINDOW_TYPE_DOCK'
)
func = globals()[f'create_{args.edge}_strut']
func(win_id, window_width, window_height)
strut = func(win_id, window_width, window_height)
make_x11_window_a_dock_window(win_id, strut)
def initial_window_size_func(opts: WindowSizeData, cached_values: Dict[str, Any]) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
@@ -140,8 +122,6 @@ def main(sys_args: List[str]) -> None:
global args
if is_macos or not os.environ.get('DISPLAY'):
raise SystemExit('Currently the panel kitten is supported only on X11 desktops')
if not shutil.which('xprop'):
raise SystemExit('The xprop program is required for the panel kitten')
args, items = parse_panel_args(sys_args[1:])
if not items:
raise SystemExit('You must specify the program to run')

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()

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,8 @@
#version GLSL_VERSION
#define left -1.0f
#define top 1.0f
#define right 1.0f
#define bottom -1.0f
#define left 0
#define top 1
#define right 2
#define bottom 3
#define tex_left 0
#define tex_top 0
#define tex_right 1
@@ -14,15 +14,10 @@
uniform float tiled;
uniform vec4 sizes; // [ window_width, window_height, image_width, image_height ]
uniform vec4 positions; // [ left, top, right, bottom ]
out vec2 texcoord;
const vec2 pos_map[] = vec2[4](
vec2(left, top),
vec2(left, bottom),
vec2(right, bottom),
vec2(right, top)
);
const vec2 tex_map[] = vec2[4](
vec2(tex_left, tex_top),
vec2(tex_left, tex_bottom),
@@ -39,6 +34,12 @@ float tiling_factor(int i) {
}
void main() {
vec2 pos_map[] = vec2[4](
vec2(positions[left], positions[top]),
vec2(positions[left], positions[bottom]),
vec2(positions[right], positions[bottom]),
vec2(positions[right], positions[top])
);
vec2 tex_coords = tex_map[gl_VertexID];
texcoord = vec2(
tex_coords[x_axis] * tiling_factor(x_axis),

View File

@@ -1,8 +1,7 @@
#version GLSL_VERSION
uniform float background_opacity;
in vec3 color;
in vec4 color;
out vec4 final_color;
void main() {
final_color = vec4(color * background_opacity, background_opacity);
final_color = color;
}

View File

@@ -6,9 +6,11 @@ uniform vec3 inactive_border_color;
uniform vec3 bell_border_color;
uniform vec3 tab_bar_bg;
uniform vec3 tab_bar_margin_color;
uniform float background_opacity;
uniform float tint_opacity, tint_premult;
in vec4 rect; // left, top, right, bottom
in uint rect_color;
out vec3 color;
out vec4 color;
// indices into the rect vector
const int LEFT = 0;
@@ -35,5 +37,9 @@ void main() {
gl_Position = vec4(rect[pos.x], rect[pos.y], 0, 1);
int rc = int(rect_color);
vec3 window_bg = vec3(to_color(rect_color >> 24), to_color(rect_color >> 16), to_color(rect_color >> 8));
color = W(0, default_bg) + W(1, active_border_color) + W(2, inactive_border_color) + W(3, window_bg) + W(4, bell_border_color) + W(5, tab_bar_bg) + W(6, tab_bar_margin_color);
vec3 color3 = W(0, default_bg) + W(1, active_border_color) + W(2, inactive_border_color) + W(3, window_bg) + W(4, bell_border_color) + W(5, tab_bar_bg) + W(6, tab_bar_margin_color);
float is_default_bg = float(rc & 1);
float final_opacity = is_default_bg * tint_opacity + (1 - is_default_bg) * background_opacity;
float final_premult_opacity = is_default_bg * tint_premult + (1 - is_default_bg) * background_opacity;
color = vec4(color3 * final_premult_opacity, final_opacity);
}

View File

@@ -86,7 +86,7 @@ class Borders:
draw_minimal_borders = opts.draw_minimal_borders and max(opts.window_margin_width) < 1
add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, BorderColor.default_bg)
has_background_image = os_window_has_background_image(self.os_window_id)
if not has_background_image:
if not has_background_image or opts.background_tint > 0.0:
for br in current_layout.blank_rects:
add_borders_rect(self.os_window_id, self.tab_id, *br, BorderColor.default_bg)
for tbr in tab_bar_rects:

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(
@@ -1653,7 +1700,11 @@ class Boss:
cmd = [kitty_exe(), '@']
aw = self.active_window
if aw is not None:
kw['env'] = {'KITTY_SHELL_ACTIVE_WINDOW_ID': str(aw.id)}
env = {'KITTY_SHELL_ACTIVE_WINDOW_ID': str(aw.id)}
at = self.active_tab
if at is not None:
env['KITTY_SHELL_ACTIVE_TAB_ID'] = str(at.id)
kw['env'] = env
if window_type == 'tab':
tab = self._new_tab(SpecialWindow(cmd, **kw))
if tab is not None:
@@ -1702,7 +1753,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:
@@ -1896,18 +1959,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)
@@ -2143,8 +2216,8 @@ class Boss:
if bad_lines:
self.show_bad_config_lines(bad_lines)
self.apply_new_options(opts)
from .open_actions import load_open_actions
load_open_actions.clear_cached()
from .open_actions import clear_caches
clear_caches()
def safe_delete_temp_file(self, path: str) -> None:
if is_path_in_temp_dir(path):
@@ -2414,8 +2487,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

@@ -687,16 +687,9 @@ render_os_window(OSWindow *os_window, unsigned int active_window_id, color_type
if (os_window->clear_count++ < 3) blank_os_window(os_window);
Tab *tab = os_window->tabs + os_window->active_tab;
BorderRects *br = &tab->border_rects;
bool static_live_resize_in_progress = os_window->live_resize.in_progress && OPT(resize_draw_strategy) == RESIZE_DRAW_STATIC;
float x_ratio = 1, y_ratio = 1;
if (static_live_resize_in_progress) {
x_ratio = (float) os_window->viewport_width / (float) os_window->live_resize.width;
y_ratio = (float) os_window->viewport_height / (float) os_window->live_resize.height;
}
if (!static_live_resize_in_progress) {
draw_borders(br->vao_idx, br->num_border_rects, br->rect_buf, br->is_dirty, os_window->viewport_width, os_window->viewport_height, active_window_bg, num_visible_windows, all_windows_have_same_bg, os_window);
br->is_dirty = false;
}
draw_borders(br->vao_idx, br->num_border_rects, br->rect_buf, br->is_dirty, os_window->viewport_width, os_window->viewport_height, active_window_bg, num_visible_windows, all_windows_have_same_bg, os_window);
br->is_dirty = false;
if (TD.screen && os_window->num_tabs >= OPT(tab_bar_min_tabs)) draw_cells(TD.vao_idx, 0, &TD, x_ratio, y_ratio, os_window, true, false, NULL);
for (unsigned int i = 0; i < tab->num_windows; i++) {
Window *w = tab->windows + i;
@@ -768,7 +761,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 +967,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
@@ -169,7 +170,7 @@ def set_default_env(val: Optional[Dict[str, str]] = None) -> None:
def set_LANG_in_default_env(val: str) -> None:
default_env()['LANG'] = val
default_env().setdefault('LANG', val)
def openpty() -> Tuple[int, int]:

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

@@ -862,6 +862,65 @@ cocoa_set_url_handler(PyObject UNUSED *self, PyObject *args) {
} // autoreleasepool
}
static PyObject*
cocoa_set_app_icon(PyObject UNUSED *self, PyObject *args) {
@autoreleasepool {
const char *icon_path = NULL, *app_path = NULL;
if (!PyArg_ParseTuple(args, "s|z", &icon_path, &app_path)) return NULL;
if (!icon_path || icon_path[0] == '\0') {
PyErr_SetString(PyExc_TypeError, "Empty icon file path");
return NULL;
}
NSString *custom_icon_path = [NSString stringWithUTF8String:icon_path];
NSString *bundle_path = @"";
if (!app_path) {
bundle_path = [[NSBundle mainBundle] bundlePath];
if (!bundle_path || bundle_path.length == 0) bundle_path = @"/Applications/kitty.app";
} else if (app_path[0] != '\0') {
bundle_path = [NSString stringWithUTF8String:app_path];
}
if (![[NSFileManager defaultManager] fileExistsAtPath:custom_icon_path]) {
PyErr_Format(PyExc_FileNotFoundError, "Icon file not found: %s", [custom_icon_path UTF8String]);
return NULL;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:bundle_path]) {
PyErr_Format(PyExc_FileNotFoundError, "Application bundle not found: %s", [bundle_path UTF8String]);
return NULL;
}
NSImage *icon_image = [[NSImage alloc] initWithContentsOfFile:custom_icon_path];
BOOL result = [[NSWorkspace sharedWorkspace] setIcon:icon_image forFile:bundle_path options:NSExcludeQuickDrawElementsIconCreationOption];
[icon_image release];
if (result) Py_RETURN_NONE;
PyErr_Format(PyExc_OSError, "Failed to set custom icon %s for %s", [custom_icon_path UTF8String], [bundle_path UTF8String]);
return NULL;
} // autoreleasepool
}
static PyObject*
cocoa_set_dock_icon(PyObject UNUSED *self, PyObject *args) {
@autoreleasepool {
const char *icon_path = NULL;
if (!PyArg_ParseTuple(args, "s", &icon_path)) return NULL;
if (!icon_path || icon_path[0] == '\0') {
PyErr_SetString(PyExc_TypeError, "Empty icon file path");
return NULL;
}
NSString *custom_icon_path = [NSString stringWithUTF8String:icon_path];
if ([[NSFileManager defaultManager] fileExistsAtPath:custom_icon_path]) {
NSImage *icon_image = [[[NSImage alloc] initWithContentsOfFile:custom_icon_path] autorelease];
[NSApplication sharedApplication].applicationIconImage = icon_image;
Py_RETURN_NONE;
}
return NULL;
} // autoreleasepool
}
static NSSound *beep_sound = nil;
static void
@@ -923,6 +982,8 @@ static PyMethodDef module_methods[] = {
{"cocoa_send_notification", (PyCFunction)cocoa_send_notification, METH_VARARGS, ""},
{"cocoa_set_notification_activated_callback", (PyCFunction)set_notification_activated_callback, METH_O, ""},
{"cocoa_set_url_handler", (PyCFunction)cocoa_set_url_handler, METH_VARARGS, ""},
{"cocoa_set_app_icon", (PyCFunction)cocoa_set_app_icon, METH_VARARGS, ""},
{"cocoa_set_dock_icon", (PyCFunction)cocoa_set_dock_icon, METH_VARARGS, ""},
{NULL, NULL, 0, NULL} /* Sentinel */
};

View File

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

View File

@@ -65,7 +65,7 @@ typedef enum MouseTrackingModes { NO_TRACKING, BUTTON_MODE, MOTION_MODE, ANY_MOD
typedef enum MouseTrackingProtocols { NORMAL_PROTOCOL, UTF8_PROTOCOL, SGR_PROTOCOL, URXVT_PROTOCOL, SGR_PIXEL_PROTOCOL} MouseTrackingProtocol;
typedef enum MouseShapes { BEAM, HAND, ARROW } MouseShape;
typedef enum { NONE, MENUBAR, WINDOW, ALL } WindowTitleIn;
typedef enum { TILING, SCALED, MIRRORED, CLAMPED } BackgroundImageLayout;
typedef enum { TILING, SCALED, MIRRORED, CLAMPED, CENTER_CLAMPED } BackgroundImageLayout;
typedef struct ImageAnchorPosition {
float canvas_x, canvas_y, image_x, image_y;
} ImageAnchorPosition;

26
kitty/emoji.h generated
View File

@@ -1,4 +1,4 @@
// unicode data, built from the unicode standard on: 2022-01-05
// unicode data, built from the unicode standard on: 2022-09-29
// see gen-wcwidth.py
#pragma once
#include "data-types.h"
@@ -739,13 +739,13 @@ is_symbol(char_type code) {
return true;
case 0x1f300 ... 0x1f6d7:
return true;
case 0x1f6dd ... 0x1f6ec:
case 0x1f6dc ... 0x1f6ec:
return true;
case 0x1f6f0 ... 0x1f6fc:
return true;
case 0x1f700 ... 0x1f773:
case 0x1f700 ... 0x1f776:
return true;
case 0x1f780 ... 0x1f7d8:
case 0x1f77b ... 0x1f7d9:
return true;
case 0x1f7e0 ... 0x1f7eb:
return true;
@@ -767,23 +767,19 @@ is_symbol(char_type code) {
return true;
case 0x1fa60 ... 0x1fa6d:
return true;
case 0x1fa70 ... 0x1fa74:
case 0x1fa70 ... 0x1fa7c:
return true;
case 0x1fa78 ... 0x1fa7c:
case 0x1fa80 ... 0x1fa88:
return true;
case 0x1fa80 ... 0x1fa86:
case 0x1fa90 ... 0x1fabd:
return true;
case 0x1fa90 ... 0x1faac:
case 0x1fabf ... 0x1fac5:
return true;
case 0x1fab0 ... 0x1faba:
case 0x1face ... 0x1fadb:
return true;
case 0x1fac0 ... 0x1fac5:
case 0x1fae0 ... 0x1fae8:
return true;
case 0x1fad0 ... 0x1fad9:
return true;
case 0x1fae0 ... 0x1fae7:
return true;
case 0x1faf0 ... 0x1faf6:
case 0x1faf0 ... 0x1faf8:
return true;
case 0x1fb00 ... 0x1fb92:
return true;

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, ...],
@@ -740,7 +730,15 @@ def cocoa_get_lang() -> Optional[str]:
pass
def cocoa_set_url_handler(url_scheme: str, bundle_id: Optional[str]) -> None:
def cocoa_set_url_handler(url_scheme: str, bundle_id: Optional[str] = None) -> None:
pass
def cocoa_set_app_icon(icon_path: str, app_path: Optional[str] = None) -> None:
pass
def cocoa_set_dock_icon(icon_path: str) -> None:
pass
@@ -764,11 +762,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 +1479,8 @@ 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: ...
def make_x11_window_a_dock_window(x11_window_id: int, strut: Tuple[int, int, int, int, int, int, int, int, int, int, int, int]) -> None: ...

20
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
@@ -461,6 +467,12 @@ load_glfw(const char* path) {
*(void **) (&glfwSetX11LaunchCommand_impl) = dlsym(handle, "glfwSetX11LaunchCommand");
if (glfwSetX11LaunchCommand_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwSetX11WindowAsDock_impl) = dlsym(handle, "glfwSetX11WindowAsDock");
if (glfwSetX11WindowAsDock_impl == NULL) dlerror(); // clear error indicator
*(void **) (&glfwSetX11WindowStrut_impl) = dlsym(handle, "glfwSetX11WindowStrut");
if (glfwSetX11WindowStrut_impl == NULL) dlerror(); // clear error indicator
return NULL;
}

41
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
@@ -2209,4 +2230,12 @@ typedef int (*glfwSetX11LaunchCommand_func)(GLFWwindow*, char**, int);
GFW_EXTERN glfwSetX11LaunchCommand_func glfwSetX11LaunchCommand_impl;
#define glfwSetX11LaunchCommand glfwSetX11LaunchCommand_impl
typedef void (*glfwSetX11WindowAsDock_func)(int32_t);
GFW_EXTERN glfwSetX11WindowAsDock_func glfwSetX11WindowAsDock_impl;
#define glfwSetX11WindowAsDock glfwSetX11WindowAsDock_impl
typedef void (*glfwSetX11WindowStrut_func)(int32_t, uint32_t[12]);
GFW_EXTERN glfwSetX11WindowStrut_func glfwSetX11WindowStrut_impl;
#define glfwSetX11WindowStrut glfwSetX11WindowStrut_impl
const char* load_glfw(const char* path);

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,111 @@ 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;
}
static PyObject*
make_x11_window_a_dock_window(PyObject *self UNUSED, PyObject *args UNUSED) {
int x11_window_id;
PyObject *dims;
if (!PyArg_ParseTuple(args, "iO!", &x11_window_id, &PyTuple_Type, &dims)) return NULL;
if (PyTuple_GET_SIZE(dims) != 12 ) { PyErr_SetString(PyExc_TypeError, "dimesions must be a tuple of length 12"); return NULL; }
if (!glfwSetX11WindowAsDock) { PyErr_SetString(PyExc_RuntimeError, "Failed to load glfwGetX11Window"); return NULL; }
uint32_t dimensions[12];
for (Py_ssize_t i = 0; i < 12; i++) dimensions[i] = PyLong_AsUnsignedLong(PyTuple_GET_ITEM(dims, i));
if (PyErr_Occurred()) return NULL;
glfwSetX11WindowAsDock(x11_window_id);
glfwSetX11WindowStrut(x11_window_id, dimensions);
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(make_x11_window_a_dock_window, METH_VARARGS),
METHODB(strip_csi, METH_O),
#ifndef __APPLE__
METHODB(dbus_send_notification, METH_VARARGS),
@@ -1654,6 +1755,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

@@ -566,7 +566,7 @@ class Splits(Layout):
ans['one'] = p.one
if isinstance(p.two, Pair):
ans['two'] = add_pair(p.two)
elif p.one is not None:
elif p.two is not None:
ans['two'] = p.two
return ans

View File

@@ -16,14 +16,13 @@ from .cli_stub import CLIOptions
from .conf.utils import BadLine
from .config import cached_values_for
from .constants import (
appname, beam_cursor_data_file, clear_handled_signals, config_dir,
glfw_path, is_macos, is_wayland, kitty_exe, logo_png_file,
running_in_kitty, website_url
appname, beam_cursor_data_file, clear_handled_signals, config_dir, glfw_path,
is_macos, is_wayland, kitty_exe, logo_png_file, running_in_kitty, website_url,
)
from .fast_data_types import (
GLFW_IBEAM_CURSOR, GLFW_MOD_ALT, GLFW_MOD_SHIFT, SingleKey,
create_os_window, free_font_data, glfw_init, glfw_terminate, load_png_data,
set_custom_cursor, set_default_window_icon, set_options
GLFW_IBEAM_CURSOR, GLFW_MOD_ALT, GLFW_MOD_SHIFT, SingleKey, create_os_window,
free_font_data, glfw_init, glfw_terminate, load_png_data, set_custom_cursor,
set_default_window_icon, set_options,
)
from .fonts.box_drawing import set_scale
from .fonts.render import set_font_family
@@ -33,8 +32,8 @@ from .os_window_size import initial_window_size_func
from .prewarm import PrewarmProcess, fork_prewarm_process
from .session import create_sessions, get_os_window_sizing_data
from .utils import (
cleanup_ssh_control_masters, detach, expandvars, log_error,
single_instance, startup_notification_handler, unix_socket_paths
cleanup_ssh_control_masters, detach, expandvars, log_error, single_instance,
startup_notification_handler, unix_socket_paths,
)
from .window import load_shader_programs
@@ -61,7 +60,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:
@@ -135,6 +134,32 @@ def get_macos_shortcut_for(
return ans
def safe_mtime(path: str) -> Optional[float]:
with suppress(OSError):
return os.path.getmtime(path)
return None
def set_macos_app_custom_icon() -> None:
for name in ('kitty.app.icns', 'kitty.app.png'):
icon_path = os.path.join(config_dir, name)
custom_icon_mtime = safe_mtime(icon_path)
if custom_icon_mtime is not None:
from .fast_data_types import cocoa_set_app_icon, cocoa_set_dock_icon
krd = getattr(sys, 'kitty_run_data')
bundle_path = os.path.dirname(os.path.dirname(krd.get('bundle_exe_dir')))
icon_sentinel = os.path.join(bundle_path, 'Icon\r')
sentinel_mtime = safe_mtime(icon_sentinel)
if sentinel_mtime is None or sentinel_mtime < custom_icon_mtime:
cocoa_set_app_icon(icon_path)
# macOS Dock does not reload icons until it is restarted, so we set
# the application icon here. This will revert when kitty quits, but
# cant be helped since there appears to be no way to get the dock
# to reload short of killing it.
cocoa_set_dock_icon(icon_path)
break
def set_x11_window_icon() -> None:
# max icon size on X11 64bits is 128x128
path, ext = os.path.splitext(logo_png_file)
@@ -167,8 +192,11 @@ def _run_app(opts: Options, args: CLIOptions, prewarm: PrewarmProcess, bad_lines
val = get_macos_shortcut_for(func_map, f'open_url {website_url()}', lookup_name='open_kitty_website')
if val is not None:
global_shortcuts['open_kitty_website'] = val
if is_macos and opts.macos_custom_beam_cursor:
set_custom_ibeam_cursor()
if opts.macos_custom_beam_cursor:
set_custom_ibeam_cursor()
set_macos_app_custom_icon()
if not is_wayland() and not is_macos: # no window icons on wayland
set_x11_window_icon()
with cached_values_for(run_app.cached_values_name) as cached_values:
@@ -208,7 +236,7 @@ class AppRunner:
free_font_data() # must free font data before glfw/freetype/fontconfig/opengl etc are finalized
if is_macos:
from kitty.fast_data_types import (
cocoa_set_notification_activated_callback
cocoa_set_notification_activated_callback,
)
cocoa_set_notification_activated_callback(None)
@@ -379,6 +407,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

@@ -5,27 +5,44 @@
# in kitty, when using the spawn launcher.
import os
from concurrent.futures import ProcessPoolExecutor
from multiprocessing import util # type: ignore
from multiprocessing import util
from multiprocessing import context, get_all_start_methods, get_context, spawn
from typing import Any, Callable, List, Optional, Tuple, Union
from typing import Any, Callable, Sequence, Optional, Tuple, Union, TYPE_CHECKING
from .constants import kitty_exe
orig_spawn_passfds = util.spawnv_passfds
orig_executable = spawn.get_executable()
if TYPE_CHECKING:
from pickle import PickleBuffer
from array import array
from mmap import mmap
from ctypes import _CData
from typing import SupportsInt, SupportsIndex, Protocol
def spawnv_passfds(path: str, args: List[str], passfds: List[int]) -> Any:
class SupportsTrunc(Protocol):
def __trunc__(self) -> int: ...
ArgsType = Sequence[
Union[str, Union[bytes, Union[bytearray, memoryview, array[Any], mmap, _CData, PickleBuffer]], SupportsInt, SupportsIndex, SupportsTrunc]
]
else:
ArgsType = Sequence[str]
def spawnv_passfds(path: bytes, args: ArgsType, passfds: Sequence[int]) -> int:
if '-c' in args:
idx = args.index('-c')
patched_args = [spawn.get_executable(), '+runpy'] + args[idx + 1:]
patched_args = [spawn.get_executable(), '+runpy'] + list(args)[idx + 1:]
else:
idx = args.index('--multiprocessing-fork')
prog = 'from multiprocessing.spawn import spawn_main; spawn_main(%s)'
prog %= ', '.join(item for item in args[idx+1:])
prog %= ', '.join(str(item) for item in args[idx+1:])
patched_args = [spawn.get_executable(), '+runpy', prog]
return orig_spawn_passfds(kitty_exe(), patched_args, passfds)
return orig_spawn_passfds(os.fsencode(kitty_exe()), patched_args, passfds)
def monkey_patch_multiprocessing() -> None:

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

@@ -221,6 +221,11 @@ def load_launch_actions() -> Tuple[OpenAction, ...]:
return tuple(parse(f))
def clear_caches() -> None:
load_open_actions.clear_cached()
load_launch_actions.clear_cached()
@run_once
def default_open_actions() -> Tuple[OpenAction, ...]:
return tuple(parse('''\

View File

@@ -402,9 +402,9 @@ opt('url_style', 'curly',
opt('open_url_with', 'default',
option_type='to_cmdline',
long_text='''
The program to open clicked URLs. The special value :code:`default` means to
use the operating system's default URL handler (:program:`open` on macOS and
:program:`xdg-open` on Linux).
The program to open clicked URLs. The special value :code:`default` with first look for any URL handlers defined via
the :doc:`open_actions` facility and if non are found, it will use the Operating System's
default URL handler (:program:`open` on macOS and :program:`xdg-open` on Linux).
'''
)
@@ -1285,10 +1285,10 @@ opt('background_image', 'none',
)
opt('background_image_layout', 'tiled',
choices=('mirror-tiled', 'scaled', 'tiled', 'clamped'), ctype='bglayout',
choices=('mirror-tiled', 'scaled', 'tiled', 'clamped', 'centered'), ctype='bglayout',
long_text='''
Whether to tile, scale or clamp the background image. The value can be one of
:code:`tiled`, :code:`mirror-tiled`, :code:`scaled`, :code:`clamped`.
:code:`tiled`, :code:`mirror-tiled`, :code:`scaled`, :code:`clamped` or :code:`centered`.
'''
)
@@ -1310,11 +1310,10 @@ this option by reloading the config is not supported.
opt('background_tint', '0.0',
option_type='unit_float', ctype='float',
long_text='''
How much to tint the background image by the background color. The tint is
applied only under the text area, not margin/borders. This option makes it
easier to read the text. Tinting is done using the current background color for
each window. This option applies only if :opt:`background_opacity` is set and
transparent windows are supported or :opt:`background_image` is set.
How much to tint the background image by the background color. This option
makes it easier to read the text. Tinting is done using the current background
color for each window. This option applies only if :opt:`background_opacity` is
set and transparent windows are supported or :opt:`background_image` is set.
'''
)

View File

@@ -73,7 +73,7 @@ class Parser:
raise ValueError(f"The value {val} is not a valid choice for background_image_layout")
ans["background_image_layout"] = val
choices_for_background_image_layout = frozenset(('mirror-tiled', 'scaled', 'tiled', 'clamped'))
choices_for_background_image_layout = frozenset(('mirror-tiled', 'scaled', 'tiled', 'clamped', 'centered'))
def background_image_linear(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:
ans['background_image_linear'] = to_bool(val)

View File

@@ -64,7 +64,9 @@ bglayout(PyObject *layout_name) {
case 't': return TILING;
case 'm': return MIRRORED;
case 's': return SCALED;
case 'c': return CLAMPED;
case 'c': {
return name[1] == 'l' ? CLAMPED : CENTER_CLAMPED;
}
default: break;
}
return TILING;

View File

@@ -15,7 +15,7 @@ import kitty.types
if typing.TYPE_CHECKING:
choices_for_allow_cloning = typing.Literal['yes', 'y', 'true', 'no', 'n', 'false', 'ask']
choices_for_allow_remote_control = typing.Literal['password', 'socket-only', 'socket', 'no', 'n', 'false', 'yes', 'y', 'true']
choices_for_background_image_layout = typing.Literal['mirror-tiled', 'scaled', 'tiled', 'clamped']
choices_for_background_image_layout = typing.Literal['mirror-tiled', 'scaled', 'tiled', 'clamped', 'centered']
choices_for_default_pointer_shape = typing.Literal['arrow', 'beam', 'hand']
choices_for_linux_display_server = typing.Literal['auto', 'wayland', 'x11']
choices_for_macos_colorspace = typing.Literal['srgb', 'default', 'displayp3']

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

@@ -17,6 +17,7 @@ class FocusTab(RemoteCommand):
'''
match/str: The tab to focus
no_response/bool: Boolean indicating whether to wait for a response
'''
short_desc = 'Focus the specified tab'
@@ -32,7 +33,7 @@ using this option means that you will not be notified of failures.
argspec = ''
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'match': opts.match}
return {'match': opts.match, 'no_response': opts.no_response}
def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType:
for tab in self.tabs_for_match_payload(boss, window, payload_get):

View File

@@ -18,6 +18,7 @@ if TYPE_CHECKING:
class FocusWindow(RemoteCommand):
'''
match/str: The window to focus
no_response/bool: Boolean indicating whether to wait for a response
'''
short_desc = 'Focus the specified window'

View File

@@ -17,6 +17,7 @@ class LastUsedLayout(RemoteCommand):
'''
match/str: Which tab to change the layout of
all/bool: Boolean to match all tabs
no_response/bool: Boolean indicating whether to wait for a response
'''
short_desc = 'Switch to the last used layout'
@@ -37,7 +38,7 @@ the command will exit with a success code.
''' + '\n\n\n' + MATCH_TAB_OPTION
def message_to_kitty(self, global_opts: RCOptions, opts: 'CLIOptions', args: ArgsType) -> PayloadType:
return {'match': opts.match, 'all': opts.all}
return {'match': opts.match, 'all': opts.all, 'no_response': opts.no_response}
def response_from_kitty(self, boss: Boss, window: Optional[Window], payload_get: PayloadGetType) -> ResponseType:
for tab in self.tabs_for_match_payload(boss, window, payload_get):

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

@@ -184,7 +184,7 @@ typedef struct {
static CellProgramLayout cell_program_layouts[NUM_PROGRAMS];
static ssize_t blit_vertex_array;
typedef struct {
GLint image_location, tiled_location, sizes_location, opacity_location, premult_location;
GLint image_location, tiled_location, sizes_location, positions_location, opacity_location, premult_location;
} BGImageProgramLayout;
static BGImageProgramLayout bgimage_program_layout = {0};
typedef struct {
@@ -212,6 +212,7 @@ init_cell_program(void) {
bgimage_program_layout.image_location = get_uniform_location(BGIMAGE_PROGRAM, "image");
bgimage_program_layout.opacity_location = get_uniform_location(BGIMAGE_PROGRAM, "opacity");
bgimage_program_layout.sizes_location = get_uniform_location(BGIMAGE_PROGRAM, "sizes");
bgimage_program_layout.positions_location = get_uniform_location(BGIMAGE_PROGRAM, "positions");
bgimage_program_layout.tiled_location = get_uniform_location(BGIMAGE_PROGRAM, "tiled");
bgimage_program_layout.premult_location = get_uniform_location(BGIMAGE_PROGRAM, "premult");
tint_program_layout.tint_color_location = get_uniform_location(TINT_PROGRAM, "tint_color");
@@ -425,16 +426,29 @@ draw_bg(OSWindow *w) {
glUniform1i(bgimage_program_layout.image_location, BGIMAGE_UNIT);
glUniform1f(bgimage_program_layout.opacity_location, OPT(background_opacity));
glUniform4f(bgimage_program_layout.sizes_location,
(GLfloat)w->window_width, (GLfloat)w->window_height, (GLfloat)w->bgimage->width, (GLfloat)w->bgimage->height);
(GLfloat)w->viewport_width, (GLfloat)w->viewport_height, (GLfloat)w->bgimage->width, (GLfloat)w->bgimage->height);
glUniform1f(bgimage_program_layout.premult_location, w->is_semi_transparent ? 1.f : 0.f);
GLfloat tiled = 0.f;;
GLfloat left = -1.0, top = 1.0, right = 1.0, bottom = -1.0;
switch (OPT(background_image_layout)) {
case TILING: case MIRRORED: case CLAMPED:
tiled = 1.f; break;
case SCALED:
tiled = 0.f; break;
case CENTER_CLAMPED:
tiled = 1.f;
if (w->viewport_width > (int)w->bgimage->width) {
GLfloat frac = (w->viewport_width - w->bgimage->width) / (GLfloat)w->viewport_width;
left += frac; right += frac;
}
if (w->viewport_height > (int)w->bgimage->height) {
GLfloat frac = (w->viewport_height - w->bgimage->height) / (GLfloat)w->viewport_height;
top -= frac; bottom -= frac;
}
break;
}
glUniform1f(bgimage_program_layout.tiled_location, tiled);
glUniform4f(bgimage_program_layout.positions_location, left, top, right, bottom);
glActiveTexture(GL_TEXTURE0 + BGIMAGE_UNIT);
glBindTexture(GL_TEXTURE_2D, w->bgimage->texture_id);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
@@ -517,12 +531,12 @@ has_bgimage(OSWindow *w) {
static void
draw_tint(bool premult, Screen *screen, const CellRenderData *crd) {
// On GNOME+Wayland this causes ghosting while rendering. Yet another GNOME bug, does not occur under KDE, Hyprland or Sway.
bind_program(TINT_PROGRAM);
color_type window_bg = colorprofile_to_color(screen->color_profile, screen->color_profile->overridden.default_bg, screen->color_profile->configured.default_bg).rgb;
#define C(shift) ((((GLfloat)((window_bg >> shift) & 0xFF)) / 255.0f))
float alpha = OPT(background_tint);
if (premult) glUniform4f(tint_program_layout.tint_color_location, C(16) * alpha, C(8) * alpha, C(0) * alpha, alpha);
else glUniform4f(tint_program_layout.tint_color_location, C(16), C(8), C(0), alpha);
#define C(shift) ((((GLfloat)((window_bg >> shift) & 0xFF)) / 255.0f)) * premult_factor
GLfloat premult_factor = premult ? OPT(background_tint) : 1.0f;
glUniform4f(tint_program_layout.tint_color_location, C(16), C(8), C(0), OPT(background_tint));
#undef C
glUniform4f(tint_program_layout.edges_location, crd->gl.xstart, crd->gl.ystart - crd->gl.height, crd->gl.xstart + crd->gl.width, crd->gl.ystart);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
@@ -707,7 +721,7 @@ draw_cells_interleaved(ssize_t vao_idx, ssize_t gvao_idx, Screen *screen, OSWind
glUniform1ui(cell_program_layouts[CELL_BG_PROGRAM].draw_bg_bitfield_location, 3);
glDrawArraysInstanced(GL_TRIANGLE_FAN, 0, 4, screen->lines * screen->columns);
} else if (OPT(background_tint) > 0) {
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE);
BLEND_ONTO_OPAQUE;
draw_tint(false, screen, crd);
BLEND_ONTO_OPAQUE;
}
@@ -924,7 +938,7 @@ draw_cells(ssize_t vao_idx, ssize_t gvao_idx, const ScreenRenderData *srd, float
// }}}
// Borders {{{
enum BorderUniforms { BORDER_viewport, BORDER_background_opacity, BORDER_default_bg, BORDER_active_border_color, BORDER_inactive_border_color, BORDER_bell_border_color, BORDER_tab_bar_bg, BORDER_tab_bar_margin_color, NUM_BORDER_UNIFORMS };
enum BorderUniforms { BORDER_viewport, BORDER_background_opacity, BORDER_tint_opacity, BORDER_tint_premult, BORDER_default_bg, BORDER_active_border_color, BORDER_inactive_border_color, BORDER_bell_border_color, BORDER_tab_bar_bg, BORDER_tab_bar_margin_color, NUM_BORDER_UNIFORMS };
static GLint border_uniform_locations[NUM_BORDER_UNIFORMS] = {0};
static void
@@ -932,6 +946,8 @@ init_borders_program(void) {
#define SET_LOC(which) border_uniform_locations[BORDER_##which] = get_uniform_location(BORDERS_PROGRAM, #which);
SET_LOC(viewport)
SET_LOC(background_opacity)
SET_LOC(tint_opacity)
SET_LOC(tint_premult)
SET_LOC(default_bg)
SET_LOC(active_border_color)
SET_LOC(inactive_border_color)
@@ -956,12 +972,17 @@ create_border_vao(void) {
void
draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_buf, bool rect_data_is_dirty, uint32_t viewport_width, uint32_t viewport_height, color_type active_window_bg, unsigned int num_visible_windows, bool all_windows_have_same_bg, OSWindow *w) {
float background_opacity = w->is_semi_transparent ? w->background_opacity: 1.0f;
float tint_opacity = background_opacity;
float tint_premult = background_opacity;
if (has_bgimage(w)) {
glEnable(GL_BLEND);
BLEND_ONTO_OPAQUE;
draw_bg(w);
BLEND_ONTO_OPAQUE;
background_opacity = 1.0f;
tint_opacity = OPT(background_tint);
tint_premult = 1.0f;
}
if (num_border_rects) {
@@ -974,7 +995,9 @@ draw_borders(ssize_t vao_idx, unsigned int num_border_rects, BorderRect *rect_bu
unmap_vao_buffer(vao_idx, 0);
}
#define CV3(x) (((float)((x >> 16) & 0xff))/255.f), (((float)((x >> 8) & 0xff))/255.f), (((float)(x & 0xff))/255.f)
glUniform1f(border_uniform_locations[BORDER_background_opacity], w->is_semi_transparent ? w->background_opacity: 1.0f);
glUniform1f(border_uniform_locations[BORDER_background_opacity], background_opacity);
glUniform1f(border_uniform_locations[BORDER_tint_opacity], tint_opacity);
glUniform1f(border_uniform_locations[BORDER_tint_premult], tint_premult);
glUniform3f(border_uniform_locations[BORDER_active_border_color], CV3(OPT(active_border_color)));
glUniform3f(border_uniform_locations[BORDER_inactive_border_color], CV3(OPT(inactive_border_color)));
glUniform3f(border_uniform_locations[BORDER_bell_border_color], CV3(OPT(bell_border_color)));

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)
@@ -182,7 +185,11 @@ def real_main(global_opts: RCOptions, encrypter: CommandEncrypter = NoEncryption
print('Use {} for assistance or {} to quit'.format(green('help'), green('exit')))
awid = os.environ.pop('KITTY_SHELL_ACTIVE_WINDOW_ID', None)
if awid is not None:
print(f'The ID of the previously active window is: {awid}')
atid = os.environ.pop('KITTY_SHELL_ACTIVE_TAB_ID', None)
am = f'Previously active window id: {awid}'
if atid is not None:
am += f' and tab id: {atid}'
print(am)
pre_prompt = set_window_title('The kitty shell') + set_cursor_shape('bar')
pre_prompt += f'\x1b]133;A;redraw={0 if is_libedit else 1}\x1b\\'

View File

@@ -134,15 +134,14 @@ window_for_window_id(id_type kitty_window_id) {
static void
send_bgimage_to_gpu(BackgroundImageLayout layout, BackgroundImage *bgimage) {
RepeatStrategy r;
RepeatStrategy r = REPEAT_DEFAULT;
switch (layout) {
case SCALED:
case CLAMPED:
case CLAMPED: case CENTER_CLAMPED:
r = REPEAT_CLAMP; break;
case MIRRORED:
r = REPEAT_MIRROR; break;
case TILING:
default:
r = REPEAT_DEFAULT; break;
}
bgimage->texture_id = 0;
@@ -822,14 +821,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 +1302,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

@@ -5,7 +5,7 @@ import os
from functools import lru_cache, partial, wraps
from string import Formatter as StringFormatter
from typing import (
Any, Callable, Dict, List, NamedTuple, Optional, Sequence, Tuple, Union
Any, Callable, Dict, List, NamedTuple, Optional, Sequence, Tuple, Union,
)
from .borders import Border, BorderColor
@@ -13,7 +13,7 @@ from .config import build_ansi_color_table
from .constants import config_dir
from .fast_data_types import (
DECAWM, Color, Region, Screen, cell_size_for_window, get_boss, get_options,
pt_to_px, set_tab_bar_render_data, viewport_for_window
pt_to_px, set_tab_bar_render_data, viewport_for_window,
)
from .rgb import alpha_blend, color_as_sgr, color_from_int, to_color
from .types import WindowGeometry, run_once
@@ -151,6 +151,10 @@ class SupSub:
class ExtraData:
prev_tab: Optional[TabBarData] = None
next_tab: Optional[TabBarData] = None
# true if the draw_tab function is called just for layout. In such cases,
# if drawing is expensive the draw_tab function should avoid drawing and
# just move the cursor to its final position, as if drawing was performed.
for_layout: bool = False
def draw_attributed_string(title: str, screen: Screen) -> None:
@@ -238,7 +242,7 @@ DrawTabFunc = Callable[[DrawData, Screen, TabBarData, int, int, int, bool, Extra
def draw_tab_with_slant(
draw_data: DrawData, screen: Screen, tab: TabBarData,
before: int, max_title_length: int, index: int, is_last: bool,
before: int, max_tab_length: int, index: int, is_last: bool,
extra_data: ExtraData
) -> int:
orig_fg = screen.cursor.fg
@@ -253,20 +257,20 @@ def draw_tab_with_slant(
screen.cursor.bg = tab_bg
screen.cursor.fg = orig_fg
max_title_length += 1
if max_title_length <= 1:
max_tab_length += 1
if max_tab_length <= 1:
screen.draw('')
elif max_title_length == 2:
elif max_tab_length == 2:
screen.draw('…|')
elif max_title_length < 6:
elif max_tab_length < 6:
draw_sep(left_sep)
screen.draw((' ' if max_title_length == 5 else '') + '' + (' ' if max_title_length >= 4 else ''))
screen.draw((' ' if max_tab_length == 5 else '') + '' + (' ' if max_tab_length >= 4 else ''))
draw_sep(right_sep)
else:
draw_sep(left_sep)
screen.draw(' ')
draw_title(draw_data, screen, tab, index, max_title_length)
extra = screen.cursor.x - before - max_title_length
draw_title(draw_data, screen, tab, index, max_tab_length)
extra = screen.cursor.x - before - max_tab_length
if extra >= 0:
screen.cursor.x -= extra + 3
screen.draw('')
@@ -281,15 +285,15 @@ def draw_tab_with_slant(
def draw_tab_with_separator(
draw_data: DrawData, screen: Screen, tab: TabBarData,
before: int, max_title_length: int, index: int, is_last: bool,
before: int, max_tab_length: int, index: int, is_last: bool,
extra_data: ExtraData
) -> int:
if draw_data.leading_spaces:
screen.draw(' ' * draw_data.leading_spaces)
draw_title(draw_data, screen, tab, index, max_title_length)
trailing_spaces = min(max_title_length - 1, draw_data.trailing_spaces)
max_title_length -= trailing_spaces
extra = screen.cursor.x - before - max_title_length
draw_title(draw_data, screen, tab, index, max_tab_length)
trailing_spaces = min(max_tab_length - 1, draw_data.trailing_spaces)
max_tab_length -= trailing_spaces
extra = screen.cursor.x - before - max_tab_length
if extra > 0:
screen.cursor.x -= extra + 1
screen.draw('')
@@ -307,7 +311,7 @@ def draw_tab_with_separator(
def draw_tab_with_fade(
draw_data: DrawData, screen: Screen, tab: TabBarData,
before: int, max_title_length: int, index: int, is_last: bool,
before: int, max_tab_length: int, index: int, is_last: bool,
extra_data: ExtraData
) -> int:
orig_bg = screen.cursor.bg
@@ -317,12 +321,12 @@ def draw_tab_with_fade(
screen.cursor.bg = bg
screen.draw(' ')
screen.cursor.bg = orig_bg
draw_title(draw_data, screen, tab, index, max(0, max_title_length - 8))
extra = screen.cursor.x - before - max_title_length
draw_title(draw_data, screen, tab, index, max(0, max_tab_length - 8))
extra = screen.cursor.x - before - max_tab_length
if extra > 0:
screen.cursor.x = before
draw_title(draw_data, screen, tab, index, max(0, max_title_length - 4))
extra = screen.cursor.x - before - max_title_length
draw_title(draw_data, screen, tab, index, max(0, max_tab_length - 4))
extra = screen.cursor.x - before - max_tab_length
if extra > 0:
screen.cursor.x -= extra + 1
screen.draw('')
@@ -346,7 +350,7 @@ powerline_symbols: Dict[PowerlineStyle, Tuple[str, str]] = {
def draw_tab_with_powerline(
draw_data: DrawData, screen: Screen, tab: TabBarData,
before: int, max_title_length: int, index: int, is_last: bool,
before: int, max_tab_length: int, index: int, is_last: bool,
extra_data: ExtraData
) -> int:
tab_bg = screen.cursor.bg
@@ -369,11 +373,11 @@ def draw_tab_with_powerline(
start_draw = 1
screen.cursor.bg = tab_bg
if min_title_length >= max_title_length:
if min_title_length >= max_tab_length:
screen.draw('')
else:
draw_title(draw_data, screen, tab, index, max_title_length)
extra = screen.cursor.x + start_draw - before - max_title_length
draw_title(draw_data, screen, tab, index, max_tab_length)
extra = screen.cursor.x + start_draw - before - max_tab_length
if extra > 0 and extra + 1 < screen.cursor.x:
screen.cursor.x -= extra + 1
screen.draw('')
@@ -416,14 +420,14 @@ def load_custom_draw_tab() -> DrawTabFunc:
@wraps(func)
def draw_tab(
draw_data: DrawData, screen: Screen, tab: TabBarData,
before: int, max_title_length: int, index: int, is_last: bool,
before: int, max_tab_length: int, index: int, is_last: bool,
extra_data: ExtraData
) -> int:
try:
return func(draw_data, screen, tab, before, max_title_length, index, is_last, extra_data)
return func(draw_data, screen, tab, before, max_tab_length, index, is_last, extra_data)
except Exception as e:
log_error(f'Custom draw tab function failed with error: {e}')
return draw_tab_with_fade(draw_data, screen, tab, before, max_title_length, index, is_last, extra_data)
return draw_tab_with_fade(draw_data, screen, tab, before, max_tab_length, index, is_last, extra_data)
return draw_tab
@@ -435,6 +439,7 @@ class TabBar:
self.num_tabs = 1
self.data_buffer_size = 0
self.blank_rects: Tuple[Border, ...] = ()
self.cell_ranges: List[Tuple[int, int]] = []
self.laid_out_once = False
self.apply_options()
@@ -571,31 +576,67 @@ class TabBar:
if not self.laid_out_once:
return
s = self.screen
s.cursor.x = 0
s.erase_in_line(2, False)
max_title_length = max(1, (self.screen.columns // max(1, len(data))) - 1)
cr = []
last_tab = data[-1] if data else None
ed = ExtraData()
for i, t in enumerate(data):
def draw_tab(i: int, tab: TabBarData, cell_ranges: List[Tuple[int, int]], max_tab_length: int) -> None:
ed.prev_tab = data[i - 1] if i > 0 else None
ed.next_tab = data[i + 1] if i + 1 < len(data) else None
s.cursor.bg = as_rgb(self.draw_data.tab_bg(t))
s.cursor.fg = as_rgb(self.draw_data.tab_fg(t))
s.cursor.bold, s.cursor.italic = self.active_font_style if t.is_active else self.inactive_font_style
before = s.cursor.x
end = self.draw_func(self.draw_data, s, t, before, max_title_length, i + 1, t is last_tab, ed)
end = self.draw_func(self.draw_data, s, t, before, max_tab_length, i + 1, t is last_tab, ed)
s.cursor.bg = s.cursor.fg = 0
cr.append((before, end))
if s.cursor.x > s.columns - max_title_length and t is not last_tab:
cell_ranges.append((before, end))
if not ed.for_layout and t is not last_tab and s.cursor.x > s.columns - max_tab_lengths[i+1]:
# Stop if there is no space for next tab
s.cursor.x = s.columns - 2
s.cursor.bg = as_rgb(color_as_int(self.draw_data.default_bg))
s.cursor.fg = as_rgb(0xff0000)
s.draw('')
raise StopIteration()
unconstrained_tab_length = max(1, s.columns - 2)
ideal_tab_lengths = [i for i in range(len(data))]
default_max_tab_length = max(1, (s.columns // max(1, len(data))) - 1)
max_tab_lengths = [default_max_tab_length for _ in range(len(data))]
active_idx = 0
extra = 0
ed.for_layout = True
for i, t in enumerate(data):
s.cursor.x = 0
draw_tab(i, t, [], unconstrained_tab_length)
ideal_tab_lengths[i] = tl = max(1, s.cursor.x)
if t.is_active:
active_idx = i
if tl < default_max_tab_length:
max_tab_lengths[i] = tl
extra += default_max_tab_length - tl
if extra > 0:
if ideal_tab_lengths[active_idx] > max_tab_lengths[active_idx]:
d = min(extra, ideal_tab_lengths[active_idx] - max_tab_lengths[active_idx])
max_tab_lengths[active_idx] += d
extra -= d
if extra > 0:
over_achievers = tuple(i for i in range(len(data)) if ideal_tab_lengths[i] > max_tab_lengths[i])
if over_achievers:
amt_per_over_achiever = extra // len(over_achievers)
if amt_per_over_achiever > 0:
for i in over_achievers:
max_tab_lengths[i] += amt_per_over_achiever
s.cursor.x = 0
s.erase_in_line(2, False)
cr: List[Tuple[int, int]] = []
ed.for_layout = False
for i, t in enumerate(data):
try:
draw_tab(i, t, cr, max_tab_lengths[i])
except StopIteration:
break
s.erase_in_line(0, False) # Ensure no long titles bleed after the last tab
self.cell_ranges = cr
s.erase_in_line(0, False) # Ensure no long titles bleed after the last tab
self.align()
def align_with_factor(self, factor: int = 1) -> None:

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,

550
kitty/unicode-data.c generated

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
#include "data-types.h"
#include "state.h"
// START_KNOWN_MARKS
static const combining_type VS15 = 1362, VS16 = 1363;
static const combining_type VS15 = 1364, VS16 = 1365;
// END_KNOWN_MARKS
bool is_combining_char(char_type ch);

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:
@@ -1096,3 +1086,13 @@ def docs_url(which: str = '', local_docs_root: Optional[str] = '') -> str:
if frag:
url += '#' + frag
return url
def sanitize_for_bracketed_paste(text: bytes) -> bytes:
pat = re.compile(b'(?:(?:\033\\\x5b)|(?:\x9b))201~')
while True:
new_text = pat.sub(b'', text)
if new_text == text:
break
text = new_text
return text

128
kitty/wcwidth-std.h generated
View File

@@ -1,4 +1,4 @@
// unicode data, built from the unicode standard on: 2022-01-05
// unicode data, built from the unicode standard on: 2022-09-29
// see gen-wcwidth.py
#pragma once
#include "data-types.h"
@@ -14,7 +14,7 @@ wcwidth_std(int32_t code) {
return 2;
// }}}
// Marks (6350 codepoints) {{{
// Marks (6399 codepoints) {{{
case 0x0:
return 0;
case 0xad:
@@ -185,6 +185,8 @@ wcwidth_std(int32_t code) {
return 0;
case 0xce2 ... 0xce3:
return 0;
case 0xcf3:
return 0;
case 0xd00 ... 0xd03:
return 0;
case 0xd3b ... 0xd3c:
@@ -221,7 +223,7 @@ wcwidth_std(int32_t code) {
return 0;
case 0xeb4 ... 0xebc:
return 0;
case 0xec8 ... 0xecd:
case 0xec8 ... 0xece:
return 0;
case 0xf18 ... 0xf19:
return 0;
@@ -439,6 +441,8 @@ wcwidth_std(int32_t code) {
return 0;
case 0x10eab ... 0x10eac:
return 0;
case 0x10efd ... 0x10eff:
return 0;
case 0x10f46 ... 0x10f50:
return 0;
case 0x10f82 ... 0x10f85:
@@ -481,6 +485,8 @@ wcwidth_std(int32_t code) {
return 0;
case 0x1123e:
return 0;
case 0x11241:
return 0;
case 0x112df ... 0x112ea:
return 0;
case 0x11300 ... 0x11303:
@@ -575,7 +581,17 @@ wcwidth_std(int32_t code) {
return 0;
case 0x11ef3 ... 0x11ef6:
return 0;
case 0x13430 ... 0x13438:
case 0x11f00 ... 0x11f01:
return 0;
case 0x11f03:
return 0;
case 0x11f34 ... 0x11f3a:
return 0;
case 0x11f3e ... 0x11f42:
return 0;
case 0x13430 ... 0x13440:
return 0;
case 0x13447 ... 0x13455:
return 0;
case 0x16af0 ... 0x16af4:
return 0;
@@ -631,12 +647,16 @@ wcwidth_std(int32_t code) {
return 0;
case 0x1e026 ... 0x1e02a:
return 0;
case 0x1e08f:
return 0;
case 0x1e130 ... 0x1e136:
return 0;
case 0x1e2ae:
return 0;
case 0x1e2ec ... 0x1e2ef:
return 0;
case 0x1e4ec ... 0x1e4ef:
return 0;
case 0x1e8d0 ... 0x1e8d6:
return 0;
case 0x1e944 ... 0x1e94a:
@@ -1257,7 +1277,7 @@ wcwidth_std(int32_t code) {
return -2;
// }}}
// East Asian double width (182470 codepoints) {{{
// East Asian double width (182492 codepoints) {{{
case 0x1100 ... 0x115e:
return 2;
case 0x231a ... 0x231b:
@@ -1402,8 +1422,12 @@ wcwidth_std(int32_t code) {
return 2;
case 0x1b000 ... 0x1b122:
return 2;
case 0x1b132:
return 2;
case 0x1b150 ... 0x1b152:
return 2;
case 0x1b155:
return 2;
case 0x1b164 ... 0x1b167:
return 2;
case 0x1b170 ... 0x1b2fb:
@@ -1474,7 +1498,7 @@ wcwidth_std(int32_t code) {
return 2;
case 0x1f6d5 ... 0x1f6d7:
return 2;
case 0x1f6dd ... 0x1f6df:
case 0x1f6dc ... 0x1f6df:
return 2;
case 0x1f6eb ... 0x1f6ec:
return 2;
@@ -1490,23 +1514,19 @@ wcwidth_std(int32_t code) {
return 2;
case 0x1f947 ... 0x1f9ff:
return 2;
case 0x1fa70 ... 0x1fa74:
case 0x1fa70 ... 0x1fa7c:
return 2;
case 0x1fa78 ... 0x1fa7c:
case 0x1fa80 ... 0x1fa88:
return 2;
case 0x1fa80 ... 0x1fa86:
case 0x1fa90 ... 0x1fabd:
return 2;
case 0x1fa90 ... 0x1faac:
case 0x1fabf ... 0x1fac5:
return 2;
case 0x1fab0 ... 0x1faba:
case 0x1face ... 0x1fadb:
return 2;
case 0x1fac0 ... 0x1fac5:
case 0x1fae0 ... 0x1fae8:
return 2;
case 0x1fad0 ... 0x1fad9:
return 2;
case 0x1fae0 ... 0x1fae7:
return 2;
case 0x1faf0 ... 0x1faf6:
case 0x1faf0 ... 0x1faf8:
return 2;
case 0x20000 ... 0x2fffd:
return 2;
@@ -1517,7 +1537,7 @@ wcwidth_std(int32_t code) {
// Emoji Presentation (0 codepoints) {{{
// }}}
// Not assigned in the unicode character database (760767 codepoints) {{{
// Not assigned in the unicode character database (760471 codepoints) {{{
case 0x378 ... 0x379:
return -4;
case 0x380 ... 0x383:
@@ -1760,7 +1780,7 @@ wcwidth_std(int32_t code) {
return -4;
case 0xcf0:
return -4;
case 0xcf3 ... 0xcff:
case 0xcf4 ... 0xcff:
return -4;
case 0xd0d:
return -4;
@@ -1820,7 +1840,7 @@ wcwidth_std(int32_t code) {
return -4;
case 0xec7:
return -4;
case 0xece ... 0xecf:
case 0xecf:
return -4;
case 0xeda ... 0xedb:
return -4;
@@ -2354,7 +2374,7 @@ wcwidth_std(int32_t code) {
return -4;
case 0x10eae ... 0x10eaf:
return -4;
case 0x10eb2 ... 0x10eff:
case 0x10eb2 ... 0x10efc:
return -4;
case 0x10f28 ... 0x10f2f:
return -4;
@@ -2390,7 +2410,7 @@ wcwidth_std(int32_t code) {
return -4;
case 0x11212:
return -4;
case 0x1123f ... 0x1127f:
case 0x11242 ... 0x1127f:
return -4;
case 0x11287:
return -4;
@@ -2494,7 +2514,9 @@ wcwidth_std(int32_t code) {
return -4;
case 0x11aa3 ... 0x11aaf:
return -4;
case 0x11af9 ... 0x11bff:
case 0x11af9 ... 0x11aff:
return -4;
case 0x11b0a ... 0x11bff:
return -4;
case 0x11c09:
return -4;
@@ -2536,7 +2558,13 @@ wcwidth_std(int32_t code) {
return -4;
case 0x11daa ... 0x11edf:
return -4;
case 0x11ef9 ... 0x11faf:
case 0x11ef9 ... 0x11eff:
return -4;
case 0x11f11:
return -4;
case 0x11f3b ... 0x11f3d:
return -4;
case 0x11f5a ... 0x11faf:
return -4;
case 0x11fb1 ... 0x11fbf:
return -4;
@@ -2552,9 +2580,7 @@ wcwidth_std(int32_t code) {
return -4;
case 0x12ff3 ... 0x12fff:
return -4;
case 0x1342f:
return -4;
case 0x13439 ... 0x143ff:
case 0x13456 ... 0x143ff:
return -4;
case 0x14647 ... 0x167ff:
return -4;
@@ -2606,9 +2632,13 @@ wcwidth_std(int32_t code) {
return -4;
case 0x1afff:
return -4;
case 0x1b123 ... 0x1b14f:
case 0x1b123 ... 0x1b131:
return -4;
case 0x1b153 ... 0x1b163:
case 0x1b133 ... 0x1b14f:
return -4;
case 0x1b153 ... 0x1b154:
return -4;
case 0x1b156 ... 0x1b163:
return -4;
case 0x1b168 ... 0x1b16f:
return -4;
@@ -2636,7 +2666,9 @@ wcwidth_std(int32_t code) {
return -4;
case 0x1d1eb ... 0x1d1ff:
return -4;
case 0x1d246 ... 0x1d2df:
case 0x1d246 ... 0x1d2bf:
return -4;
case 0x1d2d4 ... 0x1d2df:
return -4;
case 0x1d2f4 ... 0x1d2ff:
return -4;
@@ -2690,7 +2722,9 @@ wcwidth_std(int32_t code) {
return -4;
case 0x1dab0 ... 0x1deff:
return -4;
case 0x1df1f ... 0x1dfff:
case 0x1df1f ... 0x1df24:
return -4;
case 0x1df2b ... 0x1dfff:
return -4;
case 0x1e007:
return -4;
@@ -2700,7 +2734,11 @@ wcwidth_std(int32_t code) {
return -4;
case 0x1e025:
return -4;
case 0x1e02b ... 0x1e0ff:
case 0x1e02b ... 0x1e02f:
return -4;
case 0x1e06e ... 0x1e08e:
return -4;
case 0x1e090 ... 0x1e0ff:
return -4;
case 0x1e12d ... 0x1e12f:
return -4;
@@ -2714,7 +2752,9 @@ wcwidth_std(int32_t code) {
return -4;
case 0x1e2fa ... 0x1e2fe:
return -4;
case 0x1e300 ... 0x1e7df:
case 0x1e300 ... 0x1e4cf:
return -4;
case 0x1e4fa ... 0x1e7df:
return -4;
case 0x1e7e7:
return -4;
@@ -2830,15 +2870,15 @@ wcwidth_std(int32_t code) {
return -4;
case 0x1f266 ... 0x1f2ff:
return -4;
case 0x1f6d8 ... 0x1f6dc:
case 0x1f6d8 ... 0x1f6db:
return -4;
case 0x1f6ed ... 0x1f6ef:
return -4;
case 0x1f6fd ... 0x1f6ff:
return -4;
case 0x1f774 ... 0x1f77f:
case 0x1f777 ... 0x1f77a:
return -4;
case 0x1f7d9 ... 0x1f7df:
case 0x1f7da ... 0x1f7df:
return -4;
case 0x1f7ec ... 0x1f7ef:
return -4;
@@ -2860,23 +2900,19 @@ wcwidth_std(int32_t code) {
return -4;
case 0x1fa6e ... 0x1fa6f:
return -4;
case 0x1fa75 ... 0x1fa77:
return -4;
case 0x1fa7d ... 0x1fa7f:
return -4;
case 0x1fa87 ... 0x1fa8f:
case 0x1fa89 ... 0x1fa8f:
return -4;
case 0x1faad ... 0x1faaf:
case 0x1fabe:
return -4;
case 0x1fabb ... 0x1fabf:
case 0x1fac6 ... 0x1facd:
return -4;
case 0x1fac6 ... 0x1facf:
case 0x1fadc ... 0x1fadf:
return -4;
case 0x1fada ... 0x1fadf:
case 0x1fae9 ... 0x1faef:
return -4;
case 0x1fae8 ... 0x1faef:
return -4;
case 0x1faf7 ... 0x1faff:
case 0x1faf9 ... 0x1faff:
return -4;
case 0x1fb93:
return -4;

View File

@@ -14,43 +14,47 @@ from gettext import gettext as _
from itertools import chain
from time import monotonic
from typing import (
TYPE_CHECKING, Any, Callable, Deque, Dict, Iterable, List, NamedTuple,
Optional, Pattern, Sequence, Tuple, Union
TYPE_CHECKING, Any, Callable, Deque, Dict, Iterable, List, NamedTuple, Optional,
Pattern, Sequence, Tuple, Union,
)
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
appname, clear_handled_signals, config_dir, is_macos, wakeup_io_loop,
)
from .fast_data_types import (
BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM,
CELL_PROGRAM, CELL_SPECIAL_PROGRAM, CURSOR_BEAM, CURSOR_BLOCK,
CURSOR_UNDERLINE, DCS, DECORATION, DECORATION_MASK, DIM, GLFW_MOD_CONTROL,
GRAPHICS_ALPHA_MASK_PROGRAM, GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM,
MARK, MARK_MASK, NO_CURSOR_SHAPE, NUM_UNDERLINE_STYLES, OSC, REVERSE,
SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, STRIKETHROUGH, TINT_PROGRAM, Color,
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,
BGIMAGE_PROGRAM, BLIT_PROGRAM, CELL_BG_PROGRAM, CELL_FG_PROGRAM, CELL_PROGRAM,
CELL_SPECIAL_PROGRAM, CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE, DCS, DECORATION,
DECORATION_MASK, DIM, GLFW_MOD_CONTROL, GRAPHICS_ALPHA_MASK_PROGRAM,
GRAPHICS_PREMULT_PROGRAM, GRAPHICS_PROGRAM, MARK, MARK_MASK, NO_CURSOR_SHAPE,
NUM_UNDERLINE_STYLES, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE,
STRIKETHROUGH, TINT_PROGRAM, Color, 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_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
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_for_bracketed_paste, sanitize_title,
)
MatchPatternType = Union[Pattern[str], Tuple[Pattern[str], Optional[Pattern[str]]]]
@@ -462,6 +466,7 @@ global_watchers = GlobalWatchers()
class Window:
window_custom_type: str = ''
overlay_type = OverlayType.transient
def __init__(
self,
@@ -642,6 +647,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 +1005,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:
@@ -1453,11 +1461,7 @@ class Window:
if isinstance(text, str):
text = text.encode('utf-8')
if self.screen.in_bracketed_paste_mode:
while True:
new_text = text.replace(b'\033[201~', b'').replace(b'\x9b201~', b'')
if len(text) == len(new_text):
break
text = new_text
text = sanitize_for_bracketed_paste(text)
else:
# Workaround for broken editors like nano that cannot handle
# newlines in pasted text see https://github.com/kovidgoyal/kitty/issues/994

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

@@ -107,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,22 @@ 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')
def test_bracketed_paste_sanitizer(self):
from kitty.utils import sanitize_for_bracketed_paste
for x in ('\x1b[201~ab\x9b201~cd', '\x1b[201\x1b[201~~ab'): # ]]]
q = sanitize_for_bracketed_paste(x.encode('utf-8'))
self.assertNotIn(b'\x1b[201~', q)
self.assertNotIn('\x9b201~'.encode('utf-8'), q)
self.assertIn(b'ab', q)

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

@@ -66,7 +66,7 @@ def type_check() -> NoReturn:
generate_stub()
from kittens.tui.operations_stub import generate_stub # type: ignore
generate_stub()
py = shutil.which('python') or shutil.which('python3')
py = os.environ.get('PYTHON_FOR_TYPE_CHECK') or shutil.which('python') or shutil.which('python3')
os.execlp(py, py, '-m', 'mypy', '--pretty')

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'])

Some files were not shown because too many files have changed in this diff Show More