Compare commits

..

837 Commits

Author SHA1 Message Date
Kovid Goyal
cc29640604 version 0.17.3 2020-04-23 08:39:07 +05:30
Kovid Goyal
d0329460b8 Fix #2573 2020-04-21 22:04:34 +05:30
Kovid Goyal
717c0c96de Allow mapping arbitrary remote control commands to key presses in kitty.conf 2020-04-19 20:14:23 +05:30
Kovid Goyal
b3f161efce ... 2020-04-19 19:39:56 +05:30
Kovid Goyal
0286940163 Allow changing margings/padding via remote control
Fixes #2546
2020-04-19 19:29:36 +05:30
Kovid Goyal
d4a4ee386f Fix minimal borders in grid layout 2020-04-19 17:58:35 +05:30
Kovid Goyal
33498f9133 ... 2020-04-19 11:01:22 +05:30
Kovid Goyal
da3d4d1bef ... 2020-04-19 09:51:06 +05:30
Kovid Goyal
c69b8870d2 Allow individually setting margins and padding for each edge (left, right, top, bottom) 2020-04-19 09:36:26 +05:30
Kovid Goyal
85b55b31b6 Add a new :opt:listen_on option in kitty.conf
Also allow using environment variables in this option
Fixes #2569
2020-04-19 09:23:52 +05:30
Kovid Goyal
948919e42b Forgot to apply --title on creation of subsequent OS windows
Also update changelog
2020-04-19 08:42:30 +05:30
Kovid Goyal
85c40090ed Fix #2570 2020-04-19 08:36:29 +05:30
Kovid Goyal
98b50ec145 ... 2020-04-19 08:28:40 +05:30
Kovid Goyal
1ab4f7d2d0 Fix a regression in 0.17 that broke displaying graphics over SSH
The new types GraphicsCommand can generate a command with only a payload
and no control keys, as the last command during chunked transfer.
Fix the parser to handle this. Fixes #2568
2020-04-19 08:13:33 +05:30
Kovid Goyal
98801b6fa6 Update changelog 2020-04-19 07:22:02 +05:30
Kovid Goyal
bdd68284b5 Merge branch 'terminfo' of https://github.com/wladh/kitty 2020-04-19 07:21:10 +05:30
Vlad Hanciuta
a61d70ea9a Add strikethrough capabilities to terminfo
The termcap aliases are not standard, but they are used by Vim, it
seems. As far as I can tell, there are no standard termcap attribute for
strikethrough.
2020-04-18 16:37:09 +01:00
Vlad Hanciuta
e271719dd2 Add strikeout capability to terminfo 2020-04-18 16:05:30 +01:00
Kovid Goyal
a08efa1fc5 Fix failing test 2020-04-17 19:57:37 +05:30
Kovid Goyal
ffac3600e0 Layouts: Apply per window padding and margin values
Also make the calculation of blank rects more robust and with less code,
which should hopefully fix #1762
2020-04-17 19:47:34 +05:30
Kovid Goyal
6499f8ac24 Allow window objects to store custom margin/padding 2020-04-17 09:02:08 +05:30
Kovid Goyal
63307cb1d8 Code to serialize state 2020-04-17 08:42:45 +05:30
Kovid Goyal
0a6fb362f6 kitty @ kitten: Allow no_ui kittens to return data to the remote client
Fixes #2552 since the OP can now write a kitten to query the window
manager and return position data even over SSH.
2020-04-15 08:29:55 +05:30
Kovid Goyal
457fcbfb90 Merge branch 'master' of https://github.com/akobel/kitty 2020-04-15 07:44:27 +05:30
Alexander Kobel
3b6277b9e2 Hide "No render frame received" without --debug-rendering 2020-04-14 19:36:58 +02:00
Alexander Kobel
e6e61028b1 Rename --debug-gl to --debug-rendering 2020-04-14 19:35:40 +02:00
Kovid Goyal
e9be66126b Merge branch 'hints-support-empty-word-characters' of https://github.com/trygveaa/kitty 2020-04-14 08:17:13 +05:30
Trygve Aaberge
681fe7f2f2 Don't set word_characters to select_by_word_characters when empty string
This allows you to pass in --word-characters "" which will make hints
only match \w.
2020-04-13 23:02:44 +02:00
Kovid Goyal
0196920533 icat kitten: Fix display of 16-bit depth images
Fixes #2542
2020-04-13 23:05:10 +05:30
Kovid Goyal
5f5bb3ace0 Update Changelog 2020-04-12 20:10:29 +05:30
Kovid Goyal
3c970e0122 Merge branch 'kitten-send-mouse-events' of https://github.com/trygveaa/kitty 2020-04-12 20:09:45 +05:30
Trygve Aaberge
43af6e3b8a Add support for sending mouse events from a kitten
This allows you to a control a program running in kitty from a kitten
using mouse events. If the program is not receiving mouse events of that
type, it is not sent.
2020-04-12 16:19:50 +02:00
Kovid Goyal
ac8cfc12fb Merge branch 'docs-fix-kitten-custom-indentation' of https://github.com/trygveaa/kitty 2020-04-12 19:49:18 +05:30
Trygve Aaberge
b19472e0f5 Fix indentation in kittens/custom.rst to 4 spaces
This file used 3 spaces some places and 4 spaces other places. Other
files like diff.rst and hints.rst use 4 spaces.
2020-04-12 16:04:32 +02:00
Kovid Goyal
b4d08044a0 ... 2020-04-12 17:01:39 +05:30
Kovid Goyal
b2fe02e63f Merge branch 'patch-1' of https://github.com/LesnyRumcajs/kitty 2020-04-12 16:55:10 +05:30
Kovid Goyal
d1a5d30233 Add another test for marking 2020-04-12 16:54:38 +05:30
LesnyRumcajs
bba30138e1 Update Fedora link
The link was pointing to a deprecated `copr`. Let's point it to official Fedora repos! (as copr page suggests anyway)
2020-04-12 13:13:02 +02:00
Kovid Goyal
082546a1e7 Marks: Fix marks not handling wide characters and tab characters correctly
Fixes #2534
2020-04-12 13:28:21 +05:30
Kovid Goyal
41ffd58e26 A bit of cleanup 2020-04-11 21:22:17 +05:30
Kovid Goyal
b4f24e4e1e Merge branch 'must_draw_borders_flag' of https://github.com/franco/kitty 2020-04-11 21:19:52 +05:30
Franco Sebregondi
75c8ffd715 Add flag to Layout to enforce drawing a border
Layout.must_draw_borders = True enforces drawing of a border even
though only a single window is visible. See #2531.
2020-04-11 17:01:44 +02:00
Kovid Goyal
688606ff7c Merge branch 'ignore-keyboard-layout-switch-keys' of https://github.com/trygveaa/kitty 2020-04-10 21:46:07 +05:30
Trygve Aaberge
2e3f9dffa7 Ignore keys for switching keyboard layout
XKB has various options for using keys to switch the keyboard layout.
When these are set, XKB will return keysyms which are unknown to GLFW,
so kitty will fallback to using a keymap without the options set, which
causes the keys to be interpreted as the original keysyms.

However, when these options are set, kitty shouldn't interpret the keys.
Therefore, check for some specific keysyms and return before translating
to GLFW keysyms.

There may be more keysyms which should be ignored, but as far as I can
see from https://gitlab.freedesktop.org/xkeyboard-config/xkeyboard-config/-/blob/xkeyboard-config-2.29/symbols/group
these are the ones which can be triggered by XKB options.

Fixes #2519
2020-04-10 17:21:53 +02:00
Kovid Goyal
859033415b macOS: Fix incorrect horizontal positioning of some full-width East Asian characters
Fixes #1457
Also render multi-cell PUA characters centered, matching behavior on other platforms
2020-04-09 13:25:19 +05:30
Kovid Goyal
1f1b52b580 Fix specifying options for layouts in the startup session file not working
Fixes #2520
2020-04-09 12:40:09 +05:30
Kovid Goyal
4e7bf80447 diff kitten: New option to control the background color for filler lines in the margin
Fixes #2518
2020-04-08 20:42:49 +05:30
Kovid Goyal
e39df50884 Merge branch 'patch-1' of https://github.com/georgeabr/kitty 2020-04-08 18:56:15 +05:30
George Abraham
8d6a9d45c2 Changing match target to "font"
As suggested in #2516, changing match target to "font" in the fonts.conf file.
2020-04-08 14:02:54 +01:00
Kovid Goyal
0862e85577 Rewrite wcswidth as a state machine 2020-04-07 10:10:30 +05:30
Kovid Goyal
0b9a37139e Render any flag pair in two cells not just known ones 2020-04-06 23:00:34 +05:30
Kovid Goyal
6357f7b811 Update wcswidth() to match new country flag rendering 2020-04-06 22:56:34 +05:30
Kovid Goyal
24197dc422 Render known country flags designated by a pair of unicode codepoints in two cells instead of four. 2020-04-06 22:16:59 +05:30
Kovid Goyal
9bc2ab3245 Function to detect flag pairs 2020-04-06 21:16:14 +05:30
Kovid Goyal
bf4e8c490c Update to Unicode 13.0
Fixes #2513
2020-04-06 18:59:35 +05:30
Kovid Goyal
590a4d70ee Fix --stdin-add-formatting not working
Fixes #2512
2020-04-06 09:35:16 +05:30
Kovid Goyal
40341d2ddd ... 2020-04-04 09:17:38 +05:30
Kovid Goyal
0cd2c41ee3 Update changelog 2020-04-03 22:42:06 +05:30
Kovid Goyal
5d65cdb6a8 Fix reverse video not being rendered correctly when using transparency or a background image
Fixes #2419
2020-04-03 22:40:33 +05:30
Kovid Goyal
759a15ccbb X11: Handle invalid data being sent with drag events by some applications
Fixes #2505
2020-04-02 21:01:19 +05:30
Kovid Goyal
5aead5eebe Replace new-window with launch in the docs 2020-04-01 08:39:37 +05:30
Kovid Goyal
a325a5fdee Merge branch 'strikethrough-info-from-font' of https://github.com/trygveaa/kitty 2020-03-31 09:34:07 +05:30
Trygve Aaberge
931e91f1a7 Read strikethrough info from font when using FreeType
This reads the strikethrough position and thickness from the font so it
is rendered correctly.

This is only implemented for FreeType, not Core Text, because I didn't
find any way to get the info from Core Text, and I don't have a Mac to
test it on either. When using Core Text or when the font doesn't provide
the info, the same approximation as before is used.
2020-03-30 20:09:30 +02:00
Kovid Goyal
6e13509720 Allow entry points and external command combined completion on first word 2020-03-30 11:38:54 +05:30
Kovid Goyal
5b4844e71e oops
Broke normal completion in previous commit. Delegate should only be used
when actual unknown words are present
2020-03-30 11:12:00 +05:30
Kovid Goyal
66cbbea9bb Add completion for executables at kitty invocation in zsh
So now kitty vi<TAB> and kitty vim something<TAB> delegate
completion to zsh for the command that will be executed inside kitty.
2020-03-30 10:54:17 +05:30
Kovid Goyal
b65119b8a9 Color separators in the tab bar using inactive bg except for the last one 2020-03-29 09:20:17 +05:30
Kovid Goyal
86703da917 version 0.17.2 2020-03-29 09:09:16 +05:30
Kovid Goyal
610e9afdf0 Add completion of conf filenames to kitty @ set-colors 2020-03-29 08:41:27 +05:30
Kovid Goyal
e3af9f68d3 Linux: Fix selection of fonts with multiple width variants not preferring the normal width faces
Fixes #2491
2020-03-29 01:52:33 +05:30
Kovid Goyal
747ac85e7c Add an :option:launch --watcher option that allows defining callbacks that are called for various events in the window's life-cycle
Fixes #2440
2020-03-28 12:13:42 +05:30
Kovid Goyal
8c23f9e526 DRYer 2020-03-28 09:20:28 +05:30
Kovid Goyal
e6e339fcd3 ... 2020-03-28 08:44:48 +05:30
Kovid Goyal
f04680ac47 Fix a regression in 0.17 that broke the kitty @ launch remote command and also broke the --tab-title option when creating a new tab.
Fixes #2488
2020-03-28 08:43:32 +05:30
Kovid Goyal
90985cc846 Merge branch 'hints-copy-to-primary-selection' of https://github.com/trygveaa/kitty 2020-03-28 08:21:08 +05:30
Trygve Aaberge
63917944f2 Add an option to hints for copying to the primary selection 2020-03-27 20:23:02 +01:00
Kovid Goyal
793605f80f Update changelog 2020-03-26 22:06:13 +05:30
Kovid Goyal
93a281c7e3 Add a new mappable action `close_other_windows_in_tab` to close all but the active window
Fixes #2484
2020-03-26 22:05:00 +05:30
Kovid Goyal
8f1c6c4d74 Fix #2482 2020-03-26 00:53:19 +05:30
Kovid Goyal
1610dba7ab patch tab bar colors correctly 2020-03-26 00:46:30 +05:30
Kovid Goyal
d9d419991c Respect tab_bar_background again 2020-03-26 00:41:19 +05:30
Kovid Goyal
f9e86b19aa Fix #2480 2020-03-26 00:20:10 +05:30
Kovid Goyal
fffb976e43 Fix blank space at the start of tab bar in powerline style when first tab is inactive
Fix #2478
2020-03-25 19:20:35 +05:30
Kovid Goyal
0da566b49f Hints kitten: Adjust the default regex used to detect line numbers to handle line+column numbers
See #2268
2020-03-25 10:03:25 +05:30
Kovid Goyal
da55717d20 The splits layout needs its own draw_minimal_borders implementation 2020-03-25 09:54:06 +05:30
Kovid Goyal
254836902c Merge branch 'fix/window-border' of https://github.com/pyrho/kitty 2020-03-25 09:35:46 +05:30
Damien Rajon
d01f8d1865 fix(window): inactive window border fix
Fixes #2474.
2020-03-25 00:09:10 +01:00
Kovid Goyal
a46c3f7007 Replace use of cast() 2020-03-24 22:03:10 +05:30
Kovid Goyal
4e58062025 Improve rendering of :option: in cli docs 2020-03-24 22:03:00 +05:30
Kovid Goyal
aa3be0535f version 0.17.1 2020-03-24 19:45:19 +05:30
Kovid Goyal
8ebf5e007c Fix rc setting cursor_text_color to background not working 2020-03-24 19:43:31 +05:30
Kovid Goyal
c2957e7f12 Fix a regression in 0.17 that caused kitty @ set-colors to require setting cursor_text_color
Fixes #2470
2020-03-24 19:29:25 +05:30
Kovid Goyal
bf7c26d357 CoreText: Prefer less bold variants over heavier variants when auto-selecting the bold face 2020-03-24 19:27:14 +05:30
Kovid Goyal
0547102bdd Fix #2468 2020-03-24 18:33:32 +05:30
Kovid Goyal
a07369ce69 Update changelog 2020-03-24 18:14:14 +05:30
Kovid Goyal
21c5610431 ... 2020-03-24 18:02:44 +05:30
Kovid Goyal
6dbdf72f40 CoreText: When matching fonts prefer non-expanded/condensed variants 2020-03-24 17:55:38 +05:30
Kovid Goyal
76a6bba643 Fix #2466 2020-03-24 17:07:37 +05:30
Kovid Goyal
b0b8222a96 ... 2020-03-24 17:04:17 +05:30
Kovid Goyal
0d87b8fac5 Fix a regression in 0.17 that caused tab bar background to be rendered after the last tab as well
Fixes #2464
2020-03-24 16:05:02 +05:30
Kovid Goyal
5227b1f98b Fix :opt:cursor_underline_thickness not working
Fixes #2465
2020-03-24 15:53:13 +05:30
Kovid Goyal
c4f7c5c1cd More explanation for OSC 52 extension 2020-03-24 11:58:30 +05:30
Kovid Goyal
33a84a609c ... 2020-03-24 11:35:34 +05:30
Kovid Goyal
037111715a ... 2020-03-24 11:34:29 +05:30
Kovid Goyal
93271b93e6 version 0.17.0 2020-03-24 10:33:51 +05:30
Kovid Goyal
04488ada06 Merge branch 'png2icns' of https://github.com/Luflosi/kitty 2020-03-21 08:50:57 +05:30
Luflosi
21c7002c68 Add support for png2icns as an alternative to iconutil
png2icns is used when building kitty with nix because iconutil seems to be closed-source.
libicns also has an iconutil clone called icnsutil in the source tree but the last release is from 2012, which does not include this utility yet.
2020-03-21 02:11:08 +01:00
Luflosi
97cae0e355 Simplify code
This piece of code ends up just extracting `appname` from `iconset_dir`.
2020-03-21 00:10:16 +01:00
Luflosi
85fc0a59b1 Rename variable logo_dir -> iconset_dir 2020-03-21 00:10:16 +01:00
Kovid Goyal
eddd45bbc3 diff kitten: Fix images losing position when scrolling using mouse wheel/touchpad 2020-03-19 18:46:10 +05:30
Kovid Goyal
e2339697b9 Fix #2449 2020-03-19 17:22:58 +05:30
Kovid Goyal
cfe9b408ce Move startup of first child out of the constructor of Boss 2020-03-19 16:48:16 +05:30
Kovid Goyal
83e8018787 Exclude very small values for content scale 2020-03-19 14:13:23 +05:30
Kovid Goyal
372d2008a6 Actually, we should use text/plain rather than text/uri-list since we are already converting to filesystem paths 2020-03-19 13:40:02 +05:30
Kovid Goyal
ab9a36f8c1 Port cocoa backend to use new drop API 2020-03-19 13:36:03 +05:30
Kovid Goyal
2458c3a7c6 Implement drag and drop of text/plain for Wayland as well 2020-03-19 13:28:21 +05:30
Kovid Goyal
e827e6fa21 X11: Allow drag and drop of text/plain in addition to text/uri-list
Fixes #2441
2020-03-18 22:37:23 +05:30
Kovid Goyal
342f0981d8 Add tests for selection extension by right click 2020-03-18 20:57:19 +05:30
Kovid Goyal
9c19e88288 Allow extending selections by dragging with right button pressed
Fixes #2445
2020-03-18 20:52:25 +05:30
Kovid Goyal
7329bd4910 Fix incorrect first line selection rendering when scrolling down 2020-03-18 20:27:54 +05:30
Kovid Goyal
b9ad62226e Fix selection not being extended by right click when scrolling down
Fix #2443
2020-03-18 20:17:38 +05:30
Kovid Goyal
3bff3bcbe4 Fix selection not updating properly while scrolling
Fixes #2442
2020-03-18 18:48:59 +05:30
Kovid Goyal
a06fdad034 Use a dedicated exception for rc_cmd.fatal() 2020-03-17 11:40:42 +05:30
Kovid Goyal
a49bd98ec6 ... 2020-03-15 13:36:09 +05:30
Kovid Goyal
90f41108d3 ... 2020-03-15 13:30:02 +05:30
Kovid Goyal
382c31ddf2 Use a stub rather than TYPE_CHECKING 2020-03-15 13:27:40 +05:30
Kovid Goyal
871ca4dda6 oops 2020-03-15 08:58:46 +05:30
Kovid Goyal
fa003f8233 Fix #2431 2020-03-15 08:55:04 +05:30
Kovid Goyal
12f37bcfad Dont strip trailing punctuation from end of URL if next line startswith url characters 2020-03-15 08:47:52 +05:30
Kovid Goyal
e86c712424 Dont strip :code:& and :code:- from the end of URLs
Fixes #2436
2020-03-15 08:29:56 +05:30
Kovid Goyal
e8a9935cb2 Allow setting WM_CLASS and WM_NAME when creating new OS windows with the launch command 2020-03-15 08:12:28 +05:30
Kovid Goyal
f353131b44 Fix #2433 2020-03-15 07:54:55 +05:30
Kovid Goyal
7c4a2ba329 Fix #2434 2020-03-14 20:19:26 +05:30
Kovid Goyal
dfb543bd94 ... 2020-03-14 15:27:38 +05:30
Kovid Goyal
c912eef040 Minor strictness increases 2020-03-14 14:59:31 +05:30
Kovid Goyal
5f06d3a427 Disallow untyped decorators 2020-03-14 14:50:04 +05:30
Kovid Goyal
a70fa9abc5 oops 2020-03-14 14:38:56 +05:30
Kovid Goyal
5956277863 All defs are now typed 2020-03-14 14:37:11 +05:30
Kovid Goyal
d3f37eeba4 more typing work 2020-03-14 11:42:05 +05:30
Kovid Goyal
ea48332f46 more typing work 2020-03-14 09:52:56 +05:30
Kovid Goyal
eb12511646 The uber-fussy sway does not like calling wl_data_offer_finish on a version 1 data manager 2020-03-14 09:12:06 +05:30
Kovid Goyal
aa11ddf32c Wayland: Fix a crash when drag and dropping into kitty
Fixes #2432
2020-03-14 08:20:44 +05:30
Kovid Goyal
a348f64833 more typing work 2020-03-13 21:41:07 +05:30
Kovid Goyal
626a96e20f Clean up code for matching windows/tabs in rc 2020-03-13 21:03:06 +05:30
Kovid Goyal
60995ff04b Fix #2429 2020-03-13 19:14:49 +05:30
Kovid Goyal
ba4e711715 Fix #2428 2020-03-13 18:13:11 +05:30
Kovid Goyal
891b184195 misc typing fixes 2020-03-13 16:45:39 +05:30
Kovid Goyal
5414dc398e Correct a few type definitions in layout.py 2020-03-13 16:21:36 +05:30
Kovid Goyal
917559f883 more typing work 2020-03-13 16:13:26 +05:30
Kovid Goyal
9f2fb76309 more typing work 2020-03-12 15:09:21 +05:30
Kovid Goyal
b6692849d6 more typing work 2020-03-12 14:52:11 +05:30
Kovid Goyal
b27f6d5957 more typing work 2020-03-12 14:26:51 +05:30
Kovid Goyal
cda1e28b32 more typing work 2020-03-12 13:51:51 +05:30
Kovid Goyal
8a3e71d3d0 oops 2020-03-12 11:28:58 +05:30
Kovid Goyal
149ae2866a more typing work 2020-03-12 08:10:51 +05:30
Kovid Goyal
21707171bb Simplify the protocol for the send-text remote command 2020-03-12 07:14:47 +05:30
Kovid Goyal
2ebdf738ca more typing work 2020-03-11 20:08:16 +05:30
Kovid Goyal
0e871a89aa more typing work 2020-03-11 09:57:49 +05:30
Kovid Goyal
2d4f194f0d more typing work 2020-03-11 09:43:28 +05:30
Kovid Goyal
ce94a9b2df More typing work 2020-03-11 09:35:59 +05:30
Kovid Goyal
bfbb3c7068 Merge branch 'zsh_compdef_comment' of https://github.com/Luflosi/kitty 2020-03-11 07:17:47 +05:30
Luflosi
6d1fd20542 Add #compdef comment to zsh completion
This will allow adding completions without using `kitty + complete setup zsh | source /dev/stdin` by piping the code into a file named `_kitty` in a directory in `$fpath` instead of piping to `source`. This is especially handy for package managers that package kitty.
See https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org.
The `compdef _kitty kitty` line at the end doesn't seem to have any ill effects when using the completion code in a completion function file. When piping the code to `source` like before, the added line should be interpreted as a comment and be ignored.
2020-03-10 20:48:43 +01:00
Kovid Goyal
bb8cab3a02 Add type information for tui operations cmds 2020-03-10 22:25:57 +05:30
Kovid Goyal
10435c23c2 more typing work 2020-03-10 20:25:09 +05:30
Kovid Goyal
01142cdc8c Add type information to the Graphics Command infrastructure 2020-03-10 20:14:04 +05:30
Kovid Goyal
fc0adfd965 more typing work 2020-03-10 15:48:51 +05:30
Kovid Goyal
8803eeb890 more typing work 2020-03-10 12:58:53 +05:30
Kovid Goyal
25be705bcf DRYer 2020-03-09 21:19:17 +05:30
Kovid Goyal
d8f214a267 oops 2020-03-09 21:11:21 +05:30
Kovid Goyal
f74fb846e5 Fix #2421 2020-03-09 21:10:04 +05:30
Kovid Goyal
8ae110691e More typing work 2020-03-09 21:06:13 +05:30
Kovid Goyal
abff2292c1 Workaround for sphinx type info being available magically on the CI server 2020-03-09 18:37:23 +05:30
Kovid Goyal
7d690bbad2 Run mypy on CI 2020-03-09 18:24:55 +05:30
Kovid Goyal
7298de646a more typing work 2020-03-09 18:17:24 +05:30
Kovid Goyal
3c2f3ecd8e Fix #2420 2020-03-09 17:24:51 +05:30
Kovid Goyal
ac2c21e046 more typing work 2020-03-09 16:13:00 +05:30
Kovid Goyal
c817ba9eae more typing work 2020-03-09 13:32:45 +05:30
Kovid Goyal
92827ae647 More typing work 2020-03-09 09:56:37 +05:30
Kovid Goyal
5bdb405635 More typing work 2020-03-09 08:56:02 +05:30
Kovid Goyal
9beae321d7 More typing work 2020-03-08 22:08:18 +05:30
Kovid Goyal
353db678a2 Add typing to box_drawing 2020-03-08 21:30:50 +05:30
Kovid Goyal
9b973ef99c More typing work 2020-03-08 20:36:50 +05:30
Kovid Goyal
308d171dae More typing work 2020-03-08 18:55:30 +05:30
Kovid Goyal
7057bc663e Implement proper editor integration for mypy 2020-03-08 14:39:46 +05:30
Kovid Goyal
d84cff0baf The hints kitten should by default use the URL prefixes from kitty.conf 2020-03-08 13:26:55 +05:30
Kovid Goyal
645890ccc8 Make the set of URL prefixes that are recognized while hovering with the mouse configurable
Fixes #2416
2020-03-08 13:16:34 +05:30
Kovid Goyal
cc1336a616 More typing work
Also use a mypy based linter when editing
2020-03-08 11:02:14 +05:30
Kovid Goyal
9b32f18109 Refactor remote control commands into individual modules
Also add type information
2020-03-08 08:39:26 +05:30
Kovid Goyal
a0321376d5 Fix `@selection` placeholder not working with launch command
Fixes #2417
2020-03-08 08:00:36 +05:30
Kovid Goyal
0a6e264d09 Fix #2414 2020-03-07 15:15:27 +05:30
Kovid Goyal
afec07b124 More typing work 2020-03-06 14:19:46 +05:30
Kovid Goyal
8a34fede55 More typing work 2020-03-06 08:35:23 +05:30
Kovid Goyal
686b5f297e Ensure glfw is imported from the correct place 2020-03-06 08:23:01 +05:30
Kovid Goyal
6609d219f4 Add type checking for glfw module as well 2020-03-06 07:59:55 +05:30
Kovid Goyal
804998ca69 oops 2020-03-06 07:55:54 +05:30
Kovid Goyal
de5254469c Fix #2412 2020-03-06 07:48:41 +05:30
Kovid Goyal
ef879bab9c Fix #2411 2020-03-06 07:19:33 +05:30
Kovid Goyal
f729985346 More typing work
Also move notification_activated into a standalone function which will
hopefully fix #2410
2020-03-06 07:07:18 +05:30
Kovid Goyal
1e998b6a3f Use mypy in daemon mode 2020-03-05 20:38:51 +05:30
Kovid Goyal
77e9460eea Make mypy a bit stricter 2020-03-05 19:57:28 +05:30
Kovid Goyal
81a58186c6 Drop support for python 3.5 2020-03-05 18:31:20 +05:30
Kovid Goyal
ac149be2bb py3.5 compat 2020-03-05 18:23:45 +05:30
Kovid Goyal
a76a163db5 kitty.config is typed 2020-03-05 18:17:21 +05:30
Kovid Goyal
4ae59d3f77 The launch module is now fully typed 2020-03-05 16:20:56 +05:30
Kovid Goyal
d83a450260 A command for test.py to generate the typing stubs and run mypy 2020-03-05 15:58:30 +05:30
Kovid Goyal
61acc0784e Dont store generated stubs in source control 2020-03-05 15:50:54 +05:30
Kovid Goyal
f05890719d Add type checking for the various CLI options objects 2020-03-05 15:47:12 +05:30
Kovid Goyal
0f4e7921ee Create an auto-generated stub file for the kitty Options object 2020-03-04 19:14:50 +05:30
Kovid Goyal
ec8c96b8e6 diff kitten: Highlight pyi files as python 2020-03-04 11:52:58 +05:30
Kovid Goyal
f0b29e15c3 The utils and constants modules are now fully typechecked 2020-03-04 10:26:41 +05:30
Kovid Goyal
8ad62106e0 No global typing issues in all code (excluding tests) 2020-03-04 08:10:20 +05:30
Kovid Goyal
36eb52424f No top level typing issues for kittens 2020-03-04 07:49:28 +05:30
Kovid Goyal
c9ce2f47dc No top level typing issues found in kitty package 2020-03-04 07:31:22 +05:30
Kovid Goyal
5035ed61ee More typing work 2020-03-04 06:26:02 +05:30
Kovid Goyal
a4cc10c41b More typing work 2020-03-04 06:10:57 +05:30
Kovid Goyal
5730ce5f53 More typing work 2020-03-03 20:18:24 +05:30
Kovid Goyal
27524bfc3d Restore global key definitions so as not to break third party kittens that might use them 2020-03-03 15:06:10 +05:30
Kovid Goyal
8843ded1c9 Refactor key encoding
Cannot have key names in global namespace as not all key names are valid
python identifiers. So move them into a dict.
2020-03-03 14:54:30 +05:30
Kovid Goyal
64b497589f Start adding typing info 2020-03-03 14:31:25 +05:30
Kovid Goyal
ee48fd7151 ... 2020-03-03 12:25:02 +05:30
Kovid Goyal
291cd5e23f Also switch badges in manual 2020-03-01 17:13:27 +05:30
Kovid Goyal
be668cfd9a Switch status badge to github actions 2020-03-01 17:11:39 +05:30
Kovid Goyal
f889c1c75a Switch to using GitHub actions for CI 2020-03-01 17:02:53 +05:30
Kovid Goyal
55c5f3e9fd ... 2020-02-29 11:17:01 +05:30
Kovid Goyal
242d335095 A new `copy_and_clear_or_interrupt` function
Fixes #2403
2020-02-28 21:17:05 +05:30
Kovid Goyal
c763db94ce Fix #2401 2020-02-28 10:28:01 +05:30
Kovid Goyal
a391732600 left half of cell should happen only if url is truly empty 2020-02-28 07:37:10 +05:30
Kovid Goyal
06b45fcace Fix #2402 2020-02-28 07:19:31 +05:30
Kovid Goyal
bdac5f7d5b Add a bounds check in text_for_range() as well 2020-02-27 21:33:42 +05:30
Kovid Goyal
74865efddb move break condition into for loop 2020-02-27 21:31:42 +05:30
Kovid Goyal
b1f4d16692 Left in a printf() 2020-02-27 21:23:27 +05:30
Kovid Goyal
01b8e2c3f5 ... 2020-02-27 21:16:48 +05:30
Kovid Goyal
8c1ef94b26 Bounds check for apply selection
Fixes #2400
2020-02-27 20:27:20 +05:30
Kovid Goyal
507fbc7abf Fix #2399 2020-02-27 07:14:44 +05:30
Kovid Goyal
2f8002c567 Add a test for #1616 2020-02-26 09:20:08 +05:30
Kovid Goyal
551e28d8e0 Run tests python3.8 on linux 2020-02-26 07:34:31 +05:30
Kovid Goyal
e84163e838 Remove unused code 2020-02-26 07:31:43 +05:30
Kovid Goyal
3a64b835fb DRYer 2020-02-26 07:28:39 +05:30
Kovid Goyal
08a44ed4dd Fix #2397
Have to send selection data to GPU on first render.
2020-02-26 07:22:22 +05:30
Kovid Goyal
4b8cc23a86 Fix selection marking incorrect when scrolling 2020-02-25 22:06:53 +05:30
Kovid Goyal
c4b2de8939 Refactor function to not combine input and output parameters 2020-02-25 21:49:31 +05:30
Kovid Goyal
e2a603dcda Fix failing selection test
Unintialized memory use, was not creating an issue in debug builds only
on the CI servers
2020-02-25 21:44:54 +05:30
Kovid Goyal
294d01def6 Clear the click queue before a multi-click 2020-02-25 21:20:41 +05:30
Kovid Goyal
05f289f912 Make py 3.8 available in the CI docker image 2020-02-25 21:14:16 +05:30
Kovid Goyal
4343c4067e Make clang happy 2020-02-25 20:33:19 +05:30
Kovid Goyal
c167dcc4e9 Allow selection of single cells with the mouse. Also improve mouse selection to follow semantics common to most programs
Fixes #945
Fixes #1007
2020-02-25 20:19:38 +05:30
Kovid Goyal
9a67176719 Fix scrolling selection tests 2020-02-25 20:17:49 +05:30
Kovid Goyal
42d1a4755a Add tests for multiline movement 2020-02-25 20:03:14 +05:30
Kovid Goyal
1690e6933f Clean up handling of scrolling in selections 2020-02-25 20:03:14 +05:30
Kovid Goyal
5bbc15583e Non-scrolling based selection tests all pass 2020-02-25 20:03:14 +05:30
Kovid Goyal
e03aabd972 Some consting 2020-02-25 20:03:14 +05:30
Kovid Goyal
6f5968eeb0 Get rid of the ugly macro used for iteration over selections 2020-02-25 20:03:14 +05:30
Kovid Goyal
71b6e8f12a More selection fixes 2020-02-25 20:03:14 +05:30
Kovid Goyal
9c486f6e69 Track left to right for selections based on actual mouse positions
Cant use the selection boundaries as these can be programmatically
altered when extending selection by word or line.
2020-02-25 20:03:14 +05:30
Kovid Goyal
2f24588c66 Fix cell select mode tests 2020-02-25 20:03:14 +05:30
Kovid Goyal
6b1ea326a0 Initial implementation of half cell based selection 2020-02-25 20:03:14 +05:30
Kovid Goyal
4512d0dbac Update test for changes to ANSI serialization 2020-02-25 19:23:05 +05:30
Kovid Goyal
511c4da6a3 Workaround for bug in less that causes colors to reset at wrapped lines
Fixes #2381
2020-02-25 19:14:24 +05:30
Kovid Goyal
af8fdf1dbe Dont use a macro for as_text_generic 2020-02-25 15:33:12 +05:30
Kovid Goyal
b7d12de0a7 Avoid transitions in line_as_ansi on marks 2020-02-25 14:49:17 +05:30
Kovid Goyal
2d870fc84a Fix colors in scrollback pager off if the window has redefined terminal colors using escape codes
Fixes #2381
2020-02-25 10:46:51 +05:30
Kovid Goyal
6e87929603 Dont imply inline comments are supported in the config file 2020-02-24 18:52:51 +05:30
Kovid Goyal
2b441df9e9 Log an error for invalid detach_tab argument 2020-02-24 18:43:15 +05:30
Kovid Goyal
3e672ac178 Log an error for invalid detach_window arguments 2020-02-24 18:42:52 +05:30
Kovid Goyal
12a24d5c86 Remove unused code 2020-02-24 07:10:05 +05:30
Kovid Goyal
dec1a0886d typo 2020-02-23 19:34:31 +05:30
Kovid Goyal
a82e45ad71 Allow passing the currently active kitty window id in the launch command
See #2391
2020-02-23 19:17:26 +05:30
Kovid Goyal
82e5750d71 Merge branch 'tui-line-edit-cursor-end' of https://github.com/trygveaa/kitty 2020-02-23 18:21:27 +05:30
Trygve Aaberge
6a3eb3f3d7 tui line_edit: Set correct cursor position on end
Previously, the cursor would end up one character after the last typed
character when pressing end in the unicode input kitten.
2020-02-23 11:58:44 +01:00
Kovid Goyal
533a739e2b Put back the Brewfile 2020-02-22 19:07:17 +05:30
Kovid Goyal
b12a8331f1 Bump version of macOS for brew based CI 2020-02-22 18:59:34 +05:30
Kovid Goyal
8f4ec036e7 Install sphinx via pip rather than brew
That's what brew advises
2020-02-22 18:47:46 +05:30
Kovid Goyal
cba36f2240 brew bundle seems to have been removed 2020-02-22 18:36:07 +05:30
Kovid Goyal
b54ffbefe4 Ignore setlocale() failures on Linux as well
There are apparently some Linux systems that manage to bork their
locales out there. So just go on with the C locale on these systems.
2020-02-22 18:13:49 +05:30
Kovid Goyal
6d470e4da0 Merge branch 'patch-1' of https://github.com/MaxNoe/kitty 2020-02-20 15:57:18 +05:30
Kovid Goyal
4e1ebec8c2 Merge branch 'fix-menory-leak' of https://github.com/Luflosi/kitty 2020-02-20 15:55:18 +05:30
Maximilian Nöthe
e9779258e9 Make links clickable in INSTALL.md 2020-02-20 11:10:49 +01:00
Luflosi
5c83f45bc6 Fix memory leak when realloc() fails 2020-02-20 10:54:10 +01:00
Kovid Goyal
eb65aca1f4 Merge branch 'fix-boolean-assign-to-pointer' of https://github.com/Luflosi/kitty 2020-02-20 08:02:30 +05:30
Luflosi
5d7e393c93 Fix boolean value assigned to pointer
Found with Cppcheck.
2020-02-20 02:51:40 +01:00
Kovid Goyal
d6d01102fe diff kitten: Fix directory diffing with removed binary files failing
Fixes #2378
2020-02-19 19:53:19 +05:30
Kovid Goyal
1ad802b194 Make re-using the tall neighbor code safer 2020-02-19 15:42:01 +05:30
Kovid Goyal
4a191b65bb Fix a regression in the previous release that broke switching to neighboring windows in the Grid layout when there are less than four windows
Fixes #2377
2020-02-19 15:27:56 +05:30
Kovid Goyal
65cc9f4e1b Merge branch 'patch-1' of https://github.com/felixonmars/kitty 2020-02-19 08:28:15 +05:30
Felix Yan
9f4b13b523 Correct a typo in cmds.py 2020-02-19 05:12:25 +08:00
Kovid Goyal
97e1b62a1e Merge branch 'typo' of https://github.com/Luflosi/kitty 2020-02-18 21:02:48 +05:30
Luflosi
3ac966a38b Fix typo 2020-02-18 16:00:28 +01:00
Kovid Goyal
6f1fda0a48 Store cell half information in selection objects 2020-02-18 17:45:07 +05:30
Kovid Goyal
c00b486864 Track which horizontal half of the cell the mouse is in 2020-02-18 10:20:26 +05:30
Kovid Goyal
7928fff645 Update list of GLFW keys in the doc to use kitty's forked GLFW 2020-02-17 17:52:35 +05:30
Kovid Goyal
0285f5a1ad oops 2020-02-17 16:24:40 +05:30
Kovid Goyal
56e5c8be32 macOS: When switching inpt method while a pending multi-key input is in progress, clear the pending input
Fixes #2358
2020-02-17 15:30:03 +05:30
Kovid Goyal
ef569976cb Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2020-02-17 09:40:55 +05:30
Luflosi
f8ae048d60 X11: Fix setting the clipboard string to itself
From upstream: 0c27ed1d0e.
2020-02-17 01:12:52 +01:00
Kovid Goyal
c51f4df0ca When the application running in the terminal grabs the mouse, pass middle clicks to the application unless the terminal_select_modifiers are pressed
Fixes #2368
2020-02-16 22:55:18 +05:30
Kovid Goyal
84453bf15c Add a test for #2284 2020-02-16 21:14:08 +05:30
Kovid Goyal
917350f058 Fix incorrect rendering of selection when using rectangular select and scrolling
Fixes #2351
2020-02-16 21:10:09 +05:30
Kovid Goyal
0c58662eb6 Fix fade style tab bar showing opaque background after last tab 2020-02-16 15:11:55 +05:30
Kovid Goyal
52ca75855e Use some of th eglyphs from the poweline extra glyphs set in the symbol_map example 2020-02-16 12:17:34 +05:30
Kovid Goyal
49a07651b9 Merge branch 'typo' of https://github.com/Luflosi/kitty 2020-02-16 08:13:06 +05:30
Luflosi
90a8ba3789 Fix typo in comment 2020-02-15 18:52:18 +01:00
Kovid Goyal
734c3199f0 Fix rendering of combining characters with fonts that have glyphs for precomposed characters but not decomposed versions
Fix #2365
2020-02-15 21:55:17 +05:30
Kovid Goyal
64567646d9 Build with fortify source on clang as well 2020-02-13 11:20:57 +05:30
Kovid Goyal
1a1638cc7f Build with _FORTIFY_SOURCE on gcc 2020-02-13 10:39:57 +05:30
Kovid Goyal
fdf055312c DRYer 2020-02-13 10:07:55 +05:30
Kovid Goyal
b0b8b06b56 Scrolling test 2020-02-13 10:02:44 +05:30
Kovid Goyal
9c1ffb1adf Test for rectangle select 2020-02-13 08:52:33 +05:30
Kovid Goyal
563bafea08 Merge options when testing 2020-02-13 08:33:57 +05:30
Kovid Goyal
efe4c32ffd Fix mock window not being zeroes on creation 2020-02-12 21:34:19 +05:30
Kovid Goyal
0f003a9a77 Remove unused code 2020-02-12 21:28:48 +05:30
Kovid Goyal
3eff929652 More mouse interaction tests 2020-02-12 21:19:25 +05:30
Kovid Goyal
3010ebfef5 More mouse interaction tests 2020-02-12 19:02:38 +05:30
Kovid Goyal
ff14f47781 First mouse interaction test 2020-02-12 15:21:59 +05:30
Kovid Goyal
0b99a5caae APIs for testing mouse interaction 2020-02-12 10:14:17 +05:30
Kovid Goyal
467b96f2f2 Ensure vao_idx is invalid when releasing gpu resources 2020-02-12 09:20:41 +05:30
Kovid Goyal
18751a05c0 Dont use splitlines() to replace \n with \r in pasted text
splitlines breaks on a whole bunch of characters besides \r and \n.
2020-02-11 08:16:20 +05:30
Kovid Goyal
9007623d0c macOS: Fix menubar title not updating on OS Window focus change
Fixes #2350
2020-02-10 07:27:40 +05:30
Kovid Goyal
c1abb7038d Fix border/margin/padding sizes not being recalculated on DPI change
Fixes #2346
Fixes #2347
2020-02-09 08:31:09 +05:30
Kovid Goyal
53f7eebc4d Add changelog entry 2020-02-09 08:04:18 +05:30
Kovid Goyal
d54d29122d Merge branch 'Nudin-patch-1' of https://github.com/Nudin/kitty 2020-02-09 08:03:06 +05:30
Michael F. Schönitzer
1d71791b3c Bugfix exception when executing marker functions
Fixes the exception below that happened for function-markers:
```
Traceback (most recent call last):
  File "/usr/bin/../lib/kitty/kitty/boss.py", line 540, in dispatch_special_key
    return self.dispatch_action(key_action)
  File "/usr/bin/../lib/kitty/kitty/boss.py", line 612, in dispatch_action
    passthrough = f(*key_action.args)
  File "/usr/bin/../lib/kitty/kitty/window.py", line 615, in toggle_marker
    self.screen.set_marker(marker_from_spec(ftype, spec, flags))
  File "/usr/bin/../lib/kitty/kitty/marks.py", line 92, in marker_from_spec
    return marker_from_function(runpy.run_path(path, run_name='__marker__').marker)
```
2020-02-08 19:28:53 +01:00
Kovid Goyal
0c6145d4f2 ... 2020-02-08 14:08:10 +05:30
Kovid Goyal
62e273c5bb unicode input kitten: Allow pressing :kbd:ctrl+tab to change the input mode
Fixes #2343
2020-02-08 13:29:50 +05:30
Kovid Goyal
d82f399be7 ... 2020-02-06 21:16:16 +05:30
Kovid Goyal
20a7050564 Merge branch 'underline_thickness_option' of https://github.com/noomly/kitty 2020-02-06 21:14:34 +05:30
Kovid Goyal
b235f411b0 X11: Fix arrow mouse cursor using right pointing instead of the default left pointing arrow
Fixes #2341
2020-02-06 21:11:09 +05:30
Eyal Chojnowski
99faeeacfc Add PR number to changelog 2020-02-06 16:24:51 +01:00
Eyal Chojnowski
08ad4f389d Fix linting 2020-02-06 16:21:32 +01:00
Eyal Chojnowski
edba63d0d9 Make underline cursor thickness configurable 2020-02-06 16:15:12 +01:00
Kovid Goyal
e33f882abc Changelog entry for cursor beam thickness option 2020-02-06 17:52:40 +05:30
Kovid Goyal
53e059f4f7 Merge branch 'beam_thickness_configurable' of https://github.com/noomly/kitty 2020-02-06 17:49:34 +05:30
Eyal Chojnowski
830364e026 Fix option_type 2020-02-06 13:05:10 +01:00
Eyal Chojnowski
332714a925 Make cursor beam thickness configurable 2020-02-06 12:54:42 +01:00
Kovid Goyal
1eee6deb22 Fix #2339 2020-02-06 16:21:40 +05:30
Kovid Goyal
547f138703 Bound width and height for render_curosr 2020-02-06 06:55:13 +05:30
Kovid Goyal
b6c345fc3c Merge branch 'patch-2' of https://github.com/ctrlcctrlv/kitty 2020-02-05 09:58:07 +05:30
Fredrick Brennan
29c40a4957 Update changelog.rst
This change was discussed privately, lest I appear presumptuous for thanking myself :-)
2020-02-05 12:22:54 +08:00
Kovid Goyal
72af821e1b Freeing bgimage should always null its pointer 2020-02-04 09:51:35 +05:30
Kovid Goyal
5fe4d10f38 Only do processing if pager is not an absolute path 2020-02-03 08:05:34 +05:30
Kovid Goyal
3b36470e84 When resolving scrollback pager if it is not present in system PATH try using shell PATH 2020-02-03 08:03:35 +05:30
Kovid Goyal
f9e8a4050c Allow leading and trailing spaces in tab_title_template by using quotes
Fixes #2332
2020-02-02 21:02:34 +05:30
Kovid Goyal
ce308286df Dont need to bind blit vertex array as cells vertex array is already bound 2020-02-02 20:56:42 +05:30
Kovid Goyal
b670973226 When unsetting bg image borders need to be redone to ensure blank rects are present 2020-02-02 20:51:31 +05:30
Kovid Goyal
01b11b8a53 Allow removing background image via @ set-background-image 2020-02-02 20:41:52 +05:30
Kovid Goyal
75c47a8659 Proper fix for not drawing blank rects when bg image is present 2020-02-02 20:20:45 +05:30
Kovid Goyal
5b4e0ed483 Fix #2331
Some leftover unused code from the bg image merge
2020-02-02 20:09:17 +05:30
Kovid Goyal
7432a7f346 Bind empty VAO when tinting 2020-02-02 14:48:47 +05:30
Kovid Goyal
0830fc3b5a Make background_image tint useable even without a background image in a transparent window 2020-02-02 12:40:29 +05:30
Kovid Goyal
ab2c901e85 Implement bg image tint for transparent windows 2020-02-02 12:34:35 +05:30
Kovid Goyal
3774cc54c7 Fix rendering of semi-transparent bg images in semi-transparent windows
Output colors must be premultiplied
2020-02-02 11:45:48 +05:30
Kovid Goyal
330ed8e4ae Add an option to tint background images using the current background color
Note still has to be implemented for transparent windows
2020-02-02 11:22:21 +05:30
Kovid Goyal
eddae44d6d Fix leak when changing background image using remote control 2020-02-02 09:27:58 +05:30
Kovid Goyal
226488717a Add completion for image filenames at command line
Sadly could not get it to work with the shell
2020-02-01 12:33:11 +05:30
Kovid Goyal
2f9dabd344 Fix unwanted scaling when tiling bg image 2020-02-01 11:41:06 +05:30
Kovid Goyal
cf1b7c0adb Allow set_background_image to work over SSH 2020-02-01 11:35:12 +05:30
Kovid Goyal
eb57d0d431 Remote control command to set background image 2020-02-01 10:50:56 +05:30
Kovid Goyal
8116ee015a API for changing the background image of OS windows
Uses reference counting
2020-02-01 09:17:41 +05:30
Kovid Goyal
0fa3cec61f Add support for specifying a background image
Fixes #163
2020-01-31 20:22:29 +05:30
Kovid Goyal
eb60464177 No need to zero background opacity
We anyway dont draw the default background color in
draw_interleaved(_premult)
2020-01-31 18:51:02 +05:30
Kovid Goyal
44b2651a0f Fix draw_cells_interleaved_premult() for drawing with and without a background image 2020-01-31 15:40:00 +05:30
Kovid Goyal
f2e91b5ed2 Remove incorrect setting of bgimage unit in graphics program 2020-01-31 15:28:21 +05:30
Kovid Goyal
e72b60c882 tiling->tiled 2020-01-31 15:11:30 +05:30
Kovid Goyal
5263babc9a Use a single program for drawing background images 2020-01-31 15:09:47 +05:30
Kovid Goyal
9034b50b3a Dont use a separate background image opacity setting 2020-01-31 14:05:25 +05:30
Kovid Goyal
fdd7fe1948 Properly release resources used by bgimage
Also allow per OS Window bg images in the future
2020-01-31 12:17:54 +05:30
Kovid Goyal
8e7b8b70dc A VAO is not needed to draw background images 2020-01-31 11:34:34 +05:30
Kovid Goyal
9bf24bdff5 Cleanup png_path_to_bitmap() 2020-01-31 09:46:16 +05:30
Kovid Goyal
ee53edd96d Fix conversion of background_image option to C
Should handle None and not rely on the python object's lifetime.
Also some miscellaneous cleanups.
2020-01-31 09:03:19 +05:30
Kovid Goyal
1bc5d7038d Merge branch 'bgimage' of https://github.com/ctrlcctrlv/kitty 2020-01-31 07:58:55 +05:30
Fredrick Brennan
8a6b8b415c More stylistic fixes 2020-01-31 09:48:28 +08:00
Kovid Goyal
86174c81c8 Some tests for splits layout 2020-01-30 20:30:52 +05:30
Kovid Goyal
374500d7b0 py3.5 compat 2020-01-30 18:55:58 +05:30
Kovid Goyal
319a141967 grammar 2020-01-30 18:52:40 +05:30
Kovid Goyal
c0d0062ef8 Add docs for individual layouts
Also changelog entry for the splits layout
Fixes #2308
2020-01-30 18:50:28 +05:30
Fredrick Brennan
aaaa34e119 Move a bunch of unneeded stuff 2020-01-30 18:08:25 +08:00
Fredrick Brennan
fba9502a9d Fix background_image_linear 2020-01-30 18:01:28 +08:00
Kovid Goyal
6670cc1534 Dont swap when removing overlay window in splits 2020-01-30 15:28:51 +05:30
Kovid Goyal
970729dca0 Work on expanding documentation for layouts 2020-01-30 15:06:23 +05:30
Fredrick Brennan
6627586d6e Fix pedantic compiler test 2020-01-30 17:31:37 +08:00
Fredrick Brennan
9f364209af Add background_image 2020-01-30 17:23:42 +08:00
Kovid Goyal
c31a39a052 Action to convert between vertical and horizontal splits 2020-01-30 14:34:40 +05:30
Kovid Goyal
36fda90814 Implement window movement for splits 2020-01-30 13:33:15 +05:30
Kovid Goyal
7ccfbc16e9 DRYer 2020-01-30 07:26:32 +05:30
Kovid Goyal
ad3b527bc4 Neighboring windows for splits 2020-01-30 07:09:33 +05:30
Kovid Goyal
f7eef3f456 Minimal borders for the splits layout 2020-01-30 06:12:45 +05:30
Kovid Goyal
1870364888 When laying out windows in splits shove them offscreen if their widths/heights become too small as too small values will cause crashes 2020-01-30 06:12:45 +05:30
Kovid Goyal
d7dfc85cf7 A __repr__ for Pairs 2020-01-30 06:12:45 +05:30
Kovid Goyal
c047e5f16a Implement perpendicular axis resizing for splits 2020-01-30 06:12:45 +05:30
Kovid Goyal
8d21f63ec2 Resizing of a single split 2020-01-30 06:12:45 +05:30
Kovid Goyal
85b6a91e5c Blank rects for splits layout 2020-01-30 06:12:45 +05:30
Kovid Goyal
c788e30b66 Implement adding of windows to splits layout 2020-01-30 06:12:45 +05:30
Kovid Goyal
a99caed693 Basic implementation of splits layout 2020-01-30 06:12:45 +05:30
Kovid Goyal
9589fb5275 Clarify docs for background_opacity 2020-01-30 05:59:52 +05:30
Kovid Goyal
1a6dc530a4 Improve create_marker docs 2020-01-29 14:41:01 +05:30
Kovid Goyal
60835871eb Better error message when user does not specify strategy for disabling ligatures 2020-01-28 15:23:08 +05:30
Kovid Goyal
644ee646a2 version 0.16.0 2020-01-28 08:48:00 +05:30
Kovid Goyal
8f1ec053ef ... 2020-01-27 08:50:29 +05:30
Kovid Goyal
2e3037ce3a Wayland: Fix window titles being set to very long strings on the order of 8KB causing a crash
Fixes #1526
2020-01-27 08:49:25 +05:30
Kovid Goyal
109a856b91 Fix #2313 2020-01-26 08:00:06 +05:30
Kovid Goyal
eaec854bfb ... 2020-01-25 11:51:17 +05:30
Kovid Goyal
2c6cc6f047 Remove unused slot 2020-01-25 11:22:22 +05:30
Kovid Goyal
f32ae10339 Use a global cache instead of a per window cache for grid size calculation 2020-01-25 11:21:05 +05:30
Kovid Goyal
c46317f96f Use typed layout data 2020-01-25 11:11:55 +05:30
Kovid Goyal
a67e4e550e Merge branch 'issue2310' of https://github.com/ctrlcctrlv/kitty 2020-01-24 13:43:15 +05:30
Fredrick Brennan
65a7b19f40 Make each OS window have its own framebuffer
This commit fixes graphics rendering when more than one OS window is
open, all OS windows are semi-transparent, and multiple windows contain
graphics.

This commit closes #2310.
2020-01-24 16:06:57 +08:00
Kovid Goyal
1b5daf930a Make the comment explaining wayland polling more detailed 2020-01-23 15:08:54 +05:30
Kovid Goyal
748ca81d4b Wayland: Fix a freeze in rare circumstances when having multiple OS Windows
Fixes #2307
Fixes #1722
2020-01-23 15:07:22 +05:30
Kovid Goyal
0ea7d24ba4 Fix incorrect offset when rendering images and there are +z-index, -z-index and --z-index images 2020-01-21 08:09:04 +05:30
Kovid Goyal
6c9861cb76 Fix incorrect blending of default bg color when using draw_cells_interleaved_premult with images but no below images 2020-01-21 07:59:44 +05:30
Kovid Goyal
a1125b5cc8 Move draw_bg multiplication into vertex shader 2020-01-21 07:58:34 +05:30
Kovid Goyal
b4e189d2bf Merge branch 'simplify' of https://github.com/Luflosi/kitty 2020-01-21 06:54:28 +05:30
Luflosi
dbc763fca0 Remove useless code from shader 2020-01-21 00:28:55 +01:00
Kovid Goyal
4c03ffeae2 Merge branch 'patch-1' of https://github.com/ctrlcctrlv/kitty 2020-01-18 15:11:03 +05:30
Fredrick Brennan
2719ac7d01 Add :pull:2248 to changelog.rst
It was missing
2020-01-18 17:34:27 +08:00
Kovid Goyal
219b81b5ee typo 2020-01-18 08:19:42 +05:30
Kovid Goyal
8e1443360f Merge branch 'force_ltr' of https://github.com/ctrlcctrlv/kitty 2020-01-18 08:07:56 +05:30
Kovid Goyal
d55242aa82 Merge branch 'master' of https://github.com/mfilenko/kitty 2020-01-17 21:40:10 +05:30
Max Filenko
ba531969ec List layouts in their default order 2020-01-17 16:28:44 +01:00
Max Filenko
c12e7587cb Document default configuration 2020-01-17 16:28:30 +01:00
Kovid Goyal
3aac0860a2 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2020-01-17 20:28:26 +05:30
Luflosi
e4fd12001c Cocoa: Add fully dynamic loading of Vulkan loader
From upstream: 7da87aaae7.
2020-01-17 14:18:28 +01:00
Luflosi
3581ffe04b Cocoa: Select Vulkan surface extension at runtime
From upstream: 15d91801b7.
2020-01-17 14:18:21 +01:00
Luflosi
f6901e4a3d Cocoa: Add support for VK_EXT_metal_surface
From upstream: c5cb4a253a.
2020-01-17 14:18:05 +01:00
Fredrick Brennan
5a2cc4578e force_ltr also makes FriBidi usable
Add above fact to documentation.
2020-01-17 17:57:26 +08:00
Fredrick Brennan
6b5acc4d3d Add force_ltr
Close #2252
2020-01-17 16:22:40 +08:00
Kovid Goyal
43326c9bd0 Fix URL detection not working for urls of the form scheme:///url
Fixes #2292
2020-01-17 07:49:01 +05:30
Kovid Goyal
96f3253e6d goto_tab now maps numbers larger than the last tab to the last tab
Fixes #2291
2020-01-16 20:17:17 +05:30
Kovid Goyal
f5090c951a Merge branch 'with_open' of https://github.com/Luflosi/kitty 2020-01-16 19:41:55 +05:30
Luflosi
edacdc5e25 Use python context manager to open file 2020-01-16 14:11:54 +01:00
Kovid Goyal
c67661af61 oops 2020-01-15 21:39:13 +05:30
Kovid Goyal
5e60329adf Changelog entry to titlebar-only 2020-01-15 21:37:20 +05:30
Kovid Goyal
6b4f8408de Merge branch 'macos_hide_titlebar' of https://github.com/Luflosi/kitty 2020-01-15 21:36:03 +05:30
Luflosi
ea1ef709c6 macOS: Allow hiding the titlebar only instead of all window decorations
Thanks to @ntruong for writing cb8279ec54 and  6807411365, the code was very helpful.
2020-01-15 16:35:36 +01:00
Kovid Goyal
bbf247c5c0 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2020-01-15 19:32:12 +05:30
Kovid Goyal
8243fd505f Make mark colors per window 2020-01-15 18:42:37 +05:30
Luflosi
ed9f9db1b0 X11: Fix parent window handle initialization
From upstream: 9372ba95fa.
2020-01-15 13:19:33 +01:00
Luflosi
74bed658e1 X11: Fix no window position events during resize
From upstream: fe57e3c292.
2020-01-15 13:19:32 +01:00
Kovid Goyal
2f7bdc274d Fix failing test 2020-01-15 11:48:13 +05:30
Kovid Goyal
1347ff27d9 Changelog entry for triple click change 2020-01-15 11:46:50 +05:30
Kovid Goyal
5bf4d520f5 Merge branch 'master' of https://github.com/copy/kitty 2020-01-15 11:43:20 +05:30
Kovid Goyal
cf8d665eb7 Allow having more than one full height window in the :code:tall layout
Fixes #2276
2020-01-15 11:28:58 +05:30
Fabian
b2eee6df5c Multiline triple-click selections: Don't ignore leading spaces in first line 2020-01-14 21:04:59 -06:00
Kovid Goyal
e8121b39ec Add newer layout classes to tests 2020-01-15 08:14:45 +05:30
Kovid Goyal
5359abab76 next version should be 0.16.0 2020-01-15 08:05:45 +05:30
Kovid Goyal
01644ac3f0 Changelog entry for marks 2020-01-15 08:05:12 +05:30
Kovid Goyal
8fbf552494 Allow opening new tabs/windows before the current tab/window as well as after it with the :option:launch --location option. 2020-01-15 08:02:05 +05:30
Kovid Goyal
9c3390c5e6 Allow specifying a marker when launching windows 2020-01-15 07:22:32 +05:30
Kovid Goyal
24d45b99e3 typo 2020-01-15 07:13:32 +05:30
Kovid Goyal
936f2186cc Allow scrolling to marks 2020-01-15 07:11:34 +05:30
Kovid Goyal
c68b302fa2 Merge branch 'wayland' of https://github.com/kennylevinsen/kitty 2020-01-15 06:51:24 +05:30
Kenny Levinsen
ca7cab1a2d wayland: Consistently use errno for errors 2020-01-14 20:06:13 +01:00
Kenny Levinsen
083b294659 wayland: Simplify dispatch_pending error handling
wl_display_dispatch_pending does not return EAGAIN, and always sets an
error when it fails. This allows us to handle errors in a simpler
manner.
2020-01-14 20:04:23 +01:00
Kenny Levinsen
bc24716476 wayland: errno does not have to be cleared
As long as errno is only read on error from an errno-setting function,
then there is no need to reset errno between uses.
2020-01-14 20:04:23 +01:00
Kenny Levinsen
4acab65016 wayland: Only cancel display read after prepare success
The display reader count only increments when wl_display_prepare_read
succeeds.  Calling wl_display_cancel_read when wl_display_prepare_read
has not succeeded results in a negative reader count, which does not
have well-defined behavior.
2020-01-14 20:00:47 +01:00
Kenny Levinsen
f6b03f106c wayland: Cancel display read before abortOnFatalError
Calling wl_display_cancel_read immediately ensures that other readers
waiting on us will have a chance to wake up and discover the error in a
timely manner.
2020-01-14 19:59:02 +01:00
Kovid Goyal
8f9bbeabd7 Fix reference to --debug-config 2020-01-14 19:50:18 +05:30
Kovid Goyal
ac72ac7af2 Wayland: On dispatch error cancel read before running fatal error handlers
Might help #1722
2020-01-14 19:48:28 +05:30
Kovid Goyal
68e00bc8e1 Proper fix for #2270
Fixes #2270
2020-01-14 18:50:06 +05:30
Kovid Goyal
922d6b3b32 Fix #2270 2020-01-14 17:20:35 +05:30
Kovid Goyal
4dde98305e Allow setting markers using remote control 2020-01-14 15:06:52 +05:30
Kovid Goyal
3593189a75 Add a link to the marks docs 2020-01-14 13:45:37 +05:30
Kovid Goyal
4f135be86c Allow creating markers dynamically 2020-01-13 20:05:50 +05:30
Kovid Goyal
605b32f78a Merge branch 'dryer' of https://github.com/Luflosi/kitty 2020-01-13 19:42:56 +05:30
Luflosi
476a8b1429 DRYer
Inspired by e0c66ea5ba.
2020-01-13 14:35:17 +01:00
Kovid Goyal
da9165aebf More marker docs 2020-01-13 12:54:29 +05:30
Kovid Goyal
455a1e9532 Basic docs for marking 2020-01-13 12:38:58 +05:30
Kovid Goyal
f12031e284 Make the run-program example a little fancier 2020-01-13 12:16:43 +05:30
Kovid Goyal
df551bc992 Change marker 1 color 2020-01-13 11:58:18 +05:30
Kovid Goyal
35fb702833 Use only a single marker function
Multipe colors/expressions can instead be combined at definition time
2020-01-13 11:57:19 +05:30
Kovid Goyal
072cd29e3c Make add_marker easier to use with combine 2020-01-13 09:56:39 +05:30
Kovid Goyal
624dd78460 Actually apply mark colors when rendering 2020-01-13 09:51:03 +05:30
Kovid Goyal
51c4737a29 Test function marking 2020-01-13 06:50:37 +05:30
Kovid Goyal
d63ea11cac Marking now works 2020-01-13 06:50:37 +05:30
Kovid Goyal
3d307794be Function to get marks for testing 2020-01-13 06:50:36 +05:30
Kovid Goyal
d32a05925f Code to add and remove markers using maps 2020-01-13 06:50:36 +05:30
Kovid Goyal
04006ea81e Wrapper functions to handle passing matching values to C code 2020-01-13 06:50:36 +05:30
Kovid Goyal
1723616968 Code to apply match colors based on match results 2020-01-13 06:50:36 +05:30
Kovid Goyal
23bc2171c9 Avoid a double call for mark functions 2020-01-13 06:50:36 +05:30
Kovid Goyal
0b3602f764 Code to add markers to Screen 2020-01-13 06:50:36 +05:30
Kovid Goyal
0e448347cd Fix #2279 2020-01-13 06:50:14 +05:30
Kovid Goyal
e0c66ea5ba DRYer 2020-01-12 21:42:12 +05:30
Kovid Goyal
268de9a9d5 Merge branch 'add_CIRCUMFLEX_to_control_codes' of https://github.com/Luflosi/kitty 2020-01-12 21:03:27 +05:30
Luflosi
74eb523178 Add GLFW_KEY_CIRCUMFLEX to control_codes
See https://github.com/kovidgoyal/kitty/issues/1990.
2020-01-12 16:27:40 +01:00
Kovid Goyal
8c540d5a46 Fix #2277 2020-01-12 20:00:08 +05:30
Kovid Goyal
2045055ae3 Use actual color value comparison when detecting default background color
This is very slightly more GPU intensive but means that even if
an application explicitly sets the color to be the same as the default
color, it will be transparent.
2020-01-12 18:14:03 +05:30
Kovid Goyal
d8ed135b46 Clean up rendering of cell backgrounds 2020-01-12 13:20:23 +05:30
Kovid Goyal
3a4ba7e834 Remove un-needed variable 2020-01-12 10:55:14 +05:30
Kovid Goyal
fab57fbdf1 Micro-optimization 2020-01-12 10:53:27 +05:30
Kovid Goyal
76a93b6cd6 DRYer 2020-01-12 10:52:42 +05:30
Kovid Goyal
f44c3a4606 Forgot to update comment describing render pipeline 2020-01-12 10:26:25 +05:30
Kovid Goyal
714bee7f45 Only use an extra draw call for the background when drawing interleaved cells if it is actually required 2020-01-12 09:37:16 +05:30
Kovid Goyal
91673642b3 icat: Allow easily specifying z-index values below background threshold 2020-01-12 09:36:11 +05:30
Kovid Goyal
fee08d746c Merge branch 'replace_exception_with_alias' of https://github.com/Luflosi/kitty 2020-01-11 21:21:52 +05:30
Luflosi
527ff0238a Replace all instances of EnvironmentError with OSError
According to the text just above https://docs.python.org/3/library/exceptions.html#EnvironmentError, `EnvironmentError` has been an alias of `OSError` since Python 3.3. Replacing it makes the code more consistent since `OSError` is used in other places in the code too.
2020-01-11 16:47:36 +01:00
Kovid Goyal
789d649b5c defaultbg should be a local variable 2020-01-11 16:59:50 +05:30
Fredrick Brennan
8020657d8c Allow drawing images below cells w/background 2020-01-11 16:56:59 +05:30
Kovid Goyal
a15d3ff12d ... 2020-01-10 10:19:02 +05:30
Kovid Goyal
fbce95aeda Add an example for open file hints 2020-01-10 10:18:18 +05:30
Kovid Goyal
9bc1b5a2d9 hints kitten: Allow pressing :sc:goto_file_line to quickly open the selected file at the selected line in vim
Fixes #2268
2020-01-10 10:01:03 +05:30
Kovid Goyal
6a8b7bf92f Merge branch 'with_suppress' of https://github.com/Luflosi/kitty 2020-01-09 20:58:37 +05:30
Luflosi
48728bdfdc Use "with suppress()" to suppress a python exception 2020-01-09 16:21:56 +01:00
Kovid Goyal
b6416cf8a9 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2020-01-09 06:58:30 +05:30
Luflosi
053de5763a X11: Fix BadMatch focusing a window on non-EWMH WM
From upstream: aa5e313561.
2020-01-08 19:50:17 +01:00
Kovid Goyal
fdb915d0c8 Merge branch 'zindex' of https://github.com/ctrlcctrlv/kitty 2020-01-08 18:20:34 +05:30
Fredrick Brennan
c2769c9aed Add --z-index to icat kitten
Meow.
2020-01-08 20:08:01 +08:00
Kovid Goyal
57deb19d0b typo 2020-01-07 10:45:05 +05:30
Kovid Goyal
321771b150 Merge branch 'resize-by-increments' of https://github.com/aes/kitty 2020-01-07 10:44:55 +05:30
Kovid Goyal
61dd2011f5 Dont rely on fontconfig to get postscript names 2020-01-07 09:30:41 +05:30
Kovid Goyal
8a5f189213 Ignore invalid font features when parsing config 2020-01-07 09:20:04 +05:30
Kovid Goyal
14560b008a Clean up font features merge 2020-01-07 08:54:32 +05:30
Kovid Goyal
db85e07d41 Merge branch 'master' of https://github.com/ctrlcctrlv/kitty 2020-01-07 07:18:31 +05:30
Kovid Goyal
07c838fe24 Merge branch 'round_printed_timeout' of https://github.com/Luflosi/kitty 2020-01-06 07:14:53 +05:30
Luflosi
1583b1b0c6 Print warning message timeout rounded to two digits after the dot
The timeout is 0.25 seconds. Printing 0.25 instead of 0.250000 looks a lot nicer and a resolution of 10 milliseconds should be enough for this warning message anyways.
2020-01-05 16:10:10 +01:00
Fredrick Brennan
d250555cd0 Make font_feature_settings respect disable_ligatures 2020-01-04 20:11:34 +08:00
Fredrick Brennan
dbd0dab154 Revert addition of ffs_set to Font struct
It was from an earlier stage of development and is not needed
2020-01-04 15:37:05 +08:00
Fredrick Brennan
b479ea410d Prevent leaks
Thanks @martinetd
2020-01-04 15:34:47 +08:00
Fredrick Brennan
1db613e95b Add font_feature_settings
Close #2247
2020-01-04 14:36:20 +08:00
Kovid Goyal
70071fe1f6 Merge branch 'simplify' of https://github.com/Luflosi/kitty 2020-01-03 07:46:58 +05:30
Luflosi
58688dda38 Remove duplicate code
The hints, once set with `glfwWindowHint()`, retain their values until changed again. This means, that the `GLFW_DECORATED` hint only needs to be set once, even on non-macOS.
2020-01-03 03:01:53 +01:00
Kovid Goyal
756d65c41e Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2020-01-03 07:18:25 +05:30
Luflosi
b9a08eacb0 X11: Cleanup
From upstream: 8149a5fc00.
2020-01-03 02:37:19 +01:00
Kovid Goyal
b653711446 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2020-01-03 06:48:30 +05:30
Luflosi
e725c647ef Cocoa: Only create per-monitor display link once
From upstream: b4a8eb9b19.
2020-01-02 18:05:38 +01:00
Kovid Goyal
003f77b257 Merge branch 'add_macos_help_menu_item' of https://github.com/Luflosi/kitty 2019-12-29 22:03:08 +05:30
Luflosi
10b578e6d2 Add help menu to macOS menu bar
Most macOS Apps have a Help menu as the last menu item in the menu bar. They usually have a menu item with the keyboard shortcut <kbd>⌘</kbd>+<kbd>?</kbd>, which shows some kind of help or documentation. I named the menu item "Visit kitty website" and let it open the kitty website as documentation.
macOS also magically adds a search feature to the help menu.
2019-12-29 17:24:12 +01:00
Kovid Goyal
151c6c2c70 Merge branch 'always_show_menu_item' of https://github.com/Luflosi/kitty 2019-12-29 20:53:22 +05:30
Luflosi
53e6de9bd9 Always show the macOS New OS Window menu item
When there is no keyboard shortcut for a new OS window, the macOS menu item should still exist, it should just not have a keyboard shortcut.
2019-12-29 15:27:52 +01:00
Kovid Goyal
66a455a079 Merge branch 'simplify' of https://github.com/Luflosi/kitty 2019-12-29 19:45:07 +05:30
Luflosi
f8855410e0 Simplify some code in kitty/cocoa_window.m
`addItemWithTitle:` can be used instead of `initWithTitle:`. It returns an `NSMenuItem`, which removes the need for allocating an `NSMenuItem` manually and releasing it again.
2019-12-29 14:33:19 +01:00
Kovid Goyal
93e8074325 ... 2019-12-28 08:49:53 +05:30
Kovid Goyal
296f512bfb Forgot you cant use sizeof on arrays passed as function parameters 2019-12-28 08:44:32 +05:30
Kovid Goyal
c8f20d0edf Ensure key name is null terminated 2019-12-28 08:33:13 +05:30
Kovid Goyal
286f3630d7 Merge branch 'macos_fix_keyboard_shortcut_encoding' of https://github.com/Luflosi/kitty 2019-12-28 08:26:03 +05:30
Luflosi
38423505b1 Remove numpad keys from macOS keyboard shortcuts
See previous commit message for the reason.
2019-12-27 15:12:26 +01:00
Luflosi
9a3c621eb6 Fix macOS keyboard shortcut encoding
`glfwGetCocoaKeyEquivalent()` in `glfw/cocoa_window.m` expects the returned characters to be of type `unichar`, which won't work for all unicode characters because it is defined as `unsigned short` according to https://developer.apple.com/documentation/foundation/unichar?language=objc, which is only guaranteed to be at least 16 bits in size. The code calling this function also expects the encoding to be UTF-16.
When I added the various keys in https://github.com/kovidgoyal/kitty/pull/1928, I missed these facts. This means, that `glfwGetCocoaKeyEquivalent()` will behave unexpectedly when called with any of the new-ish keys. Luckily this function is currently only used for determining the macOS shortcut for `new_os_window` but I plan on using it more in the future.
Some of the constants, e.g. `NSBackspaceCharacter` are UTF-16 constants, so we can't just use UTF-8 everywhere.
I fixed the problem by using either UTF-8 characters packed into a `uint32_t` or UTF-16 characters in a `unichar` and then converting them to a UTF-8 encoded char string.

`NSEventModifierFlagNumericPad` isn't guaranteed to fit in a `unichar`, which made this undefined behaviour. It also didn't work. I tried to make it work using `NSEventModifierFlagNumericPad` as a modifier instead, as can be seen in this commit, but couldn't get it to work either because the constants used are native key codes and not unicode characters. Therefore the numpad keys will be removed in the next commit.
2019-12-27 12:42:51 +01:00
Kovid Goyal
bcf847fe98 ... 2019-12-27 15:26:31 +05:30
Kovid Goyal
ee7a5eef99 Have read_shell_environment() not fail when no shell is present 2019-12-27 12:25:01 +05:30
Kovid Goyal
7bed66a458 Ensure ans is set in read_shell_environment
Fixes #2232
2019-12-27 12:20:06 +05:30
Kovid Goyal
b26229c44e Merge branch 'remove_release' of https://github.com/Luflosi/kitty 2019-12-26 08:48:20 +05:30
Luflosi
c497c0f96a Remove call to release on an automatically memory managed object
This call even reliably caused a crash for code I was trying to develop.
2019-12-25 20:29:07 +01:00
Kovid Goyal
6df64613f2 Merge branch 'replace_void_pointer' of https://github.com/Luflosi/kitty 2019-12-25 16:35:08 +05:30
Luflosi
46dd648380 Use the correct pointer type instead of void* 2019-12-25 11:51:40 +01:00
Kovid Goyal
4cd5de78c7 Merge branch 'generate_key_table' of https://github.com/Luflosi/kitty 2019-12-25 08:34:27 +05:30
Luflosi
32fa46d6eb Regenerate key table
Since `CIRCUMFLEX` and `^` were removed from `UN_SHIFTED_PRINTABLE` in b2d428618c by 3a2a16f54c and b5229ec73c, `generate_key_table()` wasn't yet run again.
2019-12-25 02:16:37 +01:00
Kovid Goyal
902c2f6c88 Merge branch 'update_comment' of https://github.com/Luflosi/kitty 2019-12-24 19:31:37 +05:30
Luflosi
2a5196873a Update comment to remove legacy way of starting kitty
Since 9135387cfa kitty should not be started with `python3 .` anymore.
2019-12-24 14:22:37 +01:00
Kovid Goyal
d2067141d3 Fix #2226 2019-12-24 17:10:33 +05:30
Kovid Goyal
b5229ec73c oops 2019-12-23 10:19:02 +05:30
Kovid Goyal
3a2a16f54c Merge branch 'add_circumflex_key' of https://github.com/Luflosi/kitty 2019-12-23 10:08:25 +05:30
Luflosi
b2d428618c Add circumflex (^) key 2019-12-22 18:41:07 +01:00
Kovid Goyal
97af84a063 version 0.15.1 2019-12-21 14:20:28 +05:30
Kovid Goyal
18faee6250 Merge branch 'glad2-regen' of https://github.com/Dav1dde/kitty 2019-12-20 20:19:54 +05:30
David Herberth
fd90a3ddc3 regenerates glad to fix missing APIENTRY 2019-12-20 15:04:23 +01:00
Kovid Goyal
a08931d84d Fix window not being rendered for the first time until some input has been received from child process
Fixes #2216
2019-12-20 08:46:50 +05:30
Kovid Goyal
2f0b6e24c9 Use pre-multiplied alpha when clearing windows
Apparently most systems expect this. See https://github.com/glfw/glfw/issues/1538
2019-12-20 08:16:10 +05:30
Kovid Goyal
33c5fc0fb6 typo 2019-12-20 07:59:55 +05:30
Kovid Goyal
71adb2dcf8 Merge branch 'new_os_window_key_macos' of https://github.com/Luflosi/kitty 2019-12-20 07:59:08 +05:30
Luflosi
1ae324691d Change keyboard shortcut selection algorithm for new_os_window on macOS
On macOS the keyboard shortcuts are visible in the menu bar. When the keyboard shortcut is used, the corresponding menu bar item flashes to indicate which action was just executed.
kitty allows defining multiple keyboard shortcuts for the same action but macOS allows only one, so kitty needs to decide which one should be handled by macOS. Currently it chooses the first keyboard shortcut with only the command key as a modifier key or the first shortcut when there are no shortcuts with only the command key as a modifier.
When a user tries to set their own keyboard shortcut (and doesn't use `clear_all_shortcuts yes`), this won't change the shortcut displayed in the menu bar since the first (default) shortcut with the command key is <kbd>⌘</kbd>+<kbd>n</kbd>.
I think simply choosing the last defined keyboard shortcut is better. This will even allow the user to specify modifier keys other than the command key while still changing the shortcut in the menu bar. This change will not change the default behaviour because all the macOS specific keyboard shortcuts are defined after the non-macOS specific ones.
2019-12-19 22:15:41 +01:00
Kovid Goyal
689d059517 Fix lines at the edge of the window at certain windows sizes when drawing images on a transparent window
Fixes #2079
Fixes #2214
2019-12-19 20:41:31 +05:30
Kovid Goyal
18f0ab9e02 Merge branch 'fix_typo' of https://github.com/Luflosi/kitty 2019-12-19 20:01:12 +05:30
Luflosi
088ab5f137 Fix typo 2019-12-19 13:40:08 +01:00
Kovid Goyal
3dd83975ba Dont cast double to monotonic_t for literals 2019-12-19 17:33:12 +05:30
Kovid Goyal
a796a6cbef Use exponential notation in monotonic.h 2019-12-19 17:22:33 +05:30
Kovid Goyal
4766d7de82 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-12-19 17:19:42 +05:30
Luflosi
4a3686c16c X11: Cleanup
From upstream: 73a8ebb691.
2019-12-19 12:38:56 +01:00
Kovid Goyal
b49105495c Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-12-19 17:07:42 +05:30
Luflosi
ca2eda2c5b X11: Cleanup
From upstream: 4e70c95aa3.
2019-12-19 12:35:44 +01:00
Kovid Goyal
4fc7c4aafc Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-12-19 17:05:10 +05:30
Luflosi
0981828493 X11: Fix updating GLFW_FLOATING on a hidden window
From upstream: 9db156421f.
2019-12-19 12:31:31 +01:00
Kovid Goyal
6bc4f18a59 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-12-19 16:59:26 +05:30
Luflosi
79532a315c X11: Fix invalid read when clearing GLFW_FLOATING
From upstream: 0b652a44d2.
2019-12-19 12:27:36 +01:00
Kovid Goyal
7472a992ed Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-12-19 16:56:48 +05:30
Luflosi
e40616a0db X11: Fix missing checks for EWMH attention atoms
From upstream: 9b6d68ec70.
2019-12-19 12:25:09 +01:00
Kovid Goyal
dd8b97a86c Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-12-19 16:53:38 +05:30
Luflosi
5eff3897de X11: Fix maximization of hidden windows
From upstream: 4837b78ffe.
2019-12-19 12:20:58 +01:00
Kovid Goyal
530cf2eb14 monotonic.h no longer depends on time.h 2019-12-19 16:48:35 +05:30
Kovid Goyal
2baa34beb8 Move a couple of functions to where they are actually needed 2019-12-19 16:43:51 +05:30
Kovid Goyal
87e2f7f86d Missed a couple of macOS timer calls 2019-12-19 16:34:47 +05:30
Kovid Goyal
e142083d53 Remove unused code
Strip out the GLFW timer code, since we use our own kitty based
monotonic clock.
2019-12-19 16:27:25 +05:30
Kovid Goyal
2e850a0d0d Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-12-19 07:56:04 +05:30
Luflosi
aec9c31bca Formatting
From upstream: 506a6aafde.
2019-12-18 18:34:55 +01:00
Kovid Goyal
598a147500 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-12-18 22:51:17 +05:30
Luflosi
ba201c4c92 Add curly braces
From upstream: 9486ec0c02.

The upstream commit mainly changes some cmake stuff, which we don't use and only really adds curly braces to `egl_context.c` (and changes some formatting).
2019-12-18 18:12:48 +01:00
Kovid Goyal
5bc7cfaa43 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-12-18 20:33:44 +05:30
Luflosi
2804e1ff81 Cocoa: Update outdated comment
From upstream: b3544ca43e.
2019-12-18 15:41:31 +01:00
Kovid Goyal
ae27d36902 Fix previous merge 2019-12-18 18:42:59 +05:30
Kovid Goyal
08182fbe4d Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-12-18 18:36:31 +05:30
Luflosi
778474f436 Wayland: Unset the cursor shape on border exit
From upstream: ef6189f348.
2019-12-18 14:01:29 +01:00
Kovid Goyal
c6698ce305 Merge branch 'comment' of https://github.com/Luflosi/kitty 2019-12-18 18:15:30 +05:30
Luflosi
21bc215313 Make comment more accurate
Since all the time values use `monotonic_t` now, `maximum_wait` is no longer measured in seconds. Instead of replacing seconds with nanoseconds, I removed it because the definition of `monotonic_t` is supposed to be changed easily, in which case this comment would not be accurate again.
2019-12-18 12:54:14 +01:00
Kovid Goyal
88a9cc42d2 Merge branch 'fix-panel' of https://github.com/natnat-mc/kitty 2019-12-18 07:32:25 +05:30
Kovid Goyal
28f33a67cf Merge branch 'fix_oserror' of https://github.com/Luflosi/kitty 2019-12-18 07:30:37 +05:30
Nathan DECHER
6742fabfba fix kitty +kitten panel -c <config> resulting in no config at all 2019-12-18 00:33:40 +01:00
Luflosi
b3806f4533 Fix OSError when failing to create config directory on read-only file system
When calling the completion code, kitty tries to access the config directory and create it if it does not exist. If kitty has no permission to create it, a temporary directory will be created instead. This will fail on a read-only file system because that raises an `OSError` and not a `PermissionError`.
In practice this happens when building kitty on macOS Catalina using Nix because `HOME` is set to `/homeless-shelter` for purity and `/` is a read-only filesystem.
2019-12-17 23:42:31 +01:00
Kovid Goyal
6d5fb4ccc6 Fix docs build warning 2019-12-17 20:44:32 +05:30
Kovid Goyal
2487f18f24 Add a new option active_tab_title_template to specify a different template for active tab titles
Fixes #2198
2019-12-17 20:39:46 +05:30
Kovid Goyal
f59afff1d1 Add a new option tab_bar_background to specify a different color for the tab bar
Fixes #2198
2019-12-17 20:08:33 +05:30
Kovid Goyal
b92f95b092 NSGL: Fix disabling of Retina resolution
From upstream: fa60269245
2019-12-15 20:54:21 +05:30
Kovid Goyal
d68ae01448 Make the active window history available when doing kitty @ ls
Fixes #2194
2019-12-13 18:17:13 +05:30
Kovid Goyal
87eda834d3 Use a token for github 2019-12-12 10:05:11 +05:30
Luflosi
c28ff5259a Finish implementing resize by increment 2019-12-09 08:38:41 +01:00
Anders Eurenius
4619259e26 Implement resize by increment 2019-12-09 08:33:32 +01:00
Kovid Goyal
7bf0afa621 Fix #2187 2019-12-08 22:37:47 +05:30
Kovid Goyal
e3e02c7271 Fix #2180 2019-12-01 22:34:56 +05:30
Kovid Goyal
24e17cb7d8 Fix background_opacity incorrectly applying to selected text and reverse video text
Fixes #2177
2019-12-01 15:14:15 +05:30
Kovid Goyal
769998adca Document valid values for pointer_shape_when_grabbed 2019-12-01 08:22:21 +05:30
Kovid Goyal
9b5d88e92d Fix choose item mapping 2019-11-30 17:18:02 +05:30
Kovid Goyal
6d7bebee3a Merge branch 'master' of https://github.com/l-yc/kitty 2019-11-30 16:58:28 +05:30
lyc
1c38654321 Fixed bug in loading libcanberra when falling back on alternative library names 2019-11-30 14:36:22 +08:00
Kovid Goyal
da7a78691f Merge branch 'simplify' of https://github.com/Luflosi/kitty 2019-11-29 09:21:32 +05:30
Luflosi
1eaf7083ef Simplify expression 2019-11-28 23:38:42 +01:00
Kovid Goyal
79a6fcdc5c Oops 2019-11-28 10:12:21 +05:30
Kovid Goyal
90bcbbf426 No need to add index to titles when using hits kitten to choose tab/os_window 2019-11-28 10:11:14 +05:30
Kovid Goyal
41049e2a40 Avoid traceback when cancelling tab selection 2019-11-28 09:42:55 +05:30
Kovid Goyal
c9b8510e9c Also use ascending order for os window selection 2019-11-28 09:23:08 +05:30
Kovid Goyal
d854fc1cd0 Better numbering when asking for tab to detach window to
Fixes #2174
2019-11-28 09:20:38 +05:30
Kovid Goyal
5f7bcae072 hints kitten: Add an option --ascending to control if the hints numbers increase or decrease from top to bottom 2019-11-28 09:20:02 +05:30
Kovid Goyal
652eec3033 Fix a crash/incorrect rendering when detaching a window in some circumstances
Fixes #2173

Ensure all cell related GPU data is resent
2019-11-28 09:01:40 +05:30
Kovid Goyal
a9928ec98a Merge branch 'format' of https://github.com/Luflosi/kitty 2019-11-28 07:24:06 +05:30
Luflosi
e1dfb608ca Format code in kitty/cocoa_window.m a little 2019-11-27 19:56:30 +01:00
Kovid Goyal
2cee3a8809 Better fix for getting clock_gettime() from time.h
Fixes #2169
2019-11-27 22:20:55 +05:30
Kovid Goyal
804f72b31a Get monotonic.h to build on older linux distros 2019-11-27 09:55:00 +05:30
Kovid Goyal
e106dcc83b Dont use -Wfloat-conversion on old compilers 2019-11-27 09:27:23 +05:30
Kovid Goyal
9347be639b version 0.15.0 2019-11-27 09:23:01 +05:30
Kovid Goyal
ffc0c42a63 Merge branch 'faq-font-clarification' of https://github.com/ricci/kitty 2019-11-27 07:31:51 +05:30
Robert Ricci
f5e855b3ad Direct user to rebuild fc-cache in font FAQ
The configuration file that's suggested to force a font to
show up as monospace is very helpful, but it appears you may
need to force fontconfig to rebuild the cache in order for the
change to take effect.
2019-11-26 12:02:23 -07:00
Kovid Goyal
9af4dc4bff Merge branch 'redirect' of https://github.com/Luflosi/kitty 2019-11-26 22:13:51 +05:30
Luflosi
6f6180c34a Change URL to avoid redirect
Since `https://sw.kovidgoyal.net/kitty` redirects to `https://sw.kovidgoyal.net/kitty/`, it would be better to directly use the second URL.
2019-11-26 17:23:21 +01:00
Kovid Goyal
6ddd751713 Merge branch 'simplify' of https://github.com/Luflosi/kitty 2019-11-26 21:09:40 +05:30
Luflosi
2c72b06012 Simplify conditions 2019-11-26 16:06:16 +01:00
Kovid Goyal
88854c1aa4 Merge branch 'fix_stdin_source' of https://github.com/miseran/kitty 2019-11-26 06:47:21 +05:30
Reto Schnyder
5ad704469f Fix condition in stdin selection for launch 2019-11-25 20:10:18 +01:00
Kovid Goyal
c140e17a6f Merge branch 'undo_commenting_out' of https://github.com/Luflosi/kitty 2019-11-24 18:08:14 +05:30
Luflosi
4fc733a9c8 Comment commented out code back in
This piece of code is not run when the `GLFW_COCOA_MENUBAR` init hint is set to `0`, which kitty does.
This reduces the difference to GLFW upstream a little.
2019-11-24 13:29:06 +01:00
Kovid Goyal
c5c59e4c55 Merge branch 'return_type' of https://github.com/Luflosi/kitty 2019-11-24 17:56:26 +05:30
Luflosi
e0edef6719 Change return type of two functions
`on_application_reopen()` and `intercept_cocoa_fullscreen()` both only have two possible return values and can therefore have a return type of `bool`.
2019-11-24 13:06:46 +01:00
Kovid Goyal
0f46ce49e2 Merge branch 'create_menu_bar_in_callback' of https://github.com/Luflosi/kitty 2019-11-24 13:27:35 +05:30
Luflosi
f9fd39b8c1 Create macOS menu bar where GLFW creates it
GLFW creates the menu bar in the applicationWillFinishLaunching method, while kitty creates it in `create_os_window()`. This patch changes the behaviour to match GLFW.
In practice, without this change, there can be a short time where the menu bar is not fully populated.
2019-11-24 08:43:01 +01:00
Kovid Goyal
bd75157428 macOS: Disable mouse hiding by default as getting it to work robustly on Cocoa is too much effort
See #2158
2019-11-24 11:29:23 +05:30
Kovid Goyal
f987f3ba77 Merge branch 'remove_obsolete_comment' of https://github.com/Luflosi/kitty 2019-11-24 08:09:14 +05:30
Luflosi
1c16806efd Remove obsolete comment
In bef9490fa8, `unjam_event_loop()` was removed but not the corresponding comment.
2019-11-23 17:51:38 +01:00
Kovid Goyal
29155fef23 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-11-23 18:59:07 +05:30
Luflosi
c05cbed5b2 Reduce difference to GLFW upstream 2019-11-23 13:19:06 +01:00
Kovid Goyal
4f85d4db71 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-11-22 20:16:22 +05:30
Luflosi
468468ab9f Wayland: Don’t update cursor position in the frame
From upstream: a80788c17f.
2019-11-22 12:38:56 +01:00
Kovid Goyal
1b4a6da577 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-11-22 07:11:14 +05:30
Luflosi
aed7a197c7 Wayland: Don’t reload the cursor on every pointer motion
From upstream: a9f674e719.
2019-11-22 02:35:14 +01:00
Kovid Goyal
c257b7ecfd Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-11-22 07:00:04 +05:30
Luflosi
d6072f6218 X11: Assume 96 DPI if RandR monitor size is zero
From upstream: e96dc5d219.
2019-11-22 02:07:34 +01:00
Kovid Goyal
4ea72a2892 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-11-22 06:34:42 +05:30
Luflosi
89b07a1c6b Cocoa: Fix full screen window iconification
From upstream: 2c519709be.
2019-11-22 01:56:55 +01:00
Kovid Goyal
db0cd61807 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-11-22 06:17:47 +05:30
Luflosi
6feb906953 Cocoa: Cleanup
From upstream: bd452016be.
2019-11-21 18:30:03 +01:00
Kovid Goyal
7cbf8c63bc Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-11-21 20:00:30 +05:30
Luflosi
fae51d4e00 Cocoa: Add comments for Cocoa symbol macros
From upstream: 94cb0347ab.
2019-11-21 14:40:34 +01:00
Kovid Goyal
c510584afd Dont hang trying to read shell environment if the shell never quits. Fixes #2143 2019-11-20 11:53:28 +05:30
Kovid Goyal
738878c2ff unicode input kitten: Add an option :option:kitty +kitten unicode_input --emoji-variation to control the presentation variant of selected emojis
Fixes #2139
2019-11-17 13:13:37 +05:30
Kovid Goyal
d5682fe49a Fix #2137 2019-11-17 08:35:23 +05:30
Kovid Goyal
d8dd22048a Wayland: Ensure that glfw window content scale and kitty window content scale are in sync after the window is placed on a monitor 2019-11-16 10:47:05 +05:30
Kovid Goyal
737d8b96b6 Wayland: Dont bother with creating a temporary window anyway, as it is useless 2019-11-16 10:27:01 +05:30
Kovid Goyal
c8627c9713 Expand user/env vars in csutomize processing path and note that absolute paths can be used in the docs for it. 2019-11-16 10:05:25 +05:30
Kovid Goyal
65fa7da24a Wayland: Use the primary monitor scale for windows on creation
This is needed because on creation the window may not have been assigned
to any monitors, so we fallback to using the scale of the primary
monitor. Fixes #2133. Fixes #2135
2019-11-16 10:00:41 +05:30
Kovid Goyal
c4c6ef799f Dont shadow data 2019-11-13 19:47:08 +05:30
Kovid Goyal
0d18b91895 Grammar 2019-11-13 19:45:48 +05:30
Kovid Goyal
42089aaadc typo 2019-11-13 19:45:28 +05:30
Kovid Goyal
432d0f3318 hints kitten: Allow passing extra CLI args when using --customize-procesing 2019-11-13 17:27:55 +05:30
Kovid Goyal
4ade6587e8 spelling 2019-11-13 16:57:11 +05:30
Kovid Goyal
1724f11429 Deprecate pipe in favor of launch 2019-11-13 16:08:48 +05:30
Kovid Goyal
75d66d1048 Changelog entry for launch 2019-11-13 15:44:57 +05:30
Kovid Goyal
feccef67b9 Document the new launch action 2019-11-13 15:43:38 +05:30
Kovid Goyal
a7d30a199d location should also apply to tab creation 2019-11-13 15:40:46 +05:30
Kovid Goyal
37bbfb11d3 Fix location=neighbor causing a hang on a new tab 2019-11-13 15:39:38 +05:30
Kovid Goyal
79c80b4043 Fix doc generation 2019-11-13 15:03:02 +05:30
Kovid Goyal
18a1402ae2 Remote control interface for launch 2019-11-13 15:00:13 +05:30
Kovid Goyal
5d639b91b0 Switch OS windows when focusing on platforms where it is supported 2019-11-13 14:59:49 +05:30
Kovid Goyal
080c09d22e DRYer 2019-11-13 14:41:45 +05:30
Kovid Goyal
00c2670039 Fix handing of --stdin-source and getting of active window 2019-11-13 14:39:44 +05:30
Kovid Goyal
f113184165 Micro-optimization 2019-11-13 14:39:06 +05:30
Kovid Goyal
d69f8af107 Add a --keep-focus option for launch 2019-11-13 11:48:30 +05:30
Kovid Goyal
ad2bd1bf63 Allow using @selection in the launch cmdline 2019-11-13 10:46:04 +05:30
Kovid Goyal
059786ab66 Implement piping for launch 2019-11-13 10:38:58 +05:30
Kovid Goyal
72dfa5e5e6 Allow creating overlays via launch 2019-11-13 10:14:32 +05:30
Kovid Goyal
d84da6b7b8 Documentation placeholder for the new launch interface 2019-11-13 10:09:43 +05:30
Kovid Goyal
6aa82d82ad Start work on a unified interface for launching processes 2019-11-13 09:58:28 +05:30
Kovid Goyal
a649eb2a48 Coalesce neighboring symbol maps for better lookup performance 2019-11-12 20:05:48 +05:30
Kovid Goyal
83932c8099 Add a remote control command for detach_tab 2019-11-12 15:03:58 +05:30
Kovid Goyal
95e36cece1 Add a detach_window remote control command 2019-11-12 13:04:28 +05:30
Kovid Goyal
7075a7f1c0 Only detach window after the ask overlay is removed 2019-11-12 10:59:36 +05:30
Kovid Goyal
3093610d26 Allow choosing new os window/tab during detach ask 2019-11-12 10:42:07 +05:30
Kovid Goyal
86eed7f895 Speed up taking over of tab 2019-11-12 10:38:57 +05:30
Kovid Goyal
5dc14c5861 Copy cwd when taking over tab 2019-11-12 10:24:37 +05:30
Kovid Goyal
cab87f6fb2 Ensure Tab has cwd even with no initial window 2019-11-12 10:18:42 +05:30
Kovid Goyal
fb3acd1f4a Add a detach_tab command 2019-11-12 10:07:44 +05:30
Kovid Goyal
b247cef85c ... 2019-11-12 08:46:41 +05:30
Kovid Goyal
70445f33e9 Another typo 2019-11-12 06:30:21 +05:30
Kovid Goyal
cadb4f1389 ... 2019-11-12 06:29:55 +05:30
Kovid Goyal
bf75a4de61 Also document debug() 2019-11-12 06:25:27 +05:30
Kovid Goyal
400ab584ac hints kitten: Allow completely customizing the matching and actions performed by the kitten using your own script
Fixes #2124
2019-11-11 21:26:42 +05:30
Kovid Goyal
9edad62144 hints kitten: Allow using named groups in the regular expression. The named groups are passed to the invoked program for further processing. 2019-11-11 17:34:19 +05:30
Kovid Goyal
e94205098f Fix link 2019-11-11 16:00:35 +05:30
Kovid Goyal
35ab7eb472 Add a new action :ref:detach_window that can be used to move the current window into a different tab
Fixes #1310
2019-11-11 15:47:48 +05:30
Kovid Goyal
18619b14ac Merge branch 'simplify' of https://github.com/Luflosi/kitty 2019-11-10 23:35:27 +05:30
Luflosi
97e0b9b65a Simplify code 2019-11-10 16:51:25 +01:00
Kovid Goyal
5175d9f4a6 Properly re-initialize GPU resources when detaching/attaching a window 2019-11-09 13:14:50 +05:30
Kovid Goyal
a5aed0f4ab Forgot to change tab_ref when attaching window 2019-11-09 11:23:04 +05:30
Kovid Goyal
14d693a8ff When attaching window make its parent tab active 2019-11-09 11:09:29 +05:30
Kovid Goyal
645963cc24 Cleanup tab after detaching window 2019-11-09 09:42:17 +05:30
Kovid Goyal
20751c2807 Start work on a detach_window command 2019-11-09 09:20:38 +05:30
Kovid Goyal
dda886b97c Merge branch 'caps_lock_modifier' of https://github.com/Luflosi/kitty 2019-11-09 08:31:59 +05:30
Luflosi
b796ceecdd Add Caps Lock to the list of modifier keys
When a key is pressed and `mouse_hide_wait` is less than zero, kitty will hide the mouse cursor. When a key is pressed, kitty will also scroll the history to the bottom. Both of these things don't happen if the key being pressed was a modifier key. Both of these things should not happen when Caps Lock is pressed, so this key should be added to the list of modifier keys.
`is_modifier_key()` is not used for anything else.
2019-11-09 00:41:12 +01:00
Kovid Goyal
4def89ebb5 Add a note on debugging custom kittens 2019-11-08 16:53:26 +05:30
Kovid Goyal
bd1c1839dd Add API to boss.py to move a window into a different tab 2019-11-08 16:18:52 +05:30
Kovid Goyal
8739f698f9 When attaching window and font sizes changes, resize Screen 2019-11-08 14:58:08 +05:30
Kovid Goyal
b8c8482f20 Better fix for Powerline lacking a space glyph
This does not require increasing the size of the cache key, thereby
improving cache lookup performance
2019-11-08 14:41:31 +05:30
Kovid Goyal
4348ca8cc5 Remove unused function 2019-11-08 14:29:41 +05:30
Kovid Goyal
d1aa59080f Add an API to attach and detach windows from tabs 2019-11-08 14:22:02 +05:30
Kovid Goyal
bf0ffa80be Merge branch 'feature-window-to-tab' of https://github.com/0x17de/kitty
Currently only merged the function to move internal window state.
Want to refactor the changes to tabs.py and boss.py to make them more
orthogonal.
2019-11-08 09:55:51 +05:30
Kovid Goyal
2a89a0c87e Merge branch 'master' of https://github.com/martinpelikan/kitty 2019-11-08 07:57:47 +05:30
Kovid Goyal
de84489a20 Fix a regression in 0.14.5 that caused rendering of private use glyphs with and without spaces to be identical
Fixes #2117
2019-11-08 07:55:41 +05:30
Martin Pelikan
40901778ad Update fish completion docs for fish 3.0.0
As of 3.0.0, fish sources `kitty +complete fish` by default.
2019-11-07 18:11:47 -08:00
Kovid Goyal
c1febea659 Merge branch 'restore_error_reporting' of https://github.com/Luflosi/kitty 2019-11-05 07:07:03 +05:30
Luflosi
b42f4ac288 Restore proper error reporting functionality
In 841c907efc, the `%m` format specifier was replaced by `%%m`, which just prints `%m`. To keep the functionality of `%m` without using `%m`, the error string can be printed manually.
2019-11-05 00:42:29 +01:00
Kovid Goyal
fae57a5868 Website: Add a scrollbar to the sidebar if needed 2019-11-04 18:05:08 +05:30
Kovid Goyal
7a02d09508 Merge branch 'master' of https://github.com/CD3/kitty 2019-11-01 23:23:22 +05:30
C.D. Clark III
8ec6fe960d docs: added '@selection' to pipe documentation. 2019-11-01 08:46:49 -05:00
Kovid Goyal
61a77e19ef X11: Fix content scale fallback value on KDE
From upstream: 75294462b3
2019-10-31 09:10:01 +05:30
Kovid Goyal
82e88b54c7 X11: Fix decoration enabling after window creation
From upstream: 5fc4c01302
2019-10-31 09:04:45 +05:30
Kovid Goyal
ab8975f4da Merge branch 'with_suppress' of https://github.com/Luflosi/kitty 2019-10-29 10:17:19 +05:30
Luflosi
5e7fd21c8d Use "with suppress()" to suppress a python exception 2019-10-28 18:35:21 +01:00
Kovid Goyal
30cd9b2ae4 Merge branch 'fix_two_linker_error' of https://github.com/Luflosi/kitty 2019-10-28 07:31:49 +05:30
Luflosi
4f7d12f3fd Fix error when two linker processes fail
When a linker process fails, `failed` will be set to a value other than `None`. When a second linker process fails, the `else` case will be taken because `failed is None`, which executes `compile_cmd.on_success()`. This function tries to rename or move the file generated by the linker but since the linker process failed, the file will most likely not exist. This will throw an error, which will prevent printing the actual error message from the linker.
2019-10-27 21:50:22 +01:00
Kovid Goyal
62da158823 Merge branch 'fix_potential_error' of https://github.com/Luflosi/kitty 2019-10-27 07:42:21 +05:30
Luflosi
2d19b87208 Fix potential error when $PATH is not defined
When `PATH` is not in `os.environ`, kitty could potentially throw a `KeyError`.
2019-10-27 00:10:55 +02:00
Kovid Goyal
54ca775006 Merge branch 'visual' of https://github.com/toonn/kitty 2019-10-26 09:10:15 +05:30
toonn
a64a0d64cb Prefer VISUAL to EDITOR
Fix #2096
2019-10-25 18:52:17 +02:00
Kovid Goyal
6634dc81fe Merge branch 'simplify' of https://github.com/Luflosi/kitty 2019-10-25 07:45:30 +05:30
Luflosi
a2d5eef398 Simplify access to os.environ
The `if` can be removed by using `get()` with a default parameter to access `os.environ`. This also reduces the number of accesses to `os.environ`.
2019-10-24 20:27:27 +02:00
Kovid Goyal
8184ba246a Load libcanberra dynamically at runtime, as needed
Fixes #2089
2019-10-24 09:03:52 +05:30
Kovid Goyal
54ad1d103a Fix #2087 2019-10-23 18:21:34 +05:30
Kovid Goyal
d92f8a90ef Allow changing colors by mapping a keyboard shortcut to read a kitty config file with color definitions.
Fixes #2083
2019-10-21 14:43:35 +05:30
Kovid Goyal
d33c287344 typo 2019-10-21 14:23:07 +05:30
Kovid Goyal
782eebf976 typo 2019-10-21 14:21:09 +05:30
Kovid Goyal
0fbf75b95a Allow specifying that remote control only works over a socket 2019-10-21 14:20:30 +05:30
Kovid Goyal
5781facf4b Merge branch 'fix_icat' of https://github.com/Luflosi/kitty 2019-10-21 07:32:40 +05:30
Luflosi
b710ffc403 Fix icat kitten
Fixes https://github.com/kovidgoyal/kitty/issues/2081.
This bug was introduced in 10e5fcc375.

One place in the code requires the actual `screen_size_function`, not just the result of it.
2019-10-20 21:37:14 +02:00
Kovid Goyal
7d8edccb04 Merge branch 'pending_scroll_pixels_per_screen' of https://github.com/Luflosi/kitty 2019-10-20 16:58:28 +05:30
Luflosi
3750d70173 Store pending_scroll_pixels for every screen
Scrolling in one screen shouldn't affect the `pending_scroll_pixels` for another screen in the same OS window.
2019-10-20 13:22:46 +02:00
Kovid Goyal
025e0bb546 Merge branch 'var-names-consistency-wrt-scancode' of https://github.com/bew/kitty 2019-10-20 08:08:48 +05:30
Kovid Goyal
033367de91 Merge branch 'fix_line_drawing' of https://github.com/Luflosi/kitty 2019-10-19 06:26:57 +05:30
Luflosi
f93b605b75 Fix line drawing algorithm 2019-10-18 21:26:11 +02:00
Kovid Goyal
7b4141b0c8 Add a changelog entry for powerline tabbar 2019-10-18 19:38:08 +05:30
Kovid Goyal
90acef72dc Merge branch 'add_powerline_style_tabs' of https://github.com/brujoand/kitty 2019-10-18 19:36:31 +05:30
Kovid Goyal
43f200be77 Add an entry for the new box character rendering functions 2019-10-18 17:24:33 +05:30
Kovid Goyal
2fbb167803 Merge branch 'fix-triangle-symbol-scaling' of https://github.com/bew/kitty 2019-10-18 17:13:50 +05:30
Benoit de Chezelles
2b396262f0 Update glyph ids 2019-10-18 11:48:55 +02:00
Benoit de Chezelles
74deca52b9 Remove trailing whitespace 2019-10-18 11:39:51 +02:00
Benoit de Chezelles
058d956464 Merge branch 'master' into fix-triangle-symbol-scaling 2019-10-18 11:30:46 +02:00
brujoand
0f38dc1bd1 Adding powerline style tabs 2019-10-18 11:18:28 +02:00
Kovid Goyal
576b03c047 Implement drawing thick anti-aliased lines
And actually use it for the unicode diagonal box drawing chars and also
the powerline angle separators
2019-10-18 13:19:04 +05:30
Kovid Goyal
59cb1ad1e0 Implement special rendering for the diagonal line box drawing chars 2019-10-18 11:01:51 +05:30
Benoit de Chezelles
f77acdce14 remove useless import in help msg for box drawing debug 2019-10-18 06:08:45 +02:00
Benoit de Chezelles
43a98a1df1 Add missing association between char & glyph id 2019-10-18 06:08:20 +02:00
Benoit de Chezelles
d1cf771c14 Add other corner triangles & enable in C side 2019-10-18 05:45:13 +02:00
Benoit de Chezelles
10e5fcc375 Fix usage of icat function show
The `screen_size` function was not initialized yet
2019-10-18 04:54:14 +02:00
Benoit de Chezelles
2112932d04 Add corner triangle glyphs drawing 2019-10-18 04:48:03 +02:00
Benoit de Chezelles
be505ce6e0 Fix test functions for setup_for_testing's new call syntax 2019-10-18 04:47:04 +02:00
Benoit de Chezelles
4d6472128c Rename scancode to keycode for cocoa implementation 2019-10-17 20:41:48 +02:00
Kovid Goyal
26b7b1ec63 Merge branch 'typo' of https://github.com/Luflosi/kitty 2019-10-17 18:32:42 +05:30
Luflosi
9a8f0a9b5a Fix typo 2019-10-17 13:56:46 +02:00
Kovid Goyal
16dbbf3a1e Make count-lines-of-code a little nicer 2019-10-17 15:19:13 +05:30
Kovid Goyal
8492d0a264 Merge branch 'dont_cloc_parse-graphics-command.h' of https://github.com/Luflosi/kitty 2019-10-17 14:36:53 +05:30
Luflosi
d8f8f1f590 Exclude kitty/parse-graphics-command.h from the line count
This file is automatically generated and should not count towards the total line count.
2019-10-17 10:38:55 +02:00
Kovid Goyal
b762e81da0 Merge branch 'python_encoding_header' of https://github.com/Luflosi/kitty 2019-10-17 06:28:16 +05:30
Luflosi
8b3b68ea69 Unify python encoding headers
All the other python encoding headers are of the form `vim:fileencoding=utf-8`.
2019-10-16 20:34:56 +02:00
Benoit de Chezelles
28525766a5 Final changes before 'ready for review' 2019-10-16 02:47:14 +02:00
Benoit de Chezelles
d8fc23c38f remove outdated 'fixme' 2019-10-16 02:41:26 +02:00
Benoit de Chezelles
8cae70fa36 Update doc 2019-10-16 02:36:55 +02:00
Benoit de Chezelles
7f2b98fad7 Rename all uses of 'scancode' to 'native_key' where relevant 2019-10-16 02:25:20 +02:00
Benoit de Chezelles
2b6dde2ac5 Wish: rename scancode → native_key in glfw codebase 2019-10-16 02:00:24 +02:00
Benoit de Chezelles
0c254fa7c7 GLFWkeyevent: rename scancode → native_key 2019-10-16 02:00:24 +02:00
Kovid Goyal
fe7375becc Merge branch 'grammar' of https://github.com/Luflosi/kitty 2019-10-16 04:19:54 +05:30
Luflosi
b6ee88056e Minor grammar correction 2019-10-16 00:46:52 +02:00
Kovid Goyal
96f691ad64 Merge branch 'mark_parse-graphics-command.h_as_generated' of https://github.com/Luflosi/kitty 2019-10-16 03:31:32 +05:30
Luflosi
8cf3380a77 Mark kitty/parse-graphics-command.h as being generated 2019-10-15 18:16:28 +02:00
Kovid Goyal
e8d7e3e98b Merge branch 'remove_khrplatform.h_references' of https://github.com/Luflosi/kitty 2019-10-15 20:58:16 +05:30
Luflosi
8af0a8476f Remove remaining references to kitty/khrplatform.h
`kitty/khrplatform.h` was removed in f872f955b0.
2019-10-15 17:25:47 +02:00
Kovid Goyal
2de4248f76 Merge branch 'regenerate_glfw-wrapper' of https://github.com/Luflosi/kitty 2019-10-15 20:42:49 +05:30
Luflosi
8c73ab6afb Regenerate kitty/glfw-wrapper.h 2019-10-15 17:10:02 +02:00
Kovid Goyal
ee9bd77dc4 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-10-15 19:10:50 +05:30
Luflosi
81f31ca215 Reduce difference of glfw/glfw3.h to GLFW upstream 2019-10-15 15:37:35 +02:00
Kovid Goyal
51eff372f8 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-10-15 18:16:22 +05:30
Luflosi
087376b44d GLFW: Documentation work
From upstream: a43d1a4937.
2019-10-15 14:37:41 +02:00
Kovid Goyal
e49047ef7d Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-10-15 18:02:17 +05:30
Luflosi
c117f36009 GLFW: Cleanup
From upstream: d5ab3e919a.
2019-10-15 14:25:46 +02:00
Kovid Goyal
5ec83ee759 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-10-15 16:17:49 +05:30
Luflosi
410165c14d GLFW: Improve (?) reference documentation for callbacks
From upstream: 51bb76c7c3.
2019-10-15 12:11:01 +02:00
Kovid Goyal
973ef9fcda Merge branch 'fix_nix_link' of https://github.com/Luflosi/kitty 2019-10-15 04:14:04 +05:30
Luflosi
396bfb8540 Fix link to Nix kitty package
Instead of a fragment identifier, the URL now needs a query parameter.
2019-10-14 18:48:21 +02:00
Kovid Goyal
aaf597bffd Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-10-13 09:08:15 +05:30
Luflosi
63c4f64bf5 Reduce difference to GLFW upstream
From upstream: 773f4495f0.
2019-10-12 15:19:44 +02:00
Kovid Goyal
5745dc34f7 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-10-12 17:58:50 +05:30
Luflosi
114b8cff93 Cleanup
From upstream: 62b7fe8311.
2019-10-11 20:26:27 +02:00
Kovid Goyal
261571bb69 Merge branch 'modern_objective-c' of https://github.com/Luflosi/kitty 2019-10-10 22:01:30 +05:30
Manuel Herrmann
497e2c9163 #1310 move windows into separate tabs 2019-10-10 17:13:34 +02:00
Luflosi
ac01ffaa54 Cocoa: Move slightly towards modern Objective-C 2019-10-10 14:36:52 +02:00
Kovid Goyal
eb9fe06d10 Merge branch 'correct_space' of https://github.com/Luflosi/kitty 2019-10-09 22:15:38 +05:30
Luflosi
506f2f7db7 Replace weird character with space
In 4c4c6ab0e6 you added a weird unicode character, which is apparently called an Object Replacement Character, instead of a space.
2019-10-09 18:32:49 +02:00
Kovid Goyal
f476c56f91 Drop support for the gtk_primary_selection wayland protocol
sway recently decided to start complaining when binding
wp_primary_selection_device_manager interface to gtk_primary_selection
name, which means we either have to maintain an in-tree version
of the gtk_primary_selection protocol or drop support for sway.

Sway has not quite annoyed me enough yet for the latter.

See #1977
2019-10-09 07:37:03 +05:30
Kovid Goyal
474688888d Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-10-08 17:16:51 +05:30
Luflosi
7ddbb613fe Cocoa: Fix window creation blocking after re-init
From 2fbb560eb7.
2019-10-08 13:41:10 +02:00
Kovid Goyal
adc1c06089 Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-10-08 12:12:00 +05:30
Luflosi
1f8631cc99 Cocoa: Move app delegate and menu creation to init
From ea7eb2ddab.
2019-10-07 21:24:58 +02:00
Kovid Goyal
f9e4e71285 Slightly better error message when opening controlling terminal fails 2019-10-07 20:48:31 +05:30
Kovid Goyal
5529194c8d Merge branch 'glfw_upstream' of https://github.com/Luflosi/kitty 2019-10-06 20:39:25 +05:30
Luflosi
5b008163af Cocoa: Add NSApplicationDelegate protocol
From c3ed70a4b7.
2019-10-06 16:58:06 +02:00
Kovid Goyal
802da7087c diff kitten: Allow diffing remote files easily via ssh
See #727
2019-10-04 09:08:22 +05:30
Kovid Goyal
53ab7617ca Dont use space ligatures for symbol_map fonts
space ligatures breaks in font ligatures when there is a trailing space.
So only use them for PUA and cells using a fallback font. Fixes #2019
2019-10-02 07:10:51 +05:30
Kovid Goyal
4369255a25 When drawing unicode symbols that are followed by spaces, use multiple cells to avoid resized or cut-off glyphs
Fixes #1452
2019-10-01 19:56:07 +05:30
Kovid Goyal
b709ee6842 Add a function to check if a codepoint is a symbol 2019-10-01 18:57:06 +05:30
Kovid Goyal
4a8f9efe79 Wayland: Resume key repeat on focus gained if the previously repeated key is pressed 2019-09-30 18:47:43 +05:30
Kovid Goyal
19d36fbe69 Wayland: Fix key repeat not being stopped when focus leaves window. This is expected behavior on Wayland, apparently
Fixes #2014
2019-09-30 16:45:39 +05:30
Kovid Goyal
d37e73db54 Merge branch 'fix_broken_paste' of https://github.com/Luflosi/kitty
Fixes #2008
2019-09-29 08:02:31 +05:30
Luflosi
a8f6615380 Fix broken paste on X11
Fixes https://github.com/kovidgoyal/kitty/issues/2008.
2019-09-29 02:48:09 +02:00
Kovid Goyal
97fe38fbbc monotonic_start_time needs to be a global symbol, not local to each translation unit 2019-09-28 08:18:21 +05:30
Kovid Goyal
3378175450 Hopefully the last macOS float conversion fix 2019-09-27 20:14:05 +05:30
Kovid Goyal
9f60d2aea3 More macOS float conversion fixes 2019-09-27 20:11:32 +05:30
Kovid Goyal
46d49e18c0 More macOS float fixes 2019-09-27 20:07:09 +05:30
Kovid Goyal
43fc335be2 more macOS float fixes 2019-09-27 20:05:11 +05:30
Kovid Goyal
f68339c571 float conversion fixes for macOS 2019-09-27 19:56:48 +05:30
Kovid Goyal
0d68b7078c Check for float conversion issues on build
Useful to catch any errors left over from the migration of times from
double to int64_t
2019-09-27 19:47:25 +05:30
Kovid Goyal
2e71dde780 Merge branch 'monotonic_t' of https://github.com/Luflosi/kitty 2019-09-27 19:23:27 +05:30
Kovid Goyal
51ddd584d6 Merge branch 'mark_unicode_data_as_generated' of https://github.com/Luflosi/kitty 2019-09-27 09:21:29 +05:30
Luflosi
4b090259cd Mark kitty/unicode-data.c as being generated 2019-09-26 21:54:42 +02:00
Luflosi
f3b9ff5f9f Use datatype monotonic_t instead of double to keep track of time
The time is stored in a signed 64 bit integer with nanosecond accuracy. This eliminates the possibility of floating-point inaccuracies.
`monotonic_t` can currently hold values large enough to work correctly for more than 200 years into the future.
Using a typedef instead of directly using `int64_t` everywhere will also allow easily changing the datatype in the future should the need arise for more precise or bigger time values.
2019-09-25 17:43:11 +02:00
Kovid Goyal
4ec1a8d9c3 Merge branch 'extract-glfw-key-event-data' of https://github.com/bew/kitty 2019-09-25 15:44:56 +05:30
Benoit de Chezelles
aadab38487 Fix compilation for macos 2019-09-24 22:03:55 +02:00
Benoit de Chezelles
53275c9137 Extract key event data to struct 2019-09-24 19:15:35 +02:00
238 changed files with 56965 additions and 44625 deletions

View File

@@ -1,104 +0,0 @@
version: 2
jobs:
lin-sys:
docker:
- image: kovidgoyal/kitty-test:latest
steps:
- checkout
- run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
- run: python3 -m flake8 --count .
- run: python3 setup.py build --debug --verbose
- run: ./kitty/launcher/kitty +launch test.py
- run: make FAIL_WARN=-W man
- run: make FAIL_WARN=-W html
lin-package:
docker:
- image: kovidgoyal/kitty-test:latest
steps:
- checkout
- run: python3 setup.py linux-package --update-check-interval=0
lin-35:
docker:
- image: kovidgoyal/kitty-test:latest
environment:
CC: gcc
CFLAGS: -funsigned-char
LD_LIBRARY_PATH: /opt/py3.5/lib
ASAN_OPTIONS: leak_check_at_exit=0
steps:
- checkout
- run: /opt/py3.5/bin/python3 setup.py build --debug --verbose --sanitize
- run: ./kitty/launcher/kitty +launch test.py
lin-37:
docker:
- image: kovidgoyal/kitty-test:latest
environment:
CC: clang
LD_LIBRARY_PATH: /opt/py3.7/lib
ASAN_OPTIONS: leak_check_at_exit=0
steps:
- checkout
- run: /opt/py3.7/bin/python3 setup.py build --debug --verbose --sanitize
- run: ./kitty/launcher/kitty +launch test.py
lin-bundle:
docker:
- image: kovidgoyal/kitty-test:latest
environment:
ASAN_OPTIONS: leak_check_at_exit=0
steps:
- checkout
- run: echo "export LD_LIBRARY_PATH=$SW/lib" >> $BASH_ENV
- run: echo "export PKG_CONFIG_PATH=$SW/lib/pkgconfig" >> $BASH_ENV
- run: echo "export PYTHONHOME=$SW" >> $BASH_ENV
- run: $SW/bin/python3 setup.py build --debug --verbose
- run: ./kitty/launcher/kitty +launch test.py
mac-bundle:
macos:
xcode: "10.1.0"
environment:
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
SW: /Users/Shared/buildbot/sw/sw
steps:
- checkout
- run: mkdir -p $SW
- run: curl https://download.calibre-ebook.com/travis/kitty/osx.tar.xz | tar xJ -C $SW
- run: echo "export PATH=$SW/bin:$PATH" >> $BASH_ENV
- run: python3 setup.py build --debug --verbose
- run: ./kitty/launcher/kitty +launch test.py
mac-brew:
macos:
xcode: "10.2.0"
environment:
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
steps:
- checkout
- run: brew bundle
- run: python3 setup.py build --debug --verbose
- run: ./kitty/launcher/kitty +launch test.py
- run: echo "export PATH=/usr/local/opt/sphinx-doc/bin:$PATH" >> $BASH_ENV
- run: python3 setup.py linux-package --verbose
- run: python3 setup.py kitty.app --verbose
- run: kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"
workflows:
version: 2
all_jobs:
jobs:
- mac-bundle
- lin-sys
- lin-35
- lin-37
- lin-package
- lin-bundle
- mac-brew

View File

@@ -1,55 +0,0 @@
# When changing this file, go to
# https://cloud.docker.com/repository/docker/kovidgoyal/kitty-test/builds and
# click the "Trigger" button under Automated builds to rebuild
FROM ubuntu:latest
# make Apt non-interactive
ENV DEBIAN_FRONTEND=noninteractive
RUN echo 'APT::Get::Assume-Yes "true";' > /etc/apt/apt.conf.d/90circleci \
&& echo 'DPkg::Options "--force-confnew";' >> /etc/apt/apt.conf.d/90circleci \
&& echo 'Acquire::Languages "none";' >> /etc/apt/apt.conf.d/90circleci
# System setup
RUN apt-get update && mkdir -p /usr/share/man/man1 && apt-get install -y python3-flake8 python3-pip python3-pil clang git apt locales sudo openssh-client ca-certificates tar gzip parallel net-tools netcat unzip zip bzip2 gnupg curl wget build-essential apt-utils
# Setup circleci user
RUN groupadd --gid 3434 circleci \
&& useradd --uid 3434 --gid circleci --shell /bin/bash --create-home circleci \
&& echo 'circleci ALL=NOPASSWD: ALL' >> /etc/sudoers.d/50-circleci \
&& echo 'Defaults env_keep += "DEBIAN_FRONTEND"' >> /etc/sudoers.d/env_keep \
&& echo 'Defaults env_keep += "SW"' >> /etc/sudoers.d/env_keep \
&& echo 'Defaults env_keep += "LANG"' >> /etc/sudoers.d/env_keep
# Set timezone to UTC by default
RUN ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime
# Use unicode
RUN locale-gen en_US.UTF-8 || true
ENV LANG=en_US.UTF-8
# Deps needed for building python
RUN apt-get install -y libreadline-dev zlib1g-dev libssl-dev libbz2-dev libsqlite3-dev libffi-dev
# kitty deps
RUN apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev libpng-dev libfontconfig-dev libpython3-dev libxkbcommon-x11-dev python3-pygments libcanberra-dev
# Needed to build kitty docs
RUN pip3 install sphinx
# Install multiple pythons
ADD install-python.py /tmp/install-python.py
RUN python3 /tmp/install-python.py py3.5 https://www.python.org/ftp/python/3.5.6/Python-3.5.6.tar.xz
RUN python3 /tmp/install-python.py py3.7 https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tar.xz
# Install kitty bundle
ENV SW=/home/circleci/sw
RUN python3 /tmp/install-python.py bundle https://download.calibre-ebook.com/travis/kitty/linux-64.tar.xz
RUN echo SW=$SW >> /etc/environment
# Clean unused files
RUN apt-get clean -y
USER circleci
LABEL com.circleci.preserve-entrypoint=true
ENTRYPOINT sleep 2h

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env python3
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
import io
import os
import shlex
import shutil
import subprocess
import sys
import tarfile
from urllib.request import urlopen
PY, URL = sys.argv[1], sys.argv[2]
if PY == 'bundle':
SW = os.environ['SW']
os.mkdir(SW)
os.chdir(SW)
else:
os.chdir('/usr/src')
def run(cmd):
cmd = shlex.split(cmd)
p = subprocess.Popen(cmd)
if p.wait() != 0:
raise SystemExit(p.returncode)
with urlopen(URL) as f:
data = f.read()
with tarfile.open(fileobj=io.BytesIO(data), mode='r:xz') as tf:
tf.extractall()
def replace_in_file(path, src, dest):
with open(path, 'r+') as f:
n = f.read().replace(src, dest)
f.seek(0), f.truncate()
f.write(n)
if PY == 'bundle':
replaced = 0
for dirpath, dirnames, filenames in os.walk(SW):
for f in filenames:
if f.endswith('.pc') or (f.endswith('.py') and f.startswith('_sysconfig')):
replace_in_file(os.path.join(dirpath, f), '/sw/sw', SW)
replaced += 1
if replaced < 2:
raise SystemExit('Failed to replace path to SW in bundle')
else:
src = os.path.abspath(tuple(os.listdir('.'))[0])
os.chdir(src)
run(f'./configure --prefix=/opt/{PY} --enable-shared --with-system-expat --without-ensurepip')
run(f'make -j {os.cpu_count()}')
run('make install')
os.chdir('/')
shutil.rmtree(src)

4
.gitattributes vendored
View File

@@ -3,11 +3,11 @@ kitty/emoji.h linguist-generated=true
kitty/keys.h linguist-generated=true
kitty/charsets.c linguist-generated=true
kitty/key_encoding.py linguist-generated=true
kitty/unicode-data.c
kitty/unicode-data.c linguist-generated=true
kitty/rgb.py linguist-generated=true
kitty/gl-wrapper.* linguist-generated=true
kitty/khrplatform.h linguist-generated=true
kitty/glfw-wrapper.* linguist-generated=true
kitty/parse-graphics-command.h linguist-generated=true
glfw/*.c linguist-vendored=true
glfw/*.h linguist-vendored=true
kittens/unicode_input/names.h linguist-generated=true

125
.github/workflows/ci.py vendored Normal file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
import io
import os
import shlex
import subprocess
import sys
import tarfile
from urllib.request import urlopen
is_bundle = os.environ.get('KITTY_BUNDLE') == '1'
is_macos = 'darwin' in sys.platform.lower()
SW = None
def run(*a):
if len(a) == 1:
a = shlex.split(a[0])
print(' '.join(map(shlex.quote, a)))
sys.stdout.flush()
ret = subprocess.Popen(a).wait()
if ret != 0:
raise SystemExit(ret)
def install_deps():
print('Installing kitty dependencies...')
sys.stdout.flush()
if is_macos:
items = (x.strip() for x in open('Brewfile').readlines() if not x.startswith('#'))
run('brew', 'install', *items)
else:
run('sudo apt-get update')
run('sudo apt-get install -y libgl1-mesa-dev libxi-dev libxrandr-dev libxinerama-dev'
' libxcursor-dev libxcb-xkb-dev libdbus-1-dev libxkbcommon-dev libharfbuzz-dev'
' libpng-dev libfontconfig-dev libxkbcommon-x11-dev libcanberra-dev')
if is_bundle:
install_bundle()
else:
run('pip install Pillow pygments')
def build_kitty():
python = 'python3' if is_bundle else sys.executable
cmd = '{} setup.py build --verbose'.format(python)
if os.environ.get('KITTY_SANITIZE') == '1':
cmd += ' --debug --sanitize'
run(cmd)
def test_kitty():
run('./kitty/launcher/kitty +launch test.py')
def package_kitty():
py = 'python3' if is_macos else 'python'
run(py + ' setup.py linux-package --update-check-interval=0 --verbose')
if is_macos:
run('python3 setup.py kitty.app --update-check-interval=0 --verbose')
run('kitty.app/Contents/MacOS/kitty +runpy "from kitty.constants import *; print(kitty_exe())"')
def replace_in_file(path, src, dest):
with open(path, 'r+') as f:
n = f.read().replace(src, dest)
f.seek(0), f.truncate()
f.write(n)
def setup_bundle_env():
global SW
os.environ['SW'] = SW = '/Users/Shared/buildbot/sw/sw' if is_macos else os.path.join(os.environ['GITHUB_WORKSPACE'], 'sw')
os.environ['PKG_CONFIG_PATH'] = SW + '/lib/pkgconfig'
if is_macos:
os.environ['PATH'] = '{}:{}'.format('/usr/local/opt/sphinx-doc/bin', os.environ['PATH'])
else:
os.environ['LD_LIBRARY_PATH'] = SW + '/lib'
os.environ['PYTHONHOME'] = SW
os.environ['PATH'] = '{}:{}'.format(os.path.join(SW, 'bin'), os.environ['PATH'])
def install_bundle():
cwd = os.getcwd()
os.makedirs(SW)
os.chdir(SW)
with urlopen('https://download.calibre-ebook.com/travis/kitty/{}.tar.xz'.format(
'osx' if is_macos else 'linux-64')) as f:
data = f.read()
with tarfile.open(fileobj=io.BytesIO(data), mode='r:xz') as tf:
tf.extractall()
if not is_macos:
replaced = 0
for dirpath, dirnames, filenames in os.walk('.'):
for f in filenames:
if f.endswith('.pc') or (f.endswith('.py') and f.startswith('_sysconfig')):
replace_in_file(os.path.join(dirpath, f), '/sw/sw', SW)
replaced += 1
if replaced < 2:
raise SystemExit('Failed to replace path to SW in bundle')
os.chdir(cwd)
def main():
if is_bundle:
setup_bundle_env()
else:
if not is_macos:
os.environ['LD_LIBRARY_PATH'] = '{}/lib'.format(os.environ['pythonLocation'])
action = sys.argv[-1]
if action in ('build', 'package'):
install_deps()
if action == 'build':
build_kitty()
elif action == 'package':
package_kitty()
elif action == 'test':
test_kitty()
else:
raise SystemExit('Unknown action: ' + action)
if __name__ == '__main__':
main()

132
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,132 @@
name: CI
on: [push, pull_request]
env:
CI: 'true'
ASAN_OPTIONS: leak_check_at_exit=0
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
jobs:
linux:
name: Linux (python=${{ matrix.pyver }} cc=${{ matrix.cc }} sanitize=${{ matrix.sanitize }})
runs-on: ubuntu-latest
env:
CC: ${{ matrix.cc }}
KITTY_SANITIZE: ${{ matrix.sanitize }}
strategy:
matrix:
python: [a, b, c]
cc: [gcc, clang]
include:
- python: a
pyver: 3.6
sanitize: 0
- python: b
pyver: 3.7
sanitize: 1
- python: c
pyver: 3.8
sanitize: 1
exclude:
- python: a
cc: clang
- python: b
cc: clang
- python: c
cc: gcc
steps:
- name: Checkout source code
uses: actions/checkout@master
with:
fetch-depth: 10
- name: Set up Python ${{ matrix.pyver }}
uses: actions/setup-python@master
with:
python-version: ${{ matrix.pyver }}
- name: Build kitty
run: python .github/workflows/ci.py build
- name: Test kitty
run: python .github/workflows/ci.py test
linux-package:
name: Linux package
runs-on: ubuntu-latest
env:
CFLAGS: -funsigned-char
steps:
- name: Checkout source code
uses: actions/checkout@master
with:
fetch-depth: 10
- name: Test for trailing whitespace
run: if grep -Inr '\s$' kitty kitty_tests kittens docs *.py *.asciidoc *.rst .gitattributes .gitignore; then echo Trailing whitespace found, aborting.; exit 1; fi
- name: Set up Python
uses: actions/setup-python@master
with:
python-version: 3.8
- name: Install build-only deps
run: pip install flake8 mypy sphinx
- name: Run pyflakes
run: python -m flake8 --count .
- name: Build kitty package
run: python .github/workflows/ci.py package
- name: Run mypy
run: python test.py mypy
- name: Build man page
run: make FAIL_WARN=-W man
- name: Build HTML docs
run: make FAIL_WARN=-W html
bundle:
name: Bundle test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
env:
KITTY_BUNDLE: 1
steps:
- name: Checkout source code
uses: actions/checkout@master
with:
fetch-depth: 10
- name: Build kitty
run: which python3 && python3 .github/workflows/ci.py build
- name: Test kitty
run: python3 .github/workflows/ci.py test
brew:
name: macOS Brew
runs-on: macos-latest
steps:
- name: Checkout source code
uses: actions/checkout@master
with:
fetch-depth: 10
- name: Build kitty
run: python3 .github/workflows/ci.py build
- name: Test kitty
run: python3 .github/workflows/ci.py test
- name: Build kitty package
run: python3 .github/workflows/ci.py package

3
.gitignore vendored
View File

@@ -1,6 +1,8 @@
*.so
*.pyc
*.pyo
*_stub.pyi
/.dmypy.json
/tags
/build/
/linux-package/
@@ -14,4 +16,5 @@ __pycache__/
/glfw/wayland-*-client-protocol.[ch]
/docs/_build/
/docs/generated/
/.mypy_cache
.DS_Store

View File

@@ -1,5 +1,5 @@
brew 'pkg-config'
brew 'imagemagick'
brew 'harfbuzz'
brew 'python'
brew 'sphinx-doc'
pkg-config
python
imagemagick
harfbuzz
sphinx-doc

View File

@@ -1,7 +1,3 @@
To build from source:
To build from source: <https://sw.kovidgoyal.net/kitty/build.html>
https://sw.kovidgoyal.net/kitty/build.html
Pre-built binaries:
https://sw.kovidgoyal.net/kitty/binary.html
Pre-built binaries: <https://sw.kovidgoyal.net/kitty/binary.html>

View File

@@ -1,5 +1,5 @@
= kitty - the fast, featureful, GPU based, terminal emulator
See https://sw.kovidgoyal.net/kitty
See https://sw.kovidgoyal.net/kitty/
image:https://circleci.com/gh/kovidgoyal/kitty.svg?style=svg["Build status", link="https://circleci.com/gh/kovidgoyal/kitty"]
image:https://github.com/kovidgoyal/kitty/workflows/CI/badge.svg["Build status", link="https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI"]

View File

@@ -4,54 +4,56 @@
import sys
import os
from typing import List
def icat(args):
from kittens.runner import run_kitten
def icat(args: List[str]) -> None:
from kittens.runner import run_kitten as rk
sys.argv = args
run_kitten('icat')
rk('icat')
def list_fonts(args):
from kitty.fonts.list import main
main(args)
def list_fonts(args: List[str]) -> None:
from kitty.fonts.list import main as list_main
list_main(args)
def remote_control(args):
from kitty.remote_control import main
main(args)
def remote_control(args: List[str]) -> None:
from kitty.remote_control import main as rc_main
rc_main(args)
def runpy(args):
def runpy(args: List[str]) -> None:
sys.argv = ['kitty'] + args[2:]
exec(args[1])
def hold(args):
def hold(args: List[str]) -> None:
import subprocess
ret = subprocess.Popen(args[1:]).wait()
sys.stdin.read()
raise SystemExit(ret)
def complete(args):
from kitty.complete import main
main(args[1:], entry_points, namespaced_entry_points)
def complete(args: List[str]) -> None:
from kitty.complete import main as complete_main
complete_main(args[1:], entry_points, namespaced_entry_points)
def launch(args):
def launch(args: List[str]) -> None:
import runpy
sys.argv = args[1:]
exe = args[1]
if exe.startswith(':'):
import shutil
exe = shutil.which(exe[1:])
if not exe:
q = shutil.which(exe[1:])
if not q:
raise SystemExit('{} not found in PATH'.format(args[1][1:]))
exe = q
runpy.run_path(exe, run_name='__main__')
def run_kitten(args):
def run_kitten(args: List[str]) -> None:
try:
kitten = args[1]
except IndexError:
@@ -59,11 +61,11 @@ def run_kitten(args):
list_kittens()
raise SystemExit(1)
sys.argv = args[1:]
from kittens.runner import run_kitten
run_kitten(kitten)
from kittens.runner import run_kitten as rk
rk(kitten)
def namespaced(args):
def namespaced(args: List[str]) -> None:
func = namespaced_entry_points[args[1]]
func(args[1:])
@@ -84,7 +86,7 @@ namespaced_entry_points['hold'] = hold
namespaced_entry_points['complete'] = complete
def setup_openssl_environment():
def setup_openssl_environment() -> None:
# Workaround for Linux distros that have still failed to get their heads
# out of their asses and implement a common location for SSL certificates.
# It's not that hard people, there exists a wonderful tool called the symlink
@@ -92,11 +94,13 @@ def setup_openssl_environment():
if 'SSL_CERT_FILE' not in os.environ and 'SSL_CERT_DIR' not in os.environ:
if os.access('/etc/pki/tls/certs/ca-bundle.crt', os.R_OK):
os.environ['SSL_CERT_FILE'] = '/etc/pki/tls/certs/ca-bundle.crt'
setattr(sys, 'kitty_ssl_env_var', 'SSL_CERT_FILE')
elif os.path.isdir('/etc/ssl/certs'):
os.environ['SSL_CERT_DIR'] = '/etc/ssl/certs'
setattr(sys, 'kitty_ssl_env_var', 'SSL_CERT_DIR')
def main():
def main() -> None:
if getattr(sys, 'frozen', False) and 'darwin' not in sys.platform.lower():
setup_openssl_environment()
first_arg = '' if len(sys.argv) < 2 else sys.argv[1]
@@ -107,8 +111,8 @@ def main():
elif first_arg.startswith('+'):
namespaced(['+', first_arg[1:]] + sys.argv[2:])
else:
from kitty.main import main
main()
from kitty.main import main as kitty_main
kitty_main()
else:
func(sys.argv[1:])

View File

@@ -1,2 +1,27 @@
#!/usr/bin/env bash
cloc --exclude-list-file <(echo -e 'kitty/wcwidth-std.h\nkitty/glfw.c\nkitty/keys.h\nkitty/charsets.c\nkitty/unicode-data.c\nkitty/key_encoding.py\nkitty/rgb.py\nkitty/gl.h\nkitty/gl-wrapper.h\nkitty/gl-wrapper.c\nkitty/khrplatform.h\nkitty/glfw-wrapper.h\nkitty/glfw-wrapper.c\nkitty/emoji.h\nkittens/unicode_input/names.h') kitty kittens
#!/usr/bin/env python
import subprocess
files_to_exclude = '''\
kitty/wcwidth-std.h
kitty/glfw.c
kitty/keys.h
kitty/charsets.c
kitty/unicode-data.c
kitty/key_encoding.py
kitty/rgb.py
kitty/gl.h
kitty/gl-wrapper.h
kitty/gl-wrapper.c
kitty/glfw-wrapper.h
kitty/glfw-wrapper.c
kitty/emoji.h
kittens/unicode_input/names.h
kitty/parse-graphics-command.h
'''
p = subprocess.Popen([
'cloc', '--exclude-list-file', '/dev/stdin', 'kitty', 'kittens'
], stdin=subprocess.PIPE)
p.communicate(files_to_exclude.encode('utf-8'))
raise SystemExit(p.wait())

View File

@@ -78,6 +78,8 @@ body div.document {
div.sphinxsidebar {
font-size: inherit;
line-height: inherit;
max-height: 100%;
overflow-y: auto;
}
#sidebartoc li {

View File

@@ -1,9 +1,9 @@
Building kitty from source
==============================
.. image:: https://circleci.com/gh/kovidgoyal/kitty.svg?style=svg
.. image:: https://github.com/kovidgoyal/kitty/workflows/CI/badge.svg
:alt: Build status
:target: https://circleci.com/gh/kovidgoyal/kitty
:target: https://github.com/kovidgoyal/kitty/actions?query=workflow%3ACI
|kitty| is designed to run from source, for easy hackability. Make sure
@@ -29,7 +29,7 @@ Build-time dependencies:
* gcc or clang
* pkg-config
* For building on Linux in addition to the above dependencies you might also need to install the ``-dev`` packages for:
``libdbus-1-dev``, ``libxcursor-dev``, ``libxrandr-dev``, ``libxi-dev``, ``libxinerama-dev``, ``libgl1-mesa-dev``, ``libxkbcommon-x11-dev``, ``libfontconfig-dev``, ``libcanberra-dev`` and ``libpython-dev``.
``libdbus-1-dev``, ``libxcursor-dev``, ``libxrandr-dev``, ``libxi-dev``, ``libxinerama-dev``, ``libgl1-mesa-dev``, ``libxkbcommon-x11-dev``, ``libfontconfig-dev``, and ``libpython-dev``,
if they are not already installed by your distro.
Install and run from source

View File

@@ -4,6 +4,319 @@ Changelog
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
To update |kitty|, :doc:`follow the instructions <binary>`.
0.17.3 [2020-04-23]
--------------------
- Allow individually setting margins and padding for each edge (left, right,
top, bottom). Margins can also be controlled per window via remote control
(:iss:`2546`)
- Fix reverse video not being rendered correctly when using transparency or a
background image (:iss:`2419`)
- Allow mapping arbitrary remote control commands to key presses in
:file:`kitty.conf`
- X11: Fix crash when doing drag and drop from some applications (:iss:`2505`)
- Fix :option:`launch --stdin-add-formatting` not working (:iss:`2512`)
- Update to Unicode 13.0 (:iss:`2513`)
- Render country flags designated by a pair of unicode codepoints
in two cells instead of four.
- diff kitten: New option to control the background color for filler lines in
the margin (:iss:`2518`)
- Fix specifying options for layouts in the startup session file not working
(:iss:`2520`)
- macOS: Fix incorrect horizontal positioning of some full-width East Asian characters
(:iss:`1457`)
- macOS: Render multi-cell PUA characters centered, matching behavior on other
platforms
- Linux: Ignore keys if they are designated as layout/group/mode switch keys
(:iss:`2519`)
- Marks: Fix marks not handling wide characters and tab characters correctly
(:iss:`2534`)
- Add a new :opt:`listen_on` option in kitty.conf to set :option:`kitty --listen-on`
globally. Also allow using environment variables in this option (:iss:`2569`).
- Allow sending mouse events in kittens (:pull:`2538`)
- icat kitten: Fix display of 16-bit depth images (:iss:`2542`)
- Add ncurses specific terminfo definitions for strikethrough (:pull:`2567`)
- Fix a regression in 0.17 that broke displaying graphics over SSH
(:iss:`2568`)
- Fix :option:`--title` not being applied at window creation time (:iss:`2570`)
0.17.2 [2020-03-29]
--------------------
- Add a :option:`launch --watcher` option that allows defining callbacks
that are called for various events in the window's life-cycle (:iss:`2440`)
- Fix a regression in 0.17 that broke drawing of borders with non-minimal
borders (:iss:`2474`)
- Hints kitten: Allow copying to primary selection as well as clipboard
(:pull:`2487`)
- Add a new mappable action ``close_other_windows_in_tab`` to close all but the
active window (:iss:`2484`)
- Hints kitten: Adjust the default regex used to detect line numbers to handle
line+column numbers (:iss:`2268`)
- Fix blank space at the start of tab bar in the powerline style when first tab is
inactive (:iss:`2478`)
- Fix regression causing incorrect rendering of separators in tab bar when
defining a tab bar background color (:pull:`2480`)
- Fix a regression in 0.17 that broke the kitty @ launch remote command and
also broke the --tab-title option when creating a new tab. (:iss:`2488`)
- Linux: Fix selection of fonts with multiple width variants not preferring
the normal width faces (:iss:`2491`)
0.17.1 [2020-03-24]
--------------------
- Fix :opt:`cursor_underline_thickness` not working (:iss:`2465`)
- Fix a regression in 0.17 that caused tab bar background to be rendered after
the last tab as well (:iss:`2464`)
- macOS: Fix a regression in 0.17 that caused incorrect variants to be
automatically selected for some fonts (:iss:`2462`)
- Fix a regression in 0.17 that caused kitty @ set-colors to require setting
cursor_text_color (:iss:`2470`)
0.17.0 [2020-03-24]
--------------------
- :ref:`splits_layout` to arrange windows in arbitrary splits
(:iss:`2308`)
- Add support for specifying a background image, see :opt:`background_image`
(:iss:`163` and :pull:`2326`; thanks to Fredrick Brennan.)
- A new :opt:`background_tint` option to darken the background under the text
area when using background images and/or transparent windows.
- Allow selection of single cells with the mouse. Also improve mouse selection
to follow semantics common to most programs (:iss:`945`)
- New options :opt:`cursor_beam_thickness` and :opt:`cursor_underline_thickness` to control the thickness of the
beam and underline cursors (:iss:`2337` and :pull:`2342`)
- When the application running in the terminal grabs the mouse, pass middle
clicks to the application unless :opt:`terminal_select_modifiers` are
pressed (:iss:`2368`)
- A new ``copy_and_clear_or_interrupt`` function (:iss:`2403`)
- X11: Fix arrow mouse cursor using right pointing instead of the default left
pointing arrow (:iss:`2341`)
- Allow passing the currently active kitty window id in the launch command
(:iss:`2391`)
- unicode input kitten: Allow pressing :kbd:`ctrl+tab` to change the input mode
(:iss:`2343`)
- Fix a bug that prevented using custom functions with the new marks feature
(:iss:`2344`)
- Make the set of URL prefixes that are recognized while hovering with the
mouse configurable (:iss:`2416`)
- Fix border/margin/padding sizes not being recalculated on DPI change
(:iss:`2346`)
- diff kitten: Fix directory diffing with removed binary files failing
(:iss:`2378`)
- macOS: Fix menubar title not updating on OS Window focus change (:iss:`2350`)
- Fix rendering of combining characters with fonts that have glyphs for
precomposed characters but not decomposed versions (:iss:`2365`)
- Fix incorrect rendering of selection when using rectangular select and
scrolling (:iss:`2351`)
- Allow setting WM_CLASS and WM_NAME when creating new OS windows with the
launch command (:option:`launch --os-window-class`)
- macOS: When switching input method while a pending multi-key input is in
progress, clear the pending input (:iss:`2358`)
- Fix a regression in the previous release that broke switching to neighboring windows
in the Grid layout when there are less than four windows (:iss:`2377`)
- Fix colors in scrollback pager off if the window has redefined terminal
colors using escape codes (:iss:`2381`)
- Fix selection not updating properly while scrolling (:iss:`2442`)
- Allow extending selections by dragging with right button pressed
(:iss:`2445`)
- Workaround for bug in less that causes colors to reset at wrapped lines
(:iss:`2381`)
- X11/Wayland: Allow drag and drop of text/plain in addition to text/uri-list
(:iss:`2441`)
- Dont strip :code:`&` and :code:`-` from the end of URLs (:iss:`2436`)
- Fix ``@selection`` placeholder not working with launch command (:iss:`2417`)
- Drop support for python 3.5
- Wayland: Fix a crash when drag and dropping into kitty (:iss:`2432`)
- diff kitten: Fix images lingering as blank rectangles after the kitten quits
(:iss:`2449`)
- diff kitten: Fix images losing position when scrolling using mouse
wheel/touchpad
0.16.0 [2020-01-28]
--------------------
- A new :doc:`marks` feature that allows highlighting and scrolling to arbitrary
text in the terminal window.
- hints kitten: Allow pressing :sc:`goto_file_line` to quickly open
the selected file at the selected line in vim or a configurable editor (:iss:`2268`)
- Allow having more than one full height window in the :code:`tall` layout
(:iss:`2276`)
- Allow choosing OpenType features for individual fonts via the
:opt:`font_features` option. (:pull:`2248`)
- Wayland: Fix a freeze in rare circumstances when having multiple OS Windows
(:iss:`2307` and :iss:`1722`)
- Wayland: Fix window titles being set to very long strings on the order of 8KB
causing a crash (:iss:`1526`)
- Add an option :opt:`force_ltr` to turn off the display of text in RTL scripts
in right-to-left order (:pull:`2293`)
- Allow opening new tabs/windows before the current tab/window as well as after
it with the :option:`launch --location` option.
- Add a :opt:`resize_in_steps` option that can be used to resize the OS window
in steps as large as character cells (:pull:`2131`)
- When triple-click+dragging to select multiple lines, extend the selection
of the first line to match the rest on the left (:pull:`2284`)
- macOS: Add a :code:`titlebar-only` setting to
:opt:`hide_window_decorations` to only hide the title bar (:pull:`2286`)
- Fix a segfault when using :option:`kitty --debug-config` with maps (:iss:`2270`)
- ``goto_tab`` now maps numbers larger than the last tab to the last tab
(:iss:`2291`)
- Fix URL detection not working for urls of the form scheme:///url
(:iss:`2292`)
- When windows are semi-transparent and all contain graphics, correctly render
them. (:iss:`2310`)
0.15.1 [2019-12-21]
--------------------
- Fix a crash/incorrect rendering when detaching a window in some circumstances
(:iss:`2173`)
- hints kitten: Add an option :option:`kitty +kitten hints --ascending` to
control if the hints numbers increase or decrease from top to bottom
- Fix :opt:`background_opacity` incorrectly applying to selected text and
reverse video text (:iss:`2177`)
- Add a new option :opt:`tab_bar_background` to specify a different color
for the tab bar (:iss:`2198`)
- Add a new option :opt:`active_tab_title_template` to specify a different
template for active tab titles (:iss:`2198`)
- Fix lines at the edge of the window at certain windows sizes when drawing
images on a transparent window (:iss:`2079`)
- Fix window not being rendered for the first time until some input has been
received from child process (:iss:`2216`)
0.15.0 [2019-11-27]
--------------------
- Add a new action :ref:`detach_window <detach_window>` that can be used to move the current
window into a different tab (:iss:`1310`)
- Add a new action :doc:`launch <launch>` that unifies launching of processes
in new kitty windows/tabs.
- Add a new style ``powerline`` for tab bar rendering, see :opt:`tab_bar_style` (:pull:`2021`)
- Allow changing colors by mapping a keyboard shortcut to read a kitty config
file with color definitions. See the :doc:`FAQ <faq>` for details
(:iss:`2083`)
- hints kitten: Allow completely customizing the matching and actions performed
by the kitten using your own script (:iss:`2124`)
- Wayland: Fix key repeat not being stopped when focus leaves window. This is
expected behavior on Wayland, apparently (:iss:`2014`)
- When drawing unicode symbols that are followed by spaces, use multiple cells
to avoid resized or cut-off glyphs (:iss:`1452`)
- diff kitten: Allow diffing remote files easily via ssh (:iss:`727`)
- unicode input kitten: Add an option :option:`kitty +kitten unicode_input
--emoji-variation` to control the presentation variant of selected emojis
(:iss:`2139`)
- Add specialised rendering for a few more box powerline and unicode symbols
(:pull:`2074` and :pull:`2021`)
- Add a new socket only mode for :opt:`allow_remote_control`. This makes
it possible for programs running on the local machine to control kitty
but not programs running over ssh.
- hints kitten: Allow using named groups in the regular expression. The named
groups are passed to the invoked program for further processing.
- Fix a regression in 0.14.5 that caused rendering of private use glyphs
with and without spaces to be identical (:iss:`2117`)
- Wayland: Fix incorrect scale used when first creating an OS window
(:iss:`2133`)
- macOS: Disable mouse hiding by default as getting it to work robustly
on Cocoa is too much effort (:iss:`2158`)
0.14.6 [2019-09-25]
---------------------

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=utf-8
#
# Configuration file for the Sphinx documentation builder.
#
@@ -7,28 +7,34 @@
# full list see the documentation:
# https://www.sphinx-doc.org/en/master/config
import importlib
import os
import re
import subprocess
import sys
import time
from functools import partial
from typing import (
Any, Callable, Dict, Iterable, List, Match, Optional, Sequence, Tuple,
Union
)
from docutils import nodes
from docutils.parsers.rst.roles import set_classes
from pygments.lexer import RegexLexer, bygroups
from pygments.token import (
from pygments.lexer import RegexLexer, bygroups # type: ignore
from pygments.token import ( # type: ignore
Comment, Keyword, Literal, Name, Number, String, Whitespace
)
from sphinx import addnodes
from sphinx.environment.adapters.toctree import TocTree
from sphinx.util.logging import getLogger
from sphinx import addnodes # type: ignore
from sphinx.environment.adapters.toctree import TocTree # type: ignore
from sphinx.util.logging import getLogger # type: ignore
kitty_src = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if kitty_src not in sys.path:
sys.path.insert(0, kitty_src)
str_version = importlib.import_module('kitty.constants').str_version
from kitty.conf.definition import Option, Shortcut # noqa
from kitty.constants import str_version # noqa
# config {{{
# -- Project information -----------------------------------------------------
@@ -77,7 +83,7 @@ master_doc = 'index'
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language: Optional[str] = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@@ -184,7 +190,7 @@ texinfo_documents = [
# GitHub linking inline roles {{{
def num_role(which, name, rawtext, text, lineno, inliner, options={}, content=[]):
def num_role(which: str, name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []) -> Tuple[List, List]:
' Link to a github issue '
try:
issue_num = int(text)
@@ -202,10 +208,11 @@ def num_role(which, name, rawtext, text, lineno, inliner, options={}, content=[]
return [node], []
def commit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
def commit_role(name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []) -> Tuple[List, List]:
' Link to a github commit '
try:
commit_id = subprocess.check_output(f'git rev-list --max-count=1 --skip=# {text}'.split()).decode('utf-8').strip()
commit_id = subprocess.check_output(
f'git rev-list --max-count=1 --skip=# {text}'.split()).decode('utf-8').strip()
except Exception:
msg = inliner.reporter.error(
f'GitHub commit id "{text}" not recognized.', line=lineno)
@@ -213,21 +220,22 @@ def commit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
return [prb], [msg]
url = f'https://github.com/kovidgoyal/kitty/commit/{commit_id}'
set_classes(options)
short_id = subprocess.check_output(f'git rev-list --max-count=1 --abbrev-commit --skip=# {commit_id}'.split()).decode('utf-8').strip()
short_id = subprocess.check_output(
f'git rev-list --max-count=1 --abbrev-commit --skip=# {commit_id}'.split()).decode('utf-8').strip()
node = nodes.reference(rawtext, f'commit: {short_id}', refuri=url, **options)
return [node], []
# }}}
# Sidebar ToC {{{
def create_toc(app, pagename):
def create_toc(app: Any, pagename: str) -> Optional[Any]:
tt = TocTree(app.env)
toctree = tt.get_toc_for(pagename, app.builder)
if toctree is not None:
subtree = toctree[toctree.first_child_matching_class(nodes.list_item)]
bl = subtree.first_child_matching_class(nodes.bullet_list)
if bl is None:
return # Empty ToC
return None # Empty ToC
subtree = subtree[bl]
# for li in subtree.traverse(nodes.list_item):
# modify_li(li)
@@ -235,29 +243,40 @@ def create_toc(app, pagename):
return app.builder.render_partial(subtree)['fragment']
def add_html_context(app, pagename, templatename, context, *args):
def add_html_context(app: Any, pagename: str, templatename: str, context: Any, *args: Any) -> None:
if 'toc' in context:
context['toc'] = create_toc(app, pagename) or context['toc']
# }}}
# CLI docs {{{
def write_cli_docs(all_kitten_names):
def write_cli_docs(all_kitten_names: Iterable[str]) -> None:
from kitty.launch import options_spec as launch_options_spec
from kitty.cli import option_spec_as_rst
with open('generated/launch.rst', 'w') as f:
f.write(option_spec_as_rst(
appname='launch', ospec=launch_options_spec, heading_char='_',
message='''\
Launch an arbitrary program in a new kitty window/tab. Note that
if you specify a program-to-run you can use the special placeholder
:code:`@selection` which will be replaced by the current selection.
'''
))
with open('generated/cli-kitty.rst', 'w') as f:
f.write(option_spec_as_rst(appname='kitty').replace(
'kitty --to', 'kitty @ --to'))
as_rst = partial(option_spec_as_rst, heading_char='_')
from kitty.remote_control import global_options_spec, cli_msg, cmap, all_commands
from kitty.rc.base import all_command_names, command_for_name
from kitty.remote_control import global_options_spec, cli_msg
with open('generated/cli-kitty-at.rst', 'w') as f:
p = partial(print, file=f)
p('kitty @\n' + '-' * 80)
p('.. program::', 'kitty @')
p('\n\n' + as_rst(
global_options_spec, message=cli_msg, usage='command ...', appname='kitty @'))
from kitty.cmds import cli_params_for
for cmd_name in all_commands:
func = cmap[cmd_name]
from kitty.rc.base import cli_params_for
for cmd_name in all_command_names():
func = command_for_name(cmd_name)
p(f'.. _at_{func.name}:\n')
p('kitty @', func.name + '\n' + '-' * 120)
p('.. program::', 'kitty @', func.name)
@@ -276,14 +295,14 @@ def write_cli_docs(all_kitten_names):
# }}}
def write_remote_control_protocol_docs(): # {{{
from kitty.cmds import cmap
def write_remote_control_protocol_docs() -> None: # {{{
from kitty.rc.base import all_command_names, command_for_name, RemoteCommand
field_pat = re.compile(r'\s*([a-zA-Z0-9_+]+)\s*:\s*(.+)')
def format_cmd(p, name, cmd):
def format_cmd(p: Callable, name: str, cmd: RemoteCommand) -> None:
p(name)
p('-' * 80)
lines = cmd.__doc__.strip().splitlines()
lines = (cmd.__doc__ or '').strip().splitlines()
fields = []
for line in lines:
m = field_pat.match(line)
@@ -309,8 +328,8 @@ def write_remote_control_protocol_docs(): # {{{
with open(f'generated/rc.rst', 'w') as f:
p = partial(print, file=f)
for name in sorted(cmap):
cmd = cmap[name]
for name in sorted(all_command_names()):
cmd = command_for_name(name)
if not cmd.__doc__:
continue
name = name.replace('_', '-')
@@ -373,7 +392,7 @@ class SessionLexer(RegexLexer):
}
def link_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
def link_role(name: str, rawtext: str, text: str, lineno: int, inliner: Any, options: Any = {}, content: Any = []) -> Tuple[List, List]:
m = re.match(r'(.+)\s+<(.+?)>', text)
if m is None:
msg = inliner.reporter.error(f'link "{text}" not recognized', line=lineno)
@@ -385,24 +404,24 @@ def link_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
return [node], []
def expand_opt_references(conf_name, text):
def expand_opt_references(conf_name: str, text: str) -> str:
conf_name += '.'
def expand(m):
def expand(m: Match) -> str:
ref = m.group(1)
if '<' not in ref and '.' not in ref:
full_ref = conf_name + ref
return ':opt:`{} <{}>`'.format(ref, full_ref)
return m.group()
return str(m.group())
return re.sub(r':opt:`(.+?)`', expand, text)
opt_aliases = {}
shortcut_slugs = {}
opt_aliases: Dict[str, str] = {}
shortcut_slugs: Dict[str, Tuple[str, str]] = {}
def parse_opt_node(env, sig, signode):
def parse_opt_node(env: Any, sig: str, signode: Any) -> str:
"""Transform an option description into RST nodes."""
count = 0
firstname = ''
@@ -424,22 +443,22 @@ def parse_opt_node(env, sig, signode):
return firstname
def parse_shortcut_node(env, sig, signode):
def parse_shortcut_node(env: Any, sig: str, signode: Any) -> str:
"""Transform a shortcut description into RST nodes."""
conf_name, text = sig.split('.', 1)
signode += addnodes.desc_name(text, text)
return sig
def render_conf(conf_name, all_options):
from kitty.conf.definition import merged_opts, Option
def render_conf(conf_name: str, all_options: Iterable[Union['Option', Sequence['Shortcut']]]) -> str:
from kitty.conf.definition import merged_opts, Option, Group
ans = ['.. default-domain:: conf', '']
a = ans.append
current_group = None
all_options = list(all_options)
current_group: Optional[Group] = None
all_options_ = list(all_options)
kitty_mod = 'kitty_mod'
def render_group(group):
def render_group(group: Group) -> None:
a('')
a(f'.. _conf-{conf_name}-{group.name}:')
a('')
@@ -451,11 +470,12 @@ def render_conf(conf_name, all_options):
a(group.start_text)
a('')
def handle_group_end(group):
def handle_group_end(group: Group) -> None:
if group.end_text:
assert current_group is not None
a(''), a(current_group.end_text)
def handle_group(new_group, new_group_is_shortcut=False):
def handle_group(new_group: Group, new_group_is_shortcut: bool = False) -> None:
nonlocal current_group
if new_group is not current_group:
if current_group:
@@ -463,14 +483,14 @@ def render_conf(conf_name, all_options):
current_group = new_group
render_group(current_group)
def handle_option(i, opt):
def handle_option(i: int, opt: Option) -> None:
nonlocal kitty_mod
if not opt.long_text or not opt.add_to_docs:
return
handle_group(opt.group)
if opt.name == 'kitty_mod':
kitty_mod = opt.defval_as_string
mopts = list(merged_opts(all_options, opt, i))
mopts = list(merged_opts(all_options_, opt, i))
a('.. opt:: ' + ', '.join(conf_name + '.' + mo.name for mo in mopts))
a('.. code-block:: conf')
a('')
@@ -482,7 +502,7 @@ def render_conf(conf_name, all_options):
a(expand_opt_references(conf_name, opt.long_text))
a('')
def handle_shortcuts(shortcuts):
def handle_shortcuts(shortcuts: Sequence[Shortcut]) -> None:
sc = shortcuts[0]
handle_group(sc.group, True)
sc_text = f'{conf_name}.{sc.short_text}'
@@ -500,7 +520,7 @@ def render_conf(conf_name, all_options):
a(expand_opt_references(conf_name, sc.long_text))
a('')
for i, opt in enumerate(all_options):
for i, opt in enumerate(all_options_):
if isinstance(opt, Option):
handle_option(i, opt)
else:
@@ -511,7 +531,7 @@ def render_conf(conf_name, all_options):
return '\n'.join(ans)
def process_opt_link(env, refnode, has_explicit_title, title, target):
def process_opt_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
conf_name, opt = target.partition('.')[::2]
if not opt:
conf_name, opt = 'kitty', conf_name
@@ -519,7 +539,7 @@ def process_opt_link(env, refnode, has_explicit_title, title, target):
return title, opt_aliases.get(full_name, full_name)
def process_shortcut_link(env, refnode, has_explicit_title, title, target):
def process_shortcut_link(env: Any, refnode: Any, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]:
conf_name, slug = target.partition('.')[::2]
if not slug:
conf_name, slug = 'kitty', conf_name
@@ -534,7 +554,7 @@ def process_shortcut_link(env, refnode, has_explicit_title, title, target):
return title, target
def write_conf_docs(app, all_kitten_names):
def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
app.add_lexer('conf', ConfLexer())
app.add_object_type(
'opt', 'opt',
@@ -555,7 +575,7 @@ def write_conf_docs(app, all_kitten_names):
sc_role.warn_dangling = True
sc_role.process_link = process_shortcut_link
def generate_default_config(all_options, name):
def generate_default_config(all_options: Dict[str, Union[Option, Sequence[Shortcut]]], name: str) -> None:
from kitty.conf.definition import as_conf_file
with open(f'generated/conf-{name}.rst', 'w', encoding='utf-8') as f:
print('.. highlight:: conf\n', file=f)
@@ -577,13 +597,13 @@ def write_conf_docs(app, all_kitten_names):
# }}}
def setup(app):
def setup(app: Any) -> None:
os.makedirs('generated/conf', exist_ok=True)
from kittens.runner import all_kitten_names
all_kitten_names = all_kitten_names()
write_cli_docs(all_kitten_names)
kn = all_kitten_names()
write_cli_docs(kn)
write_remote_control_protocol_docs()
write_conf_docs(app, all_kitten_names)
write_conf_docs(app, kn)
app.add_lexer('session', SessionLexer())
app.add_role('link', link_role)
app.add_role('iss', partial(num_role, 'issues'))

View File

@@ -109,8 +109,13 @@ How do I change the colors in a running kitty instance?
You can either use the
`OSC terminal escape codes <https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands>`_
to set colors or you can enable :doc:`remote control <remote-control>`
for |kitty| and use :ref:`at_set-colors`.
to set colors or you can define keyboard shortcuts to set colors, for example::
map f1 set_colors --configured /path/to/some/config/file/colors.conf
Or you can enable :doc:`remote control <remote-control>` for |kitty| and use :ref:`at_set-colors`.
The shortcut mapping technique has the same syntax as the remote control
command, for details, see :ref:`at_set-colors`.
A list of pre-made color themes for kitty is available at:
`kitty-themes <https://github.com/dexpota/kitty-themes>`_
@@ -174,7 +179,7 @@ following :file:`~/.config/fontconfig/fonts.conf`::
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match target="scan">
<match target="font">
<test name="family">
<string>Your Font Family Name</string>
</test>
@@ -184,6 +189,11 @@ following :file:`~/.config/fontconfig/fonts.conf`::
</match>
</fontconfig>
After creating (or modifying) this file, you may need to run the following
command to rebuild your fontconfig cache::
fc-cache -r
Then, the font will be available in ``kitty list-fonts``.

View File

@@ -334,7 +334,9 @@ Finally, you can specify the image *z-index*, i.e. the vertical stacking order.
placed in the same location with different z-index values will be blended if
they are semi-transparent. You can specify z-index values using the ``z`` key.
Negative z-index values mean that the images will be drawn under the text. This
allows rendering of text on top of images.
allows rendering of text on top of images. Negative z-index values below
INT32_MIN/2 (-1,073,741,824) will be drawn under cells with non-default background
colors.
Deleting images
---------------------
@@ -417,7 +419,7 @@ Key Value Default Description
``Y`` Positive integer ``0`` The y-offset within the first cell at which to start displaying the image
``c`` Positive integer ``0`` The number of columns to display the image over
``r`` Positive integer ``0`` The number of rows to display the image over
``z`` Integer ``0`` The *z-index* vertical stacking order of the image
``z`` 32-bit integer ``0`` The *z-index* vertical stacking order of the image
**Keys for deleting images**
-----------------------------------------------------------
``d`` Single character. ``a`` What to delete.

View File

@@ -53,19 +53,19 @@ Quickstart
--------------
Pre-built binaries of |kitty| are available for both macOS and Linux.
See the :doc:`binary install instructions </binary>`. You can also
See the :doc:`binary install instructions </binary>`. You can
:doc:`build from source </build>`.
You can also use your favorite package manager to install the |kitty| package.
|kitty| packages are available for:
`macOS with Homebrew (Cask) <https://formulae.brew.sh/cask/kitty>`_,
`macOS and Linux with Nix <https://nixos.org/nixos/packages.html#kitty>`_,
`macOS and Linux with Nix <https://nixos.org/nixos/packages.html?channel=nixpkgs-unstable&query=kitty>`_,
`Ubuntu <https://launchpad.net/ubuntu/+source/kitty>`_,
`Debian <https://packages.debian.org/buster/kitty>`_,
`openSUSE <https://build.opensuse.org/package/show/X11:terminals/kitty>`_,
`Arch Linux <https://www.archlinux.org/packages/community/x86_64/kitty/>`_,
`Gentoo <https://packages.gentoo.org/packages/x11-terms/kitty>`_,
`Fedora <https://copr.fedorainfracloud.org/coprs/gagbo/kitty-latest/>`_,
`Fedora <https://src.fedoraproject.org/rpms/kitty>`_,
`Void Linux <https://github.com/void-linux/void-packages/blob/master/srcpkgs/kitty/template>`_,
and `Solus <https://dev.getsol.us/source/kitty/>`_.
@@ -173,6 +173,30 @@ You can also define a shortcut to switch to the previously active window::
``nth_window`` will focus the nth window for positive numbers and the
previously active windows for negative numbers.
.. _detach_window:
You can define shortcuts to detach the current window and
move it to another tab or another OS window::
# moves the window into a new OS window
map ctrl+f2 detach_window
# moves the window into a new Tab
map ctrl+f3 detach_window new-tab
# asks which tab to move the window into
map ctrl+f4 detach_window ask
Similarly, you can detach the current tab, with::
# moves the tab into a new OS window
map ctrl+f2 detach_tab
# asks which OS Window to move the tab into
map ctrl+f4 detach_tab ask
Finally, you can define a shortcut to close all windows in a tab other than
the currently active window::
map f9 close_other_windows_in_tab
Other keyboard shortcuts
----------------------------------
@@ -206,55 +230,26 @@ Reset background opacity :sc:`reset_background_opacity`
Layouts
----------
A layout is an arrangement of multiple *windows*. You can create a new window
A layout is an arrangement of multiple kitty *windows* inside a top-level OS window. You can create a new window
using the :sc:`new_window` key combination.
Currently, there are five layouts available,
Currently, there are six layouts available:
* **Stack** -- Only a single maximized window is shown at a time
* **Tall** -- One window is shown full height on the left, the rest of the windows are shown one below the other on the right
* **Fat** -- One window is shown full width on the top, the rest of the windows are shown side-by-side on the bottom
* **Fat** -- One (or optionally more) windows are shown full width on the top, the rest of the windows are shown side-by-side on the bottom
* **Grid** -- All windows are shown in a grid
* **Horizontal** -- All windows are shown side-by-side
* **Splits** -- Windows arranged in arbitrary patterns created using horizontal and vertical splits
* **Stack** -- Only a single maximized window is shown at a time
* **Tall** -- One (or optionally more) windows are shown full height on the left, the rest of the windows are shown one below the other on the right
* **Vertical** -- All windows are shown one below the other
You can switch between layouts using the :sc:`next_layout` key combination. You can
also create shortcuts to select particular layouts, and choose which layouts
you want to enable/disable, see :ref:`conf-kitty-shortcuts.layout` for examples.
By default, all layouts are enabled and you can switch between layouts using
the :sc:`next_layout` key combination. You can also create shortcuts to select
particular layouts, and choose which layouts you want to enable/disable, see
:ref:`conf-kitty-shortcuts.layout` for examples. The first layout listed in
:opt:`enabled_layouts` becomes the default layout.
You can resize windows inside layouts. Press :sc:`start_resizing_window` (also :kbd:`⌘+r` on macOS) to
enter resizing mode and follow the on-screen instructions. In a given window
layout only some operations may be possible for a particular window. For
example, in the Tall layout you can make the first window wider/narrower, but
not taller/shorter. Note that what you are resizing is actually not a window,
but a row/column in the layout, all windows in that row/column will be resized.
You can also define shortcuts in :file:`kitty.conf` to make the active window
wider, narrower, taller, or shorter by mapping to the ``resize_window``
action, for example::
map ctrl+left resize_window narrower
map ctrl+right resize_window wider
map ctrl+up resize_window taller
map ctrl+down resize_window shorter 3
The ``resize_window`` action has a second, optional argument to control
the resizing increment (a positive integer that defaults to 1).
Some layouts take options to control their behavior. For example, the ``fat``
and ``tall`` layouts accept the ``bias`` option to control how the available
space is split up. To specify the option, in :opt:`kitty.conf <enabled_layouts>` use::
enabled_layouts tall:bias=70
This will make the tall window occupy ``70%`` of available width. ``bias`` can be
any number between 10 and 90.
Writing a new layout only requires about a hundred lines of code, so if there
is some layout you want, take a look at `layout.py
<https://github.com/kovidgoyal/kitty/blob/master/kitty/layout.py>`_ and submit
a pull request!
For more details on the layouts and how to use them see :doc:`layouts`.
.. _kittens:
@@ -402,7 +397,7 @@ comfortably within the pager.
Additionally, you can pipe the contents of the scrollback buffer to an
arbitrary, command running in a new window, tab or overlay, for example::
map f1 pipe @ansi window less +G -R
map f1 launch --stdin-source=@screen_scrollback --stdin-add-formatting less +G -R
Would open the scrollback buffer in a new window when you press the :kbd:`F1`
key. See :sc:`show_scrollback` for details.
@@ -428,6 +423,14 @@ internal buffer named ``a`` and :kbd:`F2` to paste from that buffer. The buffer
names are arbitrary strings, so you can define as many such buffers as you
need.
Marks
-------------
kitty has the ability to mark text on the screen based on regular expressions.
This can be useful to highlight words or phrases when browsing output from long
running programs or similar. To learn how this feature works, see :doc:`marks`.
Frequently Asked Questions
---------------------------------
@@ -478,7 +481,8 @@ completions happens after the call to :file:`compinit`.
fish
~~~~~~~~
Add the following to your :file:`~/.config/fish/config.fish`
For versions of fish earlier than 3.0.0, add the following to your
:file:`~/.config/fish/config.fish`. Later versions source completions by default.
.. code-block:: sh

View File

@@ -20,24 +20,24 @@ your machine).
.. code-block:: python
def main(args):
# this is the main entry point of the kitten, it will be executed in
# the overlay window when the kitten is launched
answer = input('Enter some text: ')
# whatever this function returns will be available in the
# handle_result() function
return answer
def main(args: List[str]) -> str:
# this is the main entry point of the kitten, it will be executed in
# the overlay window when the kitten is launched
answer = input('Enter some text: ')
# whatever this function returns will be available in the
# handle_result() function
return answer
def handle_result(args, answer, target_window_id, boss):
# get the kitty window into which to paste answer
w = boss.window_id_map.get(target_window_id)
if w is not None:
w.paste(answer)
def handle_result(args: List[str], answer: str, target_window_id: int, boss: kitty.boss.Boss) -> None:
# get the kitty window into which to paste answer
w = boss.window_id_map.get(target_window_id)
if w is not None:
w.paste(answer)
Now in :file:`kitty.conf` add the lines::
map ctrl+k kitten mykitten.py
map ctrl+k kitten mykitten.py
Start kitty and press :kbd:`ctrl+k` and you should see the kitten running.
@@ -53,7 +53,7 @@ Passing arguments to kittens
You can pass arguments to kittens by defining them in the map directive in
:file:`kitty.conf`. For example::
map ctrl+k kitten mykitten.py arg1 arg2
map ctrl+k kitten mykitten.py arg1 arg2
These will be available as the ``args`` parameter in the ``main()`` and
``handle_result()`` functions. Note also that the current working directory
@@ -70,17 +70,18 @@ function, telling kitty what kind of input your kitten would like. For example:
.. code-block:: py
# in main, STDIN is for the kitten process and will contain
# the contents of the screen
def main(args):
return sys.stdin.read()
# in main, STDIN is for the kitten process and will contain
# the contents of the screen
def main(args):
return sys.stdin.read()
# in handle_result, STDIN is for the kitty process itself, rather
# than the kitten process and should not be read from.
def handle_result(args, stdin_data, target_window_id, boss):
pass
# in handle_result, STDIN is for the kitty process itself, rather
# than the kitten process and should not be read from.
from kittens.tui.handler import result_handler
@result_handler(type_of_input='text')
def handle_result(args, stdin_data, target_window_id, boss):
pass
handle_result.type_of_input = 'text'
This will send the plain text of the active window to the kitten's
:file:`STDIN`. For text with formatting escape codes, use ``ansi``
@@ -103,23 +104,23 @@ Create a file in the kitty config folder, :file:`~/.config/kitty/zoom_toggle.py`
.. code-block:: py
def main(args):
pass
def main(args):
pass
def handle_result(args, answer, target_window_id, boss):
tab = boss.active_tab
if tab is not None:
if tab.current_layout.name == 'stack':
tab.last_used_layout()
else:
tab.goto_layout('stack')
handle_result.no_ui = True
from kittens.tui.handler import result_handler
@result_handler(no_ui=True)
def handle_result(args, answer, target_window_id, boss):
tab = boss.active_tab
if tab is not None:
if tab.current_layout.name == 'stack':
tab.last_used_layout()
else:
tab.goto_layout('stack')
Now in kitty.conf add::
map f11 kitten zoom_toggle.py
map f11 kitten zoom_toggle.py
Pressing :kbd:`F11` will now act as a zoom toggle function. You can get even
more fancy, switching the kitty OS window to fullscreen as well as changing the
@@ -128,7 +129,53 @@ layout, by simply adding the line::
boss.toggle_fullscreen()
to the ``handle_result()`` function, above.
To the ``handle_result()`` function, above.
Sending mouse events
--------------------
If the program running in a window is receiving mouse events you can simulate
those using::
from kitty.fast_data_types import send_mouse_event
send_mouse_event(screen, x, y, button, action, mods)
``screen`` is the ``screen`` attribute of the window you want to send the event
to. ``x`` and ``y`` are the 0-indexed coordinates. ``button`` is
``GLFW_MOUSE_BUTTON_{button}`` where ``{button}`` is one of ``LEFT``,
``RIGHT``, ``MIDDLE`` or a digit from ``1`` to ``8``. ``action`` is one of
``PRESS``, ``RELEASE``, ``DRAG`` or ``MOVE``. ``mods`` is a bitmask of
``GLFW_MOD_{mod}`` where ``{mod}`` is one of ``SHIFT``, ``CONTROL`` or ``ALT``.
All the mentioned constants are imported from ``kitty.fast_data_types``.
For example, to send a left click at position x: 2, y: 3 to the active window::
from kitty.fast_data_types import send_mouse_event, GLFW_MOUSE_BUTTON_LEFT, PRESS
send_mouse_event(boss.active_window.screen, 2, 3, GLFW_MOUSE_BUTTON_LEFT, PRESS, 0)
The function will only send the event if the program is receiving events of
that type, and will return ``True`` if it sent the event, and ``False`` if not.
Debugging kittens
--------------------
The part of the kitten that runs in ``main()`` is just a normal program and
the output of print statements will be visible in the kitten window. Or
alternately, you can use::
from kittens.tui.loop import debug
debug('whatever')
The ``debug()`` function is just like ``print()`` except that the output
will appear in the ``STDOUT`` of the kitty process inside which the kitten is
running.
The ``handle_result()`` part of the kitten runs inside the kitty process.
The output of print statements will go to the ``STDOUT`` of the kitty process.
So if you run kitty from another kitty instance, the output will be visible
in the first kitty instance.
.. _external_kittens:
@@ -136,11 +183,11 @@ Kittens created by kitty users
---------------------------------------------
`vim-kitty-navigator <https://github.com/knubie/vim-kitty-navigator>`_
Allows you to navigate seamlessly between vim and kitty splits using a consistent set of hotkeys.
Allows you to navigate seamlessly between vim and kitty splits using a consistent set of hotkeys.
`smart-scroll <https://github.com/yurikhan/kitty-smart-scroll>`_
Makes the kitty scroll bindings work in full screen applications
Makes the kitty scroll bindings work in full screen applications
`insert password <https://github.com/kovidgoyal/kitty/issues/1222>`_
Insert a password from a CLI password manager, taking care to only do it at
a password prompt.
Insert a password from a CLI password manager, taking care to only do it at
a password prompt.

View File

@@ -18,11 +18,71 @@ select anything that looks like a path or filename and then insert it into the
terminal, very useful for picking files from the output of a ``git`` or ``ls`` command and
adding them to the command line for the next command.
You can also press :sc:`goto_file_line` to select anything that looks
like a path or filename followed by a colon and a line number and open
the file in vim at the specified line number. The patterns and editor
to be used can be modified using options passed to the kitten. For example::
map ctrl+g kitten hints --type=linenum --linenum-action=tab nvim +{line} +{path}
will open the selected file in a new tab inside neovim when you press
:kbd:`ctrl+g`.
The hints kitten is very powerful to see more detailed help on its various
options and modes of operation, see below. You can use these options to
create mappings in :file:`kitty.conf` to select various different text
snippets. See :sc:`insert_selected_path` for examples.
Completely customizing the matching and actions of the kitten
---------------------------------------------------------------
The hints kitten supports writing simple python scripts that can be used to
completely customize how it finds matches and what happens when a match is
selected. This allows the hints kitten to provide the user interface, while
you can provide the logic for finding matches and performing actions on them.
This is best illustrated with an example. Create the file
:file:`custom-hints.py` in the kitty config directory with the following
contents:
.. code-block:: python
import re
def mark(text, args, Mark, extra_cli_args, *a):
# This function is responsible for finding all
# matching text. extra_cli_args are any extra arguments
# passed on the command line when invoking the kitten.
# We mark all individual word for potential selection
for idx, m in enumerate(re.finditer(r'\w+', text)):
start, end = m.span()
mark_text = text[start:end].replace('\n', '').replace('\0', '')
# The empty dictionary below will be available as groupdicts
# in handle_result() and can contain arbitrary data.
yield Mark(idx, start, end, mark_text, {})
def handle_result(args, data, target_window_id, boss, extra_cli_args, *a):
# This function is responsible for performing some
# action on the selected text.
# matches is a list of the selected entries and groupdicts contains
# the arbitrary data associated with each entry in mark() above
matches, groupdicts = [], []
for m, g in zip(data['match'], data['groupdicts']):
if m:
matches.append(m), groupdicts.append(g)
for word, match_data in zip(matches, groupdicts):
# Lookup the word in a dictionary, the open_url function
# will open the provided url in the system browser
boss.open_url(f'https://www.google.com/search?q=define:{word}')
Now run kitty with::
kitty -o 'map f1 kitten hints --customize-processing custom-hints.py'
When you press the :kbd:`F1` key you will be able to select a word to
look it up in the Google dictionary.
Command Line Interface
-------------------------

View File

@@ -22,3 +22,12 @@ In :guilabel:`Name` mode you instead type words from the character name and use
keys/tab to select the character from the displayed matches. You can also type
a leading period and the index for the match if you don't like to use arrow
keys.
You can switch between modes using either the function keys or by pressing
:kbd:`Ctrl+Tab` and :kbd:`Ctrl+Shift+Tab`.
Command Line Interface
-------------------------
.. include:: ../generated/cli-kitten-unicode_input.rst

96
docs/launch.rst Normal file
View File

@@ -0,0 +1,96 @@
Launching programs in new windows/tabs
========================================
.. program:: launch
|kitty| has a :code:`launch` action that can be used to run arbitrary programs
in news windows/tabs. It can be mapped to user defined shortcuts in kitty.conf.
It is very powerful and allows sending the contents of
the current window to the launched program, as well as many other options.
In the simplest form, you can use it to open a new kitty window running the
shell, as shown below::
map f1 launch
To run a different program simply pass the command line as arguments to
launch::
map f1 launch vim path/to/some/file
To open a new window with the same working directory as the currently
active window::
map f1 launch --cwd=current
To open the new window in a new tab::
map f1 launch --type=tab
To pass the contents of the current screen and scrollback to the started process::
map f1 launch --stdin-source=@screen_scrollback less
There are many more powerful options, refer to the complete list below.
The piping environment
--------------------------
When using :option:`launch --stdin-source`, the program to which the data is
piped has a special environment variable declared, ``KITTY_PIPE_DATA`` whose
contents are::
KITTY_PIPE_DATA={scrolled_by}:{cursor_x},{cursor_y}:{lines},{columns}
where ``scrolled_by`` is the number of lines kitty is currently scrolled by,
``cursor_(x|y)`` is the position of the cursor on the screen with ``(1,1)``
being the top left corner and ``{lines},{columns}`` being the number of rows
and columns of the screen.
Special arguments
-------------------
There are a few special placeholder arguments that can be specified as part of
the command line. Namely ``@selection`` which is replaced by the current
selection and ``@active-kitty-window-id`` which is replaced by the id of the
currently active kitty window. For example::
map f1 launch my-program @active-kitty-window-id
Watching launched windows
---------------------------
The :option:`launch --watcher` option allows you to specify python functions
that will be called at specific events, such as when the window is resized or
closed. Simply specify the path to a python module that specifies callback
functions for the events you are interested in, for example:
.. code-block:: python
def on_resize(boss, window, data):
# Here data will contain old_geometry and new_geometry
def on_close(boss, window, data):
# called when window is closed, typically when the program running in
# it exits.
Every callback is passed a reference to the global ``Boss`` object as well as
the ``Window`` object the action is occurring on. The ``data`` object is
mapping that contains event dependent data. Some useful methods and attributes
for the ``Window`` object are: ``as_text(as_ans=False, add_history=False,
add_wrap_markers=False, alternate_screen=False)`` with which you can get the
contents of the window and its scrollback buffer. Similarly,
``window.child.pid`` is the PID of the processes that was launched
in the window and ``window.id`` is the internal kitty ``id`` of the
window.
Syntax reference
------------------
.. include:: /generated/launch.rst

247
docs/layouts.rst Normal file
View File

@@ -0,0 +1,247 @@
Layouts
============
kitty has the ability to define its own windows that can be tiled next to each
other in arbitrary arrangements, based on *Layouts*, see below for examples:
.. figure:: screenshots/screenshot.png
:alt: Screenshot, showing three programs in the 'Tall' layout
:align: center
:scale: 100%
Screenshot, showing vim, tig and git running in |kitty| with the 'Tall' layout
.. figure:: screenshots/splits.png
:alt: Screenshot, showing windows in the 'Splits' layout
:align: center
:scale: 100%
Screenshot, showing windows with arbitrary arrangement in the 'Splits'
layout
There are many different layouts available. They are all enabled by default,
you can switch layouts using :sc:`next_layout`. To control which layouts
are available use :opt:`enabled_layouts`, the first listed layout becomes
the default. Individual layouts and how to use them are described below.
.. contents::
The Stack Layout
------------------
This is the simplest layout it displays a single window using all available
space, other windows are hidden behind it. It has no options::
enabled_layouts stack
The Tall Layout
------------------
Displays one (or optionally more) full height windows on the left half of the
screen. Remaining windows are tiled vertically on the right half of the screen.
There are options to control how the screen is split horizontally ``bias``
(an integer between ``10`` and ``90``) and options to control how many
full-height windows there are ``full_size`` (a positive integer). The syntax
for the options is shown below::
enabled_layouts tall:bias=50;full_size=1
┌──────────────┬───────────────┐
│ │ │
│ │ │
│ │ │
│ ├───────────────┤
│ │ │
│ │ │
│ │ │
│ ├───────────────┤
│ │ │
│ │ │
│ │ │
└──────────────┴───────────────┘
The Fat Layout
----------------
Displays one (or optionally more) full width windows on the top half of the
screen. Remaining windows are tiled horizontally on the bottom half of the screen.
There are options to control how the screen is split vertically ``bias``
(an integer between ``10`` and ``90``) and options to control how many
full-height windows there are ``full_size`` (a positive integer). The syntax
for the options is shown below::
enabled_layouts fat:bias=50;full_size=1
┌──────────────────────────────┐
│ │
│ │
│ │
│ │
├─────────┬──────────┬─────────┤
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
└─────────┴──────────┴─────────┘
The Grid Layout
--------------------
Display windows in a balanced grid with all windows the same size except the
last column if there are not enough windows to fill the grid. Has no options::
enabled_layouts grid
┌─────────┬──────────┬─────────┐
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
├─────────┼──────────┼─────────┤
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
└─────────┴──────────┴─────────┘
.. _splits_layout:
The Splits Layout
--------------------
This is the most flexible layout. You can create any arrangement of windows
by splitting exiting windows repeatedly. To best use this layout you should
define a few extra keybindings in :file:`kitty.conf`::
map F5 launch --location=hsplit
map F6 launch --location=vsplit
map F7 layout_action rotate
map shift+up move_window up
map shift+left move_window left
map shift+right move_window right
map shift+down move_window down
map ctrl+left neighboring_window left
map ctrl+right neighboring_window right
map ctrl+up neighboring_window up
map ctrl+down neighboring_window down
Now you can create horizontal and vertical splits by using :kbd:`F5` and
:kbd:`F6`. You can move them around using :kbd:`shift+arrow keys`
and you can move focus to neighboring windows using :kbd:`ctrl+arrow keys`.
You can switch an existing split from horizontal to vertical and vice versa
using :kbd:`F7`. Finally, windows can be resized using :ref:`window_resizing`.
This layout takes one option, ``split_axis`` that controls whether new windows
are placed into vertical or horizontal splits, by default::
enabled_layouts splits:split_axis=horizontal
┌──────────────┬───────────────┐
│ │ │
│ │ │
│ │ │
│ ├───────┬───────┤
│ │ │ │
│ │ │ │
│ │ │ │
│ ├───────┴───────┤
│ │ │
│ │ │
│ │ │
└──────────────┴───────────────┘
The Horizontal Layout
------------------------
All windows are shown side by side. Has no options::
enabled_layouts horizontal
┌─────────┬──────────┬─────────┐
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
└─────────┴──────────┴─────────┘
The Vertical Layout
-----------------------
All windows are shown one below the other. Has no options::
enabled_layouts vertical
┌──────────────────────────────┐
│ │
│ │
│ │
├──────────────────────────────┤
│ │
│ │
│ │
├──────────────────────────────┤
│ │
│ │
│ │
└──────────────────────────────┘
.. _window_resizing:
Resizing windows
------------------
You can resize windows inside layouts. Press :sc:`start_resizing_window` (also
:kbd:`⌘+r` on macOS) to enter resizing mode and follow the on-screen
instructions. In a given window layout only some operations may be possible
for a particular window. For example, in the Tall layout you can make the first
window wider/narrower, but not taller/shorter. Note that what you are resizing
is actually not a window, but a row/column in the layout, all windows in that
row/column will be resized.
You can also define shortcuts in :file:`kitty.conf` to make the active window
wider, narrower, taller, or shorter by mapping to the ``resize_window``
action, for example::
map ctrl+left resize_window narrower
map ctrl+right resize_window wider
map ctrl+up resize_window taller
map ctrl+down resize_window shorter 3
The ``resize_window`` action has a second, optional argument to control
the resizing increment (a positive integer that defaults to 1).
Some layouts take options to control their behavior. For example, the ``fat``
and ``tall`` layouts accept the ``bias`` and ``full_size`` options to control
how the available space is split up.
To specify the option, in :opt:`kitty.conf <enabled_layouts>` use::
enabled_layouts tall:bias=70;full_size=2
This will have ``2`` instead of a single tall window, that occupy ``70%``
instead of ``50%`` of available width. ``bias`` can be any number between 10
and 90.
Writing a new layout only requires about a hundred lines of code, so if there
is some layout you want, take a look at `layout.py
<https://github.com/kovidgoyal/kitty/blob/master/kitty/layout.py>`_ and submit
a pull request!

125
docs/marks.rst Normal file
View File

@@ -0,0 +1,125 @@
Marks
=================
kitty has the ability to mark text on the screen based on regular expressions.
This can be useful to highlight words or phrases when browsing output from long
running programs or similar. Lets start with a few examples:
Suppose we want to be able to highlight the word ERROR in the current window.
Add the following to :file:`kitty.conf`::
map f1 toggle_marker text 1 ERROR
Now when you press :kbd:`F1` all instances of the word :code:`ERROR` will be
highlighted. To turn off the highlighting, press :kbd:`F1` again.
If you want to make it case-insensitive, use::
map f1 toggle_marker itext 1 ERROR
To make it match only complete words, use::
map f1 toggle_marker regex 1 \bERROR\b
Suppose you want to highlight both :code:`ERROR` and :code:`WARNING`, case
insensitively::
map f1 toggle_marker iregex 1 \bERROR\b 2 \bWARNING\b
kitty supports up to 3 mark groups (the numbers in the commands above). You
can control the colors used for these groups in :file:`kitty.conf` with::
mark1_foreground red
mark1_background gray
mark2_foreground green
...
.. note::
For performance reasons, matching is done per line only, and only when that line is
altered in any way. So you cannot match text that stretches across multiple
lines.
Creating markers dynamically
---------------------------------
If you want to create markers dynamically rather than pre-defining them in
:file:`kitty.conf` you can do so as follows::
map f1 create_marker
map f2 remove_marker
Then pressing :kbd:`F1` will allow you to enter the marker definition and set
it and pressing :kbd:`F2` will remove the marker. ``create_marker`` accepts
the same syntax as ``toggle_marker`` above. Note that while creating
markers, the prompt has history so you can easily re-use previous marker
expressions.
You can also use the facilities for :doc:`remote-control` to dynamically
add/remove markers.
Scrolling to marks
--------------------
kitty has an action to scroll to the next line that contains a mark. You can
use it by mapping it to some shortcut in :file:`kitty.conf`::
map ctrl+p scroll_to_mark prev
map ctrl+n scroll_to_mark next
Then pressing :kbd:`ctrl+p` will scroll to the first line in the scrollback
buffer above the current top line that contains a mark. Pressing :kbd:`ctrl+n`
will scroll to show the first line below the current last line that contains
a mark. If you wish to jump to a mark of a specific type, you can add that to
the mapping::
map ctrl+1 scroll_to_mark prev 1
Which will scroll only to marks of type 1.
The full syntax for creating marks
-------------------------------------
The syntax of the :code:`toggle_marker` command is::
toggle_marker <marker-type> <specification>
Here :code:`marker-type` is one of:
* :code:`text` - simple substring matching
* :code:`itext` - case-insensitive substring matching
* :code:`regex` - A python regular expression
* :code:`iregex` - A case-insensitive python regular expression
* :code:`function` - An arbitrary function defined in a python file, see
:ref:`marker_funcs`.
.. _marker_funcs:
Arbitrary marker functions
-----------------------------
You can create your own marker functions. Create a python file named
:file:`mymarker.py` and in it create a :code:`marker` function. This
function receives the text of the line as input and must yield three numbers,
the starting character position, the ending character position and the mark
group (1-3). For example:
.. code-block::
def marker(text):
# Function to highlight the letter X
for i, ch in enumerate(text):
if ch.lower() == 'x':
yield i, i, 3
Save this file somewhere and in :file:`kitty.conf`, use::
map f1 toggle_marker function /path/to/mymarker.py
If you save the file in the kitty config directory, you can use::
map f1 toggle_marker function mymarker.py

View File

@@ -1,6 +1,9 @@
Working with the screen and history buffer contents
======================================================
.. warning::
The pipe action has been deprecated in favor of the
:doc:`launch <launch>` action which is more powerful.
You can pipe the contents of the current screen and history buffer as
:file:`STDIN` to an arbitrary program using the ``pipe`` function. The program
@@ -56,6 +59,9 @@ Input placeholders
There are various different kinds of placeholders
``@selection``
Plain text, currently selected text
``@text``
Plain text, current screen + scrollback buffer

View File

@@ -219,6 +219,11 @@ first, for example::
<ESC>]52;c;!<ESC>\
Here ``!`` is not valid base64 encoded text, so it clears the clipboard.
Further, since it is invalid, it should be ignored by terminal emulators
that do not support this extension, thereby making it safe to use, simply
always send it before starting a new OSC 52 paste, even if you aren't chunking
up large pastes, that way kitty wont concatenate your paste, and it will have
no ill-effects in other terminal emulators.
In case you're using software that can't be easily adapted to this
protocol extension, it can be disabled by specifying ``no-append`` to the

View File

@@ -21,7 +21,7 @@ In order for control to work, :opt:`allow_remote_control` must be enabled in
Now, in the new |kitty| window, enter the command::
kitty @ new-window --title Output --keep-focus cat
kitty @ launch --title Output --keep-focus cat
This will open a new window, running the ``cat`` program that will appear next
to the current window.
@@ -54,7 +54,7 @@ window. Type ``Ctrl+D`` when you are ready to stop.
Now, let's open a new tab::
kitty @ new-window --new-tab --tab-title "My Tab" --keep-focus bash
kitty @ launch --type=tab --tab-title "My Tab" --keep-focus bash
This will open a new tab running the bash shell with the title "My Tab".
We can change the title of the tab with::
@@ -126,7 +126,7 @@ If you do not want to allow all programs running in |kitty| to control it, you c
enable remote control for only some |kitty| windows. Simply create a shortcut
such as::
map ctrl+k new_window @ some_program
map ctrl+k launch --allow-remote-control some_program
Then programs running in windows created with that shortcut can use ``kitty @``
to control kitty. Note that any program with the right level of permissions can
@@ -134,6 +134,19 @@ still write to the pipes of any other program on the same computer and
therefore can control |kitty|. It can, however, be useful to block programs
running on other computers (for example, over ssh) or as other users.
Mapping key presses to remote control commands
--------------------------------------------------
If you wish to trigger a remote control command easily with just a keypress,
you can map it in :file:`kitty.conf`. For example::
map F1 remote_control set-spacing margin=30
Then pressing the :kbd:`F1` key will set the active window margins to 30.
The syntax for what follows :code:`remote_control` is exactly the same
as the syntax for what follows :code:`kitty @` above.
Documentation for the remote control protocol
-----------------------------------------------

BIN
docs/screenshots/splits.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -4,18 +4,23 @@
import subprocess
from collections import defaultdict
from typing import Any, DefaultDict, Dict, FrozenSet, List, Tuple, Union
KeymapType = Dict[str, Tuple[str, Union[FrozenSet[str], str]]]
def resolve_keys(keymap):
ans = defaultdict(list)
def resolve_keys(keymap: KeymapType) -> DefaultDict[str, List[str]]:
ans: DefaultDict[str, List[str]] = defaultdict(list)
for ch, (attr, atype) in keymap.items():
if atype not in ('int', 'uint'):
atype = 'flag'
ans[atype].append(ch)
if isinstance(atype, str) and atype in ('int', 'uint'):
q = atype
else:
q = 'flag'
ans[q].append(ch)
return ans
def enum(keymap):
def enum(keymap: KeymapType) -> str:
lines = []
for ch, (attr, atype) in keymap.items():
lines.append(f"{attr}='{ch}'")
@@ -26,15 +31,15 @@ def enum(keymap):
'''.format(',\n'.join(lines))
def parse_key(keymap):
def parse_key(keymap: KeymapType) -> str:
lines = []
for attr, atype in keymap.values():
vs = atype.upper() if atype in ('uint', 'int') else 'FLAG'
vs = atype.upper() if isinstance(atype, str) and atype in ('uint', 'int') else 'FLAG'
lines.append(f'case {attr}: value_state = {vs}; break;')
return ' \n'.join(lines)
def parse_flag(keymap, type_map, command_class):
def parse_flag(keymap: KeymapType, type_map: Dict[str, Any], command_class: str) -> str:
lines = []
for ch in type_map['flag']:
attr, allowed_values = keymap[ch]
@@ -52,14 +57,14 @@ def parse_flag(keymap, type_map, command_class):
return ' \n'.join(lines)
def parse_number(keymap):
def parse_number(keymap: KeymapType) -> Tuple[str, str]:
int_keys = [f'I({attr})' for attr, atype in keymap.values() if atype == 'int']
uint_keys = [f'U({attr})' for attr, atype in keymap.values() if atype == 'uint']
return '; '.join(int_keys), '; '.join(uint_keys)
def cmd_for_report(report_name, keymap, type_map, payload_allowed):
def group(atype, conv):
def cmd_for_report(report_name: str, keymap: KeymapType, type_map: Dict[str, Any], payload_allowed: bool) -> str:
def group(atype: str, conv: str) -> Tuple[str, str]:
flag_fmt, flag_attrs = [], []
cv = {'flag': 'c', 'int': 'i', 'uint': 'I'}[atype]
for ch in type_map[atype]:
@@ -84,7 +89,15 @@ def cmd_for_report(report_name, keymap, type_map, payload_allowed):
return '\n'.join(ans)
def generate(function_name, callback_name, report_name, keymap, command_class, initial_key='a', payload_allowed=True):
def generate(
function_name: str,
callback_name: str,
report_name: str,
keymap: KeymapType,
command_class: str,
initial_key: str = 'a',
payload_allowed: bool = True
) -> str:
type_map = resolve_keys(keymap)
keys_enum = enum(keymap)
handle_key = parse_key(keymap)
@@ -124,6 +137,7 @@ static inline void
{parr}
{keys_enum}
enum KEYS key = '{initial_key}';
if (screen->parser_buf[pos] == ';') state = AFTER_VALUE;
while (pos < screen->parser_buf_pos) {{
switch(state) {{
@@ -225,7 +239,7 @@ static inline void
'''
def write_header(text, path):
def write_header(text: str, path: str) -> None:
with open(path, 'w') as f:
print(f'// This file is generated by {__file__} do not edit!', file=f, end='\n\n')
print('#pragma once', file=f)
@@ -233,9 +247,9 @@ def write_header(text, path):
subprocess.check_call(['clang-format', '-i', path])
def graphics_parser():
def graphics_parser() -> None:
flag = frozenset
keymap = {
keymap: KeymapType = {
'a': ('action', flag('tTqpd')),
'd': ('delete_action', flag('aAiIcCpPqQxXyYzZ')),
't': ('transmission_type', flag('dfts')),

View File

@@ -12,6 +12,10 @@ from functools import partial
from html.entities import html5
from itertools import groupby
from operator import itemgetter
from typing import (
Callable, DefaultDict, Dict, FrozenSet, Generator, Iterable, List,
Optional, Set, Tuple, Union
)
from urllib.request import urlopen
os.chdir(os.path.dirname(os.path.abspath(__file__)))
@@ -24,7 +28,7 @@ if len(non_characters) != 66:
emoji_skin_tone_modifiers = frozenset(range(0x1f3fb, 0x1F3FF + 1))
def get_data(fname, folder='UCD'):
def get_data(fname: str, folder: str = 'UCD') -> Iterable[str]:
url = f'https://www.unicode.org/Public/{folder}/latest/{fname}'
bn = os.path.basename(url)
local = os.path.join('/tmp', bn)
@@ -42,23 +46,25 @@ def get_data(fname, folder='UCD'):
# Map of class names to set of codepoints in class
class_maps = {}
name_map = {}
word_search_map = defaultdict(set)
class_maps: Dict[str, Set[int]] = {}
all_symbols: Set[int] = set()
name_map: Dict[int, str] = {}
word_search_map: DefaultDict[str, Set[int]] = defaultdict(set)
zwj = 0x200d
marks = set(emoji_skin_tone_modifiers) | {zwj}
flag_codepoints = frozenset(range(0x1F1E6, 0x1F1E6 + 26))
marks = set(emoji_skin_tone_modifiers) | {zwj} | flag_codepoints
not_assigned = set(range(0, sys.maxunicode))
def parse_ucd():
def parse_ucd() -> None:
def add_word(w, c):
def add_word(w: str, c: int) -> None:
if c <= 32 or c == 127 or 128 <= c <= 159:
return
if len(w) > 1:
word_search_map[w.lower()].add(c)
first = None
first: Optional[int] = None
for word, c in html5.items():
if len(c) == 1:
add_word(word.rstrip(';'), ord(c))
@@ -76,7 +82,7 @@ def parse_ucd():
category = parts[2]
s = class_maps.setdefault(category, set())
desc = parts[1]
codepoints = (codepoint,)
codepoints: Union[Tuple[int, ...], Iterable[int]] = (codepoint,)
if first is None:
if desc.endswith(', First>'):
first = codepoint
@@ -89,6 +95,8 @@ def parse_ucd():
not_assigned.discard(codepoint)
if category.startswith('M'):
marks.add(codepoint)
elif category.startswith('S'):
all_symbols.add(codepoint)
# Some common synonyms
word_search_map['bee'] |= word_search_map['honeybee']
@@ -96,43 +104,97 @@ def parse_ucd():
word_search_map['lamda'] |= word_search_map['lambda']
def split_two(line):
spec, rest = line.split(';', 1)
spec, rest = spec.strip(), rest.strip().split(' ', 1)[0].strip()
def parse_range_spec(spec: str) -> Set[int]:
spec = spec.strip()
if '..' in spec:
chars = tuple(map(lambda x: int(x, 16), filter(None, spec.split('.'))))
chars = set(range(chars[0], chars[1] + 1))
chars_ = tuple(map(lambda x: int(x, 16), filter(None, spec.split('.'))))
chars = set(range(chars_[0], chars_[1] + 1))
else:
chars = {int(spec, 16)}
return chars, rest
return chars
all_emoji = set()
emoji_categories = {}
emoji_presentation_bases = set()
def split_two(line: str) -> Tuple[Set[int], str]:
spec, rest = line.split(';', 1)
spec, rest = spec.strip(), rest.strip().split(' ', 1)[0].strip()
return parse_range_spec(spec), rest
def parse_emoji():
for line in get_data('emoji-data.txt', 'emoji'):
chars, rest = split_two(line)
s = emoji_categories.setdefault(rest, set())
s.update(chars)
all_emoji.update(chars)
for line in get_data('emoji-variation-sequences.txt', 'emoji'):
base, var, *rest = line.split()
if base.startswith('#'):
all_emoji: Set[int] = set()
emoji_presentation_bases: Set[int] = set()
narrow_emoji: Set[int] = set()
wide_emoji: Set[int] = set()
flags: Dict[int, List[int]] = {}
def parse_basic_emoji(spec: str) -> None:
parts = list(filter(None, spec.split()))
has_emoji_presentation = len(parts) < 2
chars = parse_range_spec(parts[0])
all_emoji.update(chars)
emoji_presentation_bases.update(chars)
(wide_emoji if has_emoji_presentation else narrow_emoji).update(chars)
def parse_keycap_sequence(spec: str) -> None:
base, fe0f, cc = list(filter(None, spec.split()))
chars = parse_range_spec(base)
all_emoji.update(chars)
emoji_presentation_bases.update(chars)
narrow_emoji.update(chars)
def parse_flag_emoji_sequence(spec: str) -> None:
a, b = list(filter(None, spec.split()))
left, right = int(a, 16), int(b, 16)
chars = {left, right}
all_emoji.update(chars)
wide_emoji.update(chars)
emoji_presentation_bases.update(chars)
flags.setdefault(left, []).append(right)
def parse_emoji_tag_sequence(spec: str) -> None:
a = int(spec.split()[0], 16)
all_emoji.add(a)
wide_emoji.add(a)
emoji_presentation_bases.add(a)
def parse_emoji_modifier_sequence(spec: str) -> None:
a, b = list(filter(None, spec.split()))
char, mod = int(a, 16), int(b, 16)
mod
all_emoji.add(char)
wide_emoji.add(char)
emoji_presentation_bases.add(char)
def parse_emoji() -> None:
for line in get_data('emoji-sequences.txt', 'emoji'):
parts = [x.strip() for x in line.split(';')]
if len(parts) < 2:
continue
base = int(base, 16)
if var.upper() == 'FE0F':
emoji_presentation_bases.add(base)
data, etype = parts[:2]
if etype == 'Basic_Emoji':
parse_basic_emoji(data)
elif etype == 'Emoji_Keycap_Sequence':
parse_keycap_sequence(data)
elif etype == 'RGI_Emoji_Flag_Sequence':
parse_flag_emoji_sequence(data)
elif etype == 'RGI_Emoji_Tag_Sequence':
parse_emoji_tag_sequence(data)
elif etype == 'RGI_Emoji_Modifier_Sequence':
parse_emoji_modifier_sequence(data)
doublewidth, ambiguous = set(), set()
doublewidth: Set[int] = set()
ambiguous: Set[int] = set()
def parse_eaw():
def parse_eaw() -> None:
global doublewidth, ambiguous
seen = set()
seen: Set[int] = set()
for line in get_data('ucd/EastAsianWidth.txt'):
chars, eaw = split_two(line)
if eaw == 'A':
@@ -148,7 +210,7 @@ def parse_eaw():
doublewidth |= set(range(0x30000, 0x3FFFD + 1)) - seen
def get_ranges(items):
def get_ranges(items: List[int]) -> Generator[Union[int, Tuple[int, int]], None, None]:
items.sort()
for k, g in groupby(enumerate(items), lambda m: m[0]-m[1]):
group = tuple(map(itemgetter(1), g))
@@ -159,7 +221,7 @@ def get_ranges(items):
yield a, b
def write_case(spec, p):
def write_case(spec: Union[Tuple, int], p: Callable) -> None:
if isinstance(spec, tuple):
p('\t\tcase 0x{:x} ... 0x{:x}:'.format(*spec))
else:
@@ -167,7 +229,7 @@ def write_case(spec, p):
@contextmanager
def create_header(path, include_data_types=True):
def create_header(path: str, include_data_types: bool = True) -> Generator[Callable, None, None]:
with open(path, 'w') as f:
p = partial(print, file=f)
p('// unicode data, built from the unicode standard on:', date.today())
@@ -184,7 +246,7 @@ def create_header(path, include_data_types=True):
p('END_ALLOW_CASE_RANGE')
def gen_emoji():
def gen_emoji() -> None:
with create_header('kitty/emoji.h') as p:
p('static inline bool\nis_emoji(char_type code) {')
p('\tswitch(code) {')
@@ -194,9 +256,10 @@ def gen_emoji():
p('\t\tdefault: return false;')
p('\t}')
p('\treturn false;\n}')
p('static inline bool\nis_emoji_modifier(char_type code) {')
p('static inline bool\nis_symbol(char_type code) {')
p('\tswitch(code) {')
for spec in get_ranges(list(emoji_categories['Emoji_Modifier'])):
for spec in get_ranges(list(all_symbols)):
write_case(spec, p)
p('\t\t\treturn true;')
p('\t\tdefault: return false;')
@@ -204,9 +267,17 @@ def gen_emoji():
p('\treturn false;\n}')
def category_test(name, p, classes, comment, static=False, extra_chars=frozenset(), exclude=frozenset()):
static = 'static inline ' if static else ''
chars = set()
def category_test(
name: str,
p: Callable,
classes: Iterable[str],
comment: str,
use_static: bool = False,
extra_chars: Union[FrozenSet[int], Set[int]] = frozenset(),
exclude: Union[Set[int], FrozenSet[int]] = frozenset()
) -> None:
static = 'static inline ' if use_static else ''
chars: Set[int] = set()
for c in classes:
chars |= class_maps[c]
chars |= extra_chars
@@ -221,7 +292,7 @@ def category_test(name, p, classes, comment, static=False, extra_chars=frozenset
p('\treturn false;\n}\n')
def codepoint_to_mark_map(p, mark_map):
def codepoint_to_mark_map(p: Callable, mark_map: List[int]) -> Dict[int, int]:
p('\tswitch(c) { // {{{')
rmap = {c: m for m, c in enumerate(mark_map)}
for spec in get_ranges(mark_map):
@@ -236,14 +307,14 @@ def codepoint_to_mark_map(p, mark_map):
return rmap
def classes_to_regex(classes, exclude=''):
chars = set()
def classes_to_regex(classes: Iterable[str], exclude: str = '') -> Iterable[str]:
chars: Set[int] = set()
for c in classes:
chars |= class_maps[c]
for c in map(ord, exclude):
chars.discard(c)
for x in map(ord, exclude):
chars.discard(x)
def as_string(codepoint):
def as_string(codepoint: int) -> str:
if codepoint < 256:
return r'\x{:02x}'.format(codepoint)
if codepoint <= 0xffff:
@@ -257,7 +328,7 @@ def classes_to_regex(classes, exclude=''):
yield as_string(spec)
def gen_ucd():
def gen_ucd() -> None:
cz = {c for c in class_maps if c[0] in 'CZ'}
with create_header('kitty/unicode-data.c') as p:
p('#include "unicode-data.h"')
@@ -285,7 +356,9 @@ def gen_ucd():
p('}\n')
with open('kitty/unicode-data.h') as f:
unicode_data = f.read()
expected = int(re.search(r'^#define VS15 (\d+)', unicode_data, re.M).group(1))
m = re.search(r'^#define VS15 (\d+)', unicode_data, re.M)
if m is not None:
expected = int(m.group(1))
if rmap[0xfe0e] != expected:
raise ValueError('The mark for 0xfe0e has changed, you have to update VS15 to {} and VS16 to {} in unicode-data.h'.format(
rmap[0xfe0e], rmap[0xfe0f]
@@ -294,7 +367,7 @@ def gen_ucd():
f.write("url_delimiters = '{}' # noqa".format(''.join(classes_to_regex(cz, exclude='\n'))))
def gen_names():
def gen_names() -> None:
with create_header('kittens/unicode_input/names.h') as p:
mark_to_cp = list(sorted(name_map))
cp_to_mark = {cp: m for m, cp in enumerate(mark_to_cp)}
@@ -349,28 +422,28 @@ def gen_names():
# The trie
p(f'typedef struct {{ uint32_t children_offset; uint32_t match_offset; }} word_trie;\n')
all_trie_nodes = []
all_trie_nodes: List['TrieNode'] = []
class TrieNode:
def __init__(self):
def __init__(self) -> None:
self.match_offset = 0
self.children_offset = 0
self.children = {}
self.children: Dict[int, int] = {}
def add_letter(self, letter):
def add_letter(self, letter: int) -> int:
if letter not in self.children:
self.children[letter] = len(all_trie_nodes)
all_trie_nodes.append(TrieNode())
return self.children[letter]
def __str__(self):
def __str__(self) -> str:
return f'{{ .children_offset={self.children_offset}, .match_offset={self.match_offset} }}'
root = TrieNode()
all_trie_nodes.append(root)
def add_word(word_idx, word):
def add_word(word_idx: int, word: str) -> None:
parent = root
for letter in map(ord, word):
idx = parent.add_letter(letter)
@@ -395,10 +468,10 @@ def gen_names():
p('}; // }}}\n')
def gen_wcwidth():
seen = set()
def gen_wcwidth() -> None:
seen: Set[int] = set()
def add(p, comment, chars_, ret):
def add(p: Callable, comment: str, chars_: Union[Set[int], FrozenSet[int]], ret: int) -> None:
chars = chars_ - seen
seen.update(chars)
p(f'\t\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')
@@ -412,13 +485,14 @@ def gen_wcwidth():
p('\tswitch(code) {')
non_printing = class_maps['Cc'] | class_maps['Cf'] | class_maps['Cs']
add(p, 'Flags', flag_codepoints, 2)
add(p, 'Marks', marks | {0}, 0)
add(p, 'Non-printing characters', non_printing, -1)
add(p, 'Private use', class_maps['Co'], -3)
add(p, 'Text Presentation', emoji_categories['Emoji'] - emoji_categories['Emoji_Presentation'], 1)
add(p, 'Text Presentation', narrow_emoji, 1)
add(p, 'East Asian ambiguous width', ambiguous, -2)
add(p, 'East Asian double width', doublewidth, 2)
add(p, 'Emoji Presentation', emoji_categories['Emoji_Presentation'], 2)
add(p, 'Emoji Presentation', wide_emoji, 2)
add(p, 'Not assigned in the unicode character database', not_assigned, -4)

0
glfw/__init__.py Normal file
View File

106
glfw/backend_utils.c vendored
View File

@@ -22,19 +22,6 @@
#define ppoll pollts
#endif
static inline double
monotonic(void) {
struct timespec ts = {0};
#ifdef CLOCK_HIGHRES
clock_gettime(CLOCK_HIGHRES, &ts);
#elif CLOCK_MONOTONIC_RAW
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
#else
clock_gettime(CLOCK_MONOTONIC, &ts);
#endif
return (((double)ts.tv_nsec) / 1e9) + (double)ts.tv_sec;
}
void
update_fds(EventLoopData *eld) {
for (nfds_t i = 0; i < eld->watches_count; i++) {
@@ -109,7 +96,7 @@ update_timers(EventLoopData *eld) {
}
id_type
addTimer(EventLoopData *eld, const char *name, double interval, int enabled, bool repeats, timer_callback_func cb, void *cb_data, GLFWuserdatafreefun free) {
addTimer(EventLoopData *eld, const char *name, monotonic_t interval, int enabled, bool repeats, timer_callback_func cb, void *cb_data, GLFWuserdatafreefun free) {
if (eld->timers_count >= sizeof(eld->timers)/sizeof(eld->timers[0])) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Too many timers added");
return 0;
@@ -117,7 +104,7 @@ addTimer(EventLoopData *eld, const char *name, double interval, int enabled, boo
Timer *t = eld->timers + eld->timers_count++;
t->interval = interval;
t->name = name;
t->trigger_at = enabled ? monotonic() + interval : DBL_MAX;
t->trigger_at = enabled ? monotonic() + interval : MONOTONIC_T_MAX;
t->repeats = repeats;
t->callback = cb;
t->callback_data = cb_data;
@@ -144,7 +131,7 @@ void
toggleTimer(EventLoopData *eld, id_type timer_id, int enabled) {
for (nfds_t i = 0; i < eld->timers_count; i++) {
if (eld->timers[i].id == timer_id) {
double trigger_at = enabled ? (monotonic() + eld->timers[i].interval) : DBL_MAX;
monotonic_t trigger_at = enabled ? (monotonic() + eld->timers[i].interval) : MONOTONIC_T_MAX;
if (trigger_at != eld->timers[i].trigger_at) {
eld->timers[i].trigger_at = trigger_at;
update_timers(eld);
@@ -155,7 +142,7 @@ toggleTimer(EventLoopData *eld, id_type timer_id, int enabled) {
}
void
changeTimerInterval(EventLoopData *eld, id_type timer_id, double interval) {
changeTimerInterval(EventLoopData *eld, id_type timer_id, monotonic_t interval) {
for (nfds_t i = 0; i < eld->timers_count; i++) {
if (eld->timers[i].id == timer_id) {
eld->timers[i].interval = interval;
@@ -165,22 +152,28 @@ changeTimerInterval(EventLoopData *eld, id_type timer_id, double interval) {
}
double
prepareForPoll(EventLoopData *eld, double timeout) {
monotonic_t
prepareForPoll(EventLoopData *eld, monotonic_t timeout) {
for (nfds_t i = 0; i < eld->watches_count; i++) eld->fds[i].revents = 0;
if (!eld->timers_count || eld->timers[0].trigger_at == DBL_MAX) return timeout;
double now = monotonic(), next_repeat_at = eld->timers[0].trigger_at;
if (!eld->timers_count || eld->timers[0].trigger_at == MONOTONIC_T_MAX) return timeout;
monotonic_t now = monotonic(), next_repeat_at = eld->timers[0].trigger_at;
if (timeout < 0 || now + timeout > next_repeat_at) {
timeout = next_repeat_at <= now ? 0 : next_repeat_at - now;
}
return timeout;
}
static inline struct timespec
calc_time(monotonic_t nsec) {
struct timespec result;
result.tv_sec = nsec / (1000LL * 1000LL * 1000LL);
result.tv_nsec = nsec % (1000LL * 1000LL * 1000LL);
return result;
}
int
pollWithTimeout(struct pollfd *fds, nfds_t nfds, double timeout) {
const long seconds = (long) timeout;
const long nanoseconds = (long) ((timeout - seconds) * 1e9);
struct timespec tv = { seconds, nanoseconds };
pollWithTimeout(struct pollfd *fds, nfds_t nfds, monotonic_t timeout) {
struct timespec tv = calc_time(timeout);
return ppoll(fds, nfds, &tv, NULL);
}
@@ -198,10 +191,10 @@ dispatchEvents(EventLoopData *eld) {
unsigned
dispatchTimers(EventLoopData *eld) {
if (!eld->timers_count || eld->timers[0].trigger_at == DBL_MAX) return 0;
if (!eld->timers_count || eld->timers[0].trigger_at == MONOTONIC_T_MAX) return 0;
static struct { timer_callback_func func; id_type id; void* data; bool repeats; } dispatches[sizeof(eld->timers)/sizeof(eld->timers[0])];
unsigned num_dispatches = 0;
double now = monotonic();
monotonic_t now = monotonic();
for (nfds_t i = 0; i < eld->timers_count && eld->timers[i].trigger_at <= now; i++) {
eld->timers[i].trigger_at = now + eld->timers[i].interval;
dispatches[num_dispatches].func = eld->timers[i].callback;
@@ -299,12 +292,12 @@ finalizePollData(EventLoopData *eld) {
}
int
pollForEvents(EventLoopData *eld, double timeout) {
pollForEvents(EventLoopData *eld, monotonic_t timeout, watch_callback_func display_callback) {
int read_ok = 0;
timeout = prepareForPoll(eld, timeout);
EVDBG("pollForEvents final timeout: %.3f", timeout);
EVDBG("pollForEvents final timeout: %.3f", monotonic_t_to_s_double(timeout));
int result;
double end_time = monotonic() + timeout;
monotonic_t end_time = monotonic() + timeout;
eld->wakeup_fd_ready = false;
while(1) {
@@ -312,6 +305,7 @@ pollForEvents(EventLoopData *eld, double timeout) {
errno = 0;
result = pollWithTimeout(eld->fds, eld->watches_count, timeout);
int saved_errno = errno;
if (display_callback) display_callback(result, eld->fds[0].revents && eld->watches[0].events, NULL);
dispatchTimers(eld);
if (result > 0) {
dispatchEvents(eld);
@@ -326,6 +320,7 @@ pollForEvents(EventLoopData *eld, double timeout) {
errno = 0;
result = poll(eld->fds, eld->watches_count, -1);
int saved_errno = errno;
if (display_callback) display_callback(result, eld->fds[0].revents && eld->watches[0].events, NULL);
dispatchTimers(eld);
if (result > 0) {
dispatchEvents(eld);
@@ -337,54 +332,3 @@ pollForEvents(EventLoopData *eld, double timeout) {
}
return read_ok;
}
// Splits and translates a text/uri-list into separate file paths
// NOTE: This function destroys the provided string
//
char** parseUriList(char* text, int* count)
{
const char* prefix = "file://";
char** paths = NULL;
char* line;
*count = 0;
while ((line = strtok(text, "\r\n")))
{
text = NULL;
if (line[0] == '#')
continue;
if (strncmp(line, prefix, strlen(prefix)) == 0)
{
line += strlen(prefix);
// TODO: Validate hostname
while (*line != '/')
line++;
}
(*count)++;
char* path = calloc(strlen(line) + 1, 1);
paths = realloc(paths, *count * sizeof(char*));
paths[*count - 1] = path;
while (*line)
{
if (line[0] == '%' && line[1] && line[2])
{
const char digits[3] = { line[1], line[2], '\0' };
*path = strtol(digits, NULL, 16);
line += 2;
}
else
*path = *line;
path++;
line++;
}
}
return paths;
}

14
glfw/backend_utils.h vendored
View File

@@ -25,6 +25,7 @@
//========================================================================
#pragma once
#include "../kitty/monotonic.h"
#include <poll.h>
#include <unistd.h>
#include <stdbool.h>
@@ -55,7 +56,7 @@ typedef struct {
typedef struct {
id_type id;
double interval, trigger_at;
monotonic_t interval, trigger_at;
timer_callback_func callback;
void *callback_data;
GLFWuserdatafreefun free;
@@ -82,16 +83,15 @@ void check_for_wakeup_events(EventLoopData *eld);
id_type addWatch(EventLoopData *eld, const char *name, int fd, int events, int enabled, watch_callback_func cb, void *cb_data);
void removeWatch(EventLoopData *eld, id_type watch_id);
void toggleWatch(EventLoopData *eld, id_type watch_id, int enabled);
id_type addTimer(EventLoopData *eld, const char *name, double interval, int enabled, bool repeats, timer_callback_func cb, void *cb_data, GLFWuserdatafreefun free);
id_type addTimer(EventLoopData *eld, const char *name, monotonic_t interval, int enabled, bool repeats, timer_callback_func cb, void *cb_data, GLFWuserdatafreefun free);
void removeTimer(EventLoopData *eld, id_type timer_id);
void removeAllTimers(EventLoopData *eld);
void toggleTimer(EventLoopData *eld, id_type timer_id, int enabled);
void changeTimerInterval(EventLoopData *eld, id_type timer_id, double interval);
double prepareForPoll(EventLoopData *eld, double timeout);
int pollWithTimeout(struct pollfd *fds, nfds_t nfds, double timeout);
int pollForEvents(EventLoopData *eld, double timeout);
void changeTimerInterval(EventLoopData *eld, id_type timer_id, monotonic_t interval);
monotonic_t prepareForPoll(EventLoopData *eld, monotonic_t timeout);
int pollWithTimeout(struct pollfd *fds, nfds_t nfds, monotonic_t timeout);
int pollForEvents(EventLoopData *eld, monotonic_t timeout, watch_callback_func);
unsigned dispatchTimers(EventLoopData *eld);
void finalizePollData(EventLoopData *eld);
bool initPollData(EventLoopData *eld, int display_fd);
char** parseUriList(char* text, int* count);
void wakeupEventLoop(EventLoopData *eld);

View File

@@ -27,9 +27,13 @@
//========================================================================
#include "internal.h"
#include "../kitty/monotonic.h"
#include <sys/param.h> // For MAXPATHLEN
#include <pthread.h>
// Needed for _NSGetProgname
#include <crt_externs.h>
// Change to our application bundle's resources directory, if present
//
static void changeToResourcesDirectory(void)
@@ -66,14 +70,119 @@ static void changeToResourcesDirectory(void)
chdir(resourcesPath);
}
// Set up the menu bar (manually)
// This is nasty, nasty stuff -- calls to undocumented semi-private APIs that
// could go away at any moment, lots of stuff that really should be
// localize(d|able), etc. Add a nib to save us this horror.
//
static void createMenuBar(void)
{
size_t i;
NSString* appName = nil;
NSDictionary* bundleInfo = [[NSBundle mainBundle] infoDictionary];
NSString* nameKeys[] =
{
@"CFBundleDisplayName",
@"CFBundleName",
@"CFBundleExecutable",
};
// Try to figure out what the calling application is called
for (i = 0; i < sizeof(nameKeys) / sizeof(nameKeys[0]); i++)
{
id name = bundleInfo[nameKeys[i]];
if (name &&
[name isKindOfClass:[NSString class]] &&
![name isEqualToString:@""])
{
appName = name;
break;
}
}
if (!appName)
{
char** progname = _NSGetProgname();
if (progname && *progname)
appName = @(*progname);
else
appName = @"GLFW Application";
}
NSMenu* bar = [[NSMenu alloc] init];
[NSApp setMainMenu:bar];
NSMenuItem* appMenuItem =
[bar addItemWithTitle:@"" action:NULL keyEquivalent:@""];
NSMenu* appMenu = [[NSMenu alloc] init];
[appMenuItem setSubmenu:appMenu];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"About %@", appName]
action:@selector(orderFrontStandardAboutPanel:)
keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
NSMenu* servicesMenu = [[NSMenu alloc] init];
[NSApp setServicesMenu:servicesMenu];
[[appMenu addItemWithTitle:@"Services"
action:NULL
keyEquivalent:@""] setSubmenu:servicesMenu];
[servicesMenu release];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", appName]
action:@selector(hide:)
keyEquivalent:@"h"];
[[appMenu addItemWithTitle:@"Hide Others"
action:@selector(hideOtherApplications:)
keyEquivalent:@"h"]
setKeyEquivalentModifierMask:NSEventModifierFlagOption | NSEventModifierFlagCommand];
[appMenu addItemWithTitle:@"Show All"
action:@selector(unhideAllApplications:)
keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", appName]
action:@selector(terminate:)
keyEquivalent:@"q"];
NSMenuItem* windowMenuItem =
[bar addItemWithTitle:@"" action:NULL keyEquivalent:@""];
[bar release];
NSMenu* windowMenu = [[NSMenu alloc] initWithTitle:@"Window"];
[NSApp setWindowsMenu:windowMenu];
[windowMenuItem setSubmenu:windowMenu];
[windowMenu addItemWithTitle:@"Minimize"
action:@selector(performMiniaturize:)
keyEquivalent:@"m"];
[windowMenu addItemWithTitle:@"Zoom"
action:@selector(performZoom:)
keyEquivalent:@""];
[windowMenu addItem:[NSMenuItem separatorItem]];
[windowMenu addItemWithTitle:@"Bring All to Front"
action:@selector(arrangeInFront:)
keyEquivalent:@""];
// TODO: Make this appear at the bottom of the menu (for consistency)
[windowMenu addItem:[NSMenuItem separatorItem]];
[[windowMenu addItemWithTitle:@"Enter Full Screen"
action:@selector(toggleFullScreen:)
keyEquivalent:@"f"]
setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];
// Prior to Snow Leopard, we need to use this oddly-named semi-private API
// to get the application menu working properly.
SEL setAppleMenuSelector = NSSelectorFromString(@"setAppleMenu:");
[NSApp performSelector:setAppleMenuSelector withObject:appMenu];
}
// Create key code translation tables
//
static void createKeyTables(void)
{
int scancode;
int keycode;
memset(_glfw.ns.keycodes, -1, sizeof(_glfw.ns.keycodes));
memset(_glfw.ns.scancodes, -1, sizeof(_glfw.ns.scancodes));
memset(_glfw.ns.key_to_keycode, -1, sizeof(_glfw.ns.key_to_keycode));
_glfw.ns.keycodes[0x1D] = GLFW_KEY_0;
_glfw.ns.keycodes[0x12] = GLFW_KEY_1;
@@ -190,11 +299,11 @@ static void createKeyTables(void)
_glfw.ns.keycodes[0x43] = GLFW_KEY_KP_MULTIPLY;
_glfw.ns.keycodes[0x4E] = GLFW_KEY_KP_SUBTRACT;
for (scancode = 0; scancode < 256; scancode++)
for (keycode = 0; keycode < 256; keycode++)
{
// Store the reverse translation for faster key name lookup
if (_glfw.ns.keycodes[scancode] >= 0)
_glfw.ns.scancodes[_glfw.ns.keycodes[scancode]] = scancode;
if (_glfw.ns.keycodes[keycode] >= 0)
_glfw.ns.key_to_keycode[_glfw.ns.keycodes[keycode]] = keycode;
}
}
@@ -276,6 +385,17 @@ static bool initializeTIS(void)
return updateUnicodeDataNS();
}
static void
display_reconfigured(CGDirectDisplayID display UNUSED, CGDisplayChangeSummaryFlags flags, void *userInfo UNUSED)
{
if (flags & kCGDisplayBeginConfigurationFlag) {
return;
}
if (flags & kCGDisplaySetModeFlag) {
// GPU possibly changed
}
}
@interface GLFWHelper : NSObject
@end
@@ -294,6 +414,102 @@ static bool initializeTIS(void)
@end // GLFWHelper
// Delegate for application related notifications {{{
@interface GLFWApplicationDelegate : NSObject <NSApplicationDelegate>
@end
@implementation GLFWApplicationDelegate
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
(void)sender;
_GLFWwindow* window;
for (window = _glfw.windowListHead; window; window = window->next)
_glfwInputWindowCloseRequest(window);
return NSTerminateCancel;
}
static GLFWapplicationshouldhandlereopenfun handle_reopen_callback = NULL;
- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag
{
(void)sender;
if (!handle_reopen_callback) return YES;
if (handle_reopen_callback(flag)) return YES;
return NO;
}
- (void)applicationDidChangeScreenParameters:(NSNotification *) notification
{
(void)notification;
_GLFWwindow* window;
for (window = _glfw.windowListHead; window; window = window->next)
{
if (window->context.client != GLFW_NO_API)
[window->context.nsgl.object update];
}
_glfwPollMonitorsNS();
}
static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
(void)notification;
if (_glfw.hints.init.ns.menubar)
{
// In case we are unbundled, make us a proper UI application
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
// Menu bar setup must go between sharedApplication and finishLaunching
// in order to properly emulate the behavior of NSApplicationMain
if ([[NSBundle mainBundle] pathForResource:@"MainMenu" ofType:@"nib"])
{
[[NSBundle mainBundle] loadNibNamed:@"MainMenu"
owner:NSApp
topLevelObjects:&_glfw.ns.nibObjects];
}
else
createMenuBar();
}
if (finish_launching_callback)
finish_launching_callback();
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
(void)notification;
[NSApp stop:nil];
CGDisplayRegisterReconfigurationCallback(display_reconfigured, NULL);
_glfwCocoaPostEmptyEvent();
}
- (void)applicationWillTerminate:(NSNotification *)aNotification
{
(void)aNotification;
CGDisplayRemoveReconfigurationCallback(display_reconfigured, NULL);
}
- (void)applicationDidHide:(NSNotification *)notification
{
(void)notification;
int i;
for (i = 0; i < _glfw.monitorCount; i++)
_glfwRestoreVideoModeNS(_glfw.monitors[i]);
}
@end // GLFWApplicationDelegate
// }}}
@interface GLFWApplication : NSApplication
- (void)tick_callback;
- (void)render_frame_received:(id)displayIDAsID;
@@ -313,6 +529,32 @@ static bool initializeTIS(void)
@end
//////////////////////////////////////////////////////////////////////////
////// GLFW internal API //////
//////////////////////////////////////////////////////////////////////////
void* _glfwLoadLocalVulkanLoaderNS(void)
{
CFBundleRef bundle = CFBundleGetMainBundle();
if (!bundle)
return NULL;
CFURLRef url =
CFBundleCopyAuxiliaryExecutableURL(bundle, CFSTR("libvulkan.1.dylib"));
if (!url)
return NULL;
char path[PATH_MAX];
void* handle = NULL;
if (CFURLGetFileSystemRepresentation(url, true, (UInt8*) path, sizeof(path) - 1))
handle = _glfw_dlopen(path);
CFRelease(url);
return handle;
}
//////////////////////////////////////////////////////////////////////////
////// GLFW platform API //////
//////////////////////////////////////////////////////////////////////////
@@ -335,6 +577,18 @@ is_cmd_period(NSEvent *event, NSEventModifierFlags modifierFlags) {
return false;
}
GLFWAPI GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback) {
GLFWapplicationshouldhandlereopenfun previous = handle_reopen_callback;
handle_reopen_callback = callback;
return previous;
}
GLFWAPI GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback) {
GLFWapplicationwillfinishlaunchingfun previous = finish_launching_callback;
finish_launching_callback = callback;
return previous;
}
int _glfwPlatformInit(void)
{
@autoreleasepool {
@@ -345,8 +599,21 @@ int _glfwPlatformInit(void)
toTarget:_glfw.ns.helper
withObject:nil];
if (NSApp)
_glfw.ns.finishedLaunching = true;
[GLFWApplication sharedApplication];
_glfw.ns.delegate = [[GLFWApplicationDelegate alloc] init];
if (_glfw.ns.delegate == nil)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Failed to create application delegate");
return false;
}
[NSApp setDelegate:_glfw.ns.delegate];
NSEvent* (^keydown_block)(NSEvent*) = ^ NSEvent* (NSEvent* event)
{
NSEventModifierFlags modifierFlags = [event modifierFlags] & NSEventModifierFlagDeviceIndependentFlagsMask;
@@ -385,6 +652,10 @@ int _glfwPlatformInit(void)
if (_glfw.hints.init.ns.chdir)
changeToResourcesDirectory();
// Press and Hold prevents some keys from emitting repeated characters
NSDictionary* defaults = @{@"ApplePressAndHoldEnabled":@NO};
[[NSUserDefaults standardUserDefaults] registerDefaults:defaults];
[[NSNotificationCenter defaultCenter]
addObserver:_glfw.ns.helper
selector:@selector(selectedKeyboardInputSourceChanged:)
@@ -402,7 +673,6 @@ int _glfwPlatformInit(void)
if (!initializeTIS())
return false;
_glfwInitTimerNS();
_glfwInitJoysticksNS();
_glfwPollMonitorsNS();
@@ -531,7 +801,7 @@ typedef struct {
NSTimer *os_timer;
unsigned long long id;
bool repeats;
double interval;
monotonic_t interval;
GLFWuserdatafun callback;
void *callback_data;
GLFWuserdatafun free_callback_data;
@@ -551,7 +821,7 @@ remove_timer_at(size_t idx) {
}
static void schedule_timer(Timer *t) {
t->os_timer = [NSTimer scheduledTimerWithTimeInterval:t->interval repeats:(t->repeats ? YES: NO) block:^(NSTimer *os_timer) {
t->os_timer = [NSTimer scheduledTimerWithTimeInterval:monotonic_t_to_s_double(t->interval) repeats:(t->repeats ? YES: NO) block:^(NSTimer *os_timer) {
for (size_t i = 0; i < num_timers; i++) {
if (timers[i].os_timer == os_timer) {
timers[i].callback(timers[i].id, timers[i].callback_data);
@@ -562,7 +832,7 @@ static void schedule_timer(Timer *t) {
}];
}
unsigned long long _glfwPlatformAddTimer(double interval, bool repeats, GLFWuserdatafun callback, void *callback_data, GLFWuserdatafun free_callback) {
unsigned long long _glfwPlatformAddTimer(monotonic_t interval, bool repeats, GLFWuserdatafun callback, void *callback_data, GLFWuserdatafun free_callback) {
static unsigned long long timer_counter = 0;
if (num_timers >= sizeof(timers)/sizeof(timers[0]) - 1) {
_glfwInputError(GLFW_PLATFORM_ERROR, "Too many timers added");
@@ -588,7 +858,7 @@ void _glfwPlatformRemoveTimer(unsigned long long timer_id) {
}
}
void _glfwPlatformUpdateTimer(unsigned long long timer_id, double interval, bool enabled) {
void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled) {
for (size_t i = 0; i < num_timers; i++) {
if (timers[i].id == timer_id) {
Timer *t = timers + i;

View File

@@ -146,7 +146,7 @@ static bool modeIsGood(CGDisplayModeRef mode)
// Convert Core Graphics display mode to GLFW video mode
//
static GLFWvidmode vidmodeFromCGDisplayMode(CGDisplayModeRef mode,
CVDisplayLinkRef link)
double fallbackRefreshRate)
{
GLFWvidmode result;
result.width = (int) CGDisplayModeGetWidth(mode);
@@ -154,11 +154,7 @@ static GLFWvidmode vidmodeFromCGDisplayMode(CGDisplayModeRef mode,
result.refreshRate = (int) round(CGDisplayModeGetRefreshRate(mode));
if (result.refreshRate == 0)
{
const CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link);
if (!(time.flags & kCVTimeIsIndefinite))
result.refreshRate = (int) (time.timeScale / (double) time.timeValue);
}
result.refreshRate = (int) round(fallbackRefreshRate);
#if MAC_OS_X_VERSION_MAX_ALLOWED <= 101100
CFStringRef format = CGDisplayModeCopyPixelEncoding(mode);
@@ -240,6 +236,30 @@ static bool refreshMonitorScreen(_GLFWmonitor* monitor)
return false;
}
// Returns a fallback refresh rate for when Core Graphics says it is zero
//
static double getFallbackRefreshRate(_GLFWmonitor* monitor)
{
CGDisplayModeRef mode = CGDisplayCopyDisplayMode(monitor->ns.displayID);
double refreshRate = CGDisplayModeGetRefreshRate(mode);
CGDisplayModeRelease(mode);
if (refreshRate == 0.0)
{
CVDisplayLinkRef link = NULL;
CVDisplayLinkCreateWithCGDisplay(monitor->ns.displayID, &link);
const CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link);
if (!(time.flags & kCVTimeIsIndefinite))
refreshRate = (int) (time.timeScale / (double) time.timeValue);
CVDisplayLinkRelease(link);
}
return refreshRate;
}
//////////////////////////////////////////////////////////////////////////
////// GLFW internal API //////
//////////////////////////////////////////////////////////////////////////
@@ -328,9 +348,10 @@ void _glfwPollMonitorsNS(void)
if (!name)
name = _glfw_strdup("Unknown");
_GLFWmonitor* monitor = _glfwAllocMonitor(name, size.width, size.height);
_GLFWmonitor* monitor = _glfwAllocMonitor(name, (int)size.width, (int)size.height);
monitor->ns.displayID = displays[i];
monitor->ns.unitNumber = unitNumber;
monitor->ns.fallbackRefreshRate = getFallbackRefreshRate(monitor);
createDisplayLink(monitor->ns.displayID);
free(name);
@@ -354,14 +375,12 @@ void _glfwPollMonitorsNS(void)
void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired)
{
GLFWvidmode current;
const GLFWvidmode* best = _glfwChooseVideoMode(monitor, desired);
_glfwPlatformGetVideoMode(monitor, &current);
const GLFWvidmode* best = _glfwChooseVideoMode(monitor, desired);
if (_glfwCompareVideoModes(&current, best) == 0)
return;
CVDisplayLinkRef link;
CVDisplayLinkCreateWithCGDisplay(monitor->ns.displayID, &link);
CFArrayRef modes = CGDisplayCopyAllDisplayModes(monitor->ns.displayID, NULL);
const CFIndex count = CFArrayGetCount(modes);
CGDisplayModeRef native = NULL;
@@ -372,7 +391,8 @@ void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired)
if (!modeIsGood(dm))
continue;
const GLFWvidmode mode = vidmodeFromCGDisplayMode(dm, link);
const GLFWvidmode mode =
vidmodeFromCGDisplayMode(dm, monitor->ns.fallbackRefreshRate);
if (_glfwCompareVideoModes(best, &mode) == 0)
{
native = dm;
@@ -391,7 +411,6 @@ void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired)
}
CFRelease(modes);
CVDisplayLinkRelease(link);
}
// Restore the previously saved (original) video mode
@@ -454,13 +473,13 @@ void _glfwPlatformGetMonitorWorkarea(_GLFWmonitor* monitor,
const NSRect frameRect = [monitor->ns.screen visibleFrame];
if (xpos)
*xpos = frameRect.origin.x;
*xpos = (int)frameRect.origin.x;
if (ypos)
*ypos = _glfwTransformYNS(frameRect.origin.y + frameRect.size.height - 1);
*ypos = (int)_glfwTransformYNS(frameRect.origin.y + frameRect.size.height - 1);
if (width)
*width = frameRect.size.width;
*width = (int)frameRect.size.width;
if (height)
*height = frameRect.size.height;
*height = (int)frameRect.size.height;
}
@@ -468,9 +487,6 @@ GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)
{
*count = 0;
CVDisplayLinkRef link;
CVDisplayLinkCreateWithCGDisplay(monitor->ns.displayID, &link);
CFArrayRef modes = CGDisplayCopyAllDisplayModes(monitor->ns.displayID, NULL);
const CFIndex found = CFArrayGetCount(modes);
GLFWvidmode* result = calloc(found, sizeof(GLFWvidmode));
@@ -481,7 +497,8 @@ GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)
if (!modeIsGood(dm))
continue;
const GLFWvidmode mode = vidmodeFromCGDisplayMode(dm, link);
const GLFWvidmode mode =
vidmodeFromCGDisplayMode(dm, monitor->ns.fallbackRefreshRate);
for (CFIndex j = 0; j < *count; j++)
{
@@ -498,21 +515,14 @@ GLFWvidmode* _glfwPlatformGetVideoModes(_GLFWmonitor* monitor, int* count)
}
CFRelease(modes);
CVDisplayLinkRelease(link);
return result;
}
void _glfwPlatformGetVideoMode(_GLFWmonitor* monitor, GLFWvidmode *mode)
{
CVDisplayLinkRef link;
CVDisplayLinkCreateWithCGDisplay(monitor->ns.displayID, &link);
CGDisplayModeRef native = CGDisplayCopyDisplayMode(monitor->ns.displayID);
*mode = vidmodeFromCGDisplayMode(native, link);
*mode = vidmodeFromCGDisplayMode(native, monitor->ns.fallbackRefreshRate);
CGDisplayModeRelease(native);
CVDisplayLinkRelease(link);
}
bool _glfwPlatformGetGammaRamp(_GLFWmonitor* monitor, GLFWgammaramp* ramp)

32
glfw/cocoa_platform.h vendored
View File

@@ -36,6 +36,9 @@ typedef void* id;
typedef void* CVDisplayLinkRef;
#endif
// NOTE: Many Cocoa enum values have been renamed and we need to build across
// SDK versions where one is unavailable or the other deprecated
// We use the newer names in code and these macros to handle compatibility
#if MAC_OS_X_VERSION_MAX_ALLOWED < 101200
#define NSBitmapFormatAlphaNonpremultiplied NSAlphaNonpremultipliedBitmapFormat
#define NSEventMaskAny NSAnyEventMask
@@ -62,12 +65,15 @@ typedef void* CVDisplayLinkRef;
#endif
typedef VkFlags VkMacOSSurfaceCreateFlagsMVK;
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long);
typedef int (* GLFWapplicationshouldhandlereopenfun)(int);
typedef int (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
typedef VkFlags VkMacOSSurfaceCreateFlagsMVK;
typedef VkFlags VkMetalSurfaceCreateFlagsEXT;
typedef struct VkMacOSSurfaceCreateInfoMVK
{
VkStructureType sType;
@@ -76,7 +82,16 @@ typedef struct VkMacOSSurfaceCreateInfoMVK
const void* pView;
} VkMacOSSurfaceCreateInfoMVK;
typedef struct VkMetalSurfaceCreateInfoEXT
{
VkStructureType sType;
const void* pNext;
VkMetalSurfaceCreateFlagsEXT flags;
const void* pLayer;
} VkMetalSurfaceCreateInfoEXT;
typedef VkResult (APIENTRY *PFN_vkCreateMacOSSurfaceMVK)(VkInstance,const VkMacOSSurfaceCreateInfoMVK*,const VkAllocationCallbacks*,VkSurfaceKHR*);
typedef VkResult (APIENTRY *PFN_vkCreateMetalSurfaceEXT)(VkInstance,const VkMetalSurfaceCreateInfoEXT*,const VkAllocationCallbacks*,VkSurfaceKHR*);
#include "posix_thread.h"
#include "cocoa_joystick.h"
@@ -144,7 +159,7 @@ typedef struct _GLFWDisplayLinkNS
{
CVDisplayLinkRef displayLink;
CGDirectDisplayID displayID;
double lastRenderFrameRequestedAt;
monotonic_t lastRenderFrameRequestedAt;
} _GLFWDisplayLinkNS;
// Cocoa-specific global data
@@ -153,6 +168,7 @@ typedef struct _GLFWlibraryNS
{
CGEventSourceRef eventSource;
id delegate;
bool finishedLaunching;
bool cursorHidden;
TISInputSourceRef inputSource;
IOHIDManagerRef hidManager;
@@ -160,11 +176,12 @@ typedef struct _GLFWlibraryNS
id helper;
id keyUpMonitor;
id keyDownMonitor;
id nibObjects;
char keyName[64];
char text[256];
short int keycodes[256];
short int scancodes[GLFW_KEY_LAST + 1];
short int key_to_keycode[GLFW_KEY_LAST + 1];
char* clipboardString;
CGPoint cascadePoint;
// Where to place the cursor when re-enabled
@@ -195,6 +212,7 @@ typedef struct _GLFWmonitorNS
CGDisplayModeRef previousMode;
uint32_t unitNumber;
id screen;
double fallbackRefreshRate;
} _GLFWmonitorNS;
@@ -215,14 +233,14 @@ typedef struct _GLFWtimerNS
} _GLFWtimerNS;
void _glfwInitTimerNS(void);
void _glfwPollMonitorsNS(void);
void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired);
void _glfwRestoreVideoModeNS(_GLFWmonitor* monitor);
float _glfwTransformYNS(float y);
void* _glfwLoadLocalVulkanLoaderNS(void);
void _glfwClearDisplayLinks(void);
void _glfwRestartDisplayLinks(void);
void _glfwDispatchTickCallback(void);

62
glfw/cocoa_time.c vendored
View File

@@ -1,62 +0,0 @@
//========================================================================
// GLFW 3.4 macOS - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2009-2016 Camilla Löwy <elmindreda@glfw.org>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would
// be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
// be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
//
//========================================================================
// It is fine to use C99 in this file because it will not be built with VS
//========================================================================
#include "internal.h"
#include <mach/mach_time.h>
//////////////////////////////////////////////////////////////////////////
////// GLFW internal API //////
//////////////////////////////////////////////////////////////////////////
// Initialise timer
//
void _glfwInitTimerNS(void)
{
mach_timebase_info_data_t info;
mach_timebase_info(&info);
_glfw.timer.ns.frequency = (info.denom * 1e9) / info.numer;
}
//////////////////////////////////////////////////////////////////////////
////// GLFW platform API //////
//////////////////////////////////////////////////////////////////////////
uint64_t _glfwPlatformGetTimerValue(void)
{
return mach_absolute_time();
}
uint64_t _glfwPlatformGetTimerFrequency(void)
{
return _glfw.timer.ns.frequency;
}

File diff suppressed because it is too large Load Diff

3
glfw/dbus_glfw.c vendored
View File

@@ -27,6 +27,7 @@
#include "internal.h"
#include "dbus_glfw.h"
#include "../kitty/monotonic.h"
#include <stdlib.h>
#include <string.h>
@@ -107,7 +108,7 @@ on_dbus_timer_ready(id_type timer_id UNUSED, void *data) {
static dbus_bool_t
add_dbus_timeout(DBusTimeout *timeout, void *data) {
int enabled = dbus_timeout_get_enabled(timeout) ? 1 : 0;
double interval = ((double)dbus_timeout_get_interval(timeout)) / 1000.0;
monotonic_t interval = ms_to_monotonic_t(dbus_timeout_get_interval(timeout));
if (interval < 0) return FALSE;
id_type timer_id = addTimer(dbus_data->eld, data, interval, enabled, true, on_dbus_timer_ready, timeout, NULL);
if (!timer_id) return FALSE;

31
glfw/egl_context.c vendored
View File

@@ -123,23 +123,24 @@ static bool chooseEGLConfig(const _GLFWctxconfig* ctxconfig,
continue;
#if defined(_GLFW_X11)
XVisualInfo vi = {0};
// Only consider EGLConfigs with associated Visuals
vi.visualid = getEGLConfigAttrib(n, EGL_NATIVE_VISUAL_ID);
if (!vi.visualid)
continue;
if (desired->transparent)
{
int count;
XVisualInfo* vis = XGetVisualInfo(_glfw.x11.display,
VisualIDMask, &vi,
&count);
if (vis)
XVisualInfo vi = {0};
// Only consider EGLConfigs with associated Visuals
vi.visualid = getEGLConfigAttrib(n, EGL_NATIVE_VISUAL_ID);
if (!vi.visualid)
continue;
if (desired->transparent)
{
u->transparent = _glfwIsVisualTransparentX11(vis[0].visual);
XFree(vis);
int count;
XVisualInfo* vis =
XGetVisualInfo(_glfw.x11.display, VisualIDMask, &vi, &count);
if (vis)
{
u->transparent = _glfwIsVisualTransparentX11(vis[0].visual);
XFree(vis);
}
}
}
#endif // _GLFW_X11

View File

@@ -6,18 +6,54 @@ import json
import os
import re
import sys
from typing import Callable, List, Optional, Tuple
_plat = sys.platform.lower()
is_linux = 'linux' in _plat
base = os.path.dirname(os.path.abspath(__file__))
def wayland_protocol_file_name(base, ext='c'):
class Env:
cc: str = ''
cppflags: List[str] = []
cflags: List[str] = []
ldflags: List[str] = []
ldpaths: List[str] = []
ccver: Tuple[int, int]
# glfw stuff
all_headers: List[str] = []
sources: List[str] = []
wayland_packagedir: str = ''
wayland_scanner: str = ''
wayland_scanner_code: str = ''
wayland_protocols: Tuple[str, ...] = ()
def __init__(
self, cc: str = '', cppflags: List[str] = [], cflags: List[str] = [], ldflags: List[str] = [],
ldpaths: Optional[List[str]] = None, ccver: Tuple[int, int] = (0, 0)
):
self.cc, self.cppflags, self.cflags, self.ldflags, self.ldpaths = cc, cppflags, cflags, ldflags, [] if ldpaths is None else ldpaths
self.ccver = ccver
def copy(self) -> 'Env':
ans = Env(self.cc, list(self.cppflags), list(self.cflags), list(self.ldflags), list(self.ldpaths), self.ccver)
ans.all_headers = list(self.all_headers)
ans.sources = list(self.sources)
ans.wayland_packagedir = self.wayland_packagedir
ans.wayland_scanner = self.wayland_scanner
ans.wayland_scanner_code = self.wayland_scanner_code
ans.wayland_protocols = self.wayland_protocols
return ans
def wayland_protocol_file_name(base: str, ext: str = 'c') -> str:
base = os.path.basename(base).rpartition('.')[0]
return 'wayland-{}-client-protocol.{}'.format(base, ext)
def init_env(env, pkg_config, at_least_version, test_compile, module='x11'):
def init_env(env: Env, pkg_config: Callable, at_least_version: Callable, test_compile: Callable, module: str = 'x11') -> Env:
ans = env.copy()
ans.cflags.append('-fpic')
ans.cppflags.append('-D_GLFW_' + module.upper())
@@ -46,8 +82,8 @@ def init_env(env, pkg_config, at_least_version, test_compile, module='x11'):
elif module == 'cocoa':
ans.cppflags.append('-DGL_SILENCE_DEPRECATION')
for f in 'Cocoa IOKit CoreFoundation CoreVideo'.split():
ans.ldpaths.extend(('-framework', f))
for f_ in 'Cocoa IOKit CoreFoundation CoreVideo'.split():
ans.ldpaths.extend(('-framework', f_))
elif module == 'wayland':
at_least_version('wayland-protocols', *sinfo['wayland_protocols'])
@@ -74,7 +110,7 @@ def init_env(env, pkg_config, at_least_version, test_compile, module='x11'):
return ans
def build_wayland_protocols(env, Command, parallel_run, emphasis, newer, dest_dir):
def build_wayland_protocols(env: Env, Command: Callable, parallel_run: Callable, emphasis: Callable, newer: Callable, dest_dir: str) -> None:
items = []
for protocol in env.wayland_protocols:
src = os.path.join(env.wayland_packagedir, protocol)
@@ -95,7 +131,7 @@ def build_wayland_protocols(env, Command, parallel_run, emphasis, newer, dest_di
class Arg:
def __init__(self, decl):
def __init__(self, decl: str):
self.type, self.name = decl.rsplit(' ', 1)
self.type = self.type.strip()
self.name = self.name.strip()
@@ -103,13 +139,13 @@ class Arg:
self.name = self.name[1:]
self.type = self.type + '*'
def __repr__(self):
def __repr__(self) -> str:
return 'Arg({}, {})'.format(self.type, self.name)
class Function:
def __init__(self, declaration, check_fail=True):
def __init__(self, declaration: str, check_fail: bool = True):
self.check_fail = check_fail
m = re.match(
r'(.+?)\s+(glfw[A-Z][a-zA-Z0-9]+)[(](.+)[)]$', declaration
@@ -128,14 +164,14 @@ class Function:
if not self.args:
self.args = [Arg('void v')]
def declaration(self):
return 'typedef {restype} (*{name}_func)({args});\n{name}_func {name}_impl;\n#define {name} {name}_impl'.format(
def declaration(self) -> str:
return 'typedef {restype} (*{name}_func)({args});\nGFW_EXTERN {name}_func {name}_impl;\n#define {name} {name}_impl'.format(
restype=self.restype,
name=self.name,
args=', '.join(a.type for a in self.args)
)
def load(self):
def load(self) -> str:
ans = '*(void **) (&{name}_impl) = dlsym(handle, "{name}");'.format(
name=self.name
)
@@ -146,7 +182,7 @@ class Function:
return ans
def generate_wrappers(glfw_header):
def generate_wrappers(glfw_header: str) -> None:
with open(glfw_header) as f:
src = f.read()
functions = []
@@ -165,13 +201,14 @@ def generate_wrappers(glfw_header):
GLFWcocoatextinputfilterfun glfwSetCocoaTextInputFilter(GLFWwindow* window, GLFWcocoatextinputfilterfun callback)
GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *window, GLFWcocoatogglefullscreenfun callback)
GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback)
void glfwGetCocoaKeyEquivalent(int glfw_key, int glfw_mods, void* cocoa_key, void* cocoa_mods)
GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback)
void glfwGetCocoaKeyEquivalent(int glfw_key, int glfw_mods, char* cocoa_key, size_t key_sz, int* cocoa_mods)
void glfwCocoaRequestRenderFrame(GLFWwindow *w, GLFWcocoarenderframefun callback)
void* glfwGetX11Display(void)
int32_t glfwGetX11Window(GLFWwindow* window)
void glfwSetPrimarySelectionString(GLFWwindow* window, const char* string)
const char* glfwGetPrimarySelectionString(GLFWwindow* window, void)
int glfwGetXKBScancode(const char* key_name, int case_sensitive)
int glfwGetNativeKeyForName(const char* key_name, int case_sensitive)
void glfwRequestWaylandFrameEvent(GLFWwindow *handle, unsigned long long id, GLFWwaylandframecallbackfunc callback)
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)
@@ -185,15 +222,26 @@ const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callbac
p = src.find('*/', p)
preamble = src[p + 2:first]
header = '''\
//
// THIS FILE IS GENERATED BY glfw.py
//
// SAVE YOURSELF SOME TIME, DO NOT MANUALLY EDIT
//
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "monotonic.h"
#ifndef GFW_EXTERN
#define GFW_EXTERN extern
#endif
{}
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
typedef int (* GLFWapplicationshouldhandlereopenfun)(int);
typedef int (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
typedef bool (* GLFWapplicationshouldhandlereopenfun)(int);
typedef void (* GLFWapplicationwillfinishlaunchingfun)(void);
typedef bool (* GLFWcocoatogglefullscreenfun)(GLFWwindow*);
typedef void (* GLFWcocoarenderframefun)(GLFWwindow*);
typedef void (*GLFWwaylandframecallbackfunc)(unsigned long long id);
typedef void (*GLFWDBusnotificationcreatedfun)(unsigned long long, uint32_t, void*);
@@ -206,6 +254,7 @@ const char* load_glfw(const char* path);
f.write(header)
code = '''
#define GFW_EXTERN
#include "data-types.h"
#include "glfw-wrapper.h"
#include <dlfcn.h>
@@ -235,7 +284,7 @@ unload_glfw(void) {
f.write(code)
def main():
def main() -> None:
os.chdir(os.path.dirname(os.path.abspath(__file__)))
generate_wrappers('glfw3.h')

747
glfw/glfw3.h vendored

File diff suppressed because it is too large Load Diff

29
glfw/ibus_glfw.c vendored
View File

@@ -107,10 +107,14 @@ get_ibus_text_from_message(DBusMessage *msg) {
}
static inline void
send_text(const char *text, int state) {
send_text(const char *text, int ime_state) {
_GLFWwindow *w = _glfwFocusedWindow();
if (w && w->callbacks.keyboard) {
w->callbacks.keyboard((GLFWwindow*) w, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0, text, state);
GLFWkeyevent fake_ev;
_glfwInitializeKeyEvent(&fake_ev, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0);
fake_ev.text = text;
fake_ev.ime_state = ime_state;
w->callbacks.keyboard((GLFWwindow*) w, &fake_ev);
}
}
@@ -393,31 +397,36 @@ ibus_key_state(unsigned int glfw_modifiers, int action) {
void
key_event_processed(DBusMessage *msg, const char* errmsg, void *data) {
uint32_t handled = 0;
KeyEvent *ev = (KeyEvent*)data;
bool is_release = ev->action == GLFW_RELEASE;
_GLFWIBUSKeyEvent *ev = (_GLFWIBUSKeyEvent*)data;
// Restore key's text from the text embedded in the structure.
ev->glfw_ev.text = ev->__embedded_text;
bool is_release = ev->glfw_ev.action == GLFW_RELEASE;
bool failed = false;
if (errmsg) {
_glfwInputError(GLFW_PLATFORM_ERROR, "IBUS: Failed to process key with error: %s", errmsg);
failed = true;
} else {
glfw_dbus_get_args(msg, "Failed to get IBUS handled key from reply", DBUS_TYPE_BOOLEAN, &handled, DBUS_TYPE_INVALID);
debug("IBUS processed scancode: 0x%x release: %d handled: %u\n", ev->keycode, is_release, handled);
debug("IBUS processed native_key: 0x%x release: %d handled: %u\n", ev->glfw_ev.native_key, is_release, handled);
}
glfw_xkb_key_from_ime(ev, handled ? true : false, failed);
free(ev);
}
bool
ibus_process_key(const KeyEvent *ev_, _GLFWIBUSData *ibus) {
ibus_process_key(const _GLFWIBUSKeyEvent *ev_, _GLFWIBUSData *ibus) {
if (!check_connection(ibus)) return false;
KeyEvent *ev = malloc(sizeof(KeyEvent));
_GLFWIBUSKeyEvent *ev = calloc(1, sizeof(_GLFWIBUSKeyEvent));
if (!ev) return false;
memcpy(ev, ev_, sizeof(KeyEvent));
uint32_t state = ibus_key_state(ev->glfw_modifiers, ev->action);
memcpy(ev, ev_, sizeof(_GLFWIBUSKeyEvent));
// Put the key's text in a field IN the structure, for proper serialization.
if (ev->glfw_ev.text) strncpy(ev->__embedded_text, ev->glfw_ev.text, sizeof(ev->__embedded_text) - 1);
ev->glfw_ev.text = NULL;
uint32_t state = ibus_key_state(ev->glfw_ev.mods, ev->glfw_ev.action);
if (!glfw_dbus_call_method_with_reply(
ibus->conn, IBUS_SERVICE, ibus->input_ctx_path, IBUS_INPUT_INTERFACE, "ProcessKeyEvent",
3000, key_event_processed, ev,
DBUS_TYPE_UINT32, &ev->ibus_sym, DBUS_TYPE_UINT32, &ev->ibus_keycode, DBUS_TYPE_UINT32,
DBUS_TYPE_UINT32, &ev->ibus_keysym, DBUS_TYPE_UINT32, &ev->ibus_keycode, DBUS_TYPE_UINT32,
&state, DBUS_TYPE_INVALID)) {
free(ev);
return false;

15
glfw/ibus_glfw.h vendored
View File

@@ -27,6 +27,7 @@
#pragma once
#include "internal.h"
#include "dbus_glfw.h"
#include <xkbcommon/xkbcommon.h>
@@ -38,18 +39,16 @@ typedef struct {
} _GLFWIBUSData;
typedef struct {
xkb_keycode_t keycode, ibus_keycode;
xkb_keysym_t keysym, ibus_sym;
unsigned int glfw_modifiers;
int action;
xkb_keycode_t ibus_keycode;
xkb_keysym_t ibus_keysym;
GLFWid window_id;
int glfw_keycode;
char text[64];
} KeyEvent;
GLFWkeyevent glfw_ev;
char __embedded_text[64];
} _GLFWIBUSKeyEvent;
void glfw_connect_to_ibus(_GLFWIBUSData *ibus);
void glfw_ibus_terminate(_GLFWIBUSData *ibus);
void glfw_ibus_set_focused(_GLFWIBUSData *ibus, bool focused);
void glfw_ibus_dispatch(_GLFWIBUSData *ibus);
bool ibus_process_key(const KeyEvent *ev_, _GLFWIBUSData *ibus);
bool ibus_process_key(const _GLFWIBUSKeyEvent *ev_, _GLFWIBUSData *ibus);
void glfw_ibus_set_cursor_geometry(_GLFWIBUSData *ibus, int x, int y, int w, int h);

10
glfw/init.c vendored
View File

@@ -201,7 +201,7 @@ _glfwDebug(const char *format, ...) {
{
va_list vl;
fprintf(stderr, "[%.4f] ", glfwGetTime());
fprintf(stderr, "[%.4f] ", monotonic_t_to_s_double(glfwGetTime()));
va_start(vl, format);
vfprintf(stderr, format, vl);
va_end(vl);
@@ -214,10 +214,11 @@ _glfwDebug(const char *format, ...) {
////// GLFW public API //////
//////////////////////////////////////////////////////////////////////////
GLFWAPI int glfwInit(void)
GLFWAPI int glfwInit(monotonic_t start_time)
{
if (_glfw.initialized)
return true;
monotonic_start_time = start_time;
memset(&_glfw, 0, sizeof(_glfw));
_glfw.hints.init = _glfwInitHints;
@@ -239,7 +240,6 @@ GLFWAPI int glfwInit(void)
_glfwPlatformSetTls(&_glfw.errorSlot, &_glfwMainThreadError);
_glfw.initialized = true;
_glfw.timer.offset = _glfwPlatformGetTimerValue();
glfwDefaultWindowHints();
@@ -350,13 +350,13 @@ GLFWAPI void glfwStopMainLoop(void) {
}
GLFWAPI unsigned long long glfwAddTimer(
double interval, bool repeats, GLFWuserdatafun callback,
monotonic_t interval, bool repeats, GLFWuserdatafun callback,
void *callback_data, GLFWuserdatafun free_callback)
{
return _glfwPlatformAddTimer(interval, repeats, callback, callback_data, free_callback);
}
GLFWAPI void glfwUpdateTimer(unsigned long long timer_id, double interval, bool enabled) {
GLFWAPI void glfwUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled) {
_glfwPlatformUpdateTimer(timer_id, interval, enabled);
}

79
glfw/input.c vendored
View File

@@ -28,6 +28,7 @@
//========================================================================
#include "internal.h"
#include "../kitty/monotonic.h"
#include <assert.h>
#include <float.h>
@@ -256,33 +257,44 @@ static bool parseMapping(_GLFWmapping* mapping, const char* string)
////// GLFW event API //////
//////////////////////////////////////////////////////////////////////////
void _glfwInitializeKeyEvent(GLFWkeyevent *ev, int key, int native_key, int action, int mods)
{
ev->key = key;
ev->native_key = native_key;
ev->action = action;
ev->mods = mods;
ev->text = NULL;
ev->ime_state = 0;
}
// Notifies shared code of a physical key event
//
void _glfwInputKeyboard(_GLFWwindow* window, int key, int scancode, int action, int mods, const char* text, int state)
void _glfwInputKeyboard(_GLFWwindow* window, GLFWkeyevent* ev)
{
if (key >= 0 && key <= GLFW_KEY_LAST)
if (ev->key >= 0 && ev->key <= GLFW_KEY_LAST)
{
bool repeated = false;
if (action == GLFW_RELEASE && window->keys[key] == GLFW_RELEASE)
if (ev->action == GLFW_RELEASE && window->keys[ev->key] == GLFW_RELEASE)
return;
if (action == GLFW_PRESS && window->keys[key] == GLFW_PRESS)
if (ev->action == GLFW_PRESS && window->keys[ev->key] == GLFW_PRESS)
repeated = true;
if (action == GLFW_RELEASE && window->stickyKeys)
window->keys[key] = _GLFW_STICK;
if (ev->action == GLFW_RELEASE && window->stickyKeys)
window->keys[ev->key] = _GLFW_STICK;
else
window->keys[key] = (char) action;
window->keys[ev->key] = (char) ev->action;
if (repeated)
action = GLFW_REPEAT;
ev->action = GLFW_REPEAT;
}
// FIXME: will need to update ev->virtual_mods here too?
if (window->callbacks.keyboard) {
if (!window->lockKeyMods) mods &= ~(GLFW_MOD_CAPS_LOCK | GLFW_MOD_NUM_LOCK);
window->callbacks.keyboard((GLFWwindow*) window, key, scancode, action, mods, text, state);
if (!window->lockKeyMods) ev->mods &= ~(GLFW_MOD_CAPS_LOCK | GLFW_MOD_NUM_LOCK);
window->callbacks.keyboard((GLFWwindow*) window, ev);
}
}
@@ -338,10 +350,11 @@ void _glfwInputCursorEnter(_GLFWwindow* window, bool entered)
// Notifies shared code of files or directories dropped on a window
//
void _glfwInputDrop(_GLFWwindow* window, int count, const char** paths)
int _glfwInputDrop(_GLFWwindow* window, const char *mime, const char *text, size_t sz)
{
if (window->callbacks.drop)
window->callbacks.drop((GLFWwindow*) window, count, paths);
return window->callbacks.drop((GLFWwindow*) window, mime, text, sz);
return 0;
}
// Notifies shared code of a joystick connection or disconnection
@@ -498,6 +511,7 @@ const char* _glfwGetKeyName(int key)
case GLFW_KEY_LEFT_BRACKET: return "LEFT_BRACKET";
case GLFW_KEY_BACKSLASH: return "BACKSLASH";
case GLFW_KEY_RIGHT_BRACKET: return "RIGHT_BRACKET";
case GLFW_KEY_CIRCUMFLEX: return "CIRCUMFLEX";
case GLFW_KEY_UNDERSCORE: return "UNDERSCORE";
case GLFW_KEY_GRAVE_ACCENT: return "GRAVE_ACCENT";
case GLFW_KEY_WORLD_1: return "WORLD_1";
@@ -744,7 +758,7 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value)
_glfwInputError(GLFW_INVALID_ENUM, "Invalid input mode 0x%08X", mode);
}
GLFWAPI const char* glfwGetKeyName(int key, int scancode)
GLFWAPI const char* glfwGetKeyName(int key, int native_key)
{
_GLFW_REQUIRE_INIT_OR_RETURN(NULL);
@@ -757,13 +771,13 @@ GLFWAPI const char* glfwGetKeyName(int key, int scancode)
return NULL;
}
scancode = _glfwPlatformGetKeyScancode(key);
native_key = _glfwPlatformGetNativeKeyForKey(key);
}
return _glfwPlatformGetScancodeName(scancode);
return _glfwPlatformGetNativeKeyName(native_key);
}
GLFWAPI int glfwGetKeyScancode(int key)
GLFWAPI int glfwGetNativeKeyForKey(int key)
{
_GLFW_REQUIRE_INIT_OR_RETURN(-1);
@@ -773,7 +787,7 @@ GLFWAPI int glfwGetKeyScancode(int key)
return GLFW_RELEASE;
}
return _glfwPlatformGetKeyScancode(key);
return _glfwPlatformGetNativeKeyForKey(key);
}
GLFWAPI int glfwGetKey(GLFWwindow* handle, int key)
@@ -1466,35 +1480,8 @@ GLFWAPI const char* glfwGetPrimarySelectionString(GLFWwindow* handle UNUSED)
}
#endif
GLFWAPI double glfwGetTime(void)
{
_GLFW_REQUIRE_INIT_OR_RETURN(0.0);
return (double) (_glfwPlatformGetTimerValue() - _glfw.timer.offset) /
_glfwPlatformGetTimerFrequency();
}
GLFWAPI void glfwSetTime(double time)
{
_GLFW_REQUIRE_INIT();
if (time != time || time < 0.0 || time > 18446744073.0)
{
_glfwInputError(GLFW_INVALID_VALUE, "Invalid time %f", time);
return;
}
_glfw.timer.offset = _glfwPlatformGetTimerValue() -
(uint64_t) (time * _glfwPlatformGetTimerFrequency());
}
GLFWAPI uint64_t glfwGetTimerValue(void)
GLFWAPI monotonic_t glfwGetTime(void)
{
_GLFW_REQUIRE_INIT_OR_RETURN(0);
return _glfwPlatformGetTimerValue();
}
GLFWAPI uint64_t glfwGetTimerFrequency(void)
{
_GLFW_REQUIRE_INIT_OR_RETURN(0);
return _glfwPlatformGetTimerFrequency();
return monotonic();
}

32
glfw/internal.h vendored
View File

@@ -27,6 +27,8 @@
#pragma once
#include "../kitty/monotonic.h"
#if defined(_GLFW_USE_CONFIG_H)
#include "glfw_config.h"
#endif
@@ -130,6 +132,7 @@ typedef enum VkStructureType
VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR = 1000006000,
VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR = 1000009000,
VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK = 1000123000,
VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT = 1000217000,
VK_STRUCTURE_TYPE_MAX_ENUM = 0x7FFFFFFF
} VkStructureType;
@@ -416,6 +419,7 @@ struct _GLFWwindow
int minwidth, minheight;
int maxwidth, maxheight;
int numer, denom;
int widthincr, heightincr;
bool stickyKeys;
bool stickyMouseButtons;
@@ -572,12 +576,6 @@ struct _GLFWlibrary
_GLFWtls contextSlot;
_GLFWmutex errorLock;
struct {
uint64_t offset;
// This is defined in the platform's time.h
_GLFW_PLATFORM_LIBRARY_TIMER_STATE;
} timer;
struct {
bool available;
void* handle;
@@ -591,6 +589,7 @@ struct _GLFWlibrary
bool KHR_win32_surface;
#elif defined(_GLFW_COCOA)
bool MVK_macos_surface;
bool EXT_metal_surface;
#elif defined(_GLFW_X11)
bool KHR_xlib_surface;
bool KHR_xcb_surface;
@@ -638,8 +637,8 @@ int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, GLFWCursorShape shape
void _glfwPlatformDestroyCursor(_GLFWcursor* cursor);
void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor);
const char* _glfwPlatformGetScancodeName(int scancode);
int _glfwPlatformGetKeyScancode(int key);
const char* _glfwPlatformGetNativeKeyName(int native_key);
int _glfwPlatformGetNativeKeyForKey(int key);
void _glfwPlatformFreeMonitor(_GLFWmonitor* monitor);
void _glfwPlatformGetMonitorPos(_GLFWmonitor* monitor, int* xpos, int* ypos);
@@ -661,9 +660,6 @@ const char* _glfwPlatformGetPrimarySelectionString(void);
int _glfwPlatformPollJoystick(_GLFWjoystick* js, int mode);
void _glfwPlatformUpdateGamepadGUID(char* guid);
uint64_t _glfwPlatformGetTimerValue(void);
uint64_t _glfwPlatformGetTimerFrequency(void);
int _glfwPlatformCreateWindow(_GLFWwindow* window,
const _GLFWwndconfig* wndconfig,
const _GLFWctxconfig* ctxconfig,
@@ -680,6 +676,7 @@ void _glfwPlatformSetWindowSizeLimits(_GLFWwindow* window,
int minwidth, int minheight,
int maxwidth, int maxheight);
void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer, int denom);
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr, int heightincr);
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height);
void _glfwInputLiveResize(_GLFWwindow* window, bool started);
void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
@@ -687,7 +684,7 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
int* right, int* bottom);
void _glfwPlatformGetWindowContentScale(_GLFWwindow* window,
float* xscale, float* yscale);
double _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window);
monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window);
void _glfwPlatformIconifyWindow(_GLFWwindow* window);
void _glfwPlatformRestoreWindow(_GLFWwindow* window);
void _glfwPlatformMaximizeWindow(_GLFWwindow* window);
@@ -716,7 +713,7 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, int which, int a, int b, int c,
void _glfwPlatformPollEvents(void);
void _glfwPlatformWaitEvents(void);
void _glfwPlatformWaitEventsTimeout(double timeout);
void _glfwPlatformWaitEventsTimeout(monotonic_t timeout);
void _glfwPlatformPostEmptyEvent(void);
void _glfwPlatformGetRequiredInstanceExtensions(char** extensions);
@@ -756,12 +753,13 @@ void _glfwInputWindowDamage(_GLFWwindow* window);
void _glfwInputWindowCloseRequest(_GLFWwindow* window);
void _glfwInputWindowMonitor(_GLFWwindow* window, _GLFWmonitor* monitor);
void _glfwInputKeyboard(_GLFWwindow* window, int key, int scancode, int action, int mods, const char* text, int state);
void _glfwInitializeKeyEvent(GLFWkeyevent *ev, int key, int native_key, int action, int mods);
void _glfwInputKeyboard(_GLFWwindow *window, GLFWkeyevent *ev);
void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset, int flags);
void _glfwInputMouseClick(_GLFWwindow* window, int button, int action, int mods);
void _glfwInputCursorPos(_GLFWwindow* window, double xpos, double ypos);
void _glfwInputCursorEnter(_GLFWwindow* window, bool entered);
void _glfwInputDrop(_GLFWwindow* window, int count, const char** names);
int _glfwInputDrop(_GLFWwindow* window, const char *mime, const char *text, size_t sz);
void _glfwInputJoystick(_GLFWjoystick* js, int event);
void _glfwInputJoystickAxis(_GLFWjoystick* js, int axis, float value);
void _glfwInputJoystickButton(_GLFWjoystick* js, int button, char value);
@@ -823,8 +821,8 @@ _GLFWwindow* _glfwFocusedWindow(void);
_GLFWwindow* _glfwWindowForId(GLFWid id);
void _glfwPlatformRunMainLoop(GLFWtickcallback, void*);
void _glfwPlatformStopMainLoop(void);
unsigned long long _glfwPlatformAddTimer(double interval, bool repeats, GLFWuserdatafun callback, void *callback_data, GLFWuserdatafun free_callback);
void _glfwPlatformUpdateTimer(unsigned long long timer_id, double interval, bool enabled);
unsigned long long _glfwPlatformAddTimer(monotonic_t interval, bool repeats, GLFWuserdatafun callback, void *callback_data, GLFWuserdatafun free_callback);
void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled);
void _glfwPlatformRemoveTimer(unsigned long long timer_id);
char* _glfw_strdup(const char* source);

View File

@@ -222,7 +222,8 @@ static bool openJoystickDevice(const char* path)
}
}
_GLFWjoystick* js = _glfwAllocJoystick(name, guid, axisCount, buttonCount, hatCount);
_GLFWjoystick* js =
_glfwAllocJoystick(name, guid, axisCount, buttonCount, hatCount);
if (!js)
{
close(linjs.fd);

5
glfw/main_loop.h vendored
View File

@@ -7,6 +7,7 @@
#pragma once
#include "internal.h"
#include "../kitty/monotonic.h"
#ifndef GLFW_LOOP_BACKEND
#define GLFW_LOOP_BACKEND x11
@@ -36,7 +37,7 @@ void _glfwPlatformRunMainLoop(GLFWtickcallback tick_callback, void* data) {
EVDBG("main loop exiting");
}
unsigned long long _glfwPlatformAddTimer(double interval, bool repeats, GLFWuserdatafreefun callback, void *callback_data, GLFWuserdatafreefun free_callback) {
unsigned long long _glfwPlatformAddTimer(monotonic_t interval, bool repeats, GLFWuserdatafreefun callback, void *callback_data, GLFWuserdatafreefun free_callback) {
return addTimer(&_glfw.GLFW_LOOP_BACKEND.eventLoopData, "user timer", interval, 1, repeats, callback, callback_data, free_callback);
}
@@ -44,7 +45,7 @@ void _glfwPlatformRemoveTimer(unsigned long long timer_id) {
removeTimer(&_glfw.GLFW_LOOP_BACKEND.eventLoopData, timer_id);
}
void _glfwPlatformUpdateTimer(unsigned long long timer_id, double interval, bool enabled) {
void _glfwPlatformUpdateTimer(unsigned long long timer_id, monotonic_t interval, bool enabled) {
changeTimerInterval(&_glfw.GLFW_LOOP_BACKEND.eventLoopData, timer_id, interval);
toggleTimer(&_glfw.GLFW_LOOP_BACKEND.eventLoopData, timer_id, enabled);
}

10
glfw/monotonic.c vendored Normal file
View File

@@ -0,0 +1,10 @@
/*
* monotonic.c
* Copyright (C) 2019 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#define _POSIX_C_SOURCE 200809L
#define MONOTONIC_IMPLEMENTATION
#include "../kitty/monotonic.h"

4
glfw/nsgl_context.h vendored
View File

@@ -32,8 +32,8 @@
//
typedef struct _GLFWcontextNSGL
{
id pixelFormat;
id object;
id pixelFormat;
id object;
} _GLFWcontextNSGL;

View File

@@ -271,7 +271,7 @@ bool _glfwCreateContextNSGL(_GLFWwindow* window,
return false;
}
NSOpenGLContext* share = NULL;
NSOpenGLContext* share = nil;
if (ctxconfig->share)
share = ctxconfig->share->context.nsgl.object;
@@ -293,8 +293,7 @@ bool _glfwCreateContextNSGL(_GLFWwindow* window,
forParameter:NSOpenGLContextParameterSurfaceOpacity];
}
if (window->ns.retina)
[window->ns.view setWantsBestResolutionOpenGLSurface:YES];
[window->ns.view setWantsBestResolutionOpenGLSurface:window->ns.retina];
GLint interval = 0;
[window->context.nsgl.object setValues:&interval
@@ -325,7 +324,7 @@ GLFWAPI id glfwGetNSGLContext(GLFWwindow* handle)
if (window->context.client == GLFW_NO_API)
{
_glfwInputError(GLFW_NO_WINDOW_CONTEXT, NULL);
return NULL;
return nil;
}
return window->context.nsgl.object;

2
glfw/null_init.c vendored
View File

@@ -36,7 +36,6 @@
int _glfwPlatformInit(void)
{
_glfwInitTimerPOSIX();
return true;
}
@@ -49,4 +48,3 @@ const char* _glfwPlatformGetVersionString(void)
{
return _GLFW_VERSION_NUMBER " null OSMesa";
}

15
glfw/null_window.c vendored
View File

@@ -28,6 +28,7 @@
//========================================================================
#include "internal.h"
#include "../kitty/monotonic.h"
static int createNativeWindow(_GLFWwindow* window,
@@ -127,6 +128,10 @@ void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window UNUSED, int n UNUSED,
{
}
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window UNUSED, int widthincr UNUSED, int heightincr UNUSED)
{
}
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height)
{
if (width)
@@ -150,9 +155,9 @@ void _glfwPlatformGetWindowContentScale(_GLFWwindow* window UNUSED,
*yscale = 1.f;
}
double _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
{
return 0.5;
return ms_to_monotonic_t(500ll);
}
void _glfwPlatformIconifyWindow(_GLFWwindow* window UNUSED)
@@ -257,7 +262,7 @@ void _glfwPlatformWaitEvents(void)
{
}
void _glfwPlatformWaitEventsTimeout(double timeout UNUSED)
void _glfwPlatformWaitEventsTimeout(monotonic_t timeout UNUSED)
{
}
@@ -306,12 +311,12 @@ const char* _glfwPlatformGetClipboardString(void)
return NULL;
}
const char* _glfwPlatformGetScancodeName(int scancode UNUSED)
const char* _glfwPlatformGetNativeKeyName(int native_key UNUSED)
{
return "";
}
int _glfwPlatformGetKeyScancode(int key UNUSED)
int _glfwPlatformGetNativeKeyForKey(int key UNUSED)
{
return -1;
}

87
glfw/posix_time.c vendored
View File

@@ -1,87 +0,0 @@
//========================================================================
// GLFW 3.4 POSIX - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2002-2006 Marcus Geelnard
// Copyright (c) 2006-2017 Camilla Löwy <elmindreda@glfw.org>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would
// be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
// be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
//
//========================================================================
// It is fine to use C99 in this file because it will not be built with VS
//========================================================================
#include "internal.h"
#include <sys/time.h>
#include <time.h>
//////////////////////////////////////////////////////////////////////////
////// GLFW internal API //////
//////////////////////////////////////////////////////////////////////////
// Initialise timer
//
void _glfwInitTimerPOSIX(void)
{
#if defined(CLOCK_MONOTONIC)
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0)
{
_glfw.timer.posix.monotonic = true;
_glfw.timer.posix.frequency = 1000000000;
}
else
#endif
{
_glfw.timer.posix.monotonic = false;
_glfw.timer.posix.frequency = 1000000;
}
}
//////////////////////////////////////////////////////////////////////////
////// GLFW platform API //////
//////////////////////////////////////////////////////////////////////////
uint64_t _glfwPlatformGetTimerValue(void)
{
#if defined(CLOCK_MONOTONIC)
if (_glfw.timer.posix.monotonic)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t) ts.tv_sec * (uint64_t) 1000000000 + (uint64_t) ts.tv_nsec;
}
else
#endif
{
struct timeval tv;
gettimeofday(&tv, NULL);
return (uint64_t) tv.tv_sec * (uint64_t) 1000000 + (uint64_t) tv.tv_usec;
}
}
uint64_t _glfwPlatformGetTimerFrequency(void)
{
return _glfw.timer.posix.frequency;
}

44
glfw/posix_time.h vendored
View File

@@ -1,44 +0,0 @@
//========================================================================
// GLFW 3.4 POSIX - www.glfw.org
//------------------------------------------------------------------------
// Copyright (c) 2002-2006 Marcus Geelnard
// Copyright (c) 2006-2017 Camilla Löwy <elmindreda@glfw.org>
//
// This software is provided 'as-is', without any express or implied
// warranty. In no event will the authors be held liable for any damages
// arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not
// claim that you wrote the original software. If you use this software
// in a product, an acknowledgment in the product documentation would
// be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such, and must not
// be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source
// distribution.
//
//========================================================================
#define _GLFW_PLATFORM_LIBRARY_TIMER_STATE _GLFWtimerPOSIX posix
#include <stdint.h>
// POSIX-specific global timer data
//
typedef struct _GLFWtimerPOSIX
{
bool monotonic;
uint64_t frequency;
} _GLFWtimerPOSIX;
void _glfwInitTimerPOSIX(void);

View File

@@ -13,7 +13,6 @@
"cocoa_joystick.m",
"cocoa_monitor.m",
"cocoa_window.m",
"cocoa_time.c",
"posix_thread.c",
"nsgl_context.m",
"egl_context.c",
@@ -31,6 +30,7 @@
"input.c",
"monitor.c",
"vulkan.c",
"monotonic.c",
"window.c"
]
},
@@ -38,7 +38,6 @@
"headers": [
"null_platform.h",
"null_joystick.h",
"posix_time.h",
"posix_thread.h",
"osmesa_context.h"
],
@@ -47,7 +46,6 @@
"null_monitor.c",
"null_window.c",
"null_joystick.c",
"posix_time.c",
"posix_thread.c",
"osmesa_context.c"
]
@@ -55,7 +53,6 @@
"wayland": {
"headers": [
"wl_platform.h",
"posix_time.h",
"posix_thread.h",
"xkb_glfw.h",
"dbus_glfw.h",
@@ -81,7 +78,6 @@
"wl_init.c",
"wl_monitor.c",
"wl_window.c",
"posix_time.c",
"posix_thread.c",
"xkb_glfw.c",
"dbus_glfw.c",
@@ -125,7 +121,6 @@
"dbus_glfw.h",
"ibus_glfw.h",
"backend_utils.h",
"posix_time.h",
"posix_thread.h",
"glx_context.h",
"egl_context.h",
@@ -142,7 +137,6 @@
"xkb_glfw.c",
"dbus_glfw.c",
"ibus_glfw.c",
"posix_time.c",
"posix_thread.c",
"glx_context.c",
"egl_context.c",

4
glfw/vulkan.c vendored
View File

@@ -57,6 +57,8 @@ bool _glfwInitVulkan(int mode)
_glfw.vk.handle = _glfw_dlopen("vulkan-1.dll");
#elif defined(_GLFW_COCOA)
_glfw.vk.handle = _glfw_dlopen("libvulkan.1.dylib");
if (!_glfw.vk.handle)
_glfw.vk.handle = _glfwLoadLocalVulkanLoaderNS();
#else
_glfw.vk.handle = _glfw_dlopen("libvulkan.so.1");
#endif
@@ -130,6 +132,8 @@ bool _glfwInitVulkan(int mode)
#elif defined(_GLFW_COCOA)
else if (strcmp(ep[i].extensionName, "VK_MVK_macos_surface") == 0)
_glfw.vk.MVK_macos_surface = true;
else if (strcmp(ep[i].extensionName, "VK_EXT_metal_surface") == 0)
_glfw.vk.EXT_metal_surface = true;
#elif defined(_GLFW_X11)
else if (strcmp(ep[i].extensionName, "VK_KHR_xlib_surface") == 0)
_glfw.vk.KHR_xlib_surface = true;

27
glfw/window.c vendored
View File

@@ -29,6 +29,7 @@
//========================================================================
#include "internal.h"
#include "../kitty/monotonic.h"
#include <assert.h>
#include <string.h>
@@ -56,8 +57,10 @@ void _glfwInputWindowFocus(_GLFWwindow* window, bool focused)
{
if (window->keys[key] == GLFW_PRESS)
{
const int scancode = _glfwPlatformGetKeyScancode(key);
_glfwInputKeyboard(window, key, scancode, GLFW_RELEASE, 0, "", 0);
const int native_key = _glfwPlatformGetNativeKeyForKey(key);
GLFWkeyevent ev;
_glfwInitializeKeyEvent(&ev, key, native_key, GLFW_RELEASE, 0);
_glfwInputKeyboard(window, &ev);
}
}
@@ -251,6 +254,8 @@ GLFWAPI GLFWwindow* glfwCreateWindow(int width, int height,
window->maxheight = GLFW_DONT_CARE;
window->numer = GLFW_DONT_CARE;
window->denom = GLFW_DONT_CARE;
window->widthincr = GLFW_DONT_CARE;
window->heightincr = GLFW_DONT_CARE;
// Open the actual window and create its context
if (!_glfwPlatformCreateWindow(window, &wndconfig, &ctxconfig, &fbconfig))
@@ -687,6 +692,20 @@ GLFWAPI void glfwSetWindowAspectRatio(GLFWwindow* handle, int numer, int denom)
_glfwPlatformSetWindowAspectRatio(window, numer, denom);
}
GLFWAPI void glfwSetWindowSizeIncrements(GLFWwindow* handle, int widthincr, int heightincr)
{
_GLFWwindow* window = (_GLFWwindow*) handle;
assert(window != NULL);
assert(widthincr >= 0);
assert(heightincr >= 0);
_GLFW_REQUIRE_INIT();
window->widthincr = widthincr;
window->heightincr = heightincr;
_glfwPlatformSetWindowSizeIncrements(window, window->widthincr, window->heightincr);
}
GLFWAPI void glfwGetFramebufferSize(GLFWwindow* handle, int* width, int* height)
{
_GLFWwindow* window = (_GLFWwindow*) handle;
@@ -736,12 +755,12 @@ GLFWAPI void glfwGetWindowContentScale(GLFWwindow* handle,
_glfwPlatformGetWindowContentScale(window, xscale, yscale);
}
GLFWAPI double glfwGetDoubleClickInterval(GLFWwindow* handle)
GLFWAPI monotonic_t glfwGetDoubleClickInterval(GLFWwindow* handle)
{
_GLFWwindow* window = (_GLFWwindow*) handle;
assert(window != NULL);
_GLFW_REQUIRE_INIT_OR_RETURN(0.5f);
_GLFW_REQUIRE_INIT_OR_RETURN(ms_to_monotonic_t(500ll));
return _glfwPlatformGetDoubleClickInterval(window);
}

60
glfw/wl_init.c vendored
View File

@@ -29,6 +29,7 @@
#define _GNU_SOURCE
#include "internal.h"
#include "backend_utils.h"
#include "../kitty/monotonic.h"
#include <assert.h>
#include <errno.h>
@@ -136,6 +137,7 @@ static void pointerHandleLeave(void* data UNUSED,
_glfw.wl.pointerSerial = serial;
_glfw.wl.pointerFocus = NULL;
_glfwInputCursorEnter(window, false);
_glfw.wl.cursorPreviousShape = GLFW_INVALID_CURSOR;
}
static void setCursor(GLFWCursorShape shape)
@@ -164,6 +166,7 @@ static void setCursor(GLFWCursorShape shape)
wl_surface_damage(surface, 0, 0,
image->width, image->height);
wl_surface_commit(surface);
_glfw.wl.cursorPreviousShape = shape;
}
static void pointerHandleMotion(void* data UNUSED,
@@ -174,43 +177,46 @@ static void pointerHandleMotion(void* data UNUSED,
{
_GLFWwindow* window = _glfw.wl.pointerFocus;
GLFWCursorShape cursorShape = GLFW_ARROW_CURSOR;
double x, y;
if (!window)
return;
if (window->cursorMode == GLFW_CURSOR_DISABLED)
return;
window->wl.cursorPosX = wl_fixed_to_double(sx);
window->wl.cursorPosY = wl_fixed_to_double(sy);
x = wl_fixed_to_double(sx);
y = wl_fixed_to_double(sy);
switch (window->wl.decorations.focus)
{
case mainWindow:
_glfwInputCursorPos(window,
window->wl.cursorPosX, window->wl.cursorPosY);
window->wl.cursorPosX = x;
window->wl.cursorPosY = y;
_glfwInputCursorPos(window, x, y);
_glfw.wl.cursorPreviousShape = GLFW_INVALID_CURSOR;
return;
case topDecoration:
if (window->wl.cursorPosY < _GLFW_DECORATION_WIDTH)
if (y < _GLFW_DECORATION_WIDTH)
cursorShape = GLFW_VRESIZE_CURSOR;
else
cursorShape = GLFW_ARROW_CURSOR;
break;
case leftDecoration:
if (window->wl.cursorPosY < _GLFW_DECORATION_WIDTH)
if (y < _GLFW_DECORATION_WIDTH)
cursorShape = GLFW_NW_RESIZE_CURSOR;
else
cursorShape = GLFW_HRESIZE_CURSOR;
break;
case rightDecoration:
if (window->wl.cursorPosY < _GLFW_DECORATION_WIDTH)
if (y < _GLFW_DECORATION_WIDTH)
cursorShape = GLFW_NE_RESIZE_CURSOR;
else
cursorShape = GLFW_HRESIZE_CURSOR;
break;
case bottomDecoration:
if (window->wl.cursorPosX < _GLFW_DECORATION_WIDTH)
if (x < _GLFW_DECORATION_WIDTH)
cursorShape = GLFW_SW_RESIZE_CURSOR;
else if (window->wl.cursorPosX > window->wl.width + _GLFW_DECORATION_WIDTH)
else if (x > window->wl.width + _GLFW_DECORATION_WIDTH)
cursorShape = GLFW_SE_RESIZE_CURSOR;
else
cursorShape = GLFW_VRESIZE_CURSOR;
@@ -218,7 +224,8 @@ static void pointerHandleMotion(void* data UNUSED,
default:
assert(0);
}
setCursor(cursorShape);
if (_glfw.wl.cursorPreviousShape != cursorShape)
setCursor(cursorShape);
}
static void pointerHandleButton(void* data UNUSED,
@@ -284,8 +291,8 @@ static void pointerHandleButton(void* data UNUSED,
{
xdg_toplevel_show_window_menu(window->wl.xdg.toplevel,
_glfw.wl.seat, serial,
window->wl.cursorPosX,
window->wl.cursorPosY);
(int32_t)window->wl.cursorPosX,
(int32_t)window->wl.cursorPosY);
return;
}
}
@@ -367,7 +374,7 @@ static void keyboardHandleEnter(void* data UNUSED,
struct wl_keyboard* keyboard UNUSED,
uint32_t serial UNUSED,
struct wl_surface* surface,
struct wl_array* keys UNUSED)
struct wl_array* keys)
{
// Happens in the case we just destroyed the surface.
if (!surface)
@@ -383,6 +390,15 @@ static void keyboardHandleEnter(void* data UNUSED,
_glfw.wl.keyboardFocus = window;
_glfwInputWindowFocus(window, true);
uint32_t* key;
if (keys && _glfw.wl.keyRepeatInfo.key) {
wl_array_for_each(key, keys) {
if (*key == _glfw.wl.keyRepeatInfo.key) {
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 1);
break;
}
}
}
}
static void keyboardHandleLeave(void* data UNUSED,
@@ -397,13 +413,14 @@ static void keyboardHandleLeave(void* data UNUSED,
_glfw.wl.keyboardFocus = NULL;
_glfwInputWindowFocus(window, false);
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 0);
}
static void
dispatchPendingKeyRepeats(id_type timer_id UNUSED, void *data UNUSED) {
if (_glfw.wl.keyRepeatInfo.keyboardFocus != _glfw.wl.keyboardFocus || _glfw.wl.keyboardRepeatRate == 0) return;
glfw_xkb_handle_key_event(_glfw.wl.keyRepeatInfo.keyboardFocus, &_glfw.wl.xkb, _glfw.wl.keyRepeatInfo.key, GLFW_REPEAT);
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, (1.0 / (double)_glfw.wl.keyboardRepeatRate));
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, (s_to_monotonic_t(1ll) / (monotonic_t)_glfw.wl.keyboardRepeatRate));
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, 1);
}
@@ -421,6 +438,7 @@ static void keyboardHandleKey(void* data UNUSED,
int action = state == WL_KEYBOARD_KEY_STATE_PRESSED ? GLFW_PRESS : GLFW_RELEASE;
glfw_xkb_handle_key_event(window, &_glfw.wl.xkb, key, action);
bool repeatable = false;
_glfw.wl.keyRepeatInfo.key = 0;
if (action == GLFW_PRESS && _glfw.wl.keyboardRepeatRate > 0 && glfw_xkb_should_repeat(&_glfw.wl.xkb, key))
{
@@ -429,7 +447,7 @@ static void keyboardHandleKey(void* data UNUSED,
_glfw.wl.keyRepeatInfo.keyboardFocus = window;
}
if (repeatable) {
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, (double)(_glfw.wl.keyboardRepeatDelay) / 1000.0);
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, _glfw.wl.keyboardRepeatDelay);
}
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.keyRepeatInfo.keyRepeatTimer, repeatable ? 1 : 0);
}
@@ -454,7 +472,7 @@ static void keyboardHandleRepeatInfo(void* data UNUSED,
return;
_glfw.wl.keyboardRepeatRate = rate;
_glfw.wl.keyboardRepeatDelay = delay;
_glfw.wl.keyboardRepeatDelay = ms_to_monotonic_t(delay);
}
static const struct wl_keyboard_listener keyboardListener = {
@@ -607,8 +625,7 @@ static void registryHandleGlobal(void* data UNUSED,
_glfwSetupWaylandDataDevice();
}
}
else if (strcmp(interface, "zwp_primary_selection_device_manager_v1") == 0 ||
strcmp(interface, "gtk_primary_selection_device_manager") == 0)
else if (strcmp(interface, "zwp_primary_selection_device_manager_v1") == 0)
{
_glfw.wl.primarySelectionDeviceManager =
wl_registry_bind(registry, name,
@@ -728,8 +745,8 @@ int _glfwPlatformInit(void)
"Wayland: Failed to initialize event loop data");
}
glfw_dbus_init(&_glfw.wl.dbus, &_glfw.wl.eventLoopData);
_glfw.wl.keyRepeatInfo.keyRepeatTimer = addTimer(&_glfw.wl.eventLoopData, "wayland-key-repeat", 0.5, 0, true, dispatchPendingKeyRepeats, NULL, NULL);
_glfw.wl.cursorAnimationTimer = addTimer(&_glfw.wl.eventLoopData, "wayland-cursor-animation", 0.5, 0, true, animateCursorImage, NULL, NULL);
_glfw.wl.keyRepeatInfo.keyRepeatTimer = addTimer(&_glfw.wl.eventLoopData, "wayland-key-repeat", ms_to_monotonic_t(500ll), 0, true, dispatchPendingKeyRepeats, NULL, NULL);
_glfw.wl.cursorAnimationTimer = addTimer(&_glfw.wl.eventLoopData, "wayland-cursor-animation", ms_to_monotonic_t(500ll), 0, true, animateCursorImage, NULL, NULL);
_glfw.wl.registry = wl_display_get_registry(_glfw.wl.display);
wl_registry_add_listener(_glfw.wl.registry, &registryListener, NULL);
@@ -749,7 +766,6 @@ int _glfwPlatformInit(void)
}
#endif
_glfwInitTimerPOSIX();
if (!_glfw.wl.wmBase)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
@@ -836,7 +852,7 @@ void _glfwPlatformTerminate(void)
wl_data_source_destroy(_glfw.wl.dataSourceForClipboard);
for (size_t doi=0; doi < arraysz(_glfw.wl.dataOffers); doi++) {
if (_glfw.wl.dataOffers[doi].id) {
wl_data_offer_destroy(_glfw.wl.dataOffers[doi].id);
destroy_data_offer(&_glfw.wl.dataOffers[doi]);
}
}
if (_glfw.wl.dataDevice)

29
glfw/wl_platform.h vendored
View File

@@ -43,7 +43,6 @@ typedef VkResult (APIENTRY *PFN_vkCreateWaylandSurfaceKHR)(VkInstance,const VkWa
typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR)(VkPhysicalDevice,uint32_t,struct wl_display*);
#include "posix_thread.h"
#include "posix_time.h"
#ifdef __linux__
#include "linux_joystick.h"
#else
@@ -156,6 +155,7 @@ typedef struct _GLFWwindowWayland
// We need to track the monitors the window spans on to calculate the
// optimal scaling factor.
int scale;
bool initial_scale_notified;
_GLFWmonitor** monitors;
int monitorsCount;
int monitorsSize;
@@ -195,28 +195,19 @@ typedef enum _GLFWWaylandOfferType
typedef struct _GLFWWaylandDataOffer
{
struct wl_data_offer *id;
const char *mime;
void *id;
_GLFWWaylandOfferType offer_type;
size_t idx;
int is_self_offer;
int has_uri_list;
bool is_self_offer;
bool is_primary;
const char *plain_text_mime, *mime_for_drop;
uint32_t source_actions;
uint32_t dnd_action;
struct wl_surface *surface;
const char **mimes;
size_t mimes_capacity, mimes_count;
} _GLFWWaylandDataOffer;
typedef struct _GLFWWaylandPrimaryOffer
{
struct zwp_primary_selection_offer_v1 *id;
const char *mime;
_GLFWWaylandOfferType offer_type;
size_t idx;
int is_self_offer;
int has_uri_list;
struct wl_surface *surface;
} _GLFWWaylandPrimaryOffer;
// Wayland-specific global data
//
typedef struct _GLFWlibraryWayland
@@ -247,10 +238,11 @@ typedef struct _GLFWlibraryWayland
struct wl_cursor_theme* cursorTheme;
struct wl_surface* cursorSurface;
GLFWCursorShape cursorPreviousShape;
uint32_t pointerSerial;
int32_t keyboardRepeatRate;
int32_t keyboardRepeatDelay;
monotonic_t keyboardRepeatDelay;
struct {
uint32_t key;
id_type keyRepeatTimer;
@@ -286,8 +278,6 @@ typedef struct _GLFWlibraryWayland
size_t dataOffersCounter;
_GLFWWaylandDataOffer dataOffers[8];
char* primarySelectionString;
size_t primarySelectionOffersCounter;
_GLFWWaylandPrimaryOffer primarySelectionOffers[8];
} _GLFWlibraryWayland;
// Wayland-specific per-monitor data
@@ -321,3 +311,4 @@ void _glfwSetupWaylandDataDevice(void);
void _glfwSetupWaylandPrimarySelectionDevice(void);
void animateCursorImage(id_type timer_id, void *data);
struct wl_cursor* _glfwLoadCursor(GLFWCursorShape);
void destroy_data_offer(_GLFWWaylandDataOffer*);

296
glfw/wl_window.c vendored
View File

@@ -32,6 +32,7 @@
#include "backend_utils.h"
#include "memfd.h"
#include "linux_notify.h"
#include "../kitty/monotonic.h"
#include <stdio.h>
#include <stdlib.h>
@@ -41,7 +42,7 @@
#include <sys/mman.h>
#define URI_LIST_MIME "text/uri-list"
static bool checkScaleChange(_GLFWwindow* window)
@@ -54,13 +55,18 @@ static bool checkScaleChange(_GLFWwindow* window)
if (_glfw.wl.compositorVersion < 3)
return false;
// Get the scale factor from the highest scale monitor.
// Get the scale factor from the highest scale monitor that this window is on
for (i = 0; i < window->wl.monitorsCount; ++i)
{
monitorScale = window->wl.monitors[i]->wl.scale;
if (scale < monitorScale)
scale = monitorScale;
}
if (window->wl.monitorsCount < 1 && _glfw.monitorCount > 0) {
// The window has not yet been assigned to any monitors, use the primary monitor
_GLFWmonitor *m = _glfw.monitors[0];
if (m && m->wl.scale > scale) scale = m->wl.scale;
}
// Only change the framebuffer size if the scale changed.
if (scale != window->wl.scale)
@@ -69,6 +75,10 @@ static bool checkScaleChange(_GLFWwindow* window)
wl_surface_set_buffer_scale(window->wl.surface, scale);
return true;
}
if (window->wl.monitorsCount > 0 && !window->wl.initial_scale_notified) {
window->wl.initial_scale_notified = true;
return true;
}
return false;
}
@@ -241,8 +251,8 @@ static struct wl_buffer* createShmBuffer(const GLFWimage* image)
if (fd < 0)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: Creating a buffer file for %d B failed: %%m",
length);
"Wayland: Creating a buffer file for %d B failed: %s",
length, strerror(errno));
return NULL;
}
@@ -250,7 +260,7 @@ static struct wl_buffer* createShmBuffer(const GLFWimage* image)
if (data == MAP_FAILED)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"Wayland: mmap failed: %%m");
"Wayland: mmap failed: %s", strerror(errno));
close(fd);
return NULL;
}
@@ -545,9 +555,9 @@ static void xdgToplevelHandleConfigure(void* data,
aspectRatio = (float)width / (float)height;
targetRatio = (float)window->numer / (float)window->denom;
if (aspectRatio < targetRatio)
height = width / targetRatio;
height = (int32_t)((float)width / targetRatio);
else if (aspectRatio > targetRatio)
width = height * targetRatio;
width = (int32_t)((float)height * targetRatio);
}
}
}
@@ -684,7 +694,7 @@ setCursorImage(_GLFWcursorWayland* cursorWayland)
image = cursorWayland->cursor->images[cursorWayland->currentImage];
buffer = wl_cursor_image_get_buffer(image);
if (image->delay) {
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, ((double)image->delay) / 1000.0);
changeTimerInterval(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, ms_to_monotonic_t(image->delay));
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, 1);
} else {
toggleTimer(&_glfw.wl.eventLoopData, _glfw.wl.cursorAnimationTimer, 0);
@@ -745,24 +755,23 @@ abortOnFatalError(int last_error) {
}
static void
handleEvents(double timeout)
wayland_read_events(int poll_result, int events, void *data UNUSED) {
EVDBG("wayland_read_events poll_result: %d events: %d", poll_result, events);
if (poll_result > 0 && events) wl_display_read_events(_glfw.wl.display);
else wl_display_cancel_read(_glfw.wl.display);
}
static void
handleEvents(monotonic_t timeout)
{
struct wl_display* display = _glfw.wl.display;
errno = 0;
EVDBG("starting handleEvents(%.2f)", timeout);
EVDBG("starting handleEvents(%.2f)", monotonic_t_to_s_double(timeout));
while (wl_display_prepare_read(display) != 0) {
while(1) {
errno = 0;
int num_dispatched = wl_display_dispatch_pending(display);
if (num_dispatched < 0) {
if (errno == EAGAIN) continue;
int last_error = wl_display_get_error(display);
if (last_error) abortOnFatalError(last_error);
wl_display_cancel_read(display);
return;
}
break;
if (wl_display_dispatch_pending(display) == -1) {
abortOnFatalError(errno);
return;
}
}
@@ -772,23 +781,22 @@ handleEvents(double timeout)
errno = 0;
if (wl_display_flush(display) < 0 && errno != EAGAIN)
{
abortOnFatalError(errno);
wl_display_cancel_read(display);
abortOnFatalError(errno);
return;
}
bool display_read_ok = pollForEvents(&_glfw.wl.eventLoopData, timeout);
// we pass in wayland_read_events to ensure that the above wl_display_prepare_read call
// is followed by either wl_display_cancel_read or wl_display_read_events
// before any events/timers are dispatched. This allows other wayland functions
// to be called in the event/timer handlers without causing a deadlock
bool display_read_ok = pollForEvents(&_glfw.wl.eventLoopData, timeout, wayland_read_events);
EVDBG("display_read_ok: %d", display_read_ok);
if (display_read_ok) {
wl_display_read_events(display);
int num = wl_display_dispatch_pending(display);
(void)num;
EVDBG("dispatched %d Wayland events", num);
}
else
{
wl_display_cancel_read(display);
}
glfw_ibus_dispatch(&_glfw.wl.xkb.ibus);
glfw_dbus_session_bus_dispatch();
EVDBG("other dispatch done");
@@ -950,8 +958,13 @@ void _glfwPlatformSetWindowTitle(_GLFWwindow* window, const char* title)
if (window->wl.title)
free(window->wl.title);
window->wl.title = _glfw_strdup(title);
// Wayland cannot handle requests larger than ~8200 bytes. Sending
// one causes an abort(). Since titles this large are meaningless anyway
// ensure they do not happen. One should really truncate ensuring valid UTF-8
// but I cant be bothered.
if (title && strnlen(title, 2048) >= 2048) window->wl.title[2048] = 0;
if (window->wl.xdg.toplevel)
xdg_toplevel_set_title(window->wl.xdg.toplevel, title);
xdg_toplevel_set_title(window->wl.xdg.toplevel, window->wl.title);
}
void _glfwPlatformSetWindowIcon(_GLFWwindow* window UNUSED,
@@ -1018,6 +1031,13 @@ void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window UNUSED,
// The actual limits are checked in the xdg_toplevel::configure handler.
}
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window UNUSED,
int widthincr UNUSED, int heightincr UNUSED)
{
// TODO: find out how to trigger a resize.
// The actual limits are checked in the xdg_toplevel::configure handler.
}
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window,
int* width, int* height)
{
@@ -1052,9 +1072,9 @@ void _glfwPlatformGetWindowContentScale(_GLFWwindow* window,
*yscale = (float) window->wl.scale;
}
double _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
{
return 0.5;
return ms_to_monotonic_t(500ll);
}
void _glfwPlatformIconifyWindow(_GLFWwindow* window)
@@ -1227,11 +1247,11 @@ void _glfwPlatformPollEvents(void)
void _glfwPlatformWaitEvents(void)
{
double timeout = wl_display_dispatch_pending(_glfw.wl.display) > 0 ? 0 : -1;
monotonic_t timeout = wl_display_dispatch_pending(_glfw.wl.display) > 0 ? 0 : -1;
handleEvents(timeout);
}
void _glfwPlatformWaitEventsTimeout(double timeout)
void _glfwPlatformWaitEventsTimeout(monotonic_t timeout)
{
if (wl_display_dispatch_pending(_glfw.wl.display) > 0) timeout = 0;
handleEvents(timeout);
@@ -1268,12 +1288,12 @@ void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode UNUSED)
_glfwPlatformSetCursor(window, window->wl.currentCursor);
}
const char* _glfwPlatformGetScancodeName(int scancode)
const char* _glfwPlatformGetNativeKeyName(int native_key)
{
return glfw_xkb_keysym_name(scancode);
return glfw_xkb_keysym_name(native_key);
}
int _glfwPlatformGetKeyScancode(int key)
int _glfwPlatformGetNativeKeyForKey(int key)
{
return glfw_xkb_sym_for_key(key);
}
@@ -1462,8 +1482,8 @@ static void send_text(char *text, int fd)
{
if (text) {
size_t len = strlen(text), pos = 0;
double start = glfwGetTime();
while (pos < len && glfwGetTime() - start < 2.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;
@@ -1489,14 +1509,15 @@ static void _glfwSendPrimarySelectionText(void *data UNUSED, struct zwp_primary_
send_text(_glfw.wl.primarySelectionString, fd);
}
static char* read_offer_string(int data_pipe) {
static char* read_offer_string(int data_pipe, size_t *data_sz) {
wl_display_flush(_glfw.wl.display);
size_t sz = 0, capacity = 0;
size_t capacity = 0;
char *buf = NULL;
*data_sz = 0;
struct pollfd fds;
fds.fd = data_pipe;
fds.events = POLLIN;
double start = glfwGetTime();
monotonic_t start = glfwGetTime();
#define bail(...) { \
_glfwInputError(GLFW_PLATFORM_ERROR, __VA_ARGS__); \
free(buf); buf = NULL; \
@@ -1504,7 +1525,7 @@ static char* read_offer_string(int data_pipe) {
return NULL; \
}
while (glfwGetTime() - start < 2) {
while (glfwGetTime() - start < s_to_monotonic_t(2ll)) {
int ret = poll(&fds, 1, 2000);
if (ret == -1) {
if (errno == EINTR) continue;
@@ -1513,20 +1534,20 @@ static char* read_offer_string(int data_pipe) {
if (!ret) {
bail("Wayland: Failed to read clipboard data from pipe (timed out)");
}
if (capacity <= sz || capacity - sz <= 64) {
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 + sz, capacity - sz - 1);
ret = read(data_pipe, buf + *data_sz, capacity - *data_sz - 1);
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[sz] = 0; return buf; }
sz += ret;
if (ret == 0) { close(data_pipe); buf[*data_sz] = 0; return buf; }
*data_sz += ret;
start = glfwGetTime();
}
bail("Wayland: Failed to read clipboard data from pipe (timed out)");
@@ -1539,15 +1560,16 @@ static char* read_primary_selection_offer(struct zwp_primary_selection_offer_v1
if (pipe2(pipefd, O_CLOEXEC) != 0) return NULL;
zwp_primary_selection_offer_v1_receive(primary_selection_offer, mime, pipefd[1]);
close(pipefd[1]);
return read_offer_string(pipefd[0]);
size_t sz = 0;
return read_offer_string(pipefd[0], &sz);
}
static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime) {
static char* read_data_offer(struct wl_data_offer *data_offer, const char *mime, size_t *sz) {
int pipefd[2];
if (pipe2(pipefd, O_CLOEXEC) != 0) return NULL;
wl_data_offer_receive(data_offer, mime, pipefd[1]);
close(pipefd[1]);
return read_offer_string(pipefd[0]);
return read_offer_string(pipefd[0], sz);
}
static void data_source_canceled(void *data UNUSED, struct wl_data_source *wl_data_source) {
@@ -1576,20 +1598,23 @@ static const struct zwp_primary_selection_source_v1_listener primary_selection_s
.cancelled = primary_selection_source_canceled,
};
void
destroy_data_offer(_GLFWWaylandDataOffer *offer) {
if (offer->id) {
if (offer->is_primary) zwp_primary_selection_offer_v1_destroy(offer->id);
else wl_data_offer_destroy(offer->id);
}
if (offer->mimes) {
for (size_t i = 0; i < offer->mimes_count; i++) free((char*)offer->mimes[i]);
free(offer->mimes);
}
memset(offer, 0, sizeof(_GLFWWaylandDataOffer));
}
static void prune_unclaimed_data_offers(void) {
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
if (_glfw.wl.dataOffers[i].id && !_glfw.wl.dataOffers[i].offer_type) {
wl_data_offer_destroy(_glfw.wl.dataOffers[i].id);
memset(_glfw.wl.dataOffers + i, 0, sizeof(_glfw.wl.dataOffers[0]));
}
}
}
static void prune_unclaimed_primary_selection_offers(void) {
for (size_t i = 0; i < arraysz(_glfw.wl.primarySelectionOffers); i++) {
if (_glfw.wl.primarySelectionOffers[i].id && !_glfw.wl.dataOffers[i].offer_type) {
zwp_primary_selection_offer_v1_destroy(_glfw.wl.primarySelectionOffers[i].id);
memset(_glfw.wl.primarySelectionOffers + i, 0, sizeof(_glfw.wl.primarySelectionOffers[0]));
destroy_data_offer(&_glfw.wl.dataOffers[i]);
}
}
}
@@ -1608,25 +1633,32 @@ static void mark_selection_offer(void *data UNUSED, struct wl_data_device *data_
static void mark_primary_selection_offer(void *data UNUSED, struct zwp_primary_selection_device_v1* primary_selection_device UNUSED,
struct zwp_primary_selection_offer_v1 *primary_selection_offer) {
for (size_t i = 0; i < arraysz(_glfw.wl.primarySelectionOffers); i++) {
if (_glfw.wl.primarySelectionOffers[i].id == primary_selection_offer) {
_glfw.wl.primarySelectionOffers[i].offer_type = PRIMARY_SELECTION;
} else if (_glfw.wl.primarySelectionOffers[i].offer_type == PRIMARY_SELECTION) {
_glfw.wl.primarySelectionOffers[i].offer_type = EXPIRED; // previous selection offer
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
if (_glfw.wl.dataOffers[i].id == primary_selection_offer) {
_glfw.wl.dataOffers[i].offer_type = PRIMARY_SELECTION;
} else if (_glfw.wl.dataOffers[i].offer_type == PRIMARY_SELECTION) {
_glfw.wl.dataOffers[i].offer_type = EXPIRED; // previous selection offer
}
}
prune_unclaimed_primary_selection_offers();
prune_unclaimed_data_offers();
}
static void set_offer_mimetype(struct _GLFWWaylandDataOffer* offer, const char* mime) {
if (strcmp(mime, "text/plain;charset=utf-8") == 0)
offer->mime = "text/plain;charset=utf-8";
else if (!offer->mime && strcmp(mime, "text/plain") == 0)
offer->mime = "text/plain";
else if (strcmp(mime, clipboard_mime()) == 0)
offer->is_self_offer = 1;
else if (strcmp(mime, URI_LIST_MIME) == 0)
offer->has_uri_list = 1;
static void
set_offer_mimetype(_GLFWWaylandDataOffer* offer, const char* mime) {
if (strcmp(mime, "text/plain;charset=utf-8") == 0) {
offer->plain_text_mime = "text/plain;charset=utf-8";
} else if (!offer->plain_text_mime && strcmp(mime, "text/plain")) {
offer->plain_text_mime = "text/plain";
}
if (strcmp(mime, clipboard_mime()) == 0) {
offer->is_self_offer = true;
}
if (!offer->mimes || offer->mimes_count >= offer->mimes_capacity - 1) {
offer->mimes = realloc(offer->mimes, sizeof(char*) * (offer->mimes_capacity + 64));
if (offer->mimes) offer->mimes_capacity += 64;
else return;
}
offer->mimes[offer->mimes_count++] = _glfw_strdup(mime);
}
static void handle_offer_mimetype(void *data UNUSED, struct wl_data_offer* id, const char *mime) {
@@ -1639,9 +1671,9 @@ static void handle_offer_mimetype(void *data UNUSED, struct wl_data_offer* id, c
}
static void handle_primary_selection_offer_mimetype(void *data UNUSED, struct zwp_primary_selection_offer_v1* id, const char *mime) {
for (size_t i = 0; i < arraysz(_glfw.wl.primarySelectionOffers); i++) {
if (_glfw.wl.primarySelectionOffers[i].id == id) {
set_offer_mimetype((struct _GLFWWaylandDataOffer*)&_glfw.wl.primarySelectionOffers[i], mime);
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
if (_glfw.wl.dataOffers[i].id == id) {
set_offer_mimetype((_GLFWWaylandDataOffer*)&_glfw.wl.dataOffers[i], mime);
break;
}
}
@@ -1676,7 +1708,8 @@ static const struct zwp_primary_selection_offer_v1_listener primary_selection_of
.offer = handle_primary_selection_offer_mimetype,
};
static void handle_data_offer(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, struct wl_data_offer *id) {
static size_t
handle_data_offer_generic(void *id, bool is_primary) {
size_t smallest_idx = SIZE_MAX, pos = 0;
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
if (_glfw.wl.dataOffers[i].idx && _glfw.wl.dataOffers[i].idx < smallest_idx) {
@@ -1684,47 +1717,48 @@ static void handle_data_offer(void *data UNUSED, struct wl_data_device *wl_data_
pos = i;
}
if (_glfw.wl.dataOffers[i].id == NULL) {
_glfw.wl.dataOffers[i].id = id;
_glfw.wl.dataOffers[i].idx = ++_glfw.wl.dataOffersCounter;
pos = i;
goto end;
}
}
if (_glfw.wl.dataOffers[pos].id) wl_data_offer_destroy(_glfw.wl.dataOffers[pos].id);
memset(_glfw.wl.dataOffers + pos, 0, sizeof(_glfw.wl.dataOffers[0]));
_glfw.wl.dataOffers[pos].id = id;
_glfw.wl.dataOffers[pos].idx = ++_glfw.wl.dataOffersCounter;
if (_glfw.wl.dataOffers[pos].id) destroy_data_offer(&_glfw.wl.dataOffers[pos]);
end:
_glfw.wl.dataOffers[pos].id = id;
_glfw.wl.dataOffers[pos].is_primary = is_primary;
_glfw.wl.dataOffers[pos].idx = ++_glfw.wl.dataOffersCounter;
return pos;
}
static void handle_data_offer(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, struct wl_data_offer *id) {
handle_data_offer_generic(id, false);
wl_data_offer_add_listener(id, &data_offer_listener, NULL);
}
static void handle_primary_selection_offer(void *data UNUSED, struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1 UNUSED, struct zwp_primary_selection_offer_v1 *id) {
size_t smallest_idx = SIZE_MAX, pos = 0;
for (size_t i = 0; i < arraysz(_glfw.wl.primarySelectionOffers); i++) {
if (_glfw.wl.primarySelectionOffers[i].idx && _glfw.wl.primarySelectionOffers[i].idx < smallest_idx) {
smallest_idx = _glfw.wl.primarySelectionOffers[i].idx;
pos = i;
}
if (_glfw.wl.primarySelectionOffers[i].id == NULL) {
_glfw.wl.primarySelectionOffers[i].id = id;
_glfw.wl.primarySelectionOffers[i].idx = ++_glfw.wl.primarySelectionOffersCounter;
goto end;
}
}
if (_glfw.wl.primarySelectionOffers[pos].id) zwp_primary_selection_offer_v1_destroy(_glfw.wl.primarySelectionOffers[pos].id);
memset(_glfw.wl.primarySelectionOffers + pos, 0, sizeof(_glfw.wl.primarySelectionOffers[0]));
_glfw.wl.primarySelectionOffers[pos].id = id;
_glfw.wl.primarySelectionOffers[pos].idx = ++_glfw.wl.primarySelectionOffersCounter;
end:
handle_data_offer_generic(id, true);
zwp_primary_selection_offer_v1_add_listener(id, &primary_selection_offer_listener, NULL);
}
static void drag_enter(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED, uint32_t serial, struct wl_surface *surface, wl_fixed_t x UNUSED, wl_fixed_t y UNUSED, struct wl_data_offer *id) {
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
if (_glfw.wl.dataOffers[i].id == id) {
_glfw.wl.dataOffers[i].offer_type = DRAG_AND_DROP;
_glfw.wl.dataOffers[i].surface = surface;
const char *mime = _glfw.wl.dataOffers[i].has_uri_list ? URI_LIST_MIME : NULL;
wl_data_offer_accept(id, serial, mime);
_GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i;
if (d->id == id) {
d->offer_type = DRAG_AND_DROP;
d->surface = surface;
_GLFWwindow* window = _glfw.windowListHead;
int format_priority = 0;
while (window)
{
if (window->wl.surface == surface) {
for (size_t x = 0; x < d->mimes_count; x++) {
int prio = _glfwInputDrop(window, d->mimes[x], NULL, 0);
if (prio > format_priority) d->mime_for_drop = d->mimes[x];
}
break;
}
window = window->next;
}
wl_data_offer_accept(id, serial, d->mime_for_drop);
} else if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP) {
_glfw.wl.dataOffers[i].offer_type = EXPIRED; // previous drag offer
}
@@ -1735,39 +1769,34 @@ static void drag_enter(void *data UNUSED, struct wl_data_device *wl_data_device
static void drag_leave(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED) {
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP) {
wl_data_offer_destroy(_glfw.wl.dataOffers[i].id);
memset(_glfw.wl.dataOffers + i, 0, sizeof(_glfw.wl.dataOffers[0]));
destroy_data_offer(&_glfw.wl.dataOffers[i]);
}
}
}
static void drop(void *data, struct wl_data_device *wl_data_device UNUSED) {
static void drop(void *data UNUSED, struct wl_data_device *wl_data_device UNUSED) {
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP) {
char *uri_list = read_data_offer(_glfw.wl.dataOffers[i].id, URI_LIST_MIME);
if (uri_list) {
wl_data_offer_finish(_glfw.wl.dataOffers[i].id);
int count;
char** paths = parseUriList(data, &count);
if (_glfw.wl.dataOffers[i].offer_type == DRAG_AND_DROP && _glfw.wl.dataOffers[i].mime_for_drop) {
size_t sz = 0;
char *data = read_data_offer(_glfw.wl.dataOffers[i].id, _glfw.wl.dataOffers[i].mime_for_drop, &sz);
if (data) {
// We dont do finish as this requires version 3 for wl_data_device_manager
// which then requires more work with calling set_actions for drag and drop to function
// wl_data_offer_finish(_glfw.wl.dataOffers[i].id);
_GLFWwindow* window = _glfw.windowListHead;
while (window)
{
if (window->wl.surface == _glfw.wl.dataOffers[i].surface) {
_glfwInputDrop(window, count, (const char**) paths);
_glfwInputDrop(window, _glfw.wl.dataOffers[i].mime_for_drop, data, sz);
break;
}
window = window->next;
}
for (int k = 0; k < count; k++)
free(paths[k]);
free(paths);
free(uri_list);
free(data);
}
wl_data_offer_destroy(_glfw.wl.dataOffers[i].id);
memset(_glfw.wl.dataOffers + i, 0, sizeof(_glfw.wl.dataOffers[0]));
destroy_data_offer(&_glfw.wl.dataOffers[i]);
break;
}
}
@@ -1872,10 +1901,12 @@ void _glfwPlatformSetClipboardString(const char* string)
const char* _glfwPlatformGetClipboardString(void)
{
for (size_t i = 0; i < arraysz(_glfw.wl.dataOffers); i++) {
if (_glfw.wl.dataOffers[i].id && _glfw.wl.dataOffers[i].mime && _glfw.wl.dataOffers[i].offer_type == CLIPBOARD) {
if (_glfw.wl.dataOffers[i].is_self_offer) return _glfw.wl.clipboardString;
_GLFWWaylandDataOffer *d = _glfw.wl.dataOffers + i;
if (d->id && d->offer_type == CLIPBOARD && d->plain_text_mime) {
if (d->is_self_offer) return _glfw.wl.clipboardString;
free(_glfw.wl.pasteString);
_glfw.wl.pasteString = read_data_offer(_glfw.wl.dataOffers[i].id, _glfw.wl.dataOffers[i].mime);
size_t sz = 0;
_glfw.wl.pasteString = read_data_offer(d->id, d->plain_text_mime, &sz);
return _glfw.wl.pasteString;
}
}
@@ -1922,13 +1953,14 @@ const char* _glfwPlatformGetPrimarySelectionString(void)
{
if (_glfw.wl.dataSourceForPrimarySelection != NULL) return _glfw.wl.primarySelectionString;
for (size_t i = 0; i < arraysz(_glfw.wl.primarySelectionOffers); i++) {
if (_glfw.wl.primarySelectionOffers[i].id && _glfw.wl.primarySelectionOffers[i].mime && _glfw.wl.primarySelectionOffers[i].offer_type == PRIMARY_SELECTION) {
if (_glfw.wl.primarySelectionOffers[i].is_self_offer) {
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 && d->plain_text_mime) {
if (d->is_self_offer) {
return _glfw.wl.primarySelectionString;
}
free(_glfw.wl.pasteString);
_glfw.wl.pasteString = read_primary_selection_offer(_glfw.wl.primarySelectionOffers[i].id, _glfw.wl.primarySelectionOffers[i].mime);
_glfw.wl.pasteString = read_primary_selection_offer(d->id, d->plain_text_mime);
return _glfw.wl.pasteString;
}
}
@@ -2031,7 +2063,7 @@ GLFWAPI struct wl_surface* glfwGetWaylandWindow(GLFWwindow* handle)
return window->wl.surface;
}
GLFWAPI int glfwGetXKBScancode(const char* keyName, bool caseSensitive) {
GLFWAPI int glfwGetNativeKeyForName(const char* keyName, bool caseSensitive) {
return glfw_xkb_keysym_from_name(keyName, caseSensitive);
}

15
glfw/x11_init.c vendored
View File

@@ -384,7 +384,6 @@ static bool initExtensions(void)
_glfw.x11.XdndFinished = XInternAtom(_glfw.x11.display, "XdndFinished", False);
_glfw.x11.XdndSelection = XInternAtom(_glfw.x11.display, "XdndSelection", False);
_glfw.x11.XdndTypeList = XInternAtom(_glfw.x11.display, "XdndTypeList", False);
_glfw.x11.text_uri_list = XInternAtom(_glfw.x11.display, "text/uri-list", False);
// ICCCM, EWMH and Motif window property atoms
// These can be set safely even without WM support
@@ -433,12 +432,10 @@ static bool initExtensions(void)
//
void _glfwGetSystemContentScaleX11(float* xscale, float* yscale, bool bypass_cache)
{
// NOTE: Default to the display-wide DPI as we don't currently have a policy
// for which monitor a window is considered to be on
float xdpi = DisplayWidth(_glfw.x11.display, _glfw.x11.screen) *
25.4f / DisplayWidthMM(_glfw.x11.display, _glfw.x11.screen);
float ydpi = DisplayHeight(_glfw.x11.display, _glfw.x11.screen) *
25.4f / DisplayHeightMM(_glfw.x11.display, _glfw.x11.screen);
// Start by assuming the default X11 DPI
// NOTE: Some desktop environments (KDE) may remove the Xft.dpi field when it
// would be set to 96, so assume that is the case if we cannot find it
float xdpi = 96.f, ydpi = 96.f;
// NOTE: Basing the scale on Xft.dpi where available should provide the most
// consistent user experience (matches Qt, Gtk, etc), although not
@@ -468,7 +465,7 @@ void _glfwGetSystemContentScaleX11(float* xscale, float* yscale, bool bypass_cac
if (XrmGetResource(db, "Xft.dpi", "Xft.Dpi", &type, &value))
{
if (type && strcmp(type, "String") == 0)
xdpi = ydpi = atof(value.addr);
xdpi = ydpi = (float)atof(value.addr);
}
XrmDestroyDatabase(db);
@@ -637,8 +634,6 @@ int _glfwPlatformInit(void)
}
#endif
_glfwInitTimerPOSIX();
_glfwPollMonitorsX11();
return true;
}

10
glfw/x11_monitor.c vendored
View File

@@ -166,6 +166,16 @@ void _glfwPollMonitorsX11(void)
heightMM = oi->mm_height;
}
if (widthMM <= 0 || heightMM <= 0)
{
// HACK: If RandR does not provide a physical size, assume the
// X11 default 96 DPI and calcuate from the CRTC viewport
// NOTE: These members are affected by rotation, unlike the mode
// info and output info members
widthMM = (int) (ci->width * 25.4f / 96.f);
heightMM = (int) (ci->height * 25.4f / 96.f);
}
_GLFWmonitor* monitor = _glfwAllocMonitor(oi->name, widthMM, heightMM);
monitor->x11.output = sr->outputs[i];
monitor->x11.crtc = oi->crtc;

6
glfw/x11_platform.h vendored
View File

@@ -152,7 +152,6 @@ typedef VkResult (APIENTRY *PFN_vkCreateXcbSurfaceKHR)(VkInstance,const VkXcbSur
typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceXcbPresentationSupportKHR)(VkPhysicalDevice,uint32_t,xcb_connection_t*,xcb_visualid_t);
#include "posix_thread.h"
#include "posix_time.h"
#include "glx_context.h"
#include "egl_context.h"
#include "osmesa_context.h"
@@ -181,6 +180,7 @@ typedef struct _GLFWwindowX11
{
Colormap colormap;
Window handle;
Window parent;
bool iconified;
bool maximized;
@@ -266,7 +266,6 @@ typedef struct _GLFWlibraryX11
Atom XdndFinished;
Atom XdndSelection;
Atom XdndTypeList;
Atom text_uri_list;
// Selection (clipboard) atoms
Atom TARGETS;
@@ -327,7 +326,8 @@ typedef struct _GLFWlibraryX11
struct {
int version;
Window source;
Atom format;
char format[128];
int format_priority;
} xdnd;
struct {

388
glfw/x11_window.c vendored
View File

@@ -31,6 +31,7 @@
#include "internal.h"
#include "backend_utils.h"
#include "linux_notify.h"
#include "../kitty/monotonic.h"
#include <X11/cursorfont.h>
#include <X11/Xmd.h>
@@ -51,6 +52,10 @@
#define Button6 6
#define Button7 7
// Motif WM hints flags
#define MWM_HINTS_DECORATIONS 2
#define MWM_DECOR_ALL 1
#define _GLFW_XDND_VERSION 5
@@ -61,9 +66,9 @@
static unsigned _glfwDispatchX11Events(void);
static void
handleEvents(double timeout) {
EVDBG("starting handleEvents(%.2f)", timeout);
int display_read_ok = pollForEvents(&_glfw.x11.eventLoopData, timeout);
handleEvents(monotonic_t timeout) {
EVDBG("starting handleEvents(%.2f)", monotonic_t_to_s_double(timeout));
int display_read_ok = pollForEvents(&_glfw.x11.eventLoopData, timeout, NULL);
EVDBG("display_read_ok: %d", display_read_ok);
if (display_read_ok) {
unsigned dispatched = _glfwDispatchX11Events();
@@ -77,9 +82,9 @@ handleEvents(double timeout) {
}
static bool
waitForX11Event(double timeout) {
waitForX11Event(monotonic_t timeout) {
// returns true if there is X11 data waiting to be read, does not run watches and timers
double end_time = glfwGetTime() + timeout;
monotonic_t end_time = glfwGetTime() + timeout;
while(true) {
if (timeout >= 0) {
const int result = pollWithTimeout(_glfw.x11.eventLoopData.fds, 1, timeout);
@@ -109,7 +114,7 @@ static bool waitForVisibilityNotify(_GLFWwindow* window)
VisibilityNotify,
&dummy))
{
if (!waitForX11Event(0.1))
if (!waitForX11Event(ms_to_monotonic_t(100ll)))
return false;
}
@@ -250,6 +255,14 @@ static void updateNormalHints(_GLFWwindow* window, int width, int height)
hints->min_aspect.x = hints->max_aspect.x = window->numer;
hints->min_aspect.y = hints->max_aspect.y = window->denom;
}
if (window->widthincr != GLFW_DONT_CARE &&
window->heightincr != GLFW_DONT_CARE)
{
hints->flags |= PResizeInc;
hints->width_inc = window->widthincr;
hints->height_inc = window->heightincr;
}
}
else
{
@@ -508,8 +521,8 @@ static bool createNativeWindow(_GLFWwindow* window,
if (wndconfig->scaleToMonitor)
{
width *= _glfw.x11.contentScaleX;
height *= _glfw.x11.contentScaleY;
width *= (int)_glfw.x11.contentScaleX;
height *= (int)_glfw.x11.contentScaleY;
}
// Create a colormap based on the visual used by the current context
@@ -520,46 +533,41 @@ static bool createNativeWindow(_GLFWwindow* window,
window->x11.transparent = _glfwIsVisualTransparentX11(visual);
// Create the actual window
XSetWindowAttributes wa = { 0 };
wa.colormap = window->x11.colormap;
wa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask |
PointerMotionMask | ButtonPressMask | ButtonReleaseMask |
ExposureMask | FocusChangeMask | VisibilityChangeMask |
EnterWindowMask | LeaveWindowMask | PropertyChangeMask;
_glfwGrabErrorHandlerX11();
window->x11.parent = _glfw.x11.root;
window->x11.handle = XCreateWindow(_glfw.x11.display,
_glfw.x11.root,
0, 0, // Position
width, height,
0, // Border width
depth, // Color depth
InputOutput,
visual,
CWBorderPixel | CWColormap | CWEventMask,
&wa);
_glfwReleaseErrorHandlerX11();
if (!window->x11.handle)
{
XSetWindowAttributes wa;
const unsigned long wamask = CWBorderPixel | CWColormap | CWEventMask;
wa.colormap = window->x11.colormap;
wa.border_pixel = 0;
wa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask |
PointerMotionMask | ButtonPressMask | ButtonReleaseMask |
ExposureMask | FocusChangeMask | VisibilityChangeMask |
EnterWindowMask | LeaveWindowMask | PropertyChangeMask;
_glfwGrabErrorHandlerX11();
window->x11.handle = XCreateWindow(_glfw.x11.display,
_glfw.x11.root,
0, 0,
width, height,
0, // Border width
depth, // Color depth
InputOutput,
visual,
wamask,
&wa);
_glfwReleaseErrorHandlerX11();
if (!window->x11.handle)
{
_glfwInputErrorX11(GLFW_PLATFORM_ERROR,
"X11: Failed to create window");
return false;
}
XSaveContext(_glfw.x11.display,
window->x11.handle,
_glfw.x11.context,
(XPointer) window);
_glfwInputErrorX11(GLFW_PLATFORM_ERROR,
"X11: Failed to create window");
return false;
}
XSaveContext(_glfw.x11.display,
window->x11.handle,
_glfw.x11.context,
(XPointer) window);
if (!wndconfig->decorated)
_glfwPlatformSetWindowDecorated(window, false);
@@ -589,7 +597,7 @@ static bool createNativeWindow(_GLFWwindow* window,
{
XChangeProperty(_glfw.x11.display, window->x11.handle,
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
PropModeReplace, (unsigned char*) &states, count);
PropModeReplace, (unsigned char*) states, count);
}
}
@@ -880,7 +888,7 @@ static const char* getSelectionString(Atom selection)
Atom actualType;
int actualFormat;
unsigned long itemCount, bytesAfter;
double start = glfwGetTime();
monotonic_t start = glfwGetTime();
XEvent notification, dummy;
XConvertSelection(_glfw.x11.display,
@@ -895,10 +903,10 @@ static const char* getSelectionString(Atom selection)
SelectionNotify,
&notification))
{
double time = glfwGetTime();
if (time - start > 2)
monotonic_t time = glfwGetTime();
if (time - start > s_to_monotonic_t(2ll))
return "";
waitForX11Event(2.0 - (time - start));
waitForX11Event(s_to_monotonic_t(2ll) - (time - start));
}
if (notification.xselection.property == None)
@@ -935,10 +943,10 @@ static const char* getSelectionString(Atom selection)
isSelPropNewValueNotify,
(XPointer) &notification))
{
double time = glfwGetTime();
if (time - start > 2)
monotonic_t time = glfwGetTime();
if (time - start > s_to_monotonic_t(2ll))
return "";
waitForX11Event(2.0 - (time - start));
waitForX11Event(s_to_monotonic_t(2ll) - (time - start));
}
XFree(data);
@@ -1067,6 +1075,21 @@ 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)
@@ -1177,6 +1200,12 @@ static void processEvent(XEvent *event)
switch (event->type)
{
case ReparentNotify:
{
window->x11.parent = event->xreparent.parent;
return;
}
case KeyPress:
{
UPDATE_KEYMAP_IF_NEEDED;
@@ -1370,18 +1399,28 @@ static void processEvent(XEvent *event)
window->x11.height = event->xconfigure.height;
}
if (event->xconfigure.x != window->x11.xpos ||
event->xconfigure.y != window->x11.ypos)
{
if (event->xany.send_event)
{
_glfwInputWindowPos(window,
event->xconfigure.x,
event->xconfigure.y);
int xpos = event->xconfigure.x;
int ypos = event->xconfigure.y;
window->x11.xpos = event->xconfigure.x;
window->x11.ypos = event->xconfigure.y;
}
// NOTE: ConfigureNotify events from the server are in local
// coordinates, so if we are reparented we need to translate
// the position into root (screen) coordinates
if (!event->xany.send_event && window->x11.parent != _glfw.x11.root)
{
Window dummy;
XTranslateCoordinates(_glfw.x11.display,
window->x11.parent,
_glfw.x11.root,
xpos, ypos,
&xpos, &ypos,
&dummy);
}
if (xpos != window->x11.xpos || ypos != window->x11.ypos)
{
_glfwInputWindowPos(window, xpos, ypos);
window->x11.xpos = xpos;
window->x11.ypos = ypos;
}
return;
@@ -1430,7 +1469,8 @@ static void processEvent(XEvent *event)
_glfw.x11.xdnd.source = event->xclient.data.l[0];
_glfw.x11.xdnd.version = event->xclient.data.l[1] >> 24;
_glfw.x11.xdnd.format = None;
memset(_glfw.x11.xdnd.format, 0, sizeof(_glfw.x11.xdnd.format));
_glfw.x11.xdnd.format_priority = 0;
if (_glfw.x11.xdnd.version > _GLFW_XDND_VERSION)
return;
@@ -1447,14 +1487,22 @@ static void processEvent(XEvent *event)
count = 3;
formats = (Atom*) event->xclient.data.l + 2;
}
char **atom_names = calloc(count, sizeof(char**));
if (atom_names) {
get_atom_names(formats, count, atom_names);
for (i = 0; i < count; i++)
{
if (formats[i] == _glfw.x11.text_uri_list)
for (i = 0; i < count; i++)
{
_glfw.x11.xdnd.format = _glfw.x11.text_uri_list;
break;
if (atom_names[i]) {
int prio = _glfwInputDrop(window, atom_names[i], NULL, 0);
if (prio > _glfw.x11.xdnd.format_priority) {
_glfw.x11.xdnd.format_priority = prio;
strncpy(_glfw.x11.xdnd.format, atom_names[i], arraysz(_glfw.x11.xdnd.format) - 1);
}
XFree(atom_names[i]);
}
}
free(atom_names);
}
if (list && formats)
@@ -1468,7 +1516,7 @@ static void processEvent(XEvent *event)
if (_glfw.x11.xdnd.version > _GLFW_XDND_VERSION)
return;
if (_glfw.x11.xdnd.format)
if (_glfw.x11.xdnd.format_priority > 0)
{
if (_glfw.x11.xdnd.version >= 1)
time = event->xclient.data.l[2];
@@ -1476,7 +1524,7 @@ static void processEvent(XEvent *event)
// Request the chosen format from the source window
XConvertSelection(_glfw.x11.display,
_glfw.x11.XdndSelection,
_glfw.x11.xdnd.format,
XInternAtom(_glfw.x11.display, _glfw.x11.xdnd.format, 0),
_glfw.x11.XdndSelection,
window->x11.handle,
time);
@@ -1524,7 +1572,7 @@ static void processEvent(XEvent *event)
reply.xclient.data.l[2] = 0; // Specify an empty rectangle
reply.xclient.data.l[3] = 0;
if (_glfw.x11.xdnd.format)
if (_glfw.x11.xdnd.format_priority > 0)
{
// Reply that we are ready to copy the dragged data
reply.xclient.data.l[1] = 1; // Accept with no rectangle
@@ -1554,14 +1602,7 @@ static void processEvent(XEvent *event)
if (result)
{
int i, count;
char** paths = parseUriList(data, &count);
_glfwInputDrop(window, count, (const char**) paths);
for (i = 0; i < count; i++)
free(paths[i]);
free(paths);
_glfwInputDrop(window, _glfw.x11.xdnd.format, data, result);
}
if (data)
@@ -2029,6 +2070,14 @@ void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer UNUSED, in
XFlush(_glfw.x11.display);
}
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr UNUSED, int heightincr UNUSED)
{
int width, height;
_glfwPlatformGetWindowSize(window, &width, &height);
updateNormalHints(window, width, height);
XFlush(_glfw.x11.display);
}
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height)
{
_glfwPlatformGetWindowSize(window, width, height);
@@ -2066,7 +2115,7 @@ void _glfwPlatformGetWindowFrameSize(_GLFWwindow* window,
isFrameExtentsEvent,
(XPointer) window))
{
if (!waitForX11Event(0.5))
if (!waitForX11Event(ms_to_monotonic_t(500ll)))
{
_glfwInputError(GLFW_PLATFORM_ERROR,
"X11: The window manager has a broken _NET_REQUEST_FRAME_EXTENTS implementation; please report this issue");
@@ -2103,9 +2152,9 @@ void _glfwPlatformGetWindowContentScale(_GLFWwindow* window UNUSED,
*yscale = _glfw.x11.contentScaleY;
}
double _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
monotonic_t _glfwPlatformGetDoubleClickInterval(_GLFWwindow* window UNUSED)
{
return 0.5;
return ms_to_monotonic_t(500ll);
}
void _glfwPlatformIconifyWindow(_GLFWwindow* window)
@@ -2141,18 +2190,67 @@ void _glfwPlatformRestoreWindow(_GLFWwindow* window)
void _glfwPlatformMaximizeWindow(_GLFWwindow* window)
{
if (_glfw.x11.NET_WM_STATE &&
_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT &&
_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ)
if (!_glfw.x11.NET_WM_STATE ||
!_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT ||
!_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ)
{
return;
}
if (_glfwPlatformWindowVisible(window))
{
sendEventToWM(window,
_glfw.x11.NET_WM_STATE,
_NET_WM_STATE_ADD,
_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT,
_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ,
1, 0);
XFlush(_glfw.x11.display);
_glfw.x11.NET_WM_STATE,
_NET_WM_STATE_ADD,
_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT,
_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ,
1, 0);
}
else
{
Atom* states = NULL;
unsigned long count =
_glfwGetWindowPropertyX11(window->x11.handle,
_glfw.x11.NET_WM_STATE,
XA_ATOM,
(unsigned char**) &states);
// NOTE: We don't check for failure as this property may not exist yet
// and that's fine (and we'll create it implicitly with append)
Atom missing[2] =
{
_glfw.x11.NET_WM_STATE_MAXIMIZED_VERT,
_glfw.x11.NET_WM_STATE_MAXIMIZED_HORZ
};
unsigned long missingCount = 2;
for (unsigned long i = 0; i < count; i++)
{
for (unsigned long j = 0; j < missingCount; j++)
{
if (states[i] == missing[j])
{
missing[j] = missing[missingCount - 1];
missingCount--;
}
}
}
if (states)
XFree(states);
if (!missingCount)
return;
XChangeProperty(_glfw.x11.display, window->x11.handle,
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
PropModeAppend,
(unsigned char*) missing,
missingCount);
}
XFlush(_glfw.x11.display);
}
void _glfwPlatformShowWindow(_GLFWwindow* window)
@@ -2172,6 +2270,9 @@ void _glfwPlatformHideWindow(_GLFWwindow* window)
void _glfwPlatformRequestWindowAttention(_GLFWwindow* window)
{
if (!_glfw.x11.NET_WM_STATE || !_glfw.x11.NET_WM_STATE_DEMANDS_ATTENTION)
return;
sendEventToWM(window,
_glfw.x11.NET_WM_STATE,
_NET_WM_STATE_ADD,
@@ -2188,7 +2289,7 @@ void _glfwPlatformFocusWindow(_GLFWwindow* window)
{
if (_glfw.x11.NET_ACTIVE_WINDOW)
sendEventToWM(window, _glfw.x11.NET_ACTIVE_WINDOW, 1, 0, 0, 0, 0);
else
else if (_glfwPlatformWindowVisible(window))
{
XRaiseWindow(_glfw.x11.display, window->x11.handle);
XSetInputFocus(_glfw.x11.display, window->x11.handle,
@@ -2350,33 +2451,24 @@ void _glfwPlatformSetWindowResizable(_GLFWwindow* window, bool enabled UNUSED)
void _glfwPlatformSetWindowDecorated(_GLFWwindow* window, bool enabled)
{
if (enabled)
struct
{
XDeleteProperty(_glfw.x11.display,
window->x11.handle,
_glfw.x11.MOTIF_WM_HINTS);
}
else
{
struct
{
unsigned long flags;
unsigned long functions;
unsigned long decorations;
long input_mode;
unsigned long status;
} hints;
unsigned long flags;
unsigned long functions;
unsigned long decorations;
long input_mode;
unsigned long status;
} hints = {0};
hints.flags = 2; // Set decorations
hints.decorations = 0; // No decorations
hints.flags = MWM_HINTS_DECORATIONS;
hints.decorations = enabled ? MWM_DECOR_ALL : 0;
XChangeProperty(_glfw.x11.display, window->x11.handle,
_glfw.x11.MOTIF_WM_HINTS,
_glfw.x11.MOTIF_WM_HINTS, 32,
PropModeReplace,
(unsigned char*) &hints,
sizeof(hints) / sizeof(long));
}
XChangeProperty(_glfw.x11.display, window->x11.handle,
_glfw.x11.MOTIF_WM_HINTS,
_glfw.x11.MOTIF_WM_HINTS, 32,
PropModeReplace,
(unsigned char*) &hints,
sizeof(hints) / sizeof(long));
}
void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
@@ -2386,7 +2478,7 @@ void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
if (_glfwPlatformWindowVisible(window))
{
const Atom action = enabled ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE;
const long action = enabled ? _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE;
sendEventToWM(window,
_glfw.x11.NET_WM_STATE,
action,
@@ -2395,15 +2487,16 @@ void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
}
else
{
Atom* states;
Atom* states = NULL;
unsigned long i, count;
count = _glfwGetWindowPropertyX11(window->x11.handle,
_glfw.x11.NET_WM_STATE,
XA_ATOM,
(unsigned char**) &states);
if (!states)
return;
// NOTE: We don't check for failure as this property may not exist yet
// and that's fine (and we'll create it implicitly with append)
if (enabled)
{
@@ -2413,32 +2506,36 @@ void _glfwPlatformSetWindowFloating(_GLFWwindow* window, bool enabled)
break;
}
if (i == count)
{
XChangeProperty(_glfw.x11.display, window->x11.handle,
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
PropModeAppend,
(unsigned char*) &_glfw.x11.NET_WM_STATE_ABOVE,
1);
}
if (i < count)
return;
XChangeProperty(_glfw.x11.display, window->x11.handle,
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
PropModeAppend,
(unsigned char*) &_glfw.x11.NET_WM_STATE_ABOVE,
1);
}
else
else if (states)
{
for (i = 0; i < count; i++)
{
if (states[i] == _glfw.x11.NET_WM_STATE_ABOVE)
{
states[i] = states[count - 1];
count--;
}
break;
}
if (i == count)
return;
states[i] = states[count - 1];
count--;
XChangeProperty(_glfw.x11.display, window->x11.handle,
_glfw.x11.NET_WM_STATE, XA_ATOM, 32,
PropModeReplace, (unsigned char*) &states, count);
PropModeReplace, (unsigned char*) states, count);
}
XFree(states);
if (states)
XFree(states);
}
XFlush(_glfw.x11.display);
@@ -2527,11 +2624,11 @@ void _glfwPlatformPollEvents(void)
void _glfwPlatformWaitEvents(void)
{
double timeout = _glfwDispatchX11Events() ? 0 : -1;
monotonic_t timeout = _glfwDispatchX11Events() ? 0 : -1;
handleEvents(timeout);
}
void _glfwPlatformWaitEventsTimeout(double timeout)
void _glfwPlatformWaitEventsTimeout(monotonic_t timeout)
{
if (_glfwDispatchX11Events()) timeout = 0;
handleEvents(timeout);
@@ -2585,13 +2682,13 @@ void _glfwPlatformSetCursorMode(_GLFWwindow* window, int mode)
XFlush(_glfw.x11.display);
}
const char* _glfwPlatformGetScancodeName(int scancode)
const char* _glfwPlatformGetNativeKeyName(int native_key)
{
return glfw_xkb_keysym_name(scancode);
return glfw_xkb_keysym_name(native_key);
}
int _glfwPlatformGetKeyScancode(int key)
int _glfwPlatformGetNativeKeyForKey(int key)
{
return glfw_xkb_sym_for_key(key);
}
@@ -2612,7 +2709,7 @@ int _glfwPlatformCreateStandardCursor(_GLFWcursor* cursor, GLFWCursorShape shape
int native = 0;
#define C(name, val) case name: native = val; break;
switch(shape) {
C(GLFW_ARROW_CURSOR, XC_arrow);
C(GLFW_ARROW_CURSOR, XC_left_ptr);
C(GLFW_IBEAM_CURSOR, XC_xterm);
C(GLFW_CROSSHAIR_CURSOR, XC_crosshair);
C(GLFW_HAND_CURSOR, XC_hand2);
@@ -2655,8 +2752,9 @@ void _glfwPlatformSetCursor(_GLFWwindow* window, _GLFWcursor* cursor UNUSED)
void _glfwPlatformSetClipboardString(const char* string)
{
char* copy = _glfw_strdup(string);
free(_glfw.x11.clipboardString);
_glfw.x11.clipboardString = _glfw_strdup(string);
_glfw.x11.clipboardString = copy;
XSetSelectionOwner(_glfw.x11.display,
_glfw.x11.CLIPBOARD,
@@ -2870,7 +2968,7 @@ GLFWAPI Window glfwGetX11Window(GLFWwindow* handle)
return window->x11.handle;
}
GLFWAPI int glfwGetXKBScancode(const char* keyName, bool caseSensitive) {
GLFWAPI int glfwGetNativeKeyForName(const char* keyName, bool caseSensitive) {
return glfw_xkb_keysym_from_name(keyName, caseSensitive);
}

123
glfw/xkb_glfw.c vendored
View File

@@ -61,6 +61,7 @@ START_ALLOW_CASE_RANGE
S(bracketleft, LEFT_BRACKET); \
S(backslash, BACKSLASH); \
S(bracketright, RIGHT_BRACKET); \
S(asciicircum, CIRCUMFLEX); \
S(underscore, UNDERSCORE); \
S(grave, GRAVE_ACCENT); \
R(a, z, A, Z); \
@@ -437,24 +438,22 @@ glfw_xkb_update_modifiers(_GLFWXKBData *xkb, xkb_mod_mask_t depressed, xkb_mod_m
}
bool
glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t scancode) {
glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t keycode) {
#ifdef _GLFW_WAYLAND
scancode += 8;
keycode += 8;
#endif
return xkb_keymap_key_repeats(xkb->keymap, scancode);
return xkb_keymap_key_repeats(xkb->keymap, keycode);
}
static KeyEvent key_event = {0};
static inline xkb_keysym_t
compose_symbol(struct xkb_compose_state *composeState, xkb_keysym_t sym, int *compose_completed) {
compose_symbol(struct xkb_compose_state *composeState, xkb_keysym_t sym, int *compose_completed, char *key_text, int n) {
*compose_completed = 0;
if (sym == XKB_KEY_NoSymbol || !composeState) return sym;
if (xkb_compose_state_feed(composeState, sym) != XKB_COMPOSE_FEED_ACCEPTED) return sym;
switch (xkb_compose_state_get_status(composeState)) {
case XKB_COMPOSE_COMPOSED:
xkb_compose_state_get_utf8(composeState, key_event.text, sizeof(key_event.text));
xkb_compose_state_get_utf8(composeState, key_text, n);
*compose_completed = 1;
return xkb_compose_state_get_one_sym(composeState);
case XKB_COMPOSE_COMPOSING:
@@ -534,11 +533,14 @@ glfw_xkb_update_ime_state(_GLFWwindow *w, _GLFWXKBData *xkb, int which, int a, i
}
void
glfw_xkb_key_from_ime(KeyEvent *ev, bool handled_by_ime, bool failed) {
glfw_xkb_key_from_ime(_GLFWIBUSKeyEvent *ev, bool handled_by_ime, bool failed) {
_GLFWwindow *window = _glfwWindowForId(ev->window_id);
if (failed && window && window->callbacks.keyboard) {
// notify application to remove any existing pre-edit text
window->callbacks.keyboard((GLFWwindow*) window, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0, "", 1);
GLFWkeyevent fake_ev;
_glfwInitializeKeyEvent(&fake_ev, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0);
fake_ev.ime_state = 1;
window->callbacks.keyboard((GLFWwindow*) window, &fake_ev);
}
static xkb_keycode_t last_handled_press_keycode = 0;
// We filter out release events that correspond to the last press event
@@ -547,53 +549,58 @@ glfw_xkb_key_from_ime(KeyEvent *ev, bool handled_by_ime, bool failed) {
// you'd need to implement a ring buffer to store pending key presses.
xkb_keycode_t prev_handled_press = last_handled_press_keycode;
last_handled_press_keycode = 0;
bool is_release = ev->action == GLFW_RELEASE;
debug("From IBUS: scancode: 0x%x name: %s is_release: %d\n", ev->keycode, glfw_xkb_keysym_name(ev->keysym), is_release);
if (window && !handled_by_ime && !(is_release && ev->keycode == prev_handled_press)) {
bool is_release = ev->glfw_ev.action == GLFW_RELEASE;
debug("From IBUS: native_key: 0x%x name: %s is_release: %d\n", ev->glfw_ev.native_key, glfw_xkb_keysym_name(ev->glfw_ev.key), is_release);
if (window && !handled_by_ime && !(is_release && ev->glfw_ev.native_key == (int) prev_handled_press)) {
debug("↳ to application: glfw_keycode: 0x%x (%s) keysym: 0x%x (%s) action: %s %s text: %s\n",
ev->glfw_keycode, _glfwGetKeyName(ev->glfw_keycode), ev->keysym, glfw_xkb_keysym_name(ev->keysym),
(ev->action == GLFW_RELEASE ? "RELEASE" : (ev->action == GLFW_PRESS ? "PRESS" : "REPEAT")),
format_mods(ev->glfw_modifiers), ev->text
ev->glfw_ev.native_key, _glfwGetKeyName(ev->glfw_ev.native_key), ev->glfw_ev.key, glfw_xkb_keysym_name(ev->glfw_ev.key),
(ev->glfw_ev.action == GLFW_RELEASE ? "RELEASE" : (ev->glfw_ev.action == GLFW_PRESS ? "PRESS" : "REPEAT")),
format_mods(ev->glfw_ev.mods), ev->glfw_ev.text
);
_glfwInputKeyboard(window, ev->glfw_keycode, ev->keysym, ev->action, ev->glfw_modifiers, ev->text, 0);
ev->glfw_ev.ime_state = 0;
_glfwInputKeyboard(window, &ev->glfw_ev);
} else debug("↳ discarded\n");
if (!is_release && handled_by_ime) last_handled_press_keycode = ev->keycode;
if (!is_release && handled_by_ime)
last_handled_press_keycode = ev->glfw_ev.native_key;
}
void
glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t scancode, int action) {
glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t xkb_keycode, int action) {
static char key_text[64] = {0};
const xkb_keysym_t *syms, *clean_syms, *default_syms;
xkb_keysym_t glfw_sym;
xkb_keycode_t code_for_sym = scancode;
key_event.ibus_keycode = scancode;
xkb_keysym_t xkb_sym;
xkb_keycode_t code_for_sym = xkb_keycode, ibus_keycode = xkb_keycode;
GLFWkeyevent glfw_ev;
_glfwInitializeKeyEvent(&glfw_ev, GLFW_KEY_UNKNOWN, 0, GLFW_PRESS, 0); // init with default values
#ifdef _GLFW_WAYLAND
code_for_sym += 8;
#else
key_event.ibus_keycode -= 8;
ibus_keycode -= 8;
#endif
debug("%s scancode: 0x%x ", action == GLFW_RELEASE ? "Release" : "Press", scancode);
debug("%s xkb_keycode: 0x%x ", action == GLFW_RELEASE ? "Release" : "Press", xkb_keycode);
XKBStateGroup *sg = &xkb->states;
int num_syms = xkb_state_key_get_syms(sg->state, code_for_sym, &syms);
int num_clean_syms = xkb_state_key_get_syms(sg->clean_state, code_for_sym, &clean_syms);
key_event.text[0] = 0;
key_text[0] = 0;
// According to the documentation of xkb_compose_state_feed it does not
// support multi-sym events, so we ignore them
if (num_syms != 1 || num_clean_syms != 1) {
debug("num_syms: %d num_clean_syms: %d ignoring event\n", num_syms, num_clean_syms);
return;
}
glfw_sym = clean_syms[0];
xkb_sym = clean_syms[0];
debug("clean_sym: %s ", glfw_xkb_keysym_name(clean_syms[0]));
if (action == GLFW_PRESS || action == GLFW_REPEAT) {
const char *text_type = "composed_text";
int compose_completed;
glfw_sym = compose_symbol(sg->composeState, syms[0], &compose_completed);
if (glfw_sym == XKB_KEY_NoSymbol && !compose_completed) {
xkb_sym = compose_symbol(sg->composeState, syms[0], &compose_completed, key_text, sizeof(key_text));
if (xkb_sym == XKB_KEY_NoSymbol && !compose_completed) {
debug("compose not complete, ignoring.\n");
return;
}
debug("composed_sym: %s ", glfw_xkb_keysym_name(glfw_sym));
if (glfw_sym == syms[0]) { // composed sym is the same as non-composed sym
debug("composed_sym: %s ", glfw_xkb_keysym_name(xkb_sym));
if (xkb_sym == syms[0]) { // composed sym is the same as non-composed sym
// Only use the clean_sym if no mods other than the mods we report
// are active (for example if ISO_Shift_Level_* mods are active
// they are not reported by GLFW so the key should be the shifted
@@ -601,37 +608,61 @@ glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t
xkb_mod_mask_t consumed_unknown_mods = xkb_state_key_get_consumed_mods(sg->state, code_for_sym) & sg->activeUnknownModifiers;
if (sg->activeUnknownModifiers) debug("%s", format_xkb_mods(xkb, "active_unknown_mods", sg->activeUnknownModifiers));
if (consumed_unknown_mods) { debug("%s", format_xkb_mods(xkb, "consumed_unknown_mods", consumed_unknown_mods)); }
else glfw_sym = clean_syms[0];
else xkb_sym = clean_syms[0];
// xkb returns text even if alt and/or super are pressed
if ( ((GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SUPER) & sg->modifiers) == 0) xkb_state_key_get_utf8(sg->state, code_for_sym, key_event.text, sizeof(key_event.text));
if ( ((GLFW_MOD_CONTROL | GLFW_MOD_ALT | GLFW_MOD_SUPER) & sg->modifiers) == 0) {
xkb_state_key_get_utf8(sg->state, code_for_sym, key_text, sizeof(key_text));
}
text_type = "text";
}
if ((1 <= key_event.text[0] && key_event.text[0] <= 31) || key_event.text[0] == 127) key_event.text[0] = 0; // don't send text for ascii control codes
if (key_event.text[0]) { debug("%s: %s ", text_type, key_event.text); }
if ((1 <= key_text[0] && key_text[0] <= 31) || key_text[0] == 127) {
key_text[0] = 0; // don't send text for ascii control codes
}
if (key_text[0]) { debug("%s: %s ", text_type, key_text); }
}
int glfw_keycode = glfw_key_for_sym(glfw_sym);
if (xkb_sym == XKB_KEY_ISO_First_Group || xkb_sym == XKB_KEY_ISO_Last_Group || xkb_sym == XKB_KEY_ISO_Next_Group || xkb_sym == XKB_KEY_ISO_Prev_Group || xkb_sym == XKB_KEY_Mode_switch) {
return;
}
int glfw_sym = glfw_key_for_sym(xkb_sym);
bool is_fallback = false;
if (glfw_keycode == GLFW_KEY_UNKNOWN && !key_event.text[0]) {
if (glfw_sym == GLFW_KEY_UNKNOWN && !key_text[0]) {
int num_default_syms = xkb_state_key_get_syms(sg->default_state, code_for_sym, &default_syms);
if (num_default_syms > 0) {
glfw_sym = default_syms[0];
glfw_keycode = glfw_key_for_sym(glfw_sym);
xkb_sym = default_syms[0];
glfw_sym = glfw_key_for_sym(xkb_sym);
is_fallback = true;
}
}
debug(
"%s%s: %d (%s) xkb_key: %d (%s)\n",
format_mods(sg->modifiers),
is_fallback ? "glfw_fallback_key" : "glfw_key", glfw_keycode, _glfwGetKeyName(glfw_keycode),
glfw_sym, glfw_xkb_keysym_name(glfw_sym)
is_fallback ? "glfw_fallback_key" : "glfw_key", glfw_sym, _glfwGetKeyName(glfw_sym),
xkb_sym, glfw_xkb_keysym_name(xkb_sym)
);
key_event.action = action; key_event.glfw_modifiers = sg->modifiers;
key_event.keycode = scancode; key_event.keysym = glfw_sym;
key_event.window_id = window->id; key_event.glfw_keycode = glfw_keycode;
key_event.ibus_sym = syms[0];
if (ibus_process_key(&key_event, &xkb->ibus)) {
debug("↳ to IBUS: keycode: 0x%x keysym: 0x%x (%s) %s\n", key_event.ibus_keycode, key_event.ibus_sym, glfw_xkb_keysym_name(key_event.ibus_sym), format_mods(key_event.glfw_modifiers));
// NOTE: On linux, the reported native key identifier is the XKB keysym value.
// Do not confuse `native_key` with `xkb_keycode` (the native keycode reported for the
// glfw event VS the X internal code for a key).
//
// We use the XKB keysym instead of the X keycode to be able to go back-and-forth between
// the GLFW keysym and the XKB keysym when needed, which is not possible using the X keycode,
// because of the lost information when resolving the keycode to the keysym, like consumed
// mods.
glfw_ev.native_key = xkb_sym;
glfw_ev.action = action;
glfw_ev.key = glfw_sym;
glfw_ev.mods = sg->modifiers;
glfw_ev.text = key_text;
_GLFWIBUSKeyEvent ibus_ev;
ibus_ev.glfw_ev = glfw_ev;
ibus_ev.ibus_keycode = ibus_keycode;
ibus_ev.window_id = window->id;
ibus_ev.ibus_keysym = syms[0];
if (ibus_process_key(&ibus_ev, &xkb->ibus)) {
debug("↳ to IBUS: keycode: 0x%x keysym: 0x%x (%s) %s\n", ibus_ev.ibus_keycode, ibus_ev.ibus_keysym, glfw_xkb_keysym_name(ibus_ev.ibus_keysym), format_mods(ibus_ev.glfw_ev.mods));
} else {
_glfwInputKeyboard(window, glfw_keycode, glfw_sym, action, sg->modifiers, key_event.text, 0);
_glfwInputKeyboard(window, &glfw_ev);
}
}

6
glfw/xkb_glfw.h vendored
View File

@@ -87,10 +87,10 @@ void glfw_xkb_release(_GLFWXKBData *xkb);
bool glfw_xkb_create_context(_GLFWXKBData *xkb);
bool glfw_xkb_compile_keymap(_GLFWXKBData *xkb, const char *map_str);
void glfw_xkb_update_modifiers(_GLFWXKBData *xkb, xkb_mod_mask_t depressed, xkb_mod_mask_t latched, xkb_mod_mask_t locked, xkb_layout_index_t base_group, xkb_layout_index_t latched_group, xkb_layout_index_t locked_group);
bool glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t scancode);
bool glfw_xkb_should_repeat(_GLFWXKBData *xkb, xkb_keycode_t keycode);
const char* glfw_xkb_keysym_name(xkb_keysym_t sym);
xkb_keysym_t glfw_xkb_sym_for_key(int key);
void glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t scancode, int action);
void glfw_xkb_handle_key_event(_GLFWwindow *window, _GLFWXKBData *xkb, xkb_keycode_t keycode, int action);
int glfw_xkb_keysym_from_name(const char *name, bool case_sensitive);
void glfw_xkb_update_ime_state(_GLFWwindow *w, _GLFWXKBData *xkb, int which, int a, int b, int c, int d);
void glfw_xkb_key_from_ime(KeyEvent *ev, bool handled_by_ime, bool failed);
void glfw_xkb_key_from_ime(_GLFWIBUSKeyEvent *ev, bool handled_by_ime, bool failed);

View File

@@ -4,27 +4,34 @@
import os
from contextlib import suppress
from typing import TYPE_CHECKING, List, Optional, Tuple
from kitty.cli import parse_args
from kitty.cli_stub import AskCLIOptions
from kitty.constants import cache_dir
from kitty.typing import BossType
from ..tui.handler import result_handler
from ..tui.operations import alternate_screen, styled
readline = None
if TYPE_CHECKING:
import readline
else:
readline = None
def get_history_items():
def get_history_items() -> List[str]:
return list(map(readline.get_history_item, range(1, readline.get_current_history_length() + 1)))
def sort_key(item):
def sort_key(item: str) -> Tuple[int, str]:
return len(item), item.lower()
class HistoryCompleter:
def __init__(self, name=None):
self.matches = []
def __init__(self, name: Optional[str] = None):
self.matches: List[str] = []
self.history_path = None
if name:
ddir = os.path.join(cache_dir(), 'ask')
@@ -32,7 +39,7 @@ class HistoryCompleter:
os.makedirs(ddir)
self.history_path = os.path.join(ddir, name)
def complete(self, text, state):
def complete(self, text: str, state: int) -> Optional[str]:
response = None
if state == 0:
history_values = get_history_items()
@@ -47,19 +54,19 @@ class HistoryCompleter:
response = None
return response
def __enter__(self):
def __enter__(self) -> 'HistoryCompleter':
if self.history_path:
with suppress(Exception):
readline.read_history_file(self.history_path)
readline.set_completer(self.complete)
return self
def __exit__(self, *a):
def __exit__(self, *a: object) -> None:
if self.history_path:
readline.write_history_file(self.history_path)
def option_text():
def option_text() -> str:
return '''\
--type -t
choices=line
@@ -78,7 +85,18 @@ be used for completions and via the browse history readline bindings.
'''
def main(args):
try:
from typing import TypedDict
except ImportError:
TypedDict = dict
class Response(TypedDict):
items: List[str]
response: Optional[str]
def main(args: List[str]) -> Response:
# For some reason importing readline in a key handler in the main kitty process
# causes a crash of the python interpreter, probably because of some global
# lock
@@ -88,7 +106,7 @@ def main(args):
from kitty.shell import init_readline
msg = 'Ask the user for input'
try:
args, items = parse_args(args[1:], option_text, '', msg, 'kitty ask')
cli_opts, items = parse_args(args[1:], option_text, '', msg, 'kitty ask', result_class=AskCLIOptions)
except SystemExit as e:
if e.code != 0:
print(e.args[0])
@@ -96,20 +114,21 @@ def main(args):
raise SystemExit(e.code)
init_readline(readline)
ans = {'items': items}
response = None
with alternate_screen(), HistoryCompleter(args.name):
if args.message:
print(styled(args.message, bold=True))
with alternate_screen(), HistoryCompleter(cli_opts.name):
if cli_opts.message:
print(styled(cli_opts.message, bold=True))
prompt = '> '
with suppress(KeyboardInterrupt, EOFError):
ans['response'] = input(prompt)
return ans
response = input(prompt)
return {'items': items, 'response': response}
def handle_result(args, data, target_window_id, boss):
if 'response' in data:
@result_handler()
def handle_result(args: List[str], data: Response, target_window_id: int, boss: BossType) -> None:
if data['response'] is not None:
func, *args = data['items']
getattr(boss, func)(data['response'], *args)

View File

@@ -3,39 +3,40 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from typing import Iterable, List, Union
from kitty.key_encoding import KeyEvent
from . import subseq_matcher
from ..tui.handler import Handler
from ..tui.loop import Loop
from . import subseq_matcher
def match(
input_data,
query,
threads=0,
positions=False,
level1='/',
level2='-_0123456789',
level3='.',
limit=0,
mark_before='',
mark_after='',
delimiter='\n'
):
input_data: Union[str, bytes, Iterable[Union[str, bytes]]],
query: str,
threads: int = 0,
positions: bool = False,
level1: str = '/',
level2: str = '-_0123456789',
level3: str = '.',
limit: int = 0,
mark_before: str = '',
mark_after: str = '',
delimiter: str = '\n'
) -> List[str]:
if isinstance(input_data, str):
input_data = input_data.encode('utf-8')
if isinstance(input_data, bytes):
input_data = input_data.split(delimiter.encode('utf-8'))
idata = [x.encode('utf-8') for x in input_data.split(delimiter)]
elif isinstance(input_data, bytes):
idata = input_data.split(delimiter.encode('utf-8'))
else:
input_data = [x.encode('utf-8') if isinstance(x, str) else x for x in input_data]
idata = [x.encode('utf-8') if isinstance(x, str) else x for x in input_data]
query = query.lower()
level1 = level1.lower()
level2 = level2.lower()
level3 = level3.lower()
data = subseq_matcher.match(
input_data, (level1, level2, level3), query,
idata, (level1, level2, level3), query,
positions, limit, threads,
mark_before, mark_after, delimiter)
if data is None:
@@ -45,23 +46,23 @@ def match(
class ChooseHandler(Handler):
def initialize(self):
def initialize(self) -> None:
pass
def on_text(self, text, in_bracketed_paste=False):
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
pass
def on_key(self, key_event):
def on_key(self, key_event: KeyEvent) -> None:
pass
def on_interrupt(self):
def on_interrupt(self) -> None:
self.quit_loop(1)
def on_eot(self):
def on_eot(self) -> None:
self.quit_loop(1)
def main(args):
def main(args: List[str]) -> None:
loop = Loop()
handler = ChooseHandler()
loop.loop(handler)

View File

@@ -0,0 +1,9 @@
from typing import List, Optional, Tuple
def match(
lines: List[bytes], levels: Tuple[str, str, str], needle: str,
output_positions: bool, limit: int, num_threads: int, mark_before: str,
mark_after: str, delimiter: str
) -> Optional[str]:
pass

View File

@@ -4,8 +4,10 @@
import os
import sys
from typing import List, NoReturn, Optional
from kitty.cli import parse_args
from kitty.cli_stub import ClipboardCLIOptions
from ..tui.handler import Handler
from ..tui.loop import Loop
@@ -13,12 +15,12 @@ from ..tui.loop import Loop
class Clipboard(Handler):
def __init__(self, data_to_send, args):
def __init__(self, data_to_send: Optional[bytes], args: ClipboardCLIOptions):
self.args = args
self.clipboard_contents = None
self.clipboard_contents: Optional[str] = None
self.data_to_send = data_to_send
def initialize(self):
def initialize(self) -> None:
if self.data_to_send is not None:
self.cmd.write_to_clipboard(self.data_to_send, self.args.use_primary)
if not self.args.get_clipboard:
@@ -32,17 +34,17 @@ class Clipboard(Handler):
return
self.cmd.request_from_clipboard(self.args.use_primary)
def on_clipboard_response(self, text, from_primary=False):
def on_clipboard_response(self, text: str, from_primary: bool = False) -> None:
self.clipboard_contents = text
self.quit_loop(0)
def on_capability_response(self, name, val):
def on_capability_response(self, name: str, val: str) -> None:
self.quit_loop(0)
def on_interrupt(self):
def on_interrupt(self) -> None:
self.quit_loop(1)
def on_eot(self):
def on_eot(self) -> None:
self.quit_loop(1)
@@ -80,16 +82,16 @@ To set the clipboard text, pipe in the new text on stdin. Use the
usage = ''
def main(args):
args, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten clipboard')
def main(args: List[str]) -> NoReturn:
cli_opts, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten clipboard', result_class=ClipboardCLIOptions)
if items:
raise SystemExit('Unrecognized extra command line arguments')
data = None
data: Optional[bytes] = None
if not sys.stdin.isatty():
data = sys.stdin.buffer.read()
sys.stdin = open(os.ctermid(), 'r')
loop = Loop()
handler = Clipboard(data, args)
handler = Clipboard(data, cli_opts)
loop.loop(handler)
if loop.return_code == 0 and handler.clipboard_contents:
sys.stdout.write(handler.clipboard_contents)
@@ -100,6 +102,7 @@ def main(args):
if __name__ == '__main__':
main(sys.argv)
elif __name__ == '__doc__':
sys.cli_docs['usage'] = usage
sys.cli_docs['options'] = OPTIONS
sys.cli_docs['help_text'] = help_text
cd = sys.cli_docs # type: ignore
cd['usage'] = usage
cd['options'] = OPTIONS
cd['help_text'] = help_text

View File

@@ -0,0 +1,8 @@
class GlobalData:
def __init__(self) -> None:
self.title = ''
self.cmd = ''
global_data = GlobalData

View File

@@ -4,86 +4,94 @@
import os
import re
from contextlib import suppress
from functools import lru_cache
from hashlib import md5
from mimetypes import guess_type
from contextlib import suppress
from typing import TYPE_CHECKING, Dict, List, Set, Optional, Iterator, Tuple, Union
path_name_map = {}
if TYPE_CHECKING:
from .highlight import DiffHighlight # noqa
path_name_map: Dict[str, str] = {}
class Segment:
__slots__ = ('start', 'end', 'start_code', 'end_code')
def __init__(self, start, start_code):
def __init__(self, start: int, start_code: str):
self.start = start
self.start_code = start_code
self.end: Optional[int] = None
self.end_code: Optional[str] = None
def __repr__(self):
def __repr__(self) -> str:
return 'Segment(start={!r}, start_code={!r}, end={!r}, end_code={!r})'.format(
self.start, self.start_code, getattr(self, 'end', None), getattr(self, 'end_code', None)
)
self.start, self.start_code, self.end, self.end_code)
class Collection:
def __init__(self):
self.changes = {}
self.renames = {}
self.adds = set()
self.removes = set()
self.all_paths = []
self.type_map = {}
def __init__(self) -> None:
self.changes: Dict[str, str] = {}
self.renames: Dict[str, str] = {}
self.adds: Set[str] = set()
self.removes: Set[str] = set()
self.all_paths: List[str] = []
self.type_map: Dict[str, str] = {}
self.added_count = self.removed_count = 0
def add_change(self, left_path, right_path):
def add_change(self, left_path: str, right_path: str) -> None:
self.changes[left_path] = right_path
self.all_paths.append(left_path)
self.type_map[left_path] = 'diff'
def add_rename(self, left_path, right_path):
def add_rename(self, left_path: str, right_path: str) -> None:
self.renames[left_path] = right_path
self.all_paths.append(left_path)
self.type_map[left_path] = 'rename'
def add_add(self, right_path):
def add_add(self, right_path: str) -> None:
self.adds.add(right_path)
self.all_paths.append(right_path)
self.type_map[right_path] = 'add'
if isinstance(data_for_path(right_path), str):
self.added_count += len(lines_for_path(right_path))
def add_removal(self, left_path):
def add_removal(self, left_path: str) -> None:
self.removes.add(left_path)
self.all_paths.append(left_path)
self.type_map[left_path] = 'removal'
if isinstance(data_for_path(left_path), str):
self.removed_count += len(lines_for_path(left_path))
def finalize(self):
def finalize(self) -> None:
self.all_paths.sort(key=path_name_map.get)
def __iter__(self):
def __iter__(self) -> Iterator[Tuple[str, str, Optional[str]]]:
for path in self.all_paths:
typ = self.type_map[path]
if typ == 'diff':
data = self.changes[path]
data: Optional[str] = self.changes[path]
elif typ == 'rename':
data = self.renames[path]
else:
data = None
yield path, typ, data
def __len__(self):
def __len__(self) -> int:
return len(self.all_paths)
def collect_files(collection, left, right):
left_names, right_names = set(), set()
left_path_map, right_path_map = {}, {}
def collect_files(collection: Collection, left: str, right: str) -> None:
left_names: Set[str] = set()
right_names: Set[str] = set()
left_path_map: Dict[str, str] = {}
right_path_map: Dict[str, str] = {}
def walk(base, names, pmap):
def walk(base: str, names: Set[str], pmap: Dict[str, str]) -> None:
for dirpath, dirnames, filenames in os.walk(base):
for filename in filenames:
path = os.path.abspath(os.path.join(dirpath, filename))
@@ -91,7 +99,8 @@ def collect_files(collection, left, right):
names.add(name)
pmap[name] = path
walk(left, left_names, left_path_map), walk(right, right_names, right_path_map)
walk(left, left_names, left_path_map)
walk(right, right_names, right_path_map)
common_names = left_names & right_names
changed_names = {n for n in common_names if data_for_path(left_path_map[n]) != data_for_path(right_path_map[n])}
for n in changed_names:
@@ -117,50 +126,56 @@ def collect_files(collection, left, right):
sanitize_pat = re.compile('[\x00-\x09\x0b-\x1f\x7f\x80-\x9f]')
def sanitize(text):
def sanitize(text: str) -> str:
ntext = text.replace('\r\n', '\n')
return sanitize_pat.sub('', ntext)
@lru_cache(maxsize=1024)
def mime_type_for_path(path):
def mime_type_for_path(path: str) -> str:
return guess_type(path)[0] or 'application/octet-stream'
@lru_cache(maxsize=1024)
def raw_data_for_path(path):
def raw_data_for_path(path: str) -> bytes:
with open(path, 'rb') as f:
return f.read()
def is_image(path):
return mime_type_for_path(path).startswith('image/')
def is_image(path: Optional[str]) -> bool:
return mime_type_for_path(path).startswith('image/') if path else False
@lru_cache(maxsize=1024)
def data_for_path(path):
ans = raw_data_for_path(path)
def data_for_path(path: str) -> Union[str, bytes]:
raw_bytes = raw_data_for_path(path)
if not is_image(path) and not os.path.samefile(path, os.devnull):
with suppress(UnicodeDecodeError):
ans = ans.decode('utf-8')
return ans
return raw_bytes.decode('utf-8')
return raw_bytes
class LinesForPath:
replace_tab_by = ' ' * 4
@lru_cache(maxsize=1024)
def __call__(self, path: str) -> Tuple[str, ...]:
data = data_for_path(path)
assert isinstance(data, str)
data = data.replace('\t', self.replace_tab_by)
return tuple(sanitize(data).splitlines())
lines_for_path = LinesForPath()
@lru_cache(maxsize=1024)
def lines_for_path(path):
data = data_for_path(path).replace('\t', lines_for_path.replace_tab_by)
return tuple(sanitize(data).splitlines())
lines_for_path.replace_tab_by = ' ' * 4
@lru_cache(maxsize=1024)
def hash_for_path(path):
def hash_for_path(path: str) -> bytes:
return md5(raw_data_for_path(path)).digest()
def create_collection(left, right):
def create_collection(left: str, right: str) -> Collection:
collection = Collection()
if os.path.isdir(left):
collect_files(collection, left, right)
@@ -173,13 +188,13 @@ def create_collection(left, right):
return collection
highlight_data = {}
highlight_data: Dict[str, 'DiffHighlight'] = {}
def set_highlight_data(data):
def set_highlight_data(data: Dict[str, 'DiffHighlight']) -> None:
global highlight_data
highlight_data = data
def highlights_for_path(path):
def highlights_for_path(path: str) -> 'DiffHighlight':
return highlight_data.get(path, [])

View File

@@ -3,27 +3,30 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import os
from typing import Any, Dict, Iterable, Optional, Tuple, Type, Union
from kitty.conf.utils import (
init_config as _init_config, key_func, load_config as _load_config, merge_dicts,
parse_config_base, parse_kittens_key, resolve_config
)
from kitty.conf.definition import config_lines
from kitty.conf.utils import (
init_config as _init_config, key_func, load_config as _load_config,
merge_dicts, parse_config_base, parse_kittens_key, resolve_config
)
from kitty.constants import config_dir
from kitty.options_stub import DiffOptions
from kitty.cli_stub import DiffCLIOptions
from kitty.rgb import color_as_sgr
from .config_data import type_map, all_options
from .config_data import all_options, type_convert
defaults = None
defaults: Optional[DiffOptions] = None
formats = {
formats: Dict[str, str] = {
'title': '',
'margin': '',
'text': '',
}
def set_formats(opts):
def set_formats(opts: DiffOptions) -> None:
formats['text'] = '48' + color_as_sgr(opts.background)
formats['title'] = '38' + color_as_sgr(opts.title_fg) + ';48' + color_as_sgr(opts.title_bg) + ';1'
formats['margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.margin_bg)
@@ -32,6 +35,7 @@ def set_formats(opts):
formats['added'] = '48' + color_as_sgr(opts.added_bg)
formats['removed'] = '48' + color_as_sgr(opts.removed_bg)
formats['filler'] = '48' + color_as_sgr(opts.filler_bg)
formats['margin_filler'] = '48' + color_as_sgr(opts.margin_filler_bg or opts.filler_bg)
formats['hunk_margin'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.hunk_margin_bg)
formats['hunk'] = '38' + color_as_sgr(opts.margin_fg) + ';48' + color_as_sgr(opts.hunk_bg)
formats['removed_highlight'] = '48' + color_as_sgr(opts.highlight_removed_bg)
@@ -42,7 +46,7 @@ func_with_args, args_funcs = key_func()
@func_with_args('scroll_by')
def parse_scroll_by(func, rest):
def parse_scroll_by(func: str, rest: str) -> Tuple[str, int]:
try:
return func, int(rest)
except Exception:
@@ -50,7 +54,7 @@ def parse_scroll_by(func, rest):
@func_with_args('scroll_to')
def parse_scroll_to(func, rest):
def parse_scroll_to(func: str, rest: str) -> Tuple[str, str]:
rest = rest.lower()
if rest not in {'start', 'end', 'next-change', 'prev-change', 'next-page', 'prev-page', 'next-match', 'prev-match'}:
rest = 'start'
@@ -58,7 +62,7 @@ def parse_scroll_to(func, rest):
@func_with_args('change_context')
def parse_change_context(func, rest):
def parse_change_context(func: str, rest: str) -> Tuple[str, Union[int, str]]:
rest = rest.lower()
if rest in {'all', 'default'}:
return func, rest
@@ -70,26 +74,29 @@ def parse_change_context(func, rest):
@func_with_args('start_search')
def parse_start_search(func, rest):
rest = rest.lower().split()
is_regex = rest and rest[0] == 'regex'
is_backward = len(rest) > 1 and rest[1] == 'backward'
def parse_start_search(func: str, rest: str) -> Tuple[str, Tuple[bool, bool]]:
rest_ = rest.lower().split()
is_regex = bool(rest_ and rest_[0] == 'regex')
is_backward = bool(len(rest_) > 1 and rest_[1] == 'backward')
return func, (is_regex, is_backward)
def special_handling(key, val, ans):
def special_handling(key: str, val: str, ans: Dict) -> bool:
if key == 'map':
action, *key_def = parse_kittens_key(val, args_funcs)
ans['key_definitions'][tuple(key_def)] = action
return True
x = parse_kittens_key(val, args_funcs)
if x is not None:
action, key_def = x
ans['key_definitions'][key_def] = action
return True
return False
def parse_config(lines, check_keys=True):
ans = {'key_definitions': {}}
def parse_config(lines: Iterable[str], check_keys: bool = True) -> Dict[str, Any]:
ans: Dict[str, Any] = {'key_definitions': {}}
parse_config_base(
lines,
defaults,
type_map,
type_convert,
special_handling,
ans,
check_keys=check_keys
@@ -97,7 +104,7 @@ def parse_config(lines, check_keys=True):
return ans
def merge_configs(defaults, vals):
def merge_configs(defaults: Dict, vals: Dict) -> Dict:
ans = {}
for k, v in defaults.items():
if isinstance(v, dict):
@@ -108,14 +115,16 @@ def merge_configs(defaults, vals):
return ans
def parse_defaults(lines, check_keys=False):
def parse_defaults(lines: Iterable[str], check_keys: bool = False) -> Dict[str, Any]:
return parse_config(lines, check_keys)
Options, defaults = _init_config(config_lines(all_options), parse_defaults)
x = _init_config(config_lines(all_options), parse_defaults)
Options: Type[DiffOptions] = x[0]
defaults = x[1]
def load_config(*paths, overrides=None):
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None) -> DiffOptions:
return _load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=overrides)
@@ -123,7 +132,7 @@ SYSTEM_CONF = '/etc/xdg/kitty/diff.conf'
defconf = os.path.join(config_dir, 'diff.conf')
def init_config(args):
def init_config(args: DiffCLIOptions) -> DiffOptions:
config = tuple(resolve_config(SYSTEM_CONF, defconf, args.config))
overrides = (a.replace('=', ' ', 1) for a in args.override or ())
opts = load_config(*config, overrides=overrides)

View File

@@ -4,17 +4,18 @@
# Utils {{{
from gettext import gettext as _
from functools import partial
from gettext import gettext as _
from typing import Any, Dict, Sequence, Union
from kitty.conf.definition import option_func
from kitty.conf.definition import Option, Shortcut, option_func
from kitty.conf.utils import (
positive_int, python_string, to_color
positive_int, python_string, to_color, to_color_or_none
)
# }}}
all_options = {}
all_options: Dict[str, Union[Option, Sequence[Shortcut]]] = {}
o, k, g, all_groups = option_func(all_options, {
'colors': [_('Colors')],
'diff': [_('Diffing'), ],
@@ -25,7 +26,7 @@ o, k, g, all_groups = option_func(all_options, {
g('diff')
def syntax_aliases(raw):
def syntax_aliases(raw: str) -> Dict[str, str]:
ans = {}
for x in raw.split():
a, b = x.partition(':')[::2]
@@ -34,7 +35,7 @@ def syntax_aliases(raw):
return ans
o('syntax_aliases', 'pyj:py recipe:py', option_type=syntax_aliases, long_text=_('''
o('syntax_aliases', 'pyj:py pyi:py recipe:py', option_type=syntax_aliases, long_text=_('''
File extension aliases for syntax highlight
For example, to syntax highlight :file:`file.xyz` as
:file:`file.abc` use a setting of :code:`xyz:abc`
@@ -79,6 +80,8 @@ c('highlight_added_bg', '#acf2bd')
c('added_margin_bg', '#cdffd8')
c('filler_bg', '#fafbfc', long_text=_('Filler (empty) line background'))
c('margin_filler_bg', 'none', option_type=to_color_or_none, long_text=_(
'Filler (empty) line background in margins, defaults to the filler background'))
c('hunk_margin_bg', '#dbedff', long_text=_('Hunk header colors'))
c('hunk_bg', '#f1f8ff')
@@ -121,4 +124,9 @@ k('prev_match', '<', 'scroll_to prev-match', _('Scroll to previous search match'
k('search_forward_simple', 'f', 'start_search substring forward', _('Search forward (no regex)'))
k('search_backward_simple', 'b', 'start_search substring backward', _('Search backward (no regex)'))
type_map = {o.name: o.option_type for o in all_options.values() if hasattr(o, 'option_type')}
def type_convert(name: str, val: Any) -> Any:
o = all_options.get(name)
if isinstance(o, Option):
val = o.option_type(val)
return val

View File

@@ -0,0 +1,14 @@
from typing import List, Optional, Tuple
from .collect import Segment
def split_with_highlights(
line: str, truncate_points: List[int], fg_highlights: List,
bg_highlight: Optional[Segment]
) -> List:
pass
def changed_center(left_prefix: str, right_postfix: str) -> Tuple[int, int]:
pass

View File

@@ -5,15 +5,16 @@
import concurrent
import os
import re
from typing import IO, Dict, Iterable, List, Optional, Tuple, Union, cast
from pygments import highlight
from pygments.formatter import Formatter
from pygments.lexers import get_lexer_for_filename
from pygments.util import ClassNotFound
from pygments import highlight # type: ignore
from pygments.formatter import Formatter # type: ignore
from pygments.lexers import get_lexer_for_filename # type: ignore
from pygments.util import ClassNotFound # type: ignore
from kitty.rgb import color_as_sgr, parse_sharp
from .collect import Segment, data_for_path, lines_for_path
from .collect import Collection, Segment, data_for_path, lines_for_path
class StyleNotFound(Exception):
@@ -22,7 +23,7 @@ class StyleNotFound(Exception):
class DiffFormatter(Formatter):
def __init__(self, style='default'):
def __init__(self, style: str = 'default') -> None:
try:
Formatter.__init__(self, style=style)
initialized = True
@@ -31,30 +32,34 @@ class DiffFormatter(Formatter):
if not initialized:
raise StyleNotFound('pygments style "{}" not found'.format(style))
self.styles = {}
for token, style in self.style:
self.styles: Dict[str, Tuple[str, str]] = {}
for token, token_style in self.style:
start = []
end = []
fstart = fend = ''
# a style item is a tuple in the following form:
# colors are readily specified in hex: 'RRGGBB'
if style['color']:
start.append('38' + color_as_sgr(parse_sharp(style['color'])))
end.append('39')
if style['bold']:
col = token_style['color']
if col:
pc = parse_sharp(col)
if pc is not None:
start.append('38' + color_as_sgr(pc))
end.append('39')
if token_style['bold']:
start.append('1')
end.append('22')
if style['italic']:
if token_style['italic']:
start.append('3')
end.append('23')
if style['underline']:
if token_style['underline']:
start.append('4')
end.append('24')
if start:
start = '\033[{}m'.format(';'.join(start))
end = '\033[{}m'.format(';'.join(end))
self.styles[token] = start or '', end or ''
fstart = '\033[{}m'.format(';'.join(start))
fend = '\033[{}m'.format(';'.join(end))
self.styles[token] = fstart, fend
def format(self, tokensource, outfile):
def format(self, tokensource: Iterable[Tuple[str, str]], outfile: IO[str]) -> None:
for ttype, value in tokensource:
not_found = True
if value.rstrip('\n'):
@@ -76,15 +81,15 @@ class DiffFormatter(Formatter):
outfile.write(value)
formatter = None
formatter: Optional[DiffFormatter] = None
def initialize_highlighter(style='default'):
def initialize_highlighter(style: str = 'default') -> None:
global formatter
formatter = DiffFormatter(style)
def highlight_data(code, filename, aliases=None):
def highlight_data(code: str, filename: str, aliases: Optional[Dict[str, str]] = None) -> Optional[str]:
if aliases:
base, ext = os.path.splitext(filename)
alias = aliases.get(ext[1:])
@@ -93,17 +98,16 @@ def highlight_data(code, filename, aliases=None):
try:
lexer = get_lexer_for_filename(filename, stripnl=False)
except ClassNotFound:
pass
else:
return highlight(code, lexer, formatter)
return None
return cast(str, highlight(code, lexer, formatter))
split_pat = re.compile(r'(\033\[.*?m)')
def highlight_line(line):
ans = []
current = None
def highlight_line(line: str) -> List[Segment]:
ans: List[Segment] = []
current: Optional[Segment] = None
pos = 0
for x in split_pat.split(line):
if x.startswith('\033'):
@@ -119,8 +123,11 @@ def highlight_line(line):
return ans
def highlight_for_diff(path, aliases):
ans = []
DiffHighlight = List[List[Segment]]
def highlight_for_diff(path: str, aliases: Dict[str, str]) -> DiffHighlight:
ans: DiffHighlight = []
lines = lines_for_path(path)
hd = highlight_data('\n'.join(lines), path, aliases)
if hd is not None:
@@ -129,11 +136,10 @@ def highlight_for_diff(path, aliases):
return ans
def highlight_collection(collection, aliases=None):
def highlight_collection(collection: Collection, aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, DiffHighlight]]:
jobs = {}
ans = {}
ans: Dict[str, DiffHighlight] = {}
with concurrent.futures.ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
highlight_collection.processes = executor._processes
for path, item_type, other_path in collection:
if item_type != 'rename':
for p in (path, other_path):
@@ -151,13 +157,14 @@ def highlight_collection(collection, aliases=None):
return ans
def main():
def main() -> None:
from .config import defaults
# kitty +runpy "from kittens.diff.highlight import main; main()" file
import sys
initialize_highlighter()
with open(sys.argv[-1]) as f:
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
if highlighted is None:
raise SystemExit('Unknown filetype: {}'.format(sys.argv[-1]))
print(highlighted)
if defaults is not None:
with open(sys.argv[-1]) as f:
highlighted = highlight_data(f.read(), f.name, defaults.syntax_aliases)
if highlighted is None:
raise SystemExit('Unknown filetype: {}'.format(sys.argv[-1]))
print(highlighted)

View File

@@ -2,50 +2,68 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import atexit
import os
import signal
import subprocess
import sys
import tempfile
import warnings
from collections import defaultdict
from contextlib import suppress
from functools import partial
from gettext import gettext as _
from contextlib import suppress
from typing import DefaultDict, Dict, Iterable, List, Optional, Tuple, Union
from kitty.cli import CONFIG_HELP, parse_args
from kitty.cli_stub import DiffCLIOptions
from kitty.conf.utils import KittensKeyAction
from kitty.constants import appname
from kitty.fast_data_types import wcswidth
from kitty.key_encoding import ESCAPE, RELEASE, enter_key
from kitty.key_encoding import RELEASE, KeyEvent, enter_key, key_defs as K
from kitty.options_stub import DiffOptions
from kitty.utils import ScreenSize
from ..tui.handler import Handler
from ..tui.images import ImageManager
from ..tui.line_edit import LineEdit
from ..tui.loop import Loop
from ..tui.operations import styled
from . import global_data
from .collect import (
create_collection, data_for_path, lines_for_path, sanitize,
Collection, create_collection, data_for_path, lines_for_path, sanitize,
set_highlight_data
)
from .config import init_config
from .patch import Differ, set_diff_command, worker_processes
from .render import ImageSupportWarning, LineRef, render_diff
from .patch import Differ, Patch, set_diff_command, worker_processes
from .render import (
ImagePlacement, ImageSupportWarning, Line, LineRef, Reference, render_diff
)
from .search import BadRegex, Search
from ..tui.handler import Handler
from ..tui.images import ImageManager, Placement
from ..tui.line_edit import LineEdit
from ..tui.loop import Loop
from ..tui.operations import styled
try:
from .highlight import initialize_highlighter, highlight_collection
from .highlight import initialize_highlighter, highlight_collection, DiffHighlight
has_highlighter = True
DiffHighlight
except ImportError:
initialize_highlighter = highlight_collection = None
has_highlighter = False
def highlight_collection(collection: 'Collection', aliases: Optional[Dict[str, str]] = None) -> Union[str, Dict[str, 'DiffHighlight']]:
return ''
INITIALIZING, COLLECTED, DIFFED, COMMAND, MESSAGE = range(5)
ESCAPE = K['ESCAPE']
def generate_diff(collection, context):
def generate_diff(collection: Collection, context: int) -> Union[str, Dict[str, Patch]]:
d = Differ()
for path, item_type, changed_path in collection:
if item_type == 'diff':
is_binary = isinstance(data_for_path(path), bytes) or isinstance(data_for_path(changed_path), bytes)
if not is_binary:
assert changed_path is not None
d.add_diff(path, changed_path)
return d(context)
@@ -55,35 +73,35 @@ class DiffHandler(Handler):
image_manager_class = ImageManager
def __init__(self, args, opts, left, right):
def __init__(self, args: DiffCLIOptions, opts: DiffOptions, left: str, right: str) -> None:
self.state = INITIALIZING
self.message = ''
self.current_search_is_regex = True
self.current_search = None
self.current_search: Optional[Search] = None
self.line_edit = LineEdit()
self.opts = opts
self.left, self.right = left, right
self.report_traceback_on_exit = None
self.report_traceback_on_exit: Union[str, Dict[str, Patch], None] = None
self.args = args
self.scroll_pos = self.max_scroll_pos = 0
self.current_context_count = self.original_context_count = self.args.context
if self.current_context_count < 0:
self.current_context_count = self.original_context_count = self.opts.num_context_lines
self.highlighting_done = False
self.restore_position = None
self.restore_position: Optional[Reference] = None
for key_def, action in self.opts.key_definitions.items():
self.add_shortcut(action, *key_def)
def perform_action(self, action):
def perform_action(self, action: KittensKeyAction) -> None:
func, args = action
if func == 'quit':
self.quit_loop(0)
return
if self.state <= DIFFED:
if func == 'scroll_by':
return self.scroll_lines(*args)
return self.scroll_lines(int(args[0]))
if func == 'scroll_to':
where = args[0]
where = str(args[0])
if 'change' in where:
return self.scroll_to_next_change(backwards='prev' in where)
if 'match' in where:
@@ -95,7 +113,7 @@ class DiffHandler(Handler):
return self.scroll_lines(amt)
if func == 'change_context':
new_ctx = self.current_context_count
to = args[0]
to = int(args[0])
if to == 'all':
new_ctx = 100000
elif to == 'default':
@@ -104,25 +122,25 @@ class DiffHandler(Handler):
new_ctx += to
return self.change_context_count(new_ctx)
if func == 'start_search':
self.start_search(*args)
self.start_search(bool(args[0]), bool(args[1]))
return
def create_collection(self):
def create_collection(self) -> None:
def collect_done(collection):
def collect_done(collection: Collection) -> None:
self.collection = collection
self.state = COLLECTED
self.generate_diff()
def collect(left, right):
def collect(left: str, right: str) -> None:
collection = create_collection(left, right)
self.asyncio_loop.call_soon_threadsafe(collect_done, collection)
self.asyncio_loop.run_in_executor(None, collect, self.left, self.right)
def generate_diff(self):
def generate_diff(self) -> None:
def diff_done(diff_map):
def diff_done(diff_map: Union[str, Dict[str, Patch]]) -> None:
if isinstance(diff_map, str):
self.report_traceback_on_exit = diff_map
self.quit_loop(1)
@@ -136,7 +154,7 @@ class DiffHandler(Handler):
self.current_position = self.restore_position
self.restore_position = None
self.draw_screen()
if initialize_highlighter is not None and not self.highlighting_done:
if has_highlighter and not self.highlighting_done:
from .highlight import StyleNotFound
self.highlighting_done = True
try:
@@ -147,15 +165,15 @@ class DiffHandler(Handler):
return
self.syntax_highlight()
def diff(collection, current_context_count):
def diff(collection: Collection, current_context_count: int) -> None:
diff_map = generate_diff(collection, current_context_count)
self.asyncio_loop.call_soon_threadsafe(diff_done, diff_map)
self.asyncio_loop.run_in_executor(None, diff, self.collection, self.current_context_count)
def syntax_highlight(self):
def syntax_highlight(self) -> None:
def highlighting_done(hdata):
def highlighting_done(hdata: Union[str, Dict[str, 'DiffHighlight']]) -> None:
if isinstance(hdata, str):
self.report_traceback_on_exit = hdata
self.quit_loop(1)
@@ -164,23 +182,23 @@ class DiffHandler(Handler):
self.render_diff()
self.draw_screen()
def highlight(*a):
result = highlight_collection(*a)
def highlight(collection: Collection, aliases: Optional[Dict[str, str]] = None) -> None:
result = highlight_collection(collection, aliases)
self.asyncio_loop.call_soon_threadsafe(highlighting_done, result)
self.asyncio_loop.run_in_executor(None, highlight, self.collection, self.opts.syntax_aliases)
def calculate_statistics(self):
def calculate_statistics(self) -> None:
self.added_count = self.collection.added_count
self.removed_count = self.collection.removed_count
for patch in self.diff_map.values():
self.added_count += patch.added_count
self.removed_count += patch.removed_count
def render_diff(self):
self.diff_lines = tuple(render_diff(self.collection, self.diff_map, self.args, self.screen_size.cols, self.image_manager))
def render_diff(self) -> None:
self.diff_lines: Tuple[Line, ...] = tuple(render_diff(self.collection, self.diff_map, self.args, self.screen_size.cols, self.image_manager))
self.margin_size = render_diff.margin_size
self.ref_path_map = defaultdict(list)
self.ref_path_map: DefaultDict[str, List[Tuple[int, Reference]]] = defaultdict(list)
for i, l in enumerate(self.diff_lines):
self.ref_path_map[l.ref.path].append((i, l.ref))
self.max_scroll_pos = len(self.diff_lines) - self.num_lines
@@ -188,11 +206,11 @@ class DiffHandler(Handler):
self.current_search(self.diff_lines, self.margin_size, self.screen_size.cols)
@property
def current_position(self):
def current_position(self) -> Reference:
return self.diff_lines[min(len(self.diff_lines) - 1, self.scroll_pos)].ref
@current_position.setter
def current_position(self, ref):
def current_position(self, ref: Reference) -> None:
num = None
if isinstance(ref.extra, LineRef):
sln = ref.extra.src_line_number
@@ -212,10 +230,10 @@ class DiffHandler(Handler):
self.scroll_pos = max(0, min(num, self.max_scroll_pos))
@property
def num_lines(self):
def num_lines(self) -> int:
return self.screen_size.rows - 1
def scroll_to_next_change(self, backwards=False):
def scroll_to_next_change(self, backwards: bool = False) -> None:
if backwards:
r = range(self.scroll_pos - 1, -1, -1)
else:
@@ -227,7 +245,7 @@ class DiffHandler(Handler):
return
self.cmd.bell()
def scroll_to_next_match(self, backwards=False, include_current=False):
def scroll_to_next_match(self, backwards: bool = False, include_current: bool = False) -> None:
if self.current_search is not None:
offset = 0 if include_current else 1
if backwards:
@@ -240,10 +258,10 @@ class DiffHandler(Handler):
return
self.cmd.bell()
def set_scrolling_region(self):
def set_scrolling_region(self) -> None:
self.cmd.set_scrolling_region(self.screen_size, 0, self.num_lines - 2)
def scroll_lines(self, amt=1):
def scroll_lines(self, amt: int = 1) -> None:
new_pos = max(0, min(self.scroll_pos + amt, self.max_scroll_pos))
if new_pos == self.scroll_pos:
self.cmd.bell()
@@ -263,30 +281,30 @@ class DiffHandler(Handler):
self.draw_lines(amt, self.num_lines - amt)
self.draw_status_line()
def init_terminal_state(self):
def init_terminal_state(self) -> None:
self.cmd.set_line_wrapping(False)
self.cmd.set_window_title(main.title)
self.cmd.set_window_title(global_data.title)
self.cmd.set_default_colors(
fg=self.opts.foreground, bg=self.opts.background,
cursor=self.opts.foreground, select_fg=self.opts.select_fg,
select_bg=self.opts.select_bg)
self.cmd.set_cursor_shape('bar')
def finalize(self):
def finalize(self) -> None:
self.cmd.set_default_colors()
self.cmd.set_cursor_visible(True)
self.cmd.set_scrolling_region()
def initialize(self):
def initialize(self) -> None:
self.init_terminal_state()
self.set_scrolling_region()
self.draw_screen()
self.create_collection()
def enforce_cursor_state(self):
def enforce_cursor_state(self) -> None:
self.cmd.set_cursor_visible(self.state == COMMAND)
def draw_lines(self, num, offset=0):
def draw_lines(self, num: int, offset: int = 0) -> None:
offset += self.scroll_pos
image_involved = False
limit = len(self.diff_lines)
@@ -307,7 +325,43 @@ class DiffHandler(Handler):
if image_involved:
self.place_images()
def place_images(self):
def update_image_placement_for_resend(self, image_id: int, pl: Placement) -> bool:
offset = self.scroll_pos
limit = len(self.diff_lines)
in_image = False
def adjust(row: int, candidate: ImagePlacement, is_left: bool) -> bool:
if candidate.image.image_id == image_id:
q = self.xpos_for_image(row, candidate, is_left)
if q is not None:
pl['x'] = q[0]
pl['y'] = row
return True
return False
for row in range(self.num_lines):
lpos = offset + row
if lpos >= limit:
break
line = self.diff_lines[lpos]
if in_image:
if line.image_data is None:
in_image = False
continue
if line.image_data is not None:
left_placement, right_placement = line.image_data
if left_placement is not None:
if adjust(row, left_placement, True):
return True
in_image = True
if right_placement is not None:
if adjust(row, right_placement, False):
return True
in_image = True
return False
def place_images(self) -> None:
self.image_manager.update_image_placement_for_resend = self.update_image_placement_for_resend
self.cmd.clear_images_on_screen()
offset = self.scroll_pos
limit = len(self.diff_lines)
@@ -330,19 +384,26 @@ class DiffHandler(Handler):
self.place_image(row, right_placement, False)
in_image = True
def place_image(self, row, placement, is_left):
def xpos_for_image(self, row: int, placement: ImagePlacement, is_left: bool) -> Optional[Tuple[int, float]]:
xpos = (0 if is_left else (self.screen_size.cols // 2)) + placement.image.margin_size
image_height_in_rows = placement.image.rows
topmost_visible_row = placement.row
num_visible_rows = image_height_in_rows - topmost_visible_row
visible_frac = min(num_visible_rows / image_height_in_rows, 1)
if visible_frac > 0:
if visible_frac <= 0:
return None
return xpos, visible_frac
def place_image(self, row: int, placement: ImagePlacement, is_left: bool) -> None:
q = self.xpos_for_image(row, placement, is_left)
if q is not None:
xpos, visible_frac = q
height = int(visible_frac * placement.image.height)
top = placement.image.height - height
self.image_manager.show_image(placement.image.image_id, xpos, row, src_rect=(
0, top, placement.image.width, height))
def draw_screen(self):
def draw_screen(self) -> None:
self.enforce_cursor_state()
if self.state < DIFFED:
self.cmd.clear_screen()
@@ -353,7 +414,7 @@ class DiffHandler(Handler):
self.draw_lines(self.num_lines)
self.draw_status_line()
def draw_status_line(self):
def draw_status_line(self) -> None:
if self.state < DIFFED:
return
self.enforce_cursor_state()
@@ -380,7 +441,7 @@ class DiffHandler(Handler):
text = '{}{}{}'.format(prefix, ' ' * filler, suffix)
self.write(text)
def change_context_count(self, new_ctx):
def change_context_count(self, new_ctx: int) -> None:
new_ctx = max(0, new_ctx)
if new_ctx != self.current_context_count:
self.current_context_count = new_ctx
@@ -389,7 +450,7 @@ class DiffHandler(Handler):
self.restore_position = self.current_position
self.draw_screen()
def start_search(self, is_regex, is_backward):
def start_search(self, is_regex: bool, is_backward: bool) -> None:
if self.state != DIFFED:
self.cmd.bell()
return
@@ -399,7 +460,7 @@ class DiffHandler(Handler):
self.current_search_is_regex = is_regex
self.draw_status_line()
def do_search(self):
def do_search(self) -> None:
self.current_search = None
query = self.line_edit.current_input
if len(query) < 2:
@@ -418,7 +479,7 @@ class DiffHandler(Handler):
self.message = sanitize(_('No matches found'))
self.cmd.bell()
def on_text(self, text, in_bracketed_paste=False):
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
if self.state is COMMAND:
self.line_edit.on_text(text, in_bracketed_paste)
self.draw_status_line()
@@ -431,7 +492,7 @@ class DiffHandler(Handler):
if action is not None:
return self.perform_action(action)
def on_key(self, key_event):
def on_key(self, key_event: KeyEvent) -> None:
if self.state is MESSAGE:
if key_event.type is not RELEASE:
self.state = DIFFED
@@ -464,7 +525,7 @@ class DiffHandler(Handler):
if action is not None:
return self.perform_action(action)
def on_resize(self, screen_size):
def on_resize(self, screen_size: ScreenSize) -> None:
self.screen_size = screen_size
self.set_scrolling_region()
if self.state > COLLECTED:
@@ -472,10 +533,10 @@ class DiffHandler(Handler):
self.render_diff()
self.draw_screen()
def on_interrupt(self):
def on_interrupt(self) -> None:
self.quit_loop(1)
def on_eot(self):
def on_eot(self) -> None:
self.quit_loop(1)
@@ -500,40 +561,60 @@ Syntax: :italic:`name=value`. For example: :italic:`-o background=gray`
'''.format, config_help=CONFIG_HELP.format(conf_name='diff', appname=appname))
def showwarning(message, category, filename, lineno, file=None, line=None):
if category is ImageSupportWarning:
showwarning.warnings.append(message)
class ShowWarning:
def __init__(self) -> None:
self.warnings: List[str] = []
def __call__(self, message: str, category: object, filename: str, lineno: int, file: object = None, line: object = None) -> None:
if category is ImageSupportWarning:
showwarning.warnings.append(message)
showwarning.warnings = []
help_text = 'Show a side-by-side diff of the specified files/directories'
showwarning = ShowWarning()
help_text = 'Show a side-by-side diff of the specified files/directories. You can also use ssh:hostname:remote-file-path to diff remote files.'
usage = 'file_or_directory_left file_or_directory_right'
def terminate_processes(processes):
def terminate_processes(processes: Iterable[int]) -> None:
for pid in processes:
with suppress(Exception):
os.kill(pid, signal.SIGKILL)
def main(args):
def get_remote_file(path: str) -> str:
if path.startswith('ssh:'):
parts = path.split(':', 2)
if len(parts) == 3:
hostname, rpath = parts[1:]
with tempfile.NamedTemporaryFile(suffix='-' + os.path.basename(rpath), prefix='remote:', delete=False) as tf:
atexit.register(os.remove, tf.name)
p = subprocess.Popen(['ssh', hostname, 'cat', rpath], stdout=tf)
if p.wait() != 0:
raise SystemExit(p.returncode)
return tf.name
return path
def main(args: List[str]) -> None:
warnings.showwarning = showwarning
args, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten diff')
cli_opts, items = parse_args(args[1:], OPTIONS, usage, help_text, 'kitty +kitten diff', result_class=DiffCLIOptions)
if len(items) != 2:
raise SystemExit('You must specify exactly two files/directories to compare')
left, right = items
main.title = _('{} vs. {}').format(left, right)
global_data.title = _('{} vs. {}').format(left, right)
if os.path.isdir(left) != os.path.isdir(right):
raise SystemExit('The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.')
opts = init_config(args)
opts = init_config(cli_opts)
set_diff_command(opts.diff_cmd)
lines_for_path.replace_tab_by = opts.replace_tab_by
left, right = map(get_remote_file, (left, right))
for f in left, right:
if not os.path.exists(f):
raise SystemExit('{} does not exist'.format(f))
loop = Loop()
handler = DiffHandler(args, opts, left, right)
handler = DiffHandler(cli_opts, opts, left, right)
loop.loop(handler)
for message in showwarning.warnings:
from kitty.utils import safe_print
@@ -551,9 +632,10 @@ def main(args):
if __name__ == '__main__':
main(sys.argv)
elif __name__ == '__doc__':
sys.cli_docs['usage'] = usage
sys.cli_docs['options'] = OPTIONS
sys.cli_docs['help_text'] = help_text
cd = sys.cli_docs # type: ignore
cd['usage'] = usage
cd['options'] = OPTIONS
cd['help_text'] = help_text
elif __name__ == '__conf__':
from .config import all_options
sys.all_options = all_options
from .config_data import all_options
sys.all_options = all_options # type: ignore

View File

@@ -7,40 +7,44 @@ import os
import shlex
import shutil
import subprocess
from typing import Dict, Iterator, List, Optional, Sequence, Tuple, Union
from . import global_data
from .collect import lines_for_path
from .diff_speedup import changed_center
left_lines = right_lines = None
left_lines: Tuple[str, ...] = ()
right_lines: Tuple[str, ...] = ()
GIT_DIFF = 'git diff --no-color --no-ext-diff --exit-code -U_CONTEXT_ --no-index --'
DIFF_DIFF = 'diff -p -U _CONTEXT_ --'
worker_processes = []
worker_processes: List[int] = []
def find_differ():
def find_differ() -> Optional[str]:
if shutil.which('git') and subprocess.Popen(['git', '--help'], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL).wait() == 0:
return GIT_DIFF
if shutil.which('diff'):
return DIFF_DIFF
def set_diff_command(opt):
def set_diff_command(opt: str) -> None:
if opt == 'auto':
cmd = find_differ()
if cmd is None:
raise SystemExit('Failed to find either the git or diff programs on your system')
else:
cmd = opt
set_diff_command.cmd = cmd
global_data.cmd = cmd
def run_diff(file1, file2, context=3):
def run_diff(file1: str, file2: str, context: int = 3) -> Tuple[bool, Union[int, bool], str]:
# returns: ok, is_different, patch
cmd = shlex.split(set_diff_command.cmd.replace('_CONTEXT_', str(context)))
cmd = shlex.split(global_data.cmd.replace('_CONTEXT_', str(context)))
# we resolve symlinks because git diff does not follow symlinks, while diff
# does. We want consistent behavior, also for integration with git difftool
# we always want symlinks to be followed.
path1, path2 = map(os.path.realpath, (file1, file2))
path1 = os.path.realpath(file1)
path2 = os.path.realpath(file2)
p = subprocess.Popen(
cmd + [path1, path2],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL)
@@ -57,49 +61,49 @@ class Chunk:
__slots__ = ('is_context', 'left_start', 'right_start', 'left_count', 'right_count', 'centers')
def __init__(self, left_start, right_start, is_context=False):
def __init__(self, left_start: int, right_start: int, is_context: bool = False) -> None:
self.is_context = is_context
self.left_start = left_start
self.right_start = right_start
self.left_count = self.right_count = 0
self.centers = None
self.centers: Optional[Tuple[Tuple[int, int], ...]] = None
def add_line(self):
def add_line(self) -> None:
self.right_count += 1
def remove_line(self):
def remove_line(self) -> None:
self.left_count += 1
def context_line(self):
def context_line(self) -> None:
self.left_count += 1
self.right_count += 1
def finalize(self):
def finalize(self) -> None:
if not self.is_context and self.left_count == self.right_count:
self.centers = tuple(
changed_center(left_lines[self.left_start + i], right_lines[self.right_start + i])
for i in range(self.left_count)
)
def __repr__(self):
def __repr__(self) -> str:
return 'Chunk(is_context={}, left_start={}, left_count={}, right_start={}, right_count={})'.format(
self.is_context, self.left_start, self.left_count, self.right_start, self.right_count)
class Hunk:
def __init__(self, title, left, right):
def __init__(self, title: str, left: Tuple[int, int], right: Tuple[int, int]) -> None:
self.left_start, self.left_count = left
self.right_start, self.right_count = right
self.left_start -= 1 # 0-index
self.right_start -= 1 # 0-index
self.title = title
self.added_count = self.removed_count = 0
self.chunks = []
self.current_chunk = None
self.chunks: List[Chunk] = []
self.current_chunk: Optional[Chunk] = None
self.largest_line_number = max(self.left_start + self.left_count, self.right_start + self.right_count)
def new_chunk(self, is_context=False):
def new_chunk(self, is_context: bool = False) -> Chunk:
if self.chunks:
c = self.chunks[-1]
left_start = c.left_start + c.left_count
@@ -109,36 +113,40 @@ class Hunk:
right_start = self.right_start
return Chunk(left_start, right_start, is_context)
def ensure_diff_chunk(self):
def ensure_diff_chunk(self) -> None:
if self.current_chunk is None:
self.current_chunk = self.new_chunk(is_context=False)
elif self.current_chunk.is_context:
self.chunks.append(self.current_chunk)
self.current_chunk = self.new_chunk(is_context=False)
def ensure_context_chunk(self):
def ensure_context_chunk(self) -> None:
if self.current_chunk is None:
self.current_chunk = self.new_chunk(is_context=True)
elif not self.current_chunk.is_context:
self.chunks.append(self.current_chunk)
self.current_chunk = self.new_chunk(is_context=True)
def add_line(self):
def add_line(self) -> None:
self.ensure_diff_chunk()
self.current_chunk.add_line()
if self.current_chunk is not None:
self.current_chunk.add_line()
self.added_count += 1
def remove_line(self):
def remove_line(self) -> None:
self.ensure_diff_chunk()
self.current_chunk.remove_line()
if self.current_chunk is not None:
self.current_chunk.remove_line()
self.removed_count += 1
def context_line(self):
def context_line(self) -> None:
self.ensure_context_chunk()
self.current_chunk.context_line()
if self.current_chunk is not None:
self.current_chunk.context_line()
def finalize(self):
self.chunks.append(self.current_chunk)
def finalize(self) -> None:
if self.current_chunk is not None:
self.chunks.append(self.current_chunk)
del self.current_chunk
# Sanity check
c = self.chunks[-1]
@@ -150,15 +158,15 @@ class Hunk:
c.finalize()
def parse_range(x):
def parse_range(x: str) -> Tuple[int, int]:
parts = x[1:].split(',', 1)
start = abs(int(parts[0]))
count = 1 if len(parts) < 2 else int(parts[1])
return start, count
def parse_hunk_header(line):
parts = tuple(filter(None, line.split('@@', 2)))
def parse_hunk_header(line: str) -> Hunk:
parts: Tuple[str, ...] = tuple(filter(None, line.split('@@', 2)))
linespec = parts[0].strip()
title = ''
if len(parts) == 2:
@@ -169,20 +177,20 @@ def parse_hunk_header(line):
class Patch:
def __init__(self, all_hunks):
def __init__(self, all_hunks: Sequence[Hunk]):
self.all_hunks = all_hunks
self.largest_line_number = self.all_hunks[-1].largest_line_number if self.all_hunks else 0
self.added_count = sum(h.added_count for h in all_hunks)
self.removed_count = sum(h.removed_count for h in all_hunks)
def __iter__(self):
def __iter__(self) -> Iterator[Hunk]:
return iter(self.all_hunks)
def __len__(self):
def __len__(self) -> int:
return len(self.all_hunks)
def parse_patch(raw):
def parse_patch(raw: str) -> Patch:
all_hunks = []
current_hunk = None
for line in raw.splitlines():
@@ -208,22 +216,23 @@ def parse_patch(raw):
class Differ:
diff_executor = None
diff_executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
def __init__(self):
self.jmap = {}
self.jobs = []
def __init__(self) -> None:
self.jmap: Dict[str, str] = {}
self.jobs: List[str] = []
if Differ.diff_executor is None:
Differ.diff_executor = self.diff_executor = concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count())
def add_diff(self, file1, file2):
def add_diff(self, file1: str, file2: str) -> None:
self.jmap[file1] = file2
self.jobs.append(file1)
def __call__(self, context=3):
def __call__(self, context: int = 3) -> Union[str, Dict[str, Patch]]:
global left_lines, right_lines
ans = {}
executor = Differ.diff_executor
ans: Dict[str, Patch] = {}
executor = self.diff_executor
assert executor is not None
jobs = {executor.submit(run_diff, key, self.jmap[key], context): key for key in self.jobs}
for future in concurrent.futures.as_completed(jobs):
key = jobs[future]

View File

@@ -3,40 +3,44 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import warnings
from functools import lru_cache
from gettext import gettext as _
from itertools import repeat, zip_longest
from math import ceil
from typing import Callable, Dict, Generator, Iterable, List, Optional, Tuple
from kitty.fast_data_types import truncate_point_for_length, wcswidth
from kitty.cli_stub import DiffCLIOptions
from kitty.utils import ScreenSize
from ..tui.images import can_display_images
from .collect import (
Segment, data_for_path, highlights_for_path, is_image, lines_for_path,
path_name_map, sanitize
Collection, Segment, data_for_path, highlights_for_path, is_image,
lines_for_path, path_name_map, sanitize
)
from .config import formats
from .diff_speedup import split_with_highlights as _split_with_highlights
from .patch import Chunk, Hunk, Patch
from ..tui.images import ImageManager, can_display_images
class ImageSupportWarning(Warning):
pass
def images_supported():
ans = getattr(images_supported, 'ans', None)
if ans is None:
images_supported.ans = ans = can_display_images()
if not ans:
warnings.warn('ImageMagick not found images cannot be displayed', ImageSupportWarning)
@lru_cache(maxsize=2)
def images_supported() -> bool:
ans = can_display_images()
if not ans:
warnings.warn('ImageMagick not found images cannot be displayed', ImageSupportWarning)
return ans
class Ref:
def __setattr__(self, name, value):
def __setattr__(self, name: str, value: object) -> None:
raise AttributeError("can't set attribute")
def __repr__(self):
def __repr__(self) -> str:
return '{}({})'.format(self.__class__.__name__, ', '.join(
'{}={}'.format(n, getattr(self, n)) for n in self.__slots__ if n != '_hash'))
@@ -44,8 +48,10 @@ class Ref:
class LineRef(Ref):
__slots__ = ('src_line_number', 'wrapped_line_idx')
src_line_number: int
wrapped_line_idx: int
def __init__(self, sln, wli=0):
def __init__(self, sln: int, wli: int = 0) -> None:
object.__setattr__(self, 'src_line_number', sln)
object.__setattr__(self, 'wrapped_line_idx', wli)
@@ -53,8 +59,10 @@ class LineRef(Ref):
class Reference(Ref):
__slots__ = ('path', 'extra')
path: str
extra: Optional[LineRef]
def __init__(self, path, extra=None):
def __init__(self, path: str, extra: Optional[LineRef] = None) -> None:
object.__setattr__(self, 'path', path)
object.__setattr__(self, 'extra', extra)
@@ -63,35 +71,41 @@ class Line:
__slots__ = ('text', 'ref', 'is_change_start', 'image_data')
def __init__(self, text, ref, change_start=False, image_data=None):
def __init__(
self,
text: str,
ref: Reference,
change_start: bool = False,
image_data: Optional[Tuple[Optional['ImagePlacement'], Optional['ImagePlacement']]] = None
) -> None:
self.text = text
self.ref = ref
self.is_change_start = change_start
self.image_data = image_data
def yield_lines_from(iterator, reference, is_change_start=True):
def yield_lines_from(iterator: Iterable[str], reference: Reference, is_change_start: bool = True) -> Generator[Line, None, None]:
for text in iterator:
yield Line(text, reference, is_change_start)
is_change_start = False
def human_readable(size, sep=' '):
def human_readable(size: int, sep: str = ' ') -> str:
""" Convert a size in bytes into a human readable form """
divisor, suffix = 1, "B"
for i, candidate in enumerate(('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')):
if size < (1 << ((i + 1) * 10)):
divisor, suffix = (1 << (i * 10)), candidate
break
size = str(float(size)/divisor)
if size.find(".") > -1:
size = size[:size.find(".")+2]
if size.endswith('.0'):
size = size[:-2]
return size + sep + suffix
s = str(float(size)/divisor)
if s.find(".") > -1:
s = s[:s.find(".")+2]
if s.endswith('.0'):
s = s[:-2]
return s + sep + suffix
def fit_in(text, count):
def fit_in(text: str, count: int) -> str:
p = truncate_point_for_length(text, count)
if p >= len(text):
return text
@@ -100,19 +114,19 @@ def fit_in(text, count):
return text[:p] + ''
def fill_in(text, sz):
def fill_in(text: str, sz: int) -> str:
w = wcswidth(text)
if w < sz:
text += ' ' * (sz - w)
return text
def place_in(text, sz):
def place_in(text: str, sz: int) -> str:
return fill_in(fit_in(text, sz), sz)
def format_func(which):
def formatted(text):
def format_func(which: str) -> Callable[[str], str]:
def formatted(text: str) -> str:
fmt = formats[which]
return '\x1b[' + fmt + 'm' + text + '\x1b[0m'
formatted.__name__ = which + '_format'
@@ -127,44 +141,47 @@ removed_format = format_func('removed')
removed_margin_format = format_func('removed_margin')
added_margin_format = format_func('added_margin')
filler_format = format_func('filler')
margin_filler_format = format_func('margin_filler')
hunk_margin_format = format_func('hunk_margin')
hunk_format = format_func('hunk')
highlight_map = {'remove': ('removed_highlight', 'removed'), 'add': ('added_highlight', 'added')}
def highlight_boundaries(ltype):
def highlight_boundaries(ltype: str) -> Tuple[str, str]:
s, e = highlight_map[ltype]
start = '\x1b[' + formats[s] + 'm'
stop = '\x1b[' + formats[e] + 'm'
return start, stop
def title_lines(left_path, right_path, args, columns, margin_size):
def title_lines(left_path: Optional[str], right_path: Optional[str], args: DiffCLIOptions, columns: int, margin_size: int) -> Generator[str, None, None]:
m = ' ' * margin_size
left_name, right_name = map(path_name_map.get, (left_path, right_path))
left_name = path_name_map.get(left_path) if left_path else None
right_name = path_name_map.get(right_path) if right_path else None
if right_name and right_name != left_name:
n1 = fit_in(m + sanitize(left_name), columns // 2 - margin_size)
n1 = fit_in(m + sanitize(left_name or ''), columns // 2 - margin_size)
n1 = place_in(n1, columns // 2)
n2 = fit_in(m + sanitize(right_name), columns // 2 - margin_size)
n2 = place_in(n2, columns // 2)
name = n1 + n2
else:
name = place_in(m + sanitize(left_name), columns)
name = place_in(m + sanitize(left_name or ''), columns)
yield title_format(place_in(name, columns))
yield title_format('' * columns)
def binary_lines(path, other_path, columns, margin_size):
def binary_lines(path: Optional[str], other_path: Optional[str], columns: int, margin_size: int) -> Generator[str, None, None]:
template = _('Binary file: {}')
available_cols = columns // 2 - margin_size
def fl(path, fmt):
def fl(path: str, fmt: Callable[[str], str]) -> str:
text = template.format(human_readable(len(data_for_path(path))))
text = place_in(text, available_cols)
return margin_format(' ' * margin_size) + fmt(text)
if path is None:
filler = render_diff_line('', '', 'filler', margin_size, available_cols)
assert other_path is not None
yield filler + fl(other_path, added_format)
elif other_path is None:
filler = render_diff_line('', '', 'filler', margin_size, available_cols)
@@ -173,7 +190,7 @@ def binary_lines(path, other_path, columns, margin_size):
yield fl(path, removed_format) + fl(other_path, added_format)
def split_to_size(line, width):
def split_to_size(line: str, width: int) -> Generator[str, None, None]:
if not line:
yield line
while line:
@@ -182,7 +199,7 @@ def split_to_size(line, width):
line = line[p:]
def truncate_points(line, width):
def truncate_points(line: str, width: int) -> Generator[int, None, None]:
pos = 0
sz = len(line)
while True:
@@ -193,18 +210,18 @@ def truncate_points(line, width):
break
def split_with_highlights(line, width, highlights, bg_highlight=None):
def split_with_highlights(line: str, width: int, highlights: List, bg_highlight: Optional[Segment] = None) -> List:
truncate_pts = list(truncate_points(line, width))
return _split_with_highlights(line, truncate_pts, highlights, bg_highlight)
margin_bg_map = {'filler': filler_format, 'remove': removed_margin_format, 'add': added_margin_format, 'context': margin_format}
margin_bg_map = {'filler': margin_filler_format, 'remove': removed_margin_format, 'add': added_margin_format, 'context': margin_format}
text_bg_map = {'filler': filler_format, 'remove': removed_format, 'add': added_format, 'context': text_format}
class DiffData:
def __init__(self, left_path, right_path, available_cols, margin_size):
def __init__(self, left_path: str, right_path: str, available_cols: int, margin_size: int):
self.left_path, self.right_path = left_path, right_path
self.available_cols = available_cols
self.margin_size = margin_size
@@ -215,24 +232,28 @@ class DiffData:
self.left_hdata = highlights_for_path(left_path)
self.right_hdata = highlights_for_path(right_path)
def left_highlights_for_line(self, line_num):
def left_highlights_for_line(self, line_num: int) -> List[Segment]:
if line_num < len(self.left_hdata):
return self.left_hdata[line_num]
return []
def right_highlights_for_line(self, line_num):
def right_highlights_for_line(self, line_num: int) -> List[Segment]:
if line_num < len(self.right_hdata):
return self.right_hdata[line_num]
return []
def render_diff_line(number, text, ltype, margin_size, available_cols):
margin = margin_bg_map[ltype](place_in(number, margin_size))
def render_diff_line(number: Optional[str], text: str, ltype: str, margin_size: int, available_cols: int) -> str:
margin = margin_bg_map[ltype](place_in(number or '', margin_size))
content = text_bg_map[ltype](fill_in(text or '', available_cols))
return margin + content
def render_diff_pair(left_line_number, left, left_is_change, right_line_number, right, right_is_change, is_first, margin_size, available_cols):
def render_diff_pair(
left_line_number: Optional[str], left: str, left_is_change: bool,
right_line_number: Optional[str], right: str, right_is_change: bool,
is_first: bool, margin_size: int, available_cols: int
) -> str:
ltype = 'filler' if left_line_number is None else ('remove' if left_is_change else 'context')
rtype = 'filler' if right_line_number is None else ('add' if right_is_change else 'context')
return (
@@ -241,14 +262,22 @@ def render_diff_pair(left_line_number, left, left_is_change, right_line_number,
)
def hunk_title(hunk_num, hunk, margin_size, available_cols):
def hunk_title(hunk_num: int, hunk: Hunk, margin_size: int, available_cols: int) -> str:
m = hunk_margin_format(' ' * margin_size)
t = '@@ -{},{} +{},{} @@ {}'.format(hunk.left_start + 1, hunk.left_count, hunk.right_start + 1, hunk.right_count, hunk.title)
return m + hunk_format(place_in(t, available_cols))
def render_half_line(line_number, line, highlights, ltype, margin_size, available_cols, changed_center=None):
bg_highlight = None
def render_half_line(
line_number: int,
line: str,
highlights: List,
ltype: str,
margin_size: int,
available_cols: int,
changed_center: Optional[Tuple[int, int]] = None
) -> Generator[str, None, None]:
bg_highlight: Optional[Segment] = None
if changed_center is not None and changed_center[0]:
prefix_count, suffix_count = changed_center
line_sz = len(line)
@@ -259,40 +288,41 @@ def render_half_line(line_number, line, highlights, ltype, margin_size, availabl
seg.end_code = stop
bg_highlight = seg
if highlights or bg_highlight:
lines = split_with_highlights(line, available_cols, highlights, bg_highlight)
lines: Iterable[str] = split_with_highlights(line, available_cols, highlights, bg_highlight)
else:
lines = split_to_size(line, available_cols)
line_number = str(line_number + 1)
lnum = str(line_number + 1)
for line in lines:
yield render_diff_line(line_number, line, ltype, margin_size, available_cols)
line_number = ''
yield render_diff_line(lnum, line, ltype, margin_size, available_cols)
lnum = ''
def lines_for_chunk(data, hunk_num, chunk, chunk_num):
def lines_for_chunk(data: DiffData, hunk_num: int, chunk: Chunk, chunk_num: int) -> Generator[Line, None, None]:
if chunk.is_context:
for i in range(chunk.left_count):
left_line_number = line_ref = chunk.left_start + i
right_line_number = chunk.right_start + i
highlights = data.left_highlights_for_line(left_line_number)
if highlights:
lines = split_with_highlights(data.left_lines[left_line_number], data.available_cols, highlights)
lines: Iterable[str] = split_with_highlights(data.left_lines[left_line_number], data.available_cols, highlights)
else:
lines = split_to_size(data.left_lines[left_line_number], data.available_cols)
left_line_number = str(left_line_number + 1)
right_line_number = str(right_line_number + 1)
left_line_number_s = str(left_line_number + 1)
right_line_number_s = str(right_line_number + 1)
for wli, text in enumerate(lines):
line = render_diff_line(left_line_number, text, 'context', data.margin_size, data.available_cols)
if right_line_number == left_line_number:
line = render_diff_line(left_line_number_s, text, 'context', data.margin_size, data.available_cols)
if right_line_number_s == left_line_number_s:
r = line
else:
r = render_diff_line(right_line_number, text, 'context', data.margin_size, data.available_cols)
r = render_diff_line(right_line_number_s, text, 'context', data.margin_size, data.available_cols)
ref = Reference(data.left_path, LineRef(line_ref, wli))
yield Line(line + r, ref)
left_line_number = right_line_number = ''
left_line_number_s = right_line_number_s = ''
else:
common = min(chunk.left_count, chunk.right_count)
for i in range(max(chunk.left_count, chunk.right_count)):
ll, rl = [], []
ll: List[str] = []
rl: List[str] = []
if i < chunk.left_count:
rln = ref_ln = chunk.left_start + i
ll.extend(render_half_line(
@@ -327,7 +357,7 @@ def lines_for_chunk(data, hunk_num, chunk, chunk_num):
yield Line(left_line + right_line, ref, i == 0 and wli == 0)
def lines_for_diff(left_path, right_path, hunks, args, columns, margin_size):
def lines_for_diff(left_path: str, right_path: str, hunks: Iterable[Hunk], args: DiffCLIOptions, columns: int, margin_size: int) -> Generator[Line, None, None]:
available_cols = columns // 2 - margin_size
data = DiffData(left_path, right_path, available_cols, margin_size)
@@ -337,7 +367,7 @@ def lines_for_diff(left_path, right_path, hunks, args, columns, margin_size):
yield from lines_for_chunk(data, hunk_num, chunk, cnum)
def all_lines(path, args, columns, margin_size, is_add=True):
def all_lines(path: str, args: DiffCLIOptions, columns: int, margin_size: int, is_add: bool = True) -> Generator[Line, None, None]:
available_cols = columns // 2 - margin_size
ltype = 'add' if is_add else 'remove'
lines = lines_for_path(path)
@@ -345,7 +375,7 @@ def all_lines(path, args, columns, margin_size, is_add=True):
msg_written = False
hdata = highlights_for_path(path)
def highlights(num):
def highlights(num: int) -> List[Segment]:
return hdata[num] if num < len(hdata) else []
for line_number, line in enumerate(lines):
@@ -362,7 +392,7 @@ def all_lines(path, args, columns, margin_size, is_add=True):
yield Line(text, ref, line_number == 0 and i == 0)
def rename_lines(path, other_path, args, columns, margin_size):
def rename_lines(path: str, other_path: str, args: DiffCLIOptions, columns: int, margin_size: int) -> Generator[str, None, None]:
m = ' ' * margin_size
for line in split_to_size(_('The file {0} was renamed to {1}').format(
sanitize(path_name_map[path]), sanitize(path_name_map[other_path])), columns - margin_size):
@@ -371,7 +401,7 @@ def rename_lines(path, other_path, args, columns, margin_size):
class Image:
def __init__(self, image_id, width, height, margin_size, screen_size):
def __init__(self, image_id: int, width: int, height: int, margin_size: int, screen_size: ScreenSize) -> None:
self.image_id = image_id
self.width, self.height = width, height
self.rows = int(ceil(self.height / screen_size.cell_height))
@@ -381,18 +411,23 @@ class Image:
class ImagePlacement:
def __init__(self, image, row):
def __init__(self, image: Image, row: int) -> None:
self.image = image
self.row = row
def render_image(path, is_left, available_cols, margin_size, image_manager):
def render_image(
path: str,
is_left: bool,
available_cols: int, margin_size: int,
image_manager: ImageManager
) -> Generator[Tuple[str, Reference, Optional[ImagePlacement]], None, None]:
lnum = 0
margin_fmt = removed_margin_format if is_left else added_margin_format
m = margin_fmt(' ' * margin_size)
fmt = removed_format if is_left else added_format
def yield_split(text):
def yield_split(text: str) -> Generator[Tuple[str, Reference, Optional[ImagePlacement]], None, None]:
nonlocal lnum
for i, line in enumerate(split_to_size(text, available_cols)):
yield m + fmt(place_in(line, available_cols)), Reference(path, LineRef(lnum, i)), None
@@ -414,9 +449,16 @@ def render_image(path, is_left, available_cols, margin_size, image_manager):
lnum += 1
def image_lines(left_path, right_path, columns, margin_size, image_manager):
def image_lines(
left_path: Optional[str],
right_path: Optional[str],
columns: int,
margin_size: int,
image_manager: ImageManager
) -> Generator[Line, None, None]:
available_cols = columns // 2 - margin_size
left_lines, right_lines = iter(()), iter(())
left_lines: Iterable[Tuple[str, Reference, Optional[ImagePlacement]]] = iter(())
right_lines: Iterable[Tuple[str, Reference, Optional[ImagePlacement]]] = iter(())
if left_path is not None:
left_lines = render_image(left_path, True, available_cols, margin_size, image_manager)
if right_path is not None:
@@ -439,52 +481,68 @@ def image_lines(left_path, right_path, columns, margin_size, image_manager):
is_change_start = False
def render_diff(collection, diff_map, args, columns, image_manager):
largest_line_number = 0
for path, item_type, other_path in collection:
if item_type == 'diff':
patch = diff_map.get(path)
if patch is not None:
largest_line_number = max(largest_line_number, patch.largest_line_number)
class RenderDiff:
margin_size = render_diff.margin_size = max(3, len(str(largest_line_number)) + 1)
last_item_num = len(collection) - 1
margin_size: int = 0
for i, (path, item_type, other_path) in enumerate(collection):
item_ref = Reference(path)
is_binary = isinstance(data_for_path(path), bytes)
if not is_binary and item_type == 'diff' and isinstance(data_for_path(other_path), bytes):
is_binary = True
is_img = is_binary and (is_image(path) or is_image(other_path)) and images_supported()
yield from yield_lines_from(title_lines(path, other_path, args, columns, margin_size), item_ref, False)
if item_type == 'diff':
if is_binary:
if is_img:
ans = image_lines(path, other_path, columns, margin_size, image_manager)
def __call__(
self,
collection: Collection,
diff_map: Dict[str, Patch],
args: DiffCLIOptions,
columns: int,
image_manager: ImageManager
) -> Generator[Line, None, None]:
largest_line_number = 0
for path, item_type, other_path in collection:
if item_type == 'diff':
patch = diff_map.get(path)
if patch is not None:
largest_line_number = max(largest_line_number, patch.largest_line_number)
margin_size = self.margin_size = max(3, len(str(largest_line_number)) + 1)
last_item_num = len(collection) - 1
for i, (path, item_type, other_path) in enumerate(collection):
item_ref = Reference(path)
is_binary = isinstance(data_for_path(path), bytes)
if not is_binary and item_type == 'diff' and isinstance(data_for_path(other_path), bytes):
is_binary = True
is_img = is_binary and (is_image(path) or is_image(other_path)) and images_supported()
yield from yield_lines_from(title_lines(path, other_path, args, columns, margin_size), item_ref, False)
if item_type == 'diff':
if is_binary:
if is_img:
ans = image_lines(path, other_path, columns, margin_size, image_manager)
else:
ans = yield_lines_from(binary_lines(path, other_path, columns, margin_size), item_ref)
else:
ans = yield_lines_from(binary_lines(path, other_path, columns, margin_size), item_ref)
else:
ans = lines_for_diff(path, other_path, diff_map[path], args, columns, margin_size)
elif item_type == 'add':
if is_binary:
if is_img:
ans = image_lines(None, path, columns, margin_size, image_manager)
assert other_path is not None
ans = lines_for_diff(path, other_path, diff_map[path], args, columns, margin_size)
elif item_type == 'add':
if is_binary:
if is_img:
ans = image_lines(None, path, columns, margin_size, image_manager)
else:
ans = yield_lines_from(binary_lines(None, path, columns, margin_size), item_ref)
else:
ans = yield_lines_from(binary_lines(None, path, columns, margin_size), item_ref)
else:
ans = all_lines(path, args, columns, margin_size, is_add=True)
elif item_type == 'removal':
if is_binary:
if is_img:
ans = image_lines(path, None, columns, margin_size, image_manager)
ans = all_lines(path, args, columns, margin_size, is_add=True)
elif item_type == 'removal':
if is_binary:
if is_img:
ans = image_lines(path, None, columns, margin_size, image_manager)
else:
ans = yield_lines_from(binary_lines(path, None, columns, margin_size), item_ref)
else:
ans = yield_lines_from(binary_lines(path, None, columns, margin_size), item_ref)
ans = all_lines(path, args, columns, margin_size, is_add=False)
elif item_type == 'rename':
assert other_path is not None
ans = yield_lines_from(rename_lines(path, other_path, args, columns, margin_size), item_ref)
else:
ans = all_lines(path, args, columns, margin_size, is_add=False)
elif item_type == 'rename':
ans = yield_lines_from(rename_lines(path, other_path, args, columns, margin_size), item_ref)
else:
raise ValueError('Unsupported item type: {}'.format(item_type))
yield from ans
if i < last_item_num:
yield Line('', item_ref)
raise ValueError('Unsupported item type: {}'.format(item_type))
yield from ans
if i < last_item_num:
yield Line('', item_ref)
render_diff = RenderDiff()

View File

@@ -3,11 +3,17 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import re
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Tuple
from kitty.fast_data_types import wcswidth
from kitty.options_stub import DiffOptions
from ..tui.operations import styled
if TYPE_CHECKING:
from .render import Line
Line
class BadRegex(ValueError):
pass
@@ -15,8 +21,8 @@ class BadRegex(ValueError):
class Search:
def __init__(self, opts, query, is_regex, is_backward):
self.matches = {}
def __init__(self, opts: DiffOptions, query: str, is_regex: bool, is_backward: bool):
self.matches: Dict[int, List[Tuple[int, str]]] = {}
self.count = 0
self.style = styled('|', fg=opts.search_fg, bg=opts.search_bg).split('|', 1)[0]
if not is_regex:
@@ -26,7 +32,7 @@ class Search:
except Exception:
raise BadRegex('Not a valid regex: {}'.format(query))
def __call__(self, diff_lines, margin_size, cols):
def __call__(self, diff_lines: Iterable['Line'], margin_size: int, cols: int) -> bool:
self.matches = {}
self.count = 0
half_width = cols // 2
@@ -38,7 +44,7 @@ class Search:
left, right = text[margin_size:half_width + 1], text[right_offset:]
matches = []
def add(which, offset):
def add(which: str, offset: int) -> None:
for m in find(which):
before = which[:m.start()]
matches.append((wcswidth(before) + offset, m.group()))
@@ -50,13 +56,13 @@ class Search:
self.matches[i] = matches
return bool(self.matches)
def __contains__(self, i):
def __contains__(self, i: int) -> bool:
return i in self.matches
def __len__(self):
def __len__(self) -> int:
return self.count
def highlight_line(self, write, line_num):
def highlight_line(self, write: Callable[[str], None], line_num: int) -> bool:
highlights = self.matches.get(line_num)
if not highlights:
return False

View File

@@ -9,32 +9,52 @@ import sys
from functools import lru_cache
from gettext import gettext as _
from itertools import repeat
from typing import (
Any, Callable, Dict, Generator, Iterable, List, Optional, Pattern,
Sequence, Set, Tuple, Type, cast
)
from kitty.cli import parse_args
from kitty.cli_stub import HintsCLIOptions
from kitty.fast_data_types import set_clipboard_string
from kitty.key_encoding import ESCAPE, backspace_key, enter_key
from kitty.utils import screen_size_function
from kitty.key_encoding import (
KeyEvent, backspace_key, enter_key, key_defs as K
)
from kitty.typing import BossType, KittyCommonOpts
from kitty.utils import ScreenSize, screen_size_function, set_primary_selection
from ..tui.handler import Handler
from ..tui.handler import Handler, result_handler
from ..tui.loop import Loop
from ..tui.operations import faint, styled
URL_PREFIXES = 'http https file ftp'.split()
@lru_cache()
def kitty_common_opts() -> KittyCommonOpts:
import json
v = os.environ.get('KITTY_COMMON_OPTS')
if v:
return cast(KittyCommonOpts, json.loads(v))
from kitty.config import common_opts_as_dict
return common_opts_as_dict()
DEFAULT_HINT_ALPHABET = string.digits + string.ascii_lowercase
screen_size = screen_size_function()
DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$'
ESCAPE = K['ESCAPE']
class Mark:
__slots__ = ('index', 'start', 'end', 'text')
__slots__ = ('index', 'start', 'end', 'text', 'groupdict')
def __init__(self, index, start, end, text):
def __init__(self, index: int, start: int, end: int, text: str, groupdict: Any):
self.index, self.start, self.end = index, start, end
self.text = text
self.groupdict = groupdict
@lru_cache(maxsize=2048)
def encode_hint(num, alphabet):
def encode_hint(num: int, alphabet: str) -> str:
res = ''
d = len(alphabet)
while not res or num > 0:
@@ -43,7 +63,7 @@ def encode_hint(num, alphabet):
return res
def decode_hint(x, alphabet=DEFAULT_HINT_ALPHABET):
def decode_hint(x: str, alphabet: str = DEFAULT_HINT_ALPHABET) -> int:
base = len(alphabet)
index_map = {c: i for i, c in enumerate(alphabet)}
i = 0
@@ -52,7 +72,7 @@ def decode_hint(x, alphabet=DEFAULT_HINT_ALPHABET):
return i
def highlight_mark(m, text, current_input, alphabet):
def highlight_mark(m: Mark, text: str, current_input: str, alphabet: str) -> str:
hint = encode_hint(m.index, alphabet)
if current_input and not hint.startswith(current_input):
return faint(text)
@@ -68,7 +88,7 @@ def highlight_mark(m, text, current_input, alphabet):
)
def render(text, current_input, all_marks, ignore_mark_indices, alphabet):
def render(text: str, current_input: str, all_marks: Sequence[Mark], ignore_mark_indices: Set[int], alphabet: str) -> str:
for mark in reversed(all_marks):
if mark.index in ignore_mark_indices:
continue
@@ -82,39 +102,47 @@ def render(text, current_input, all_marks, ignore_mark_indices, alphabet):
class Hints(Handler):
def __init__(self, text, all_marks, index_map, args):
def __init__(self, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], args: HintsCLIOptions):
self.text, self.index_map = text, index_map
self.alphabet = args.alphabet or DEFAULT_HINT_ALPHABET
self.all_marks = all_marks
self.ignore_mark_indices = set()
self.ignore_mark_indices: Set[int] = set()
self.args = args
self.window_title = _('Choose URL') if args.type == 'url' else _('Choose text')
self.multiple = args.multiple
self.match_suffix = self.get_match_suffix(args)
self.chosen = []
self.chosen: List[Mark] = []
self.reset()
def get_match_suffix(self, args):
@property
def text_matches(self) -> List[str]:
return [m.text + self.match_suffix for m in self.chosen]
@property
def groupdicts(self) -> List[Any]:
return [m.groupdict for m in self.chosen]
def get_match_suffix(self, args: HintsCLIOptions) -> str:
if args.add_trailing_space == 'always':
return ' '
if args.add_trailing_space == 'never':
return ''
return ' ' if args.multiple else ''
def reset(self):
def reset(self) -> None:
self.current_input = ''
self.current_text = None
self.current_text: Optional[str] = None
def init_terminal_state(self):
def init_terminal_state(self) -> None:
self.cmd.set_cursor_visible(False)
self.cmd.set_window_title(self.window_title)
self.cmd.set_line_wrapping(False)
def initialize(self):
def initialize(self) -> None:
self.init_terminal_state()
self.draw_screen()
def on_text(self, text, in_bracketed_paste):
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
changed = False
for c in text:
if c in self.alphabet:
@@ -126,7 +154,7 @@ class Hints(Handler):
if encode_hint(idx, self.alphabet).startswith(self.current_input)
]
if len(matches) == 1:
self.chosen.append(matches[0].text + self.match_suffix)
self.chosen.append(matches[0])
if self.multiple:
self.ignore_mark_indices.add(matches[0].index)
self.reset()
@@ -136,7 +164,7 @@ class Hints(Handler):
self.current_text = None
self.draw_screen()
def on_key(self, key_event):
def on_key(self, key_event: KeyEvent) -> None:
if key_event is backspace_key:
self.current_input = self.current_input[:-1]
self.current_text = None
@@ -144,7 +172,7 @@ class Hints(Handler):
elif key_event is enter_key and self.current_input:
try:
idx = decode_hint(self.current_input, self.alphabet)
self.chosen.append(self.index_map[idx].text + self.match_suffix)
self.chosen.append(self.index_map[idx])
self.ignore_mark_indices.add(idx)
except Exception:
self.current_input = ''
@@ -159,43 +187,45 @@ class Hints(Handler):
elif key_event.key is ESCAPE:
self.quit_loop(0 if self.multiple else 1)
def on_interrupt(self):
def on_interrupt(self) -> None:
self.quit_loop(1)
def on_eot(self):
def on_eot(self) -> None:
self.quit_loop(1)
def on_resize(self, new_size):
def on_resize(self, new_size: ScreenSize) -> None:
self.draw_screen()
def draw_screen(self):
def draw_screen(self) -> None:
if self.current_text is None:
self.current_text = render(self.text, self.current_input, self.all_marks, self.ignore_mark_indices, self.alphabet)
self.cmd.clear_screen()
self.write(self.current_text)
def regex_finditer(pat, minimum_match_length, text):
def regex_finditer(pat: Pattern, minimum_match_length: int, text: str) -> Generator[Tuple[int, int, Dict], None, None]:
has_named_groups = bool(pat.groupindex)
for m in pat.finditer(text):
s, e = m.span(pat.groups)
s, e = m.span(0 if has_named_groups else pat.groups)
while e > s + 1 and text[e-1] == '\0':
e -= 1
if e - s >= minimum_match_length:
yield s, e
yield s, e, m.groupdict()
closing_bracket_map = {'(': ')', '[': ']', '{': '}', '<': '>', '*': '*', '"': '"', "'": "'"}
opening_brackets = ''.join(closing_bracket_map)
postprocessor_map = {}
PostprocessorFunc = Callable[[str, int, int], Tuple[int, int]]
postprocessor_map: Dict[str, PostprocessorFunc] = {}
def postprocessor(func):
def postprocessor(func: PostprocessorFunc) -> PostprocessorFunc:
postprocessor_map[func.__name__] = func
return func
@postprocessor
def url(text, s, e):
def url(text: str, s: int, e: int) -> Tuple[int, int]:
if s > 4 and text[s - 5:s] == 'link:': # asciidoc URLs
url = text[s:e]
idx = url.rfind('[')
@@ -217,7 +247,7 @@ def url(text, s, e):
@postprocessor
def brackets(text, s, e):
def brackets(text: str, s: int, e: int) -> Tuple[int, int]:
# Remove matching brackets
if s < e <= len(text):
before = text[s]
@@ -228,7 +258,7 @@ def brackets(text, s, e):
@postprocessor
def quotes(text, s, e):
def quotes(text: str, s: int, e: int) -> Tuple[int, int]:
# Remove matching quotes
if s < e <= len(text):
before = text[s]
@@ -238,36 +268,42 @@ def quotes(text, s, e):
return s, e
def mark(pattern, post_processors, text, args):
def mark(pattern: str, post_processors: Iterable[PostprocessorFunc], text: str, args: HintsCLIOptions) -> Generator[Mark, None, None]:
pat = re.compile(pattern)
for idx, (s, e) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
for idx, (s, e, groupdict) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
for func in post_processors:
s, e = func(text, s, e)
mark_text = text[s:e].replace('\n', '').replace('\0', '')
yield Mark(idx, s, e, mark_text)
yield Mark(idx, s, e, mark_text, groupdict)
def run_loop(args, text, all_marks, index_map):
def run_loop(args: HintsCLIOptions, text: str, all_marks: Sequence[Mark], index_map: Dict[int, Mark], extra_cli_args: Sequence[str] = ()) -> Dict[str, Any]:
loop = Loop()
handler = Hints(text, all_marks, index_map, args)
loop.loop(handler)
if handler.chosen and loop.return_code == 0:
return {'match': handler.chosen, 'programs': args.program,
'multiple_joiner': args.multiple_joiner,
'type': args.type}
return {
'match': handler.text_matches, 'programs': args.program,
'multiple_joiner': args.multiple_joiner, 'customize_processing': args.customize_processing,
'type': args.type, 'groupdicts': handler.groupdicts, 'extra_cli_args': extra_cli_args, 'linenum_action': args.linenum_action
}
raise SystemExit(loop.return_code)
def escape(chars):
def escape(chars: str) -> str:
return chars.replace('\\', '\\\\').replace('-', r'\-').replace(']', r'\]')
def functions_for(args):
def functions_for(args: HintsCLIOptions) -> Tuple[str, List[PostprocessorFunc]]:
post_processors = []
if args.type == 'url':
if args.url_prefixes == 'default':
url_prefixes = kitty_common_opts()['url_prefixes']
else:
url_prefixes = tuple(args.url_prefixes.split(','))
from .url_regex import url_delimiters
pattern = '(?:{})://[^{}]{{3,}}'.format(
'|'.join(args.url_prefixes.split(',')), url_delimiters
'|'.join(url_prefixes), url_delimiters
)
post_processors.append(url)
elif args.type == 'path':
@@ -280,8 +316,7 @@ def functions_for(args):
elif args.type == 'word':
chars = args.word_characters
if chars is None:
import json
chars = json.loads(os.environ['KITTY_COMMON_OPTS'])['select_by_word_characters']
chars = kitty_common_opts()['select_by_word_characters']
pattern = r'(?u)[{}\w]{{{},}}'.format(escape(chars), args.minimum_match_length)
post_processors.extend((brackets, quotes))
else:
@@ -289,8 +324,8 @@ def functions_for(args):
return pattern, post_processors
def convert_text(text, cols):
lines = []
def convert_text(text: str, cols: int) -> str:
lines: List[str] = []
empty_line = '\0' * cols
for full_line in text.split('\n'):
if full_line:
@@ -303,29 +338,61 @@ def convert_text(text, cols):
return '\n'.join(lines)
def parse_input(text):
def parse_input(text: str) -> str:
try:
cols = int(os.environ['OVERLAID_WINDOW_COLS'])
except KeyError:
cols = screen_size().cols
cols = screen_size_function()().cols
return convert_text(text, cols)
def run(args, text):
def linenum_marks(text: str, args: HintsCLIOptions, Mark: Type[Mark], extra_cli_args: Sequence[str], *a: Any) -> Generator[Mark, None, None]:
regex = args.regex
if regex == DEFAULT_REGEX:
regex = r'(?P<path>(?:\S*/\S+?)|(?:\S+[.][a-zA-Z0-9]{2,7})):(?P<line>\d+)'
yield from mark(regex, [brackets, quotes], text, args)
def load_custom_processor(customize_processing: str) -> Any:
if customize_processing.startswith('::import::'):
import importlib
m = importlib.import_module(customize_processing[len('::import::'):])
return {k: getattr(m, k) for k in dir(m)}
if customize_processing == '::linenum::':
return {'mark': linenum_marks, 'handle_result': linenum_handle_result}
from kitty.constants import resolve_custom_file
custom_path = resolve_custom_file(customize_processing)
import runpy
return runpy.run_path(custom_path, run_name='__main__')
def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]:
try:
pattern, post_processors = functions_for(args)
text = parse_input(text)
all_marks = tuple(mark(pattern, post_processors, text, args))
pattern, post_processors = functions_for(args)
if args.type == 'linenum':
args.customize_processing = '::linenum::'
if args.customize_processing:
m = load_custom_processor(args.customize_processing)
if 'mark' in m:
all_marks = tuple(m['mark'](text, args, Mark, extra_cli_args))
else:
all_marks = tuple(mark(pattern, post_processors, text, args))
else:
all_marks = tuple(mark(pattern, post_processors, text, args))
if not all_marks:
input(_('No {} found, press Enter to quit.').format(
'URLs' if args.type == 'url' else 'matches'
))
return
return None
largest_index = all_marks[-1].index
offset = max(0, args.hints_offset)
for m in all_marks:
m.index = largest_index - m.index + offset
if args.ascending:
m.index += offset
else:
m.index = largest_index - m.index + offset
index_map = {m.index: m for m in all_marks}
except Exception:
import traceback
@@ -333,7 +400,7 @@ def run(args, text):
input('Press Enter to quit.')
raise SystemExit(1)
return run_loop(args, text, all_marks, index_map)
return run_loop(args, text, all_marks, index_map, extra_cli_args)
# CLI {{{
@@ -342,30 +409,52 @@ OPTIONS = r'''
type=list
What program to use to open matched text. Defaults to the default open program
for the operating system. Use a value of :file:`-` to paste the match into the
terminal window instead. A value of :file:`@` will copy the match to the clipboard.
A value of :file:`default` will run the default open program. Can be specified
multiple times to run multiple programs.
terminal window instead. A value of :file:`@` will copy the match to the
clipboard. A value of :file:`*` will copy the match to the primary selection
(on systems that support primary selections). A value of :file:`default` will
run the default open program. Can be specified multiple times to run multiple
programs.
--type
default=url
choices=url,regex,path,line,hash,word
The type of text to search for.
choices=url,regex,path,line,hash,word,linenum
The type of text to search for. A value of :code:`linenum` looks for error messages
using the pattern specified with :option:`--regex`, which must have the named groups,
path and line. If not specified, will look for :code:`path:line`.
The :option:`--linenum-action` option controls what to do with the selected error message.
--regex
default=(?m)^\s*(.+)\s*$
default={default_regex}
The regular expression to use when :option:`kitty +kitten hints --type`=regex.
The regular expression is in python syntax. If you specify a group in
The regular expression is in python syntax. If you specify a numbered group in
the regular expression only the group will be matched. This allow you to match
text ignoring a prefix/suffix, as needed. The default expression matches lines.
To match text over multiple lines you should prefix the regular expression with
:code:`(?ms)`, which turns on MULTILINE and DOTALL modes for the regex engine.
If you specify named groups and a :option:`kitty +kitten hints --program` then
the program will be passed arguments corresponding to each named group of
the form key=value.
--linenum-action
default=self
type=choice
choices=self,window,tab,os_window,background
The action to perform on the matched errors. The actual action is whatever
arguments are provided to the kitten, for example:
:code:`kitty + kitten hints --type=linenum vim +{line} {path}`
will open the matched path at the matched line number in vim. This option
controls where the action is executed: :code:`self` means the current window,
:code:`window` a new kitty window, :code:`tab` a new tab, :code:`os_window`
a new OS window and :code:`background` run in the background.
--url-prefixes
default={0}
Comma separated list of recognized URL prefixes.
default=default
Comma separated list of recognized URL prefixes. Defaults, to
the list of prefixes defined in kitty.conf.
--word-characters
@@ -418,50 +507,105 @@ unless you specify the hints offset as zero the first match will be highlighted
the second character you specify.
'''.format(','.join(sorted(URL_PREFIXES))).format
--ascending
type=bool-set
Have the hints increase from top to bottom instead of decreasing from top to bottom.
--customize-processing
Name of a python file in the kitty config directory which will be imported to provide
custom implementations for pattern finding and performing actions
on selected matches. See https://sw.kovidgoyal.net/kitty/kittens/hints.html
for details. You can also specify absolute paths to load the script from elsewhere.
'''.format(
default_regex=DEFAULT_REGEX,
line='{{line}}', path='{{path}}'
).format
help_text = 'Select text from the screen using the keyboard. Defaults to searching for URLs.'
usage = ''
def parse_hints_args(args):
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten hints')
def parse_hints_args(args: List[str]) -> Tuple[HintsCLIOptions, List[str]]:
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten hints', result_class=HintsCLIOptions)
def main(args):
def main(args: List[str]) -> Optional[Dict[str, Any]]:
text = ''
if sys.stdin.isatty():
if '--help' not in args and '-h' not in args:
print('You must pass the text to be hinted on STDIN', file=sys.stderr)
input(_('Press Enter to quit'))
return
return None
else:
text = sys.stdin.buffer.read().decode('utf-8')
sys.stdin = open(os.ctermid())
try:
args, items = parse_hints_args(args[1:])
opts, items = parse_hints_args(args[1:])
except SystemExit as e:
if e.code != 0:
print(e.args[0], file=sys.stderr)
input(_('Press Enter to quit'))
return
if items:
return None
if items and not (opts.customize_processing or opts.type == 'linenum'):
print('Extra command line arguments present: {}'.format(' '.join(items)), file=sys.stderr)
input(_('Press Enter to quit'))
return run(args, text)
return run(opts, text, items)
def handle_result(args, data, target_window_id, boss):
def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType, extra_cli_args: Sequence[str], *a: Any) -> None:
for m, g in zip(data['match'], data['groupdicts']):
if m:
path, line = g['path'], g['line']
path = path.split(':')[-1]
line = int(line)
break
else:
return
cmd = [x.format(path=path, line=line) for x in extra_cli_args or ('vim', '+{line}', '{path}')]
w = boss.window_id_map.get(target_window_id)
action = data['linenum_action']
if action == 'self':
if w is not None:
import shlex
text = ' '.join(shlex.quote(arg) for arg in cmd)
w.paste_bytes(text + '\r')
elif action == 'background':
import subprocess
subprocess.Popen(cmd)
else:
getattr(boss, {
'window': 'new_window_with_cwd', 'tab': 'new_tab_with_cwd', 'os_window': 'new_os_window_with_cwd'
}[action])(*cmd)
@result_handler(type_of_input='screen')
def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType) -> None:
if data['customize_processing']:
m = load_custom_processor(data['customize_processing'])
if 'handle_result' in m:
m['handle_result'](args, data, target_window_id, boss, data['extra_cli_args'])
return None
programs = data['programs'] or ('default',)
matches = tuple(filter(None, data['match']))
matches: List[str] = []
groupdicts = []
for m, g in zip(data['match'], data['groupdicts']):
if m:
matches.append(m)
groupdicts.append(g)
joiner = data['multiple_joiner']
try:
is_int = int(joiner)
is_int: Optional[int] = int(joiner)
except Exception:
is_int = None
text_type = data['type']
@lru_cache()
def joined_text():
def joined_text() -> str:
if is_int is not None:
try:
return matches[is_int]
@@ -483,26 +627,30 @@ def handle_result(args, data, target_window_id, boss):
w.paste(joined_text())
elif program == '@':
set_clipboard_string(joined_text())
elif program == '*':
set_primary_selection(joined_text())
else:
cwd = None
w = boss.window_id_map.get(target_window_id)
if w is not None:
cwd = w.cwd_of_child
program = None if program == 'default' else program
for m in matches:
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)
handle_result.type_of_input = 'screen'
if __name__ == '__main__':
# Run with kitty +kitten hints
ans = main(sys.argv)
if ans:
print(ans)
elif __name__ == '__doc__':
sys.cli_docs['usage'] = usage
sys.cli_docs['options'] = OPTIONS
sys.cli_docs['help_text'] = help_text
cd = sys.cli_docs # type: ignore
cd['usage'] = usage
cd['options'] = OPTIONS
cd['help_text'] = help_text
# }}}

View File

@@ -9,20 +9,27 @@ import signal
import sys
import zlib
from base64 import standard_b64encode
from collections import namedtuple
from functools import lru_cache
from math import ceil
from tempfile import NamedTemporaryFile
from typing import (
Dict, Generator, List, NamedTuple, Optional, Pattern, Tuple, Union
)
from kitty.cli import parse_args
from kitty.cli_stub import IcatCLIOptions
from kitty.constants import appname
from kitty.utils import TTYIO, fit_image, screen_size_function
from kitty.typing import GRT_f, GRT_t
from kitty.utils import (
TTYIO, ScreenSize, ScreenSizeGetter, fit_image, screen_size_function
)
from ..tui.images import (
ConvertFailed, NoImageMagick, OpenFailed, convert, fsenc, identify
ConvertFailed, GraphicsCommand, NoImageMagick, OpenFailed, convert, fsenc,
identify
)
from ..tui.operations import clear_images_on_screen, serialize_gr_command
from ..tui.operations import clear_images_on_screen
screen_size = None
OPTIONS = '''\
--align
type=choices
@@ -94,23 +101,43 @@ but you can turn it off or on explicitly, if needed.
--silent
type=bool-set
Do not print out anything to stdout during operation.
--z-index -z
default=0
Z-index of the image. When negative, text will be displayed on top of the image. Use
a double minus for values under the threshold for drawing images under cell background
colors. For example, --1 evaluates as -1,073,741,825.
'''
def options_spec():
if not hasattr(options_spec, 'ans'):
options_spec.ans = OPTIONS.format(
appname='{}-icat'.format(appname),
)
return options_spec.ans
screen_size: Optional[ScreenSizeGetter] = None
can_transfer_with_files = False
def write_gr_cmd(cmd, payload=None):
sys.stdout.buffer.write(serialize_gr_command(cmd, payload))
def get_screen_size_function() -> ScreenSizeGetter:
global screen_size
if screen_size is None:
screen_size = screen_size_function()
return screen_size
def get_screen_size() -> ScreenSize:
screen_size = get_screen_size_function()
return screen_size()
@lru_cache(maxsize=2)
def options_spec() -> str:
return OPTIONS.format(appname='{}-icat'.format(appname))
def write_gr_cmd(cmd: GraphicsCommand, payload: Optional[bytes] = None) -> None:
sys.stdout.buffer.write(cmd.serialize(payload or b''))
sys.stdout.flush()
def calculate_in_cell_x_offset(width, cell_width, align):
def calculate_in_cell_x_offset(width: int, cell_width: int, align: str) -> int:
if align == 'left':
return 0
extra_pixels = width % cell_width
@@ -121,17 +148,17 @@ def calculate_in_cell_x_offset(width, cell_width, align):
return (cell_width - extra_pixels) // 2
def set_cursor(cmd, width, height, align):
ss = screen_size()
def set_cursor(cmd: GraphicsCommand, width: int, height: int, align: str) -> None:
ss = get_screen_size()
cw = int(ss.width / ss.cols)
num_of_cells_needed = int(ceil(width / cw))
if num_of_cells_needed > ss.cols:
w, h = fit_image(width, height, ss.width, height)
ch = int(ss.height / ss.rows)
num_of_rows_needed = int(ceil(height / ch))
cmd['c'], cmd['r'] = ss.cols, num_of_rows_needed
cmd.c, cmd.r = ss.cols, num_of_rows_needed
else:
cmd['X'] = calculate_in_cell_x_offset(width, cw, align)
cmd.X = calculate_in_cell_x_offset(width, cw, align)
extra_cells = 0
if align == 'center':
extra_cells = (ss.cols - num_of_cells_needed) // 2
@@ -141,12 +168,12 @@ def set_cursor(cmd, width, height, align):
sys.stdout.buffer.write(b' ' * extra_cells)
def set_cursor_for_place(place, cmd, width, height, align):
def set_cursor_for_place(place: 'Place', cmd: GraphicsCommand, width: int, height: int, align: str) -> None:
x = place.left + 1
ss = screen_size()
ss = get_screen_size()
cw = int(ss.width / ss.cols)
num_of_cells_needed = int(ceil(width / cw))
cmd['X'] = calculate_in_cell_x_offset(width, cw, align)
cmd.X = calculate_in_cell_x_offset(width, cw, align)
extra_cells = 0
if align == 'center':
extra_cells = (place.width - num_of_cells_needed) // 2
@@ -155,27 +182,38 @@ def set_cursor_for_place(place, cmd, width, height, align):
sys.stdout.buffer.write('\033[{};{}H'.format(place.top + 1, x + extra_cells).encode('ascii'))
def write_chunked(cmd, data):
if cmd['f'] != 100:
def write_chunked(cmd: GraphicsCommand, data: bytes) -> None:
if cmd.f != 100:
data = zlib.compress(data)
cmd['o'] = 'z'
cmd.o = 'z'
data = standard_b64encode(data)
while data:
chunk, data = data[:4096], data[4096:]
m = 1 if data else 0
cmd['m'] = m
cmd.m = 1 if data else 0
write_gr_cmd(cmd, chunk)
cmd.clear()
def show(outfile, width, height, fmt, transmit_mode='t', align='center', place=None):
cmd = {'a': 'T', 'f': fmt, 's': width, 'v': height}
def show(
outfile: str,
width: int, height: int, zindex: int,
fmt: 'GRT_f',
transmit_mode: 'GRT_t' = 't',
align: str = 'center',
place: Optional['Place'] = None
) -> None:
cmd = GraphicsCommand()
cmd.a = 'T'
cmd.f = fmt
cmd.s = width
cmd.v = height
cmd.z = zindex
if place:
set_cursor_for_place(place, cmd, width, height, align)
else:
set_cursor(cmd, width, height, align)
if detect_support.has_files:
cmd['t'] = transmit_mode
if can_transfer_with_files:
cmd.t = transmit_mode
write_gr_cmd(cmd, standard_b64encode(os.path.abspath(outfile).encode(fsenc)))
else:
with open(outfile, 'rb') as f:
@@ -183,35 +221,49 @@ def show(outfile, width, height, fmt, transmit_mode='t', align='center', place=N
if transmit_mode == 't':
os.unlink(outfile)
if fmt == 100:
cmd['S'] = len(data)
cmd.S = len(data)
write_chunked(cmd, data)
def process(path, args, is_tempfile):
def parse_z_index(val: str) -> int:
origin = 0
if val.startswith('--'):
val = val[1:]
origin = -1073741824
return origin + int(val)
class ParsedOpts:
place: Optional['Place'] = None
z_index: int = 0
def process(path: str, args: IcatCLIOptions, parsed_opts: ParsedOpts, is_tempfile: bool) -> bool:
m = identify(path)
ss = screen_size()
available_width = args.place.width * (ss.width / ss.cols) if args.place else ss.width
available_height = args.place.height * (ss.height / ss.rows) if args.place else 10 * m.height
ss = get_screen_size()
available_width = parsed_opts.place.width * (ss.width // ss.cols) if parsed_opts.place else ss.width
available_height = parsed_opts.place.height * (ss.height // ss.rows) if parsed_opts.place else 10 * m.height
needs_scaling = m.width > available_width or m.height > available_height
needs_scaling = needs_scaling or args.scale_up
file_removed = False
if m.fmt == 'png' and not needs_scaling:
outfile = path
transmit_mode = 't' if is_tempfile else 'f'
fmt = 100
transmit_mode: 'GRT_t' = 't' if is_tempfile else 'f'
fmt: 'GRT_f' = 100
width, height = m.width, m.height
file_removed = transmit_mode == 't'
else:
fmt = 24 if m.mode == 'rgb' else 32
transmit_mode = 't'
outfile, width, height = convert(path, m, available_width, available_height, args.scale_up)
show(outfile, width, height, fmt, transmit_mode, align=args.align, place=args.place)
show(outfile, width, height, parsed_opts.z_index, fmt, transmit_mode, align=args.align, place=parsed_opts.place)
if not args.place:
print() # ensure cursor is on a new line
return file_removed
def scan(d):
def scan(d: str) -> Generator[Tuple[str, str], None, None]:
for dirpath, dirnames, filenames in os.walk(d):
for f in filenames:
mt = mimetypes.guess_type(f)[0]
@@ -219,23 +271,24 @@ def scan(d):
yield os.path.join(dirpath, f), mt
def detect_support(wait_for=10, silent=False):
def detect_support(wait_for: float = 10, silent: bool = False) -> bool:
global can_transfer_with_files
if not silent:
print('Checking for graphics ({}s max. wait)...'.format(wait_for), end='\r')
sys.stdout.flush()
try:
received = b''
responses = {}
responses: Dict[int, bool] = {}
def parse_responses():
def parse_responses() -> None:
for m in re.finditer(b'\033_Gi=([1|2]);(.+?)\033\\\\', received):
iid = m.group(1)
if iid in (b'1', b'2'):
iid = int(iid.decode('ascii'))
if iid not in responses:
responses[iid] = m.group(2) == b'OK'
iid_ = int(iid.decode('ascii'))
if iid_ not in responses:
responses[iid_] = m.group(2) == b'OK'
def more_needed(data):
def more_needed(data: bytes) -> bool:
nonlocal received
received += data
parse_responses()
@@ -243,23 +296,36 @@ def detect_support(wait_for=10, silent=False):
with NamedTemporaryFile() as f:
f.write(b'abcd'), f.flush()
write_gr_cmd(dict(a='q', s=1, v=1, i=1), standard_b64encode(b'abcd'))
write_gr_cmd(dict(a='q', s=1, v=1, i=2, t='f'), standard_b64encode(f.name.encode(fsenc)))
gc = GraphicsCommand()
gc.a = 'q'
gc.s = gc.v = gc.i = 1
write_gr_cmd(gc, standard_b64encode(b'abcd'))
gc.t = 'f'
gc.i = 2
write_gr_cmd(gc, standard_b64encode(f.name.encode(fsenc)))
with TTYIO() as io:
io.recv(more_needed, timeout=float(wait_for))
io.recv(more_needed, timeout=wait_for)
finally:
if not silent:
sys.stdout.buffer.write(b'\033[J'), sys.stdout.flush()
detect_support.has_files = bool(responses.get(2))
can_transfer_with_files = bool(responses.get(2))
return responses.get(1, False)
def parse_place(raw):
class Place(NamedTuple):
width: int
height: int
left: int
top: int
def parse_place(raw: str) -> Optional[Place]:
if raw:
area, pos = raw.split('@', 1)
w, h = map(int, area.split('x'))
l, t = map(int, pos.split('x'))
return namedtuple('Place', 'width height left top')(w, h, l, t)
return Place(w, h, l, t)
return None
help_text = (
@@ -273,7 +339,13 @@ help_text = (
usage = 'image-file-or-url-or-directory ...'
def process_single_item(item, args, url_pat=None, maybe_dir=True):
def process_single_item(
item: Union[bytes, str],
args: IcatCLIOptions,
parsed_opts: ParsedOpts,
url_pat: Optional[Pattern] = None,
maybe_dir: bool = True
) -> None:
is_tempfile = False
file_removed = False
try:
@@ -291,34 +363,35 @@ def process_single_item(item, args, url_pat=None, maybe_dir=True):
raise SystemExit('Failed to download image at URL: {} with error: {}'.format(item, e))
item = tf.name
is_tempfile = True
file_removed = process(item, args, is_tempfile)
file_removed = process(item, args, parsed_opts, is_tempfile)
elif item.lower().startswith('file://'):
from urllib.parse import urlparse
from urllib.request import url2pathname
item = urlparse(item)
pitem = urlparse(item)
if os.sep == '\\':
item = item.netloc + item.path
item = pitem.netloc + pitem.path
else:
item = item.path
item = pitem.path
item = url2pathname(item)
file_removed = process(item, args, is_tempfile)
file_removed = process(item, args, parsed_opts, is_tempfile)
else:
if maybe_dir and os.path.isdir(item):
for (x, mt) in scan(item):
process_single_item(x, args, url_pat=None, maybe_dir=False)
process_single_item(x, args, parsed_opts, url_pat=None, maybe_dir=False)
else:
file_removed = process(item, args, is_tempfile)
file_removed = process(item, args, parsed_opts, is_tempfile)
finally:
if is_tempfile and not file_removed:
os.remove(item)
def main(args=sys.argv):
global screen_size
args, items = parse_args(args[1:], options_spec, usage, help_text, '{} +kitten icat'.format(appname))
def main(args: List[str] = sys.argv) -> None:
global can_transfer_with_files
cli_opts, items_ = parse_args(args[1:], options_spec, usage, help_text, '{} +kitten icat'.format(appname), result_class=IcatCLIOptions)
items: List[Union[str, bytes]] = list(items_)
if args.print_window_size:
screen_size_function.ans = None
if cli_opts.print_window_size:
screen_size_function.cache_clear()
with open(os.ctermid()) as tty:
ss = screen_size_function(tty)()
print('{}x{}'.format(ss.width, ss.height), end='')
@@ -327,58 +400,65 @@ def main(args=sys.argv):
if not sys.stdout.isatty():
sys.stdout = open(os.ctermid(), 'w')
stdin_data = None
if args.stdin == 'yes' or (not sys.stdin.isatty() and args.stdin == 'detect'):
if cli_opts.stdin == 'yes' or (not sys.stdin.isatty() and cli_opts.stdin == 'detect'):
stdin_data = sys.stdin.buffer.read()
if stdin_data:
items.insert(0, stdin_data)
sys.stdin.close()
sys.stdin = open(os.ctermid(), 'r')
screen_size = screen_size_function()
screen_size = get_screen_size_function()
signal.signal(signal.SIGWINCH, lambda signum, frame: setattr(screen_size, 'changed', True))
if screen_size().width == 0:
if args.detect_support:
if cli_opts.detect_support:
raise SystemExit(1)
raise SystemExit(
'Terminal does not support reporting screen sizes via the TIOCGWINSZ ioctl'
)
try:
args.place = parse_place(args.place)
except Exception:
raise SystemExit('Not a valid place specification: {}'.format(args.place))
parsed_opts = ParsedOpts()
if cli_opts.place:
try:
parsed_opts.place = parse_place(cli_opts.place)
except Exception:
raise SystemExit('Not a valid place specification: {}'.format(cli_opts.place))
if args.detect_support:
if not detect_support(wait_for=args.detection_timeout, silent=True):
try:
parsed_opts.z_index = parse_z_index(cli_opts.z_index)
except Exception:
raise SystemExit('Not a valid z-index specification: {}'.format(cli_opts.z_index))
if cli_opts.detect_support:
if not detect_support(wait_for=cli_opts.detection_timeout, silent=True):
raise SystemExit(1)
print('file' if detect_support.has_files else 'stream', end='', file=sys.stderr)
print('file' if can_transfer_with_files else 'stream', end='', file=sys.stderr)
return
if args.transfer_mode == 'detect':
if not detect_support(wait_for=args.detection_timeout, silent=args.silent):
if cli_opts.transfer_mode == 'detect':
if not detect_support(wait_for=cli_opts.detection_timeout, silent=cli_opts.silent):
raise SystemExit('This terminal emulator does not support the graphics protocol, use a terminal emulator such as kitty that does support it')
else:
detect_support.has_files = args.transfer_mode == 'file'
can_transfer_with_files = cli_opts.transfer_mode == 'file'
errors = []
if args.clear:
sys.stdout.buffer.write(clear_images_on_screen(delete_data=True))
if cli_opts.clear:
sys.stdout.write(clear_images_on_screen(delete_data=True))
if not items:
return
if not items:
raise SystemExit('You must specify at least one file to cat')
if args.place:
if parsed_opts.place:
if len(items) > 1 or (isinstance(items[0], str) and os.path.isdir(items[0])):
raise SystemExit(f'The --place option can only be used with a single image, not {items}')
sys.stdout.buffer.write(b'\0337') # save cursor
url_pat = re.compile(r'(?:https?|ftp)://', flags=re.I)
for item in items:
try:
process_single_item(item, args, url_pat)
process_single_item(item, cli_opts, parsed_opts, url_pat)
except NoImageMagick as e:
raise SystemExit(str(e))
except ConvertFailed as e:
raise SystemExit(str(e))
except OpenFailed as e:
errors.append(e)
if args.place:
if parsed_opts.place:
sys.stdout.buffer.write(b'\0338') # restore cursor
if not errors:
return
@@ -390,6 +470,7 @@ def main(args=sys.argv):
if __name__ == '__main__':
main()
elif __name__ == '__doc__':
sys.cli_docs['usage'] = usage
sys.cli_docs['options'] = options_spec
sys.cli_docs['help_text'] = help_text
cd = sys.cli_docs # type: ignore
cd['usage'] = usage
cd['options'] = options_spec
cd['help_text'] = help_text

View File

@@ -3,9 +3,11 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
import sys
from typing import List
from kitty.key_encoding import (
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, SUPER, encode_key_event
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, SUPER, KeyEvent,
encode_key_event
)
from ..tui.handler import Handler
@@ -14,41 +16,41 @@ from ..tui.loop import Loop
class KeysHandler(Handler):
def initialize(self):
def initialize(self) -> None:
self.cmd.set_window_title('Kitty extended keyboard protocol demo')
self.cmd.set_cursor_visible(False)
self.print('Press any keys - Ctrl+C or Ctrl+D will terminate')
def on_text(self, text, in_bracketed_paste=False):
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
self.print('Text input: ' + text)
def on_key(self, key_event):
def on_key(self, key_event: KeyEvent) -> None:
etype = {
PRESS: 'PRESS',
REPEAT: 'REPEAT',
RELEASE: 'RELEASE'
}[key_event.type]
mods = []
lmods = []
for m, name in {
SHIFT: 'Shift',
ALT: 'Alt',
CTRL: 'Ctrl',
SUPER: 'Super'}.items():
if key_event.mods & m:
mods.append(name)
mods = '+'.join(mods)
lmods.append(name)
mods = '+'.join(lmods)
if mods:
mods += '+'
self.print('Key {}: {}{} [{}]'.format(etype, mods, key_event.key, encode_key_event(key_event)))
def on_interrupt(self):
def on_interrupt(self) -> None:
self.quit_loop(0)
def on_eot(self):
def on_eot(self) -> None:
self.quit_loop(0)
def main(args):
def main(args: List[str]) -> None:
loop = Loop()
handler = KeysHandler()
loop.loop(handler)

View File

@@ -6,8 +6,11 @@ import os
import shutil
import subprocess
import sys
from typing import Callable, Dict, List, Tuple
from kitty.cli import parse_args
from kitty.cli_stub import PanelCLIOptions
from kitty.options_stub import Options
from kitty.constants import is_macos
OPTIONS = r'''
@@ -42,19 +45,19 @@ Syntax: :italic:`name=value`. For example: :option:`kitty +kitten panel -o` font
'''.format
args = None
args = PanelCLIOptions()
help_text = 'Use a command line program to draw a GPU accelerated panel on your X11 desktop'
usage = 'program-to-run'
def parse_panel_args(args):
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten panel')
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, silent=False):
cmd = ['xprop'] + list(cmd)
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)
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:
@@ -62,11 +65,11 @@ def call_xprop(*cmd, silent=False):
def create_strut(
win_id,
left=0, right=0, top=0, bottom=0, left_start_y=0, left_end_y=0,
right_start_y=0, right_end_y=0, top_start_x=0, top_end_x=0,
bottom_start_x=0, bottom_end_x=0
):
win_id: int,
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',
@@ -77,50 +80,54 @@ def create_strut(
)
def create_top_strut(win_id, width, height):
def create_top_strut(win_id: int, width: int, height: int) -> None:
create_strut(win_id, top=height, top_end_x=width)
def create_bottom_strut(win_id, width, height):
def create_bottom_strut(win_id: int, width: int, height: int) -> None:
create_strut(win_id, bottom=height, bottom_end_x=width)
def create_left_strut(win_id, width, height):
def create_left_strut(win_id: int, width: int, height: int) -> None:
create_strut(win_id, left=width, left_end_y=height)
def create_right_strut(win_id, width, height):
def create_right_strut(win_id: int, width: int, height: int) -> None:
create_strut(win_id, right=width, right_end_y=height)
def setup_x11_window(win_id):
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()['create_{}_strut'.format(args.edge)]
func(win_id, initial_window_size_func.width, initial_window_size_func.height)
func(win_id, window_width, window_height)
def initial_window_size_func(opts, *a):
def initial_window_size_func(opts: Options, cached_values: Dict) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
from kitty.fast_data_types import glfw_primary_monitor_size, set_smallest_allowed_resize
def initial_window_size(cell_width, cell_height, dpi_x, dpi_y, xscale, yscale):
def initial_window_size(cell_width: int, cell_height: int, dpi_x: float, dpi_y: float, xscale: float, yscale: float) -> Tuple[int, int]:
global window_width, window_height
monitor_width, monitor_height = glfw_primary_monitor_size()
if args.edge in {'top', 'bottom'}:
h = initial_window_size_func.height = cell_height * args.lines + 1
initial_window_size_func.width = monitor_width
h = window_height = cell_height * args.lines + 1
window_width = monitor_width
set_smallest_allowed_resize(100, h)
else:
w = initial_window_size_func.width = cell_width * args.columns + 1
initial_window_size_func.height = monitor_height
w = window_width = cell_width * args.columns + 1
window_height = monitor_height
set_smallest_allowed_resize(w, 100)
return initial_window_size_func.width, initial_window_size_func.height
return window_width, window_height
return initial_window_size
def main(sys_args):
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')
@@ -130,21 +137,22 @@ def main(sys_args):
if not items:
raise SystemExit('You must specify the program to run')
sys.argv = ['kitty']
if args.config:
sys.argv.append('--config={}'.format(args.config))
for config in args.config:
sys.argv.append('--config={}'.format(config))
for override in args.override:
sys.argv.append('--override={}'.format(override))
sys.argv.extend(items)
from kitty.main import run_app, main
from kitty.main import run_app, main as real_main
run_app.cached_values_name = 'panel'
run_app.first_window_callback = setup_x11_window
run_app.initial_window_size_func = initial_window_size_func
main()
real_main()
if __name__ == '__main__':
main(sys.argv)
elif __name__ == '__doc__':
sys.cli_docs['usage'] = usage
sys.cli_docs['options'] = OPTIONS
sys.cli_docs['help_text'] = help_text
cd: dict = sys.cli_docs # type: ignore
cd['usage'] = usage
cd['options'] = OPTIONS
cd['help_text'] = help_text

View File

@@ -4,28 +4,36 @@
import sys
from typing import Any, Dict, List, Optional
from kitty.cli import parse_args
from kitty.cmds import cmap, parse_subcommand_cli
from kitty.cli_stub import RCOptions, ResizeCLIOptions
from kitty.constants import version
from kitty.key_encoding import CTRL, ESCAPE, RELEASE, N, S, T, W
from kitty.key_encoding import CTRL, RELEASE, KeyEvent, key_defs as K
from kitty.rc.base import command_for_name, parse_subcommand_cli
from kitty.remote_control import encode_send, parse_rc_args
from kitty.utils import ScreenSize
from ..tui.handler import Handler
from ..tui.loop import Loop
from ..tui.operations import styled
global_opts = None
global_opts = RCOptions()
ESCAPE = K['ESCAPE']
N = K['N']
S = K['S']
T = K['T']
W = K['W']
class Resize(Handler):
print_on_fail = None
print_on_fail: Optional[str] = None
def __init__(self, opts):
def __init__(self, opts: ResizeCLIOptions):
self.opts = opts
def initialize(self):
def initialize(self) -> None:
global global_opts
global_opts = parse_rc_args(['kitty', '@resize-window'])[0]
self.original_size = self.screen_size
@@ -33,8 +41,8 @@ class Resize(Handler):
self.cmd.set_line_wrapping(False)
self.draw_screen()
def do_window_resize(self, is_decrease=False, is_horizontal=True, reset=False, multiplier=1):
resize_window = cmap['resize-window']
def do_window_resize(self, is_decrease: bool = False, is_horizontal: bool = True, reset: bool = False, multiplier: int = 1) -> None:
resize_window = command_for_name('resize_window')
increment = self.opts.horizontal_increment if is_horizontal else self.opts.vertical_increment
increment *= multiplier
if is_decrease:
@@ -42,11 +50,11 @@ class Resize(Handler):
axis = 'reset' if reset else ('horizontal' if is_horizontal else 'vertical')
cmdline = [resize_window.name, '--self', '--increment={}'.format(increment), '--axis=' + axis]
opts, items = parse_subcommand_cli(resize_window, cmdline)
payload = resize_window(global_opts, opts, items)
payload = resize_window.message_to_kitty(global_opts, opts, items)
send = {'cmd': resize_window.name, 'version': version, 'payload': payload, 'no_response': False}
self.write(encode_send(send))
def on_kitty_cmd_response(self, response):
def on_kitty_cmd_response(self, response: Dict[str, Any]) -> None:
if not response.get('ok'):
err = response['error']
if response.get('tb'):
@@ -58,14 +66,14 @@ class Resize(Handler):
if res:
self.cmd.bell()
def on_text(self, text, in_bracketed_paste=False):
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
text = text.upper()
if text in 'WNTSR':
self.do_window_resize(is_decrease=text in 'NS', is_horizontal=text in 'WN', reset=text == 'R')
elif text == 'Q':
self.quit_loop(0)
def on_key(self, key_event):
def on_key(self, key_event: KeyEvent) -> None:
if key_event.type is RELEASE:
return
if key_event.key is ESCAPE:
@@ -73,10 +81,10 @@ class Resize(Handler):
elif key_event.key in (W, N, T, S) and key_event.mods & CTRL:
self.do_window_resize(is_decrease=key_event.key in (N, S), is_horizontal=key_event.key in (W, N), multiplier=2)
def on_resize(self, new_size):
def on_resize(self, new_size: ScreenSize) -> None:
self.draw_screen()
def draw_screen(self):
def draw_screen(self) -> None:
self.cmd.clear_screen()
print = self.print
print(styled('Resize this window', bold=True, fg='gray', fg_intense=True))
@@ -94,7 +102,7 @@ class Resize(Handler):
print(styled('Sizes', bold=True, fg='white', fg_intense=True))
print('Original: {} rows {} cols'.format(self.original_size.rows, self.original_size.cols))
print('Current: {} rows {} cols'.format(
styled(self.screen_size.rows, fg='magenta'), styled(self.screen_size.cols, fg='magenta')))
styled(str(self.screen_size.rows), fg='magenta'), styled(str(self.screen_size.cols), fg='magenta')))
OPTIONS = r'''
@@ -111,10 +119,10 @@ The base vertical increment.
'''.format
def main(args):
def main(args: List[str]) -> None:
msg = 'Resize the current window'
try:
args, items = parse_args(args[1:], OPTIONS, '', msg, 'resize_window')
cli_opts, items = parse_args(args[1:], OPTIONS, '', msg, 'resize_window', result_class=ResizeCLIOptions)
except SystemExit as e:
if e.code != 0:
print(e.args[0], file=sys.stderr)
@@ -122,7 +130,7 @@ def main(args):
return
loop = Loop()
handler = Resize(args)
handler = Resize(cli_opts)
loop.loop(handler)
if handler.print_on_fail:
print(handler.print_on_fail, file=sys.stderr)

View File

@@ -6,17 +6,17 @@
import importlib
import os
import sys
from contextlib import suppress
from functools import partial
from functools import lru_cache, partial
from typing import Any, Dict, FrozenSet, List
aliases = {'url_hints': 'hints'}
def resolved_kitten(k):
def resolved_kitten(k: str) -> str:
return aliases.get(k, k).replace('-', '_')
def path_to_custom_kitten(config_dir, kitten):
def path_to_custom_kitten(config_dir: str, kitten: str) -> str:
path = os.path.expanduser(kitten)
if not os.path.isabs(path):
path = os.path.join(config_dir, path)
@@ -24,7 +24,7 @@ def path_to_custom_kitten(config_dir, kitten):
return path
def import_kitten_main_module(config_dir, kitten):
def import_kitten_main_module(config_dir: str, kitten: str) -> Dict[str, Any]:
if kitten.endswith('.py'):
path_modified = False
path = path_to_custom_kitten(config_dir, kitten)
@@ -43,26 +43,26 @@ def import_kitten_main_module(config_dir, kitten):
kitten = resolved_kitten(kitten)
m = importlib.import_module('kittens.{}.main'.format(kitten))
return {'start': m.main, 'end': getattr(m, 'handle_result', lambda *a, **k: None)}
return {'start': getattr(m, 'main'), 'end': getattr(m, 'handle_result', lambda *a, **k: None)}
def create_kitten_handler(kitten, orig_args):
def create_kitten_handler(kitten: str, orig_args: List[str]) -> Any:
from kitty.constants import config_dir
kitten = resolved_kitten(kitten)
m = import_kitten_main_module(config_dir, kitten)
ans = partial(m['end'], [kitten] + orig_args)
ans.type_of_input = getattr(m['end'], 'type_of_input', None)
ans.no_ui = getattr(m['end'], 'no_ui', False)
setattr(ans, 'type_of_input', getattr(m['end'], 'type_of_input', None))
setattr(ans, 'no_ui', getattr(m['end'], 'no_ui', False))
return ans
def set_debug(kitten):
def set_debug(kitten: str) -> None:
from kittens.tui.loop import debug
import builtins
builtins.debug = debug
setattr(builtins, 'debug', debug)
def launch(args):
def launch(args: List[str]) -> None:
config_dir, kitten = args[:2]
kitten = resolved_kitten(kitten)
del args[:2]
@@ -84,7 +84,7 @@ def launch(args):
sys.stdout.flush()
def deserialize(output):
def deserialize(output: str) -> Any:
import json
if output.startswith('OK: '):
try:
@@ -94,14 +94,16 @@ def deserialize(output):
raise ValueError('Failed to parse kitten output: {!r}'.format(output))
def run_kitten(kitten, run_name='__main__'):
def run_kitten(kitten: str, run_name: str = '__main__') -> None:
import runpy
original_kitten_name = kitten
kitten = resolved_kitten(kitten)
set_debug(kitten)
with suppress(ImportError):
try:
runpy.run_module('kittens.{}.main'.format(kitten), run_name=run_name)
return
except ImportError:
pass
# Look for a custom kitten
if not kitten.endswith('.py'):
kitten += '.py'
@@ -116,21 +118,19 @@ def run_kitten(kitten, run_name='__main__'):
m['main'](sys.argv)
def all_kitten_names():
ans = getattr(all_kitten_names, 'ans', None)
if ans is None:
n = []
import glob
base = os.path.dirname(os.path.abspath(__file__))
for x in glob.glob(os.path.join(base, '*', '__init__.py')):
q = os.path.basename(os.path.dirname(x))
if q != 'tui':
n.append(q)
all_kitten_names.ans = ans = frozenset(n)
return ans
@lru_cache(maxsize=2)
def all_kitten_names() -> FrozenSet[str]:
n = []
import glob
base = os.path.dirname(os.path.abspath(__file__))
for x in glob.glob(os.path.join(base, '*', '__init__.py')):
q = os.path.basename(os.path.dirname(x))
if q != 'tui':
n.append(q)
return frozenset(n)
def list_kittens():
def list_kittens() -> None:
print('You must specify the name of a kitten to run')
print('Choose from:')
print()
@@ -138,24 +138,24 @@ def list_kittens():
print(kitten)
def get_kitten_cli_docs(kitten):
sys.cli_docs = {}
def get_kitten_cli_docs(kitten: str) -> Any:
setattr(sys, 'cli_docs', {})
run_kitten(kitten, run_name='__doc__')
ans = sys.cli_docs
del sys.cli_docs
ans = getattr(sys, 'cli_docs')
delattr(sys, 'cli_docs')
if 'help_text' in ans and 'usage' in ans and 'options' in ans:
return ans
def get_kitten_conf_docs(kitten):
sys.all_options = None
def get_kitten_conf_docs(kitten: str) -> Any:
setattr(sys, 'all_options', None)
run_kitten(kitten, run_name='__conf__')
ans = sys.all_options
del sys.all_options
ans = getattr(sys, 'all_options')
delattr(sys, 'all_options')
return ans
def main():
def main() -> None:
try:
args = sys.argv[1:]
launch(args)

View File

@@ -4,8 +4,11 @@
import os
import sys
from contextlib import suppress
from typing import List
from kitty.cli import parse_args
from kitty.cli_stub import ErrorCLIOptions
from ..tui.operations import styled
@@ -16,23 +19,22 @@ The title for the error message.
'''.format
def real_main(args):
def real_main(args: List[str]) -> None:
msg = 'Show an error message'
args, items = parse_args(args[1:], OPTIONS, '', msg, 'hints')
cli_opts, items = parse_args(args[1:], OPTIONS, '', msg, 'hints', result_class=ErrorCLIOptions)
error_message = sys.stdin.buffer.read().decode('utf-8')
sys.stdin = open(os.ctermid())
print(styled(args.title, fg_intense=True, fg='red', bold=True))
print(styled(cli_opts.title, fg_intense=True, fg='red', bold=True))
print()
print(error_message)
print()
input('Press Enter to close.')
def main(args):
def main(args: List[str]) -> None:
try:
real_main(args)
except KeyboardInterrupt:
pass
with suppress(KeyboardInterrupt):
real_main(args)
except Exception:
import traceback
traceback.print_exc()

View File

@@ -7,12 +7,13 @@ import re
import shlex
import subprocess
import sys
from typing import List, NoReturn, Set, Tuple
SHELL_SCRIPT = '''\
#!/bin/sh
# macOS ships with an ancient version of tic that cannot read from stdin, so we
# create a temp file for it
tmp=$(mktemp /tmp/terminfo.XXXXXX)
tmp=$(mktemp)
cat >$tmp << 'TERMEOF'
TERMINFO
TERMEOF
@@ -42,9 +43,12 @@ exec -a "-$shell_name" "$0"
'''
def get_ssh_cli():
other_ssh_args, boolean_ssh_args = [], []
raw = subprocess.Popen(['ssh'], stderr=subprocess.PIPE).stderr.read().decode('utf-8')
def get_ssh_cli() -> Tuple[Set[str], Set[str]]:
other_ssh_args: List[str] = []
boolean_ssh_args: List[str] = []
stderr = subprocess.Popen(['ssh'], stderr=subprocess.PIPE).stderr
assert stderr is not None
raw = stderr.read().decode('utf-8')
for m in re.finditer(r'\[(.+?)\]', raw):
q = m.group(1)
if len(q) < 2 or q[0] != '-':
@@ -56,11 +60,11 @@ def get_ssh_cli():
return set('-' + x for x in boolean_ssh_args), set('-' + x for x in other_ssh_args)
def parse_ssh_args(args):
def parse_ssh_args(args: List[str]) -> Tuple[List[str], List[str], bool]:
boolean_ssh_args, other_ssh_args = get_ssh_cli()
passthrough_args = {'-' + x for x in 'Nnf'}
ssh_args = []
server_args = []
server_args: List[str] = []
expecting_option_val = False
passthrough = False
for arg in args:
@@ -76,6 +80,9 @@ def parse_ssh_args(args):
if arg in boolean_ssh_args:
ssh_args.append(arg)
continue
if arg.startswith('-p') and arg[2:].isdigit():
ssh_args.append(arg)
continue
if arg in other_ssh_args:
if i != len(all_args) - 1:
raise SystemExit('Option {} cannot occur in the middle'.format(arg))
@@ -94,7 +101,7 @@ def parse_ssh_args(args):
return ssh_args, server_args, passthrough
def quote(x):
def quote(x: str) -> str:
# we have to escape unbalanced quotes and other unparsable
# args as they will break the shell script
# But we do not want to quote things like * or 'echo hello'
@@ -106,7 +113,7 @@ def quote(x):
return x
def main(args):
def main(args: List[str]) -> NoReturn:
ssh_args, server_args, passthrough = parse_ssh_args(args[1:])
if passthrough:
cmd = ['ssh'] + ssh_args + server_args
@@ -114,8 +121,8 @@ def main(args):
terminfo = subprocess.check_output(['infocmp']).decode('utf-8')
sh_script = SHELL_SCRIPT.replace('TERMINFO', terminfo, 1)
if len(server_args) > 1:
command_to_execute = [quote(c) for c in server_args[1:]]
command_to_execute = 'exec ' + ' '.join(command_to_execute)
command_to_executeg = (quote(c) for c in server_args[1:])
command_to_execute = 'exec ' + ' '.join(command_to_executeg)
else:
command_to_execute = ''
sh_script = sh_script.replace('EXEC_CMD', command_to_execute)

View File

@@ -3,27 +3,49 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from .operations import commander
from types import TracebackType
from typing import (
Any, Callable, ContextManager, Dict, Optional, Sequence, Type, Union
)
from kitty.typing import (
AbstractEventLoop, BossType, Debug, ImageManagerType, KeyEventType,
KittensKeyActionType, LoopType, MouseEvent, ScreenSize, TermManagerType
)
class Handler:
image_manager_class = None
image_manager_class: Optional[Type[ImageManagerType]] = None
def _initialize(self, screen_size, term_manager, schedule_write, tui_loop, debug, image_manager=None):
def _initialize(
self,
screen_size: ScreenSize,
term_manager: TermManagerType,
schedule_write: Callable[[bytes], None],
tui_loop: LoopType,
debug: Debug,
image_manager: Optional[ImageManagerType] = None
) -> None:
from .operations import commander
self.screen_size = screen_size
self._term_manager = term_manager
self._tui_loop = tui_loop
self._schedule_write = schedule_write
self.debug = debug
self.cmd = commander(self)
self.image_manager = image_manager
self._image_manager = image_manager
@property
def asyncio_loop(self):
def image_manager(self) -> ImageManagerType:
assert self._image_manager is not None
return self._image_manager
@property
def asyncio_loop(self) -> AbstractEventLoop:
return self._tui_loop.asycio_loop
def add_shortcut(self, action, key, mods=None, is_text=False):
def add_shortcut(self, action: KittensKeyActionType, key: str, mods: Optional[int] = None, is_text: Optional[bool] = False) -> None:
if not hasattr(self, '_text_shortcuts'):
self._text_shortcuts, self._key_shortcuts = {}, {}
if is_text:
@@ -31,70 +53,95 @@ class Handler:
else:
self._key_shortcuts[(key, mods or 0)] = action
def shortcut_action(self, key_event_or_text):
def shortcut_action(self, key_event_or_text: Union[str, KeyEventType]) -> Optional[KittensKeyActionType]:
if isinstance(key_event_or_text, str):
return self._text_shortcuts.get(key_event_or_text)
return self._key_shortcuts.get((key_event_or_text.key, key_event_or_text.mods))
def __enter__(self):
if self.image_manager is not None:
self.image_manager.__enter__()
def __enter__(self) -> None:
if self._image_manager is not None:
self._image_manager.__enter__()
self.debug.fobj = self
self.initialize()
def __exit__(self, *a):
def __exit__(self, etype: type, value: Exception, tb: TracebackType) -> None:
del self.debug.fobj
self.finalize()
if self.image_manager is not None:
self.image_manager.__exit__(*a)
if self._image_manager is not None:
self._image_manager.__exit__(etype, value, tb)
def initialize(self):
def initialize(self) -> None:
pass
def finalize(self):
def finalize(self) -> None:
pass
def on_resize(self, screen_size):
def on_resize(self, screen_size: ScreenSize) -> None:
self.screen_size = screen_size
def quit_loop(self, return_code=None):
def quit_loop(self, return_code: Optional[int] = None) -> None:
self._tui_loop.quit(return_code)
def on_term(self):
def on_term(self) -> None:
self._tui_loop.quit(1)
def on_text(self, text, in_bracketed_paste=False):
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
pass
def on_key(self, key_event):
def on_key(self, key_event: KeyEventType) -> None:
pass
def on_mouse(self, mouse_event):
def on_mouse(self, mouse_event: 'MouseEvent') -> None:
pass
def on_interrupt(self):
def on_interrupt(self) -> None:
pass
def on_eot(self):
def on_eot(self) -> None:
pass
def on_kitty_cmd_response(self, response):
def on_kitty_cmd_response(self, response: Dict) -> None:
pass
def on_clipboard_response(self, text, from_primary=False):
def on_clipboard_response(self, text: str, from_primary: bool = False) -> None:
pass
def on_capability_response(self, name, val):
def on_capability_response(self, name: str, val: str) -> None:
pass
def write(self, data):
def write(self, data: Union[bytes, str]) -> None:
if isinstance(data, str):
data = data.encode('utf-8')
self._schedule_write(data)
def print(self, *args, sep=' ', end='\r\n'):
def flush(self) -> None:
pass
def print(self, *args: object, sep: str = ' ', end: str = '\r\n') -> None:
data = sep.join(map(str, args)) + end
self.write(data)
def suspend(self):
def suspend(self) -> ContextManager[TermManagerType]:
return self._term_manager.suspend()
class HandleResult:
type_of_input: Optional[str] = None
no_ui: bool = False
def __init__(self, impl: Callable, type_of_input: Optional[str], no_ui: bool):
self.impl = impl
self.no_ui = no_ui
self.type_of_input = type_of_input
def __call__(self, args: Sequence[str], data: Any, target_window_id: int, boss: BossType) -> Any:
return self.impl(args, data, target_window_id, boss)
def result_handler(type_of_input: Optional[str] = None, no_ui: bool = False) -> Callable[[Callable], HandleResult]:
def wrapper(impl: Callable) -> HandleResult:
return HandleResult(impl, type_of_input, no_ui)
return wrapper

View File

@@ -7,10 +7,18 @@ import os
import sys
from base64 import standard_b64encode
from collections import defaultdict, deque
from itertools import count
from contextlib import suppress
from itertools import count
from typing import (
Any, Callable, DefaultDict, Deque, Dict, List, Optional, Sequence, Tuple,
Union
)
from kitty.utils import fit_image
from kitty.typing import (
CompletedProcess, GRT_a, GRT_d, GRT_f, GRT_m, GRT_o, GRT_t, HandlerType,
TypedDict
)
from kitty.utils import ScreenSize, fit_image
from .operations import cursor
@@ -23,14 +31,14 @@ except Exception:
class ImageData:
def __init__(self, fmt, width, height, mode):
def __init__(self, fmt: str, width: int, height: int, mode: str):
self.width, self.height, self.fmt, self.mode = width, height, fmt, mode
self.transmit_fmt = str(24 if self.mode == 'rgb' else 32)
self.transmit_fmt: GRT_f = (24 if self.mode == 'rgb' else 32)
class OpenFailed(ValueError):
def __init__(self, path, message):
def __init__(self, path: str, message: str):
ValueError.__init__(
self, 'Failed to open image: {} with error: {}'.format(path, message)
)
@@ -39,7 +47,7 @@ class OpenFailed(ValueError):
class ConvertFailed(ValueError):
def __init__(self, path, message):
def __init__(self, path: str, message: str):
ValueError.__init__(
self, 'Failed to convert image: {} with error: {}'.format(path, message)
)
@@ -50,7 +58,7 @@ class NoImageMagick(Exception):
pass
def run_imagemagick(path, cmd, keep_stdout=True):
def run_imagemagick(path: str, cmd: Sequence[str], keep_stdout: bool = True) -> CompletedProcess:
import subprocess
try:
p = subprocess.run(cmd, stdout=subprocess.PIPE if keep_stdout else subprocess.DEVNULL, stderr=subprocess.PIPE)
@@ -61,14 +69,19 @@ def run_imagemagick(path, cmd, keep_stdout=True):
return p
def identify(path):
def identify(path: str) -> ImageData:
p = run_imagemagick(path, ['identify', '-format', '%m %w %h %A', '--', path])
parts = tuple(filter(None, p.stdout.decode('utf-8').split()))
parts: Tuple[str, ...] = tuple(filter(None, p.stdout.decode('utf-8').split()))
mode = 'rgb' if parts[3].lower() == 'false' else 'rgba'
return ImageData(parts[0].lower(), int(parts[1]), int(parts[2]), mode)
def convert(path, m, available_width, available_height, scale_up, tdir=None):
def convert(
path: str, m: ImageData,
available_width: int, available_height: int,
scale_up: bool,
tdir: Optional[str] = None
) -> Tuple[str, int, int]:
from tempfile import NamedTemporaryFile
width, height = m.width, m.height
cmd = ['convert', '-background', 'none', '--', path]
@@ -81,6 +94,7 @@ def convert(path, m, available_width, available_height, scale_up, tdir=None):
if scaled or width > available_width or height > available_height:
width, height = fit_image(width, height, available_width, available_height)
cmd += ['-resize', '{}x{}!'.format(width, height)]
cmd += ['-depth', '8']
with NamedTemporaryFile(prefix='icat-', suffix='.' + m.mode, delete=False, dir=tdir) as outfile:
run_imagemagick(path, cmd + [outfile.name])
# ImageMagick sometimes generated rgba images smaller than the specified
@@ -100,58 +114,122 @@ def convert(path, m, available_width, available_height, scale_up, tdir=None):
return outfile.name, width, height
def can_display_images():
def can_display_images() -> bool:
import shutil
ans = getattr(can_display_images, 'ans', None)
ans: Optional[bool] = getattr(can_display_images, 'ans', None)
if ans is None:
ans = shutil.which('convert') is not None
can_display_images.ans = ans
setattr(can_display_images, 'ans', ans)
return ans
ImageKey = Tuple[str, int, int]
SentImageKey = Tuple[int, int, int]
class GraphicsCommand:
a: GRT_a = 't' # action
f: GRT_f = 32 # image data format
t: GRT_t = 'd' # transmission medium
s: int = 0 # sent image width
v: int = 0 # sent image height
S: int = 0 # size of data to read from file
O: int = 0 # offset of data to read from file
i: int = 0 # image id
o: Optional[GRT_o] = None # type of compression
m: GRT_m = 0 # 0 or 1 whether there is more chunked data
x: int = 0 # left edge of image area to display
y: int = 0 # top edge of image area to display
w: int = 0 # image width to display
h: int = 0 # image height to display
X: int = 0 # X-offset within cell
Y: int = 0 # Y-offset within cell
c: int = 0 # number of cols to display image over
r: int = 0 # number of rows to display image over
z: int = 0 # z-index
d: GRT_d = 'a' # what to delete
def serialize(self, payload: bytes = b'') -> bytes:
items = []
for k in GraphicsCommand.__annotations__:
val: Union[str, None, int] = getattr(self, k)
defval: Union[str, None, int] = getattr(GraphicsCommand, k)
if val != defval and val is not None:
items.append('{}={}'.format(k, val))
ans: List[bytes] = []
w = ans.append
w(b'\033_G')
w(','.join(items).encode('ascii'))
if payload:
w(b';')
w(payload)
w(b'\033\\')
return b''.join(ans)
def clear(self) -> None:
for k in GraphicsCommand.__annotations__:
defval: Union[str, None, int] = getattr(GraphicsCommand, k)
setattr(self, k, defval)
class Placement(TypedDict):
cmd: GraphicsCommand
x: int
y: int
class ImageManager:
def __init__(self, handler):
def __init__(self, handler: HandlerType):
self.image_id_counter = count()
self.handler = handler
self.filesystem_ok = None
self.image_data = {}
self.failed_images = {}
self.converted_images = {}
self.sent_images = {}
self.image_id_to_image_data = {}
self.image_id_to_converted_data = {}
self.transmission_status = {}
self.placements_in_flight = defaultdict(deque)
self.filesystem_ok: Optional[bool] = None
self.image_data: Dict[str, ImageData] = {}
self.failed_images: Dict[str, Exception] = {}
self.converted_images: Dict[ImageKey, ImageKey] = {}
self.sent_images: Dict[ImageKey, int] = {}
self.image_id_to_image_data: Dict[int, ImageData] = {}
self.image_id_to_converted_data: Dict[int, ImageKey] = {}
self.transmission_status: Dict[int, Union[str, int]] = {}
self.placements_in_flight: DefaultDict[int, Deque[Placement]] = defaultdict(deque)
self.update_image_placement_for_resend: Optional[Callable[[int, Placement], bool]]
@property
def next_image_id(self):
def next_image_id(self) -> int:
return next(self.image_id_counter) + 2
@property
def screen_size(self):
def screen_size(self) -> ScreenSize:
return self.handler.screen_size
def __enter__(self):
def __enter__(self) -> None:
import tempfile
self.tdir = tempfile.mkdtemp(prefix='kitten-images-')
with tempfile.NamedTemporaryFile(dir=self.tdir, delete=False) as f:
f.write(b'abcd')
self.handler.cmd.gr_command(dict(a='q', s=1, v=1, i=1, t='f'), standard_b64encode(f.name.encode(fsenc)))
gc = GraphicsCommand()
gc.a = 'q'
gc.s = gc.v = gc.i = 1
gc.t = 'f'
self.handler.cmd.gr_command(gc, standard_b64encode(f.name.encode(fsenc)))
def __exit__(self, *a):
def __exit__(self, *a: Any) -> None:
import shutil
shutil.rmtree(self.tdir, ignore_errors=True)
self.handler.cmd.clear_images_on_screen(delete_data=True)
self.delete_all_sent_images()
del self.handler
def delete_all_sent_images(self):
def delete_all_sent_images(self) -> None:
gc = GraphicsCommand()
gc.a = 'd'
for img_id in self.transmission_status:
self.handler.cmd.gr_command({'a': 'd', 'i': img_id})
gc.i = img_id
self.handler.cmd.gr_command(gc)
self.transmission_status.clear()
def handle_response(self, apc):
def handle_response(self, apc: str) -> None:
cdata, payload = apc[1:].partition(';')[::2]
control = {}
for x in cdata.split(','):
@@ -178,7 +256,9 @@ class ImageManager:
if not in_flight:
self.placements_in_flight.pop(image_id, None)
def resend_image(self, image_id, pl):
def resend_image(self, image_id: int, pl: Placement) -> None:
if self.update_image_placement_for_resend is not None and not self.update_image_placement_for_resend(image_id, pl):
return
image_data = self.image_id_to_image_data[image_id]
skey = self.image_id_to_converted_data[image_id]
self.transmit_image(image_data, image_id, *skey)
@@ -186,7 +266,7 @@ class ImageManager:
self.handler.cmd.set_cursor_position(pl['x'], pl['y'])
self.handler.cmd.gr_command(pl['cmd'])
def send_image(self, path, max_cols=None, max_rows=None, scale_up=False):
def send_image(self, path: str, max_cols: Optional[int] = None, max_rows: Optional[int] = None, scale_up: bool = False) -> SentImageKey:
path = os.path.abspath(path)
if path in self.failed_images:
raise self.failed_images[path]
@@ -224,41 +304,50 @@ class ImageManager:
self.image_id_to_image_data[image_id] = m
return image_id, skey[1], skey[2]
def hide_image(self, image_id):
self.handler.cmd.gr_command({'a': 'd', 'i': image_id})
def hide_image(self, image_id: int) -> None:
gc = GraphicsCommand()
gc.a = 'd'
gc.i = image_id
self.handler.cmd.gr_command(gc)
def show_image(self, image_id, x, y, src_rect=None):
cmd = {'a': 'p', 'i': image_id}
def show_image(self, image_id: int, x: int, y: int, src_rect: Optional[Tuple[int, int, int, int]] = None) -> None:
gc = GraphicsCommand()
gc.a = 'p'
gc.i = image_id
if src_rect is not None:
cmd['x'], cmd['y'], cmd['w'], cmd['h'] = map(int, src_rect)
self.placements_in_flight[image_id].append({'cmd': cmd, 'x': x, 'y': y})
gc.x, gc.y, gc.w, gc.h = map(int, src_rect)
self.placements_in_flight[image_id].append({'cmd': gc, 'x': x, 'y': y})
with cursor(self.handler.write):
self.handler.cmd.set_cursor_position(x, y)
self.handler.cmd.gr_command(cmd)
self.handler.cmd.gr_command(gc)
def convert_image(self, path, available_width, available_height, image_data, scale_up=False):
def convert_image(self, path: str, available_width: int, available_height: int, image_data: ImageData, scale_up: bool = False) -> ImageKey:
rgba_path, width, height = convert(path, image_data, available_width, available_height, scale_up, tdir=self.tdir)
return rgba_path, width, height
def transmit_image(self, image_data, image_id, rgba_path, width, height):
def transmit_image(self, image_data: ImageData, image_id: int, rgba_path: str, width: int, height: int) -> int:
self.transmission_status[image_id] = 0
cmd = {'a': 't', 'f': image_data.transmit_fmt, 's': width, 'v': height, 'i': image_id}
gc = GraphicsCommand()
gc.a = 't'
gc.f = image_data.transmit_fmt
gc.s = width
gc.v = height
gc.i = image_id
if self.filesystem_ok:
cmd['t'] = 'f'
gc.t = 'f'
self.handler.cmd.gr_command(
cmd, standard_b64encode(rgba_path.encode(fsenc)))
gc, standard_b64encode(rgba_path.encode(fsenc)))
else:
import zlib
with open(rgba_path, 'rb') as f:
data = f.read()
cmd['S'] = len(data)
gc.S = len(data)
data = zlib.compress(data)
cmd['o'] = 'z'
gc.o = 'z'
data = standard_b64encode(data)
while data:
chunk, data = data[:4096], data[4096:]
m = 1 if data else 0
cmd['m'] = m
self.handler.cmd.gr_command(cmd, chunk)
cmd.clear()
gc.m = 1 if data else 0
self.handler.cmd.gr_command(gc, chunk)
gc.clear()
return image_id

View File

@@ -2,27 +2,36 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Callable, Tuple
from kitty.fast_data_types import truncate_point_for_length, wcswidth
from kitty.key_encoding import RELEASE, HOME, END, BACKSPACE, DELETE, LEFT, RIGHT
from kitty.key_encoding import RELEASE, KeyEvent, key_defs as K
HOME = K['HOME']
END = K['END']
BACKSPACE = K['BACKSPACE']
DELETE = K['DELETE']
LEFT = K['LEFT']
RIGHT = K['RIGHT']
class LineEdit:
def __init__(self):
def __init__(self) -> None:
self.clear()
def clear(self):
def clear(self) -> None:
self.current_input = ''
self.cursor_pos = 0
self.pending_bell = False
def split_at_cursor(self, delta=0):
def split_at_cursor(self, delta: int = 0) -> Tuple[str, str]:
pos = max(0, self.cursor_pos + delta)
x = truncate_point_for_length(self.current_input, pos) if pos else 0
before, after = self.current_input[:x], self.current_input[x:]
return before, after
def write(self, write, prompt=''):
def write(self, write: Callable[[str], None], prompt: str = '') -> None:
if self.pending_bell:
write('\a')
self.pending_bell = False
@@ -30,7 +39,7 @@ class LineEdit:
write(self.current_input)
write('\r\x1b[{}C'.format(self.cursor_pos + wcswidth(prompt)))
def add_text(self, text):
def add_text(self, text: str) -> None:
if self.current_input:
x = truncate_point_for_length(self.current_input, self.cursor_pos) if self.cursor_pos else 0
self.current_input = self.current_input[:x] + text + self.current_input[x:]
@@ -38,10 +47,10 @@ class LineEdit:
self.current_input = text
self.cursor_pos += wcswidth(text)
def on_text(self, text, in_bracketed_paste):
def on_text(self, text: str, in_bracketed_paste: bool) -> None:
self.add_text(text)
def backspace(self, num=1):
def backspace(self, num: int = 1) -> bool:
before, after = self.split_at_cursor()
nbefore = before[:-num]
if nbefore != before:
@@ -51,7 +60,7 @@ class LineEdit:
self.pending_bell = True
return False
def delete(self, num=1):
def delete(self, num: int = 1) -> bool:
before, after = self.split_at_cursor()
nafter = after[num:]
if nafter != after:
@@ -61,7 +70,7 @@ class LineEdit:
self.pending_bell = True
return False
def _left(self):
def _left(self) -> None:
if not self.current_input:
self.cursor_pos = 0
return
@@ -69,7 +78,7 @@ class LineEdit:
before, after = self.split_at_cursor(-1)
self.cursor_pos = wcswidth(before)
def _right(self):
def _right(self) -> None:
if not self.current_input:
self.cursor_pos = 0
return
@@ -80,33 +89,37 @@ class LineEdit:
before, after = self.split_at_cursor(1)
self.cursor_pos += 1 + int(wcswidth(before) == self.cursor_pos)
def _move_loop(self, func, num):
def _move_loop(self, func: Callable[[], None], num: int) -> bool:
before = self.cursor_pos
while func() and num > 0:
changed = False
while num > 0:
func()
changed = self.cursor_pos != before
if not changed:
break
num -= 1
changed = self.cursor_pos != before
if not changed:
self.pending_bell = True
return changed
def left(self, num=1):
def left(self, num: int = 1) -> bool:
return self._move_loop(self._left, num)
def right(self, num=1):
def right(self, num: int = 1) -> bool:
return self._move_loop(self._right, num)
def home(self):
def home(self) -> bool:
if self.cursor_pos:
self.cursor_pos = 0
return True
return False
def end(self):
def end(self) -> bool:
orig = self.cursor_pos
self.cursor_pos = wcswidth(self.current_input) + 1
self.cursor_pos = wcswidth(self.current_input)
return self.cursor_pos != orig
def on_key(self, key_event):
def on_key(self, key_event: KeyEvent) -> bool:
if key_event.type is RELEASE:
return False
elif key_event.key is HOME:

View File

@@ -10,65 +10,83 @@ import re
import selectors
import signal
import sys
from collections import namedtuple
from contextlib import contextmanager
from functools import partial
from typing import Any, Callable, Dict, Generator, List, NamedTuple, Optional
from kitty.constants import is_macos
from kitty.fast_data_types import (
close_tty, normal_tty, open_tty, parse_input_from_terminal, raw_tty
)
from kitty.key_encoding import (
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, C, D, backspace_key,
decode_key_event, enter_key
ALT, CTRL, PRESS, RELEASE, REPEAT, SHIFT, backspace_key, decode_key_event,
enter_key, key_defs as K
)
from kitty.utils import screen_size_function, write_all
from kitty.typing import ImageManagerType, KeyEventType, Protocol
from kitty.utils import ScreenSizeGetter, screen_size_function, write_all
from .handler import Handler
from .operations import init_state, reset_state
C, D = K['C'], K['D']
def debug(*a, **kw):
from base64 import standard_b64encode
buf = io.StringIO()
kw['file'] = buf
print(*a, **kw)
text = buf.getvalue()
text = b'\x1bP@kitty-print|' + standard_b64encode(text.encode('utf-8')) + b'\x1b\\'
fobj = getattr(debug, 'fobj', sys.stdout.buffer)
fobj.write(text)
if hasattr(fobj, 'flush'):
class BinaryWrite(Protocol):
def write(self, data: bytes) -> None:
pass
def flush(self) -> None:
pass
class Debug:
fobj: Optional[BinaryWrite] = None
def __call__(self, *a: Any, **kw: Any) -> None:
from base64 import standard_b64encode
buf = io.StringIO()
kw['file'] = buf
print(*a, **kw)
stext = buf.getvalue()
text = b'\x1bP@kitty-print|' + standard_b64encode(stext.encode('utf-8')) + b'\x1b\\'
fobj = self.fobj or sys.stdout.buffer
fobj.write(text)
fobj.flush()
debug = Debug()
class TermManager:
def __init__(self):
self.extra_finalize = None
def __init__(self) -> None:
self.extra_finalize: Optional[str] = None
def set_state_for_loop(self, set_raw=True):
def set_state_for_loop(self, set_raw: bool = True) -> None:
if set_raw:
raw_tty(self.tty_fd, self.original_termios)
write_all(self.tty_fd, init_state())
def reset_state_to_original(self):
def reset_state_to_original(self) -> None:
normal_tty(self.tty_fd, self.original_termios)
if self.extra_finalize:
write_all(self.tty_fd, self.extra_finalize)
write_all(self.tty_fd, reset_state())
@contextmanager
def suspend(self):
def suspend(self) -> Generator['TermManager', None, None]:
self.reset_state_to_original()
yield self
self.set_state_for_loop()
def __enter__(self):
def __enter__(self) -> 'TermManager':
self.tty_fd, self.original_termios = open_tty()
self.set_state_for_loop(set_raw=False)
return self
def __exit__(self, *a):
def __exit__(self, *a: object) -> None:
self.reset_state_to_original()
close_tty(self.tty_fd, self.original_termios)
del self.tty_fd, self.original_termios
@@ -76,7 +94,6 @@ class TermManager:
LEFT, MIDDLE, RIGHT, FOURTH, FIFTH = 1, 2, 4, 8, 16
DRAG = REPEAT
MouseEvent = namedtuple('MouseEvent', 'x y type buttons mods')
bmap = {0: LEFT, 1: MIDDLE, 2: RIGHT}
MOTION_INDICATOR = 1 << 5
EXTRA_BUTTON_INDICATOR = 1 << 6
@@ -85,10 +102,18 @@ ALT_INDICATOR = 1 << 3
CTRL_INDICATOR = 1 << 4
def decode_sgr_mouse(text):
cb, x, y = text.split(';')
m, y = y[-1], y[:-1]
cb, x, y = map(int, (cb, x, y))
class MouseEvent(NamedTuple):
x: int
y: int
type: int
buttons: int
mods: int
def decode_sgr_mouse(text: str) -> MouseEvent:
cb_, x_, y_ = text.split(';')
m, y_ = y_[-1], y_[:-1]
cb, x, y = map(int, (cb_, x_, y_))
typ = RELEASE if m == 'm' else (DRAG if cb & MOTION_INDICATOR else PRESS)
buttons = 0
cb3 = cb & 3
@@ -109,10 +134,10 @@ def decode_sgr_mouse(text):
class UnhandledException(Handler):
def __init__(self, tb):
def __init__(self, tb: str) -> None:
self.tb = tb
def initialize(self):
def initialize(self) -> None:
self.cmd.clear_screen()
self.cmd.set_scrolling_region()
self.cmd.set_cursor_visible(True)
@@ -121,41 +146,49 @@ class UnhandledException(Handler):
self.write('\r\n')
self.write('Press the Enter key to quit')
def on_key(self, key_event):
def on_key(self, key_event: KeyEventType) -> None:
if key_event is enter_key:
self.quit_loop(1)
def on_interrupt(self):
def on_interrupt(self) -> None:
self.quit_loop(1)
on_eot = on_term = on_interrupt
class SignalManager:
def __init__(self, loop, on_winch, on_interrupt, on_term):
def __init__(
self,
loop: asyncio.AbstractEventLoop,
on_winch: Callable,
on_interrupt: Callable,
on_term: Callable
) -> None:
self.asycio_loop = loop
self.on_winch, self.on_interrupt, self.on_term = on_winch, on_interrupt, on_term
def __enter__(self):
def __enter__(self) -> None:
tuple(map(lambda x: self.asycio_loop.add_signal_handler(*x), (
(signal.SIGWINCH, self.on_winch),
(signal.SIGINT, self.on_interrupt),
(signal.SIGTERM, self.on_term)
)))
def __exit__(self, *a):
def __exit__(self, *a: Any) -> None:
tuple(map(self.asycio_loop.remove_signal_handler, (
signal.SIGWINCH, signal.SIGINT, signal.SIGTERM)))
class Loop:
def __init__(self,
sanitize_bracketed_paste='[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]'):
def __init__(
self,
sanitize_bracketed_paste: str = '[\x03\x04\x0e\x0f\r\x07\x7f\x8d\x8e\x8f\x90\x9b\x9d\x9e\x9f]'
):
if is_macos:
# On macOS PTY devices are not supported by the KqueueSelector and
# the PollSelector is broken, causes 100% CPU usage
self.asycio_loop = asyncio.SelectorEventLoop(selectors.SelectSelector())
self.asycio_loop: asyncio.AbstractEventLoop = asyncio.SelectorEventLoop(selectors.SelectSelector())
asyncio.set_event_loop(self.asycio_loop)
else:
self.asycio_loop = asyncio.get_event_loop()
@@ -173,14 +206,14 @@ class Loop:
if self.sanitize_bracketed_paste:
self.sanitize_ibp_pat = re.compile(sanitize_bracketed_paste)
def _read_ready(self, handler, fd):
def _read_ready(self, handler: Handler, fd: int) -> None:
try:
data = os.read(fd, io.DEFAULT_BUFFER_SIZE)
bdata = os.read(fd, io.DEFAULT_BUFFER_SIZE)
except BlockingIOError:
return
if not data:
if not bdata:
raise EOFError('The input stream is closed')
data = self.decoder.decode(data)
data = self.decoder.decode(bdata)
if self.read_buf:
data = self.read_buf + data
self.read_buf = data
@@ -194,7 +227,7 @@ class Loop:
del self.handler
# terminal input callbacks {{{
def _on_text(self, text):
def _on_text(self, text: str) -> None:
if self.in_bracketed_paste and self.sanitize_bracketed_paste:
text = self.sanitize_ibp_pat.sub('', text)
@@ -213,8 +246,7 @@ class Loop:
elif chunk:
self.handler.on_text(chunk, self.in_bracketed_paste)
def _on_dcs(self, dcs):
debug(dcs)
def _on_dcs(self, dcs: str) -> None:
if dcs.startswith('@kitty-cmd'):
import json
self.handler.on_kitty_cmd_response(json.loads(dcs[len('@kitty-cmd'):]))
@@ -229,7 +261,7 @@ class Loop:
continue
self.handler.on_capability_response(name, val)
def _on_csi(self, csi):
def _on_csi(self, csi: str) -> None:
q = csi[-1]
if q in 'mM':
if csi.startswith('<'):
@@ -246,10 +278,10 @@ class Loop:
elif csi == '201~':
self.in_bracketed_paste = False
def _on_pm(self, pm):
def _on_pm(self, pm: str) -> None:
pass
def _on_osc(self, osc):
def _on_osc(self, osc: str) -> None:
m = re.match(r'(\d+);', osc)
if m is not None:
code = int(m.group(1))
@@ -260,7 +292,7 @@ class Loop:
from base64 import standard_b64decode
self.handler.on_clipboard_response(standard_b64decode(rest).decode('utf-8'), from_primary)
def _on_apc(self, apc):
def _on_apc(self, apc: str) -> None:
if apc.startswith('K'):
try:
k = decode_key_event(apc)
@@ -280,7 +312,7 @@ class Loop:
self.handler.image_manager.handle_response(apc)
# }}}
def _write_ready(self, handler, fd):
def _write_ready(self, handler: Handler, fd: int) -> None:
if len(self.write_buf) > self.iov_limit:
self.write_buf[self.iov_limit - 1] = b''.join(self.write_buf[self.iov_limit - 1:])
del self.write_buf[self.iov_limit:]
@@ -292,7 +324,7 @@ class Loop:
if not written:
raise EOFError('The output stream is closed')
if written >= sum(sizes):
self.write_buf = []
self.write_buf: List[bytes] = []
self.asycio_loop.remove_writer(fd)
self.waiting_for_writes = False
else:
@@ -308,24 +340,24 @@ class Loop:
break
del self.write_buf[:consumed]
def quit(self, return_code=None):
def quit(self, return_code: Optional[int] = None) -> None:
if return_code is not None:
self.return_code = return_code
self.asycio_loop.stop()
def loop_impl(self, handler, term_manager, image_manager=None):
def loop_impl(self, handler: Handler, term_manager: TermManager, image_manager: Optional[ImageManagerType] = None) -> Optional[str]:
self.write_buf = []
tty_fd = term_manager.tty_fd
tb = None
self.waiting_for_writes = True
def schedule_write(data):
def schedule_write(data: bytes) -> None:
self.write_buf.append(data)
if not self.waiting_for_writes:
self.asycio_loop.add_writer(tty_fd, self._write_ready, handler, tty_fd)
self.waiting_for_writes = True
def handle_exception(loop, context):
def handle_exception(loop: asyncio.AbstractEventLoop, context: Dict[str, Any]) -> None:
nonlocal tb
loop.stop()
tb = context['message']
@@ -347,17 +379,17 @@ class Loop:
self.asycio_loop.remove_writer(tty_fd)
return tb
def loop(self, handler):
tb = None
def loop(self, handler: Handler) -> None:
tb: Optional[str] = None
def _on_sigwinch():
def _on_sigwinch() -> None:
self._get_screen_size.changed = True
handler.screen_size = self._get_screen_size()
handler.on_resize(handler.screen_size)
signal_manager = SignalManager(self.asycio_loop, _on_sigwinch, handler.on_interrupt, handler.on_term)
with TermManager() as term_manager, signal_manager:
self._get_screen_size = screen_size_function(term_manager.tty_fd)
self._get_screen_size: ScreenSizeGetter = screen_size_function(term_manager.tty_fd)
image_manager = None
if handler.image_manager_class is not None:
image_manager = handler.image_manager_class(handler)
@@ -372,5 +404,5 @@ class Loop:
self.return_code = 1
self._report_error_loop(tb, term_manager)
def _report_error_loop(self, tb, term_manager):
def _report_error_loop(self, tb: str, term_manager: TermManager) -> None:
self.loop_impl(UnhandledException(tb), term_manager)

View File

@@ -5,9 +5,16 @@
import sys
from contextlib import contextmanager
from functools import wraps
from typing import (
IO, Any, Callable, Dict, Generator, Optional, Tuple, TypeVar, Union
)
from kitty.rgb import Color, color_as_sharp, to_color
from kitty.typing import GraphicsCommandType, HandlerType, ScreenSize
from .operations_stub import CMD
GraphicsCommandType, ScreenSize # needed for stub generation
S7C1T = '\033 F'
SAVE_CURSOR = '\0337'
RESTORE_CURSOR = '\0338'
@@ -35,57 +42,77 @@ MODES = dict(
EXTENDED_KEYBOARD=(2017, '?'),
)
def set_mode(which, private=True) -> str:
num, private = MODES[which]
return '\033[{}{}h'.format(private, num)
F = TypeVar('F')
all_cmds: Dict[str, Callable] = {}
def reset_mode(which) -> str:
def cmd(f: F) -> F:
all_cmds[f.__name__] = f # type: ignore
return f
@cmd
def set_mode(which: str, private: bool = True) -> str:
num, private_ = MODES[which]
return '\033[{}{}h'.format(private_, num)
@cmd
def reset_mode(which: str) -> str:
num, private = MODES[which]
return '\033[{}{}l'.format(private, num)
@cmd
def clear_screen() -> str:
return '\033[H\033[2J'
@cmd
def clear_to_eol() -> str:
return '\033[K'
@cmd
def bell() -> str:
return '\a'
@cmd
def beep() -> str:
return '\a'
def set_window_title(value) -> str:
return ('\033]2;' + value.replace('\033', '').replace('\x9c', '') + '\033\\')
@cmd
def set_window_title(value: str) -> str:
return '\033]2;' + value.replace('\033', '').replace('\x9c', '') + '\033\\'
def set_line_wrapping(yes_or_no) -> str:
return (set_mode if yes_or_no else reset_mode)('DECAWM')
@cmd
def set_line_wrapping(yes_or_no: bool) -> str:
return set_mode('DECAWM') if yes_or_no else reset_mode('DECAWM')
def set_cursor_visible(yes_or_no) -> str:
return (set_mode if yes_or_no else reset_mode)('DECTCEM')
@cmd
def set_cursor_visible(yes_or_no: bool) -> str:
return set_mode('DECTCEM') if yes_or_no else reset_mode('DECTCEM')
def set_cursor_position(x, y) -> str: # (0, 0) is top left
@cmd
def set_cursor_position(x: int, y: int) -> str: # (0, 0) is top left
return '\033[{};{}H'.format(y + 1, x + 1)
def set_cursor_shape(shape='block', blink=True) -> str:
@cmd
def set_cursor_shape(shape: str = 'block', blink: bool = True) -> str:
val = {'block': 1, 'underline': 3, 'bar': 5}.get(shape, 1)
if not blink:
val += 1
return '\033[{} q'.format(val)
def set_scrolling_region(screen_size=None, top=None, bottom=None) -> str:
@cmd
def set_scrolling_region(screen_size: Optional['ScreenSize'] = None, top: Optional[int] = None, bottom: Optional[int] = None) -> str:
if screen_size is None:
return '\033[r'
if top is None:
@@ -99,7 +126,8 @@ def set_scrolling_region(screen_size=None, top=None, bottom=None) -> str:
return '\033[{};{}r'.format(top + 1, bottom + 1)
def scroll_screen(amt=1) -> str:
@cmd
def scroll_screen(amt: int = 1) -> str:
return '\033[' + str(abs(amt)) + ('T' if amt < 0 else 'S')
@@ -110,7 +138,10 @@ UNDERLINE_STYLES = {name: i + 1 for i, name in enumerate(
'straight double curly'.split())}
def color_code(color, intense=False, base=30):
ColorSpec = Union[int, str, Tuple[int, int, int]]
def color_code(color: ColorSpec, intense: bool = False, base: int = 30) -> str:
if isinstance(color, str):
e = str((base + 60 if intense else base) + STANDARD_COLORS[color])
elif isinstance(color, int):
@@ -120,20 +151,41 @@ def color_code(color, intense=False, base=30):
return e
def sgr(*parts) -> str:
@cmd
def sgr(*parts: str) -> str:
return '\033[{}m'.format(';'.join(parts))
def colored(text, color, intense=False, reset_to=None, reset_to_intense=False) -> str:
@cmd
def colored(
text: str,
color: ColorSpec,
intense: bool = False,
reset_to: Optional[ColorSpec] = None,
reset_to_intense: bool = False
) -> str:
e = color_code(color, intense)
return '\033[{}m{}\033[{}m'.format(e, text, 39 if reset_to is None else color_code(reset_to, reset_to_intense))
def faint(text) -> str:
@cmd
def faint(text: str) -> str:
return colored(text, 'black', True)
def styled(text, fg=None, bg=None, fg_intense=False, bg_intense=False, italic=None, bold=None, underline=None, underline_color=None, reverse=None) -> str:
@cmd
def styled(
text: str,
fg: Optional[ColorSpec] = None,
bg: Optional[ColorSpec] = None,
fg_intense: bool = False,
bg_intense: bool = False,
italic: Optional[bool] = None,
bold: Optional[bool] = None,
underline: Optional[str] = None,
underline_color: Optional[ColorSpec] = None,
reverse: Optional[bool] = None
) -> str:
start, end = [], []
if fg is not None:
start.append(color_code(fg, fg_intense))
@@ -151,39 +203,48 @@ def styled(text, fg=None, bg=None, fg_intense=False, bg_intense=False, italic=No
end.append('4:0')
if italic is not None:
s, e = (start, end) if italic else (end, start)
s.append('3'), e.append('23')
s.append('3')
e.append('23')
if bold is not None:
s, e = (start, end) if bold else (end, start)
s.append('1'), e.append('22')
s.append('1')
e.append('22')
if reverse is not None:
s, e = (start, end) if reverse else (end, start)
s.append('7'), e.append('27')
s.append('7')
e.append('27')
if not start:
return text
return '\033[{}m{}\033[{}m'.format(';'.join(start), text, ';'.join(end))
def serialize_gr_command(cmd, payload=None):
cmd = ','.join('{}={}'.format(k, v) for k, v in cmd.items())
ans = []
w = ans.append
w(b'\033_G'), w(cmd.encode('ascii'))
if payload:
w(b';')
w(payload)
w(b'\033\\')
return b''.join(ans)
def serialize_gr_command(cmd: Dict[str, Union[int, str]], payload: Optional[bytes] = None) -> bytes:
from .images import GraphicsCommand
gc = GraphicsCommand()
for k, v in cmd.items():
setattr(gc, k, v)
return gc.serialize(payload or b'')
def gr_command(cmd, payload=None) -> str:
return serialize_gr_command(cmd, payload)
@cmd
def gr_command(cmd: Union[Dict, 'GraphicsCommandType'], payload: Optional[bytes] = None) -> str:
if isinstance(cmd, dict):
raw = serialize_gr_command(cmd, payload)
else:
raw = cmd.serialize(payload or b'')
return raw.decode('ascii')
def clear_images_on_screen(delete_data=False) -> str:
return serialize_gr_command({'a': 'd', 'd': 'A' if delete_data else 'a'})
@cmd
def clear_images_on_screen(delete_data: bool = False) -> str:
from .images import GraphicsCommand
gc = GraphicsCommand()
gc.a = 'd'
gc.d = 'A' if delete_data else 'a'
return gc.serialize().decode('ascii')
def init_state(alternate_screen=True):
def init_state(alternate_screen: bool = True) -> str:
ans = (
S7C1T + SAVE_CURSOR + SAVE_PRIVATE_MODE_VALUES + reset_mode('LNM') +
reset_mode('IRM') + reset_mode('DECKM') + reset_mode('DECSCNM') +
@@ -202,7 +263,7 @@ def init_state(alternate_screen=True):
return ans
def reset_state(normal_screen=True):
def reset_state(normal_screen: bool = True) -> str:
ans = ''
if normal_screen:
ans += reset_mode('ALTERNATE_SCREEN')
@@ -213,29 +274,42 @@ def reset_state(normal_screen=True):
@contextmanager
def cursor(write):
def cursor(write: Callable[[str], None]) -> Generator[None, None, None]:
write(SAVE_CURSOR)
yield
write(RESTORE_CURSOR)
@contextmanager
def alternate_screen(f=None):
def alternate_screen(f: Optional[IO[str]] = None) -> Generator[None, None, None]:
f = f or sys.stdout
print(set_mode('ALTERNATE_SCREEN'), end='', file=f)
yield
print(reset_mode('ALTERNATE_SCREEN'), end='', file=f)
def set_default_colors(fg=None, bg=None, cursor=None, select_bg=None, select_fg=None) -> str:
@cmd
def set_default_colors(
fg: Optional[Union[Color, str]] = None,
bg: Optional[Union[Color, str]] = None,
cursor: Optional[Union[Color, str]] = None,
select_bg: Optional[Union[Color, str]] = None,
select_fg: Optional[Union[Color, str]] = None
) -> str:
ans = ''
def item(which, num):
def item(which: Optional[Union[Color, str]], num: int) -> None:
nonlocal ans
if which is None:
ans += '\x1b]1{}\x1b\\'.format(num)
else:
ans += '\x1b]{};{}\x1b\\'.format(num, color_as_sharp(which if isinstance(which, Color) else to_color(which)))
if isinstance(which, Color):
q = color_as_sharp(which)
else:
x = to_color(which)
assert x is not None
q = color_as_sharp(x)
ans += '\x1b]{};{}\x1b\\'.format(num, q)
item(fg, 10)
item(bg, 11)
@@ -245,37 +319,67 @@ def set_default_colors(fg=None, bg=None, cursor=None, select_bg=None, select_fg=
return ans
def write_to_clipboard(data, use_primary=False) -> str:
@cmd
def write_to_clipboard(data: Union[str, bytes], use_primary: bool = False) -> str:
if isinstance(data, str):
data = data.encode('utf-8')
from base64 import standard_b64encode
fmt = 'p' if use_primary else 'c'
def esc(chunk):
def esc(chunk: str) -> str:
return '\x1b]52;{};{}\x07'.format(fmt, chunk)
ans = esc('!') # clear clipboard buffer
for chunk in (data[i:i+512] for i in range(0, len(data), 512)):
chunk = standard_b64encode(chunk).decode('ascii')
ans += esc(chunk)
s = standard_b64encode(chunk).decode('ascii')
ans += esc(s)
return ans
def request_from_clipboard(use_primary=False) -> str:
@cmd
def request_from_clipboard(use_primary: bool = False) -> str:
return '\x1b]52;{};?\x07'.format('p' if use_primary else 'c')
all_cmds = tuple(
(name, obj) for name, obj in globals().items()
if hasattr(obj, '__annotations__') and obj.__annotations__.get('return') is str)
# Boilerplate to make operations availble via Handler.cmd {{{
def writer(handler, func):
def writer(handler: HandlerType, func: Callable) -> Callable:
@wraps(func)
def f(self, *a, **kw):
def f(*a: Any, **kw: Any) -> None:
handler.write(func(*a, **kw))
return f
def commander(handler):
ans = {name: writer(handler, obj) for name, obj in all_cmds}
return type('CMD', (), ans)()
def commander(handler: HandlerType) -> CMD:
ans = CMD()
for name, func in all_cmds.items():
setattr(ans, name, writer(handler, func))
return ans
def func_sig(func: Callable) -> Generator[str, None, None]:
import inspect
import re
s = inspect.signature(func)
for val in s.parameters.values():
yield re.sub(r'ForwardRef\([\'"](\w+?)[\'"]\)', r'\1', str(val).replace('NoneType', 'None'))
def as_type_stub() -> str:
ans = [
'from typing import * # noqa',
'from kitty.typing import GraphicsCommandType, ScreenSize',
'from kitty.rgb import Color',
'import kitty.rgb',
]
methods = []
for name, func in all_cmds.items():
args = ', '.join(func_sig(func))
if args:
args = ', ' + args
methods.append(' def {}(self{}) -> str: pass'.format(name, args))
ans += ['', '', 'class CMD:'] + methods
return '\n'.join(ans) + '\n\n\n'
# }}}

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
class CMD:
pass
def generate_stub() -> None:
from kittens.tui.operations import as_type_stub
from kitty.conf.definition import save_type_stub
text = as_type_stub()
save_type_stub(text, __file__)

View File

@@ -5,28 +5,46 @@
import os
import string
import subprocess
import sys
from contextlib import suppress
from functools import lru_cache
from gettext import gettext as _
from contextlib import suppress
from kitty.config import cached_values_for
from kitty.constants import config_dir
from kitty.utils import get_editor
from kitty.fast_data_types import wcswidth
from kitty.key_encoding import (
DOWN, ESCAPE, F1, F2, F3, F4, F12, LEFT, RELEASE, RIGHT, SHIFT, TAB, UP,
enter_key
from typing import (
Any, Dict, FrozenSet, Generator, Iterable, List, Optional, Sequence, Tuple,
Union
)
from kitty.cli import parse_args
from kitty.cli_stub import UnicodeCLIOptions
from kitty.config import cached_values_for
from kitty.constants import config_dir
from kitty.fast_data_types import is_emoji_presentation_base, wcswidth
from kitty.key_encoding import (
CTRL, RELEASE, SHIFT, KeyEvent, enter_key, key_defs as K
)
from kitty.typing import BossType
from kitty.utils import ScreenSize, get_editor
from ..tui.handler import Handler, result_handler
from ..tui.line_edit import LineEdit
from ..tui.handler import Handler
from ..tui.loop import Loop
from ..tui.operations import (
clear_screen, colored, cursor, faint, set_line_wrapping,
set_window_title, sgr, styled
clear_screen, colored, cursor, faint, set_line_wrapping, set_window_title,
sgr, styled
)
HEX, NAME, EMOTICONS, FAVORITES = 'HEX', 'NAME', 'EMOTICONS', 'FAVORITES'
UP = K['UP']
DOWN = K['DOWN']
LEFT = K['LEFT']
RIGHT = K['RIGHT']
TAB = K['TAB']
ESCAPE = K['ESCAPE']
F1 = K['F1']
F2 = K['F2']
F3 = K['F3']
F4 = K['F4']
F12 = K['F12']
favorites_path = os.path.join(config_dir, 'unicode-input-favorites.conf')
INDEX_CHAR = '.'
DEFAULT_SET = tuple(map(
@@ -36,28 +54,33 @@ DEFAULT_SET = tuple(map(
'îïðñòóôõöøœš' 'ùúûüýÿþªºαΩ∞'
))
EMOTICONS_SET = tuple(range(0x1f600, 0x1f64f + 1))
all_modes = (
(_('Code'), 'F1', HEX),
(_('Name'), 'F2', NAME),
(_('Emoji'), 'F3', EMOTICONS),
(_('Favorites'), 'F4', FAVORITES),
)
def codepoint_ok(code):
def codepoint_ok(code: int) -> bool:
return not (code <= 32 or code == 127 or 128 <= code <= 159 or 0xd800 <= code <= 0xdbff or 0xDC00 <= code <= 0xDFFF)
@lru_cache(maxsize=256)
def points_for_word(w):
def points_for_word(w: str) -> FrozenSet[int]:
from .unicode_names import codepoints_for_word
return codepoints_for_word(w.lower())
@lru_cache(maxsize=4096)
def name(cp):
def name(cp: Union[int, str]) -> str:
from .unicode_names import name_for_codepoint
if isinstance(cp, str):
cp = ord(cp[0])
return (name_for_codepoint(cp) or '').capitalize()
c = ord(cp[0]) if isinstance(cp, str) else cp
return (name_for_codepoint(c) or '').capitalize()
@lru_cache(maxsize=256)
def codepoints_matching_search(parts):
def codepoints_matching_search(parts: Sequence[str]) -> List[int]:
ans = []
if parts and parts[0] and len(parts[0]) > 1:
codepoints = points_for_word(parts[0])
@@ -68,13 +91,13 @@ def codepoints_matching_search(parts):
if intersection:
codepoints = intersection
continue
codepoints = {c for c in codepoints if word in name(c).lower()}
codepoints = frozenset(c for c in codepoints if word in name(c).lower())
if codepoints:
ans = list(sorted(codepoints))
return ans
def parse_favorites(raw):
def parse_favorites(raw: str) -> Generator[int, None, None]:
for line in raw.splitlines():
line = line.strip()
if line.startswith('#') or not line:
@@ -92,7 +115,7 @@ def parse_favorites(raw):
yield code
def serialize_favorites(favorites):
def serialize_favorites(favorites: Iterable[int]) -> str:
ans = '''\
# Favorite characters for unicode input
# Enter the hex code for each favorite character on a new line. Blank lines are
@@ -104,19 +127,20 @@ def serialize_favorites(favorites):
return '\n'.join(ans)
def load_favorites(refresh=False):
ans = getattr(load_favorites, 'ans', None)
def load_favorites(refresh: bool = False) -> List[int]:
ans: Optional[List[int]] = getattr(load_favorites, 'ans', None)
if ans is None or refresh:
try:
with open(favorites_path, 'rb') as f:
raw = f.read().decode('utf-8')
ans = load_favorites.ans = list(parse_favorites(raw)) or list(DEFAULT_SET)
ans = list(parse_favorites(raw)) or list(DEFAULT_SET)
except FileNotFoundError:
ans = load_favorites.ans = list(DEFAULT_SET)
ans = list(DEFAULT_SET)
setattr(load_favorites, 'ans', ans)
return ans
def encode_hint(num, digits=string.digits + string.ascii_lowercase):
def encode_hint(num: int, digits: str = string.digits + string.ascii_lowercase) -> str:
res = ''
d = len(digits)
while not res or num > 0:
@@ -125,49 +149,53 @@ def encode_hint(num, digits=string.digits + string.ascii_lowercase):
return res
def decode_hint(x):
def decode_hint(x: str) -> int:
return int(x, 36)
class Table:
def __init__(self):
self.layout_dirty = True
def __init__(self, emoji_variation: str) -> None:
self.emoji_variation = emoji_variation
self.layout_dirty: bool = True
self.last_rows = self.last_cols = -1
self.codepoints = []
self.codepoints: List[int] = []
self.current_idx = 0
self.text = ''
self.num_cols = 0
self.mode = HEX
@property
def current_codepoint(self):
def current_codepoint(self) -> Optional[int]:
if self.codepoints:
return self.codepoints[self.current_idx]
def set_codepoints(self, codepoints, mode=HEX):
def set_codepoints(self, codepoints: List[int], mode: str = HEX) -> None:
self.codepoints = codepoints
self.mode = mode
self.layout_dirty = True
self.current_idx = 0
def codepoint_at_hint(self, hint):
def codepoint_at_hint(self, hint: str) -> int:
return self.codepoints[decode_hint(hint)]
def layout(self, rows, cols):
def layout(self, rows: int, cols: int) -> Optional[str]:
if not self.layout_dirty and self.last_cols == cols and self.last_rows == rows:
return self.text
self.last_cols, self.last_rows = cols, rows
self.layout_dirty = False
def safe_chr(codepoint):
return chr(codepoint).encode('utf-8', 'replace').decode('utf-8')
def safe_chr(codepoint: int) -> str:
ans = chr(codepoint).encode('utf-8', 'replace').decode('utf-8')
if self.emoji_variation and is_emoji_presentation_base(codepoint):
ans += self.emoji_variation
return ans
if self.mode is NAME:
def as_parts(i, codepoint):
def as_parts(i: int, codepoint: int) -> Tuple[str, str, str]:
return encode_hint(i).ljust(idx_size), safe_chr(codepoint), name(codepoint)
def cell(i, idx, c, desc):
def cell(i: int, idx: str, c: str, desc: str) -> Generator[str, None, None]:
is_current = i == self.current_idx
text = colored(idx, 'green') + ' ' + sgr('49') + c + ' '
w = wcswidth(c)
@@ -184,10 +212,10 @@ class Table:
yield styled(text, reverse=True if is_current else None)
else:
def as_parts(i, codepoint):
def as_parts(i: int, codepoint: int) -> Tuple[str, str, str]:
return encode_hint(i).ljust(idx_size), safe_chr(codepoint), ''
def cell(i, idx, c, desc):
def cell(i: int, idx: str, c: str, desc: str) -> Generator[str, None, None]:
yield colored(idx, 'green') + ' '
yield colored(c, 'gray', True)
w = wcswidth(c)
@@ -211,7 +239,7 @@ class Table:
col_width = min(col_width, 40)
space_for_desc = col_width - 2 - idx_size - 4
num_cols = self.num_cols = max(cols // col_width, 1)
buf = []
buf: List[str] = []
a = buf.append
rows_left = rows
@@ -220,13 +248,13 @@ class Table:
rows_left -= 1
if rows_left == 0:
break
buf.append('\r\n')
a('\r\n')
buf.extend(cell(i, idx, c, desc))
a(' ')
self.text = ''.join(buf)
return self.text
def move_current(self, rows=0, cols=0):
def move_current(self, rows: int = 0, cols: int = 0) -> None:
if len(self.codepoints) == 0:
return
if cols:
@@ -239,31 +267,45 @@ class Table:
self.layout_dirty = True
def is_index(w):
with suppress(Exception):
def is_index(w: str) -> bool:
try:
int(w.lstrip(INDEX_CHAR), 16)
return True
return False
except Exception:
return False
class UnicodeInput(Handler):
def __init__(self, cached_values):
def __init__(self, cached_values: Dict[str, Any], emoji_variation: str = 'none') -> None:
self.cached_values = cached_values
self.emoji_variation = ''
if emoji_variation == 'text':
self.emoji_variation = '\ufe0e'
elif emoji_variation == 'graphic':
self.emoji_variation = '\ufe0f'
self.line_edit = LineEdit()
self.recent = list(self.cached_values.get('recent', DEFAULT_SET))
self.current_char = None
self.current_char: Optional[str] = None
self.prompt_template = '{}> '
self.last_updated_code_point_at = None
self.last_updated_code_point_at: Optional[Tuple[str, Union[Sequence[int], None, str]]] = None
self.choice_line = ''
self.mode = globals().get(cached_values.get('mode', 'HEX'), 'HEX')
self.table = Table()
self.table = Table(self.emoji_variation)
self.update_prompt()
def update_codepoints(self):
@property
def resolved_current_char(self) -> Optional[str]:
ans = self.current_char
if ans:
if self.emoji_variation and is_emoji_presentation_base(ord(ans[0])):
ans += self.emoji_variation
return ans
def update_codepoints(self) -> None:
codepoints = None
if self.mode is HEX:
q = self.mode, None
q: Tuple[str, Optional[Union[str, Sequence[int]]]] = (self.mode, None)
codepoints = self.recent
elif self.mode is EMOTICONS:
q = self.mode, None
@@ -282,14 +324,14 @@ class UnicodeInput(Handler):
words = words[:index_words[0]]
codepoints = codepoints_matching_search(tuple(words))
if index_words:
index_word = int(index_word.lstrip(INDEX_CHAR), 16)
if index_word < len(codepoints):
codepoints = [codepoints[index_word]]
iindex_word = int(index_word.lstrip(INDEX_CHAR), 16)
if codepoints and iindex_word < len(codepoints):
codepoints = [codepoints[iindex_word]]
if q != self.last_updated_code_point_at:
self.last_updated_code_point_at = q
self.table.set_codepoints(codepoints, self.mode)
self.table.set_codepoints(codepoints or [], self.mode)
def update_current_char(self):
def update_current_char(self) -> None:
self.update_codepoints()
self.current_char = None
if self.mode is HEX:
@@ -313,33 +355,30 @@ class UnicodeInput(Handler):
if not codepoint_ok(code):
self.current_char = None
def update_prompt(self):
def update_prompt(self) -> None:
self.update_current_char()
if self.current_char is None:
c, color = '??', 'red'
self.choice_line = ''
else:
c, color = self.current_char, 'green'
if self.emoji_variation and is_emoji_presentation_base(ord(c[0])):
c += self.emoji_variation
self.choice_line = _('Chosen:') + ' {} U+{} {}'.format(
colored(c, 'green'), hex(ord(c))[2:], faint(styled(name(c) or '', italic=True)))
colored(c, 'green'), hex(ord(c[0]))[2:], faint(styled(name(c) or '', italic=True)))
self.prompt = self.prompt_template.format(colored(c, color))
def init_terminal_state(self):
def init_terminal_state(self) -> None:
self.write(set_line_wrapping(False))
self.write(set_window_title(_('Unicode input')))
def initialize(self):
def initialize(self) -> None:
self.init_terminal_state()
self.draw_screen()
def draw_title_bar(self):
def draw_title_bar(self) -> None:
entries = []
for name, key, mode in [
(_('Code'), 'F1', HEX),
(_('Name'), 'F2', NAME),
(_('Emoji'), 'F3', EMOTICONS),
(_('Favorites'), 'F4', FAVORITES),
]:
for name, key, mode in all_modes:
entry = ' {} ({}) '.format(name, key)
if mode is self.mode:
entry = styled(entry, reverse=False, bold=True)
@@ -350,12 +389,12 @@ class UnicodeInput(Handler):
text += ' ' * extra
self.print(styled(text, reverse=True))
def draw_screen(self):
def draw_screen(self) -> None:
self.write(clear_screen())
self.draw_title_bar()
y = 1
def writeln(text=''):
def writeln(text: str = '') -> None:
nonlocal y
self.print(text)
y += 1
@@ -377,17 +416,19 @@ class UnicodeInput(Handler):
elif self.mode is FAVORITES:
writeln(faint(_('Press F12 to edit the list of favorites')))
self.table_at = y
self.write(self.table.layout(self.screen_size.rows - self.table_at, self.screen_size.cols))
q = self.table.layout(self.screen_size.rows - self.table_at, self.screen_size.cols)
if q:
self.write(q)
def refresh(self):
def refresh(self) -> None:
self.update_prompt()
self.draw_screen()
def on_text(self, text, in_bracketed_paste):
def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
self.line_edit.on_text(text, in_bracketed_paste)
self.refresh()
def on_key(self, key_event):
def on_key(self, key_event: KeyEvent) -> None:
if self.mode is HEX and key_event.type is not RELEASE and not key_event.mods:
try:
val = int(self.line_edit.current_input, 16)
@@ -409,21 +450,27 @@ class UnicodeInput(Handler):
if self.mode is NAME and key_event.type is not RELEASE and not key_event.mods:
if key_event.key is TAB:
if key_event.mods == SHIFT:
self.table.move_current(cols=-1), self.refresh()
self.table.move_current(cols=-1)
self.refresh()
elif not key_event.mods:
self.table.move_current(cols=1), self.refresh()
self.table.move_current(cols=1)
self.refresh()
return
elif key_event.key is LEFT and not key_event.mods:
self.table.move_current(cols=-1), self.refresh()
self.table.move_current(cols=-1)
self.refresh()
return
elif key_event.key is RIGHT and not key_event.mods:
self.table.move_current(cols=1), self.refresh()
self.table.move_current(cols=1)
self.refresh()
return
elif key_event.key is UP and not key_event.mods:
self.table.move_current(rows=-1), self.refresh()
self.table.move_current(rows=-1)
self.refresh()
return
elif key_event.key is DOWN and not key_event.mods:
self.table.move_current(rows=1), self.refresh()
self.table.move_current(rows=1)
self.refresh()
return
if self.line_edit.on_key(key_event):
@@ -431,21 +478,26 @@ class UnicodeInput(Handler):
return
if key_event is enter_key:
self.quit_loop(0)
elif key_event.type is RELEASE and not key_event.mods:
if key_event.key is ESCAPE:
self.quit_loop(1)
elif key_event.key is F1:
self.switch_mode(HEX)
elif key_event.key is F2:
self.switch_mode(NAME)
elif key_event.key is F3:
self.switch_mode(EMOTICONS)
elif key_event.key is F4:
self.switch_mode(FAVORITES)
elif key_event.key is F12 and self.mode is FAVORITES:
self.edit_favorites()
elif key_event.type is RELEASE:
if not key_event.mods:
if key_event.key is ESCAPE:
self.quit_loop(1)
elif key_event.key is F1:
self.switch_mode(HEX)
elif key_event.key is F2:
self.switch_mode(NAME)
elif key_event.key is F3:
self.switch_mode(EMOTICONS)
elif key_event.key is F4:
self.switch_mode(FAVORITES)
elif key_event.key is F12 and self.mode is FAVORITES:
self.edit_favorites()
elif key_event.mods == CTRL and key_event.key is TAB:
self.next_mode()
elif key_event.mods == CTRL | SHIFT and key_event.key is TAB:
self.next_mode(-1)
def edit_favorites(self):
def edit_favorites(self) -> None:
if not os.path.exists(favorites_path):
with open(favorites_path, 'wb') as f:
f.write(serialize_favorites(load_favorites()).encode('utf-8'))
@@ -456,7 +508,7 @@ class UnicodeInput(Handler):
self.init_terminal_state()
self.refresh()
def switch_mode(self, mode):
def switch_mode(self, mode: str) -> None:
if mode is not self.mode:
self.mode = mode
self.cached_values['mode'] = mode
@@ -465,42 +517,76 @@ class UnicodeInput(Handler):
self.choice_line = ''
self.refresh()
def on_interrupt(self):
def next_mode(self, delta: int = 1) -> None:
modes = tuple(x[-1] for x in all_modes)
idx = (modes.index(self.mode) + delta + len(modes)) % len(modes)
self.switch_mode(modes[idx])
def on_interrupt(self) -> None:
self.quit_loop(1)
def on_eot(self):
def on_eot(self) -> None:
self.quit_loop(1)
def on_resize(self, new_size):
def on_resize(self, new_size: ScreenSize) -> None:
self.refresh()
def main(args):
help_text = 'Input a unicode character'
usage = ''
OPTIONS = '''
--emoji-variation
type=choices
default=none
choices=none,graphic,text
Whether to use the textual or the graphical form for emoji. By default the
default form specified in the unicode standard for the symbol is used.
'''.format
def parse_unicode_input_args(args: List[str]) -> Tuple[UnicodeCLIOptions, List[str]]:
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten unicode_input', result_class=UnicodeCLIOptions)
def main(args: List[str]) -> Optional[str]:
try:
cli_opts, items = parse_unicode_input_args(args[1:])
except SystemExit as e:
if e.code != 0:
print(e.args[0], file=sys.stderr)
input(_('Press Enter to quit'))
return None
loop = Loop()
with cached_values_for('unicode-input') as cached_values:
handler = UnicodeInput(cached_values)
handler = UnicodeInput(cached_values, cli_opts.emoji_variation)
loop.loop(handler)
if handler.current_char and loop.return_code == 0:
with suppress(Exception):
handler.recent.remove(ord(handler.current_char))
recent = [ord(handler.current_char)] + handler.recent
cached_values['recent'] = recent[:len(DEFAULT_SET)]
return handler.current_char
return handler.resolved_current_char
if loop.return_code != 0:
raise SystemExit(loop.return_code)
return None
def handle_result(args, current_char, target_window_id, boss):
@result_handler()
def handle_result(args: List[str], current_char: str, target_window_id: int, boss: BossType) -> None:
w = boss.window_id_map.get(target_window_id)
if w is not None:
w.paste(current_char)
if __name__ == '__main__':
import sys
if '-h' in sys.argv or '--help' in sys.argv:
print('Choose a unicode character to input into the terminal')
raise SystemExit(0)
ans = main(sys.argv)
if ans:
print(ans)
elif __name__ == '__doc__':
cd = sys.cli_docs # type: ignore
cd['usage'] = usage
cd['options'] = OPTIONS
cd['help_text'] = help_text

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,13 @@
from typing import Tuple, FrozenSet, Optional
def all_words() -> Tuple[str, ...]:
pass
def codepoints_for_word(word: str) -> FrozenSet[int]:
pass
def name_for_codepoint(cp: int) -> Optional[str]:
pass

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