Compare commits

...

463 Commits

Author SHA1 Message Date
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
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
197 changed files with 21865 additions and 14701 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)

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

@@ -2,4 +2,4 @@
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,13 +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'
sys.kitty_ssl_env_var = 'SSL_CERT_FILE'
setattr(sys, 'kitty_ssl_env_var', 'SSL_CERT_FILE')
elif os.path.isdir('/etc/ssl/certs'):
os.environ['SSL_CERT_DIR'] = '/etc/ssl/certs'
sys.kitty_ssl_env_var = 'SSL_CERT_DIR'
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]
@@ -109,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,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

View File

@@ -4,6 +4,159 @@ Changelog
|kitty| is a feature full, cross-platform, *fast*, GPU based terminal emulator.
To update |kitty|, :doc:`follow the instructions <binary>`.
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]
--------------------

View File

@@ -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,14 +243,14 @@ 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:
@@ -258,16 +266,17 @@ if you specify a program-to-run you can use the special placeholder
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)
@@ -286,14 +295,14 @@ if you specify a program-to-run you can use the special placeholder
# }}}
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)
@@ -319,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('_', '-')
@@ -383,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)
@@ -395,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 = ''
@@ -434,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('')
@@ -461,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:
@@ -473,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('')
@@ -492,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}'
@@ -510,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:
@@ -521,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
@@ -529,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
@@ -544,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',
@@ -565,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)
@@ -587,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

@@ -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,7 +53,7 @@ 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.
@@ -178,14 +178,19 @@ previously active windows for negative numbers.
You can define shortcuts to detach the current window and
move it to another tab or another OS window::
map ctrl+f2 detach_window # moves the window into a new OS window
map ctrl+f3 detach_window new-tab # moves the window into a new Tab
map ctrl+f4 detach_window ask # asks which tab to move the window into
# 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::
map ctrl+f2 detach_tab # moves the tab into a new OS window
map ctrl+f4 detach_tab ask # asks which OS Window to move the tab into
# 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
Other keyboard shortcuts
----------------------------------
@@ -219,55 +224,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:
@@ -441,6 +417,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
---------------------------------

View File

@@ -20,7 +20,7 @@ your machine).
.. code-block:: python
def main(args):
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: ')
@@ -28,7 +28,7 @@ your machine).
# handle_result() function
return answer
def handle_result(args, answer, target_window_id, boss):
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:
@@ -77,10 +77,11 @@ function, telling kitty what kind of input your kitten would like. For example:
# 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``
@@ -106,6 +107,8 @@ Create a file in the kitty config folder, :file:`~/.config/kitty/zoom_toggle.py`
def main(args):
pass
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:
@@ -114,8 +117,6 @@ Create a file in the kitty config folder, :file:`~/.config/kitty/zoom_toggle.py`
else:
tab.goto_layout('stack')
handle_result.no_ui = True
Now in kitty.conf add::

View File

@@ -18,6 +18,16 @@ 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

View File

@@ -23,6 +23,10 @@ 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
-------------------------

View File

@@ -50,6 +50,17 @@ 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
Syntax reference
------------------

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

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

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)
@@ -225,7 +238,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 +246,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,24 +46,24 @@ def get_data(fname, folder='UCD'):
# Map of class names to set of codepoints in class
class_maps = {}
all_symbols = set()
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}
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))
@@ -77,7 +81,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
@@ -99,43 +103,45 @@ def parse_ucd():
word_search_map['lamda'] |= word_search_map['lambda']
def split_two(line):
def split_two(line: str) -> Tuple[Set[int], str]:
spec, rest = line.split(';', 1)
spec, rest = spec.strip(), rest.strip().split(' ', 1)[0].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
all_emoji = set()
emoji_categories = {}
emoji_presentation_bases = set()
all_emoji: Set[int] = set()
emoji_categories: Dict[str, Set[int]] = {}
emoji_presentation_bases: Set[int] = set()
def parse_emoji():
def parse_emoji() -> None:
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()
parts = line.split()
base, var = parts[0], parts[1]
if base.startswith('#'):
continue
base = int(base, 16)
if var.upper() == 'FE0F':
emoji_presentation_bases.add(base)
ibase = int(base, 16)
emoji_presentation_bases.add(ibase)
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':
@@ -151,7 +157,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))
@@ -162,7 +168,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:
@@ -170,7 +176,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())
@@ -187,7 +193,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) {')
@@ -217,9 +223,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
@@ -234,7 +248,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):
@@ -249,14 +263,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:
@@ -270,7 +284,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"')
@@ -298,7 +312,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]
@@ -307,7 +323,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)}
@@ -362,28 +378,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)
@@ -408,10 +424,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_: Set[int], ret: int) -> None:
chars = chars_ - seen
seen.update(chars)
p(f'\t\t// {comment} ({len(chars)} codepoints)' + ' {{' '{')

0
glfw/__init__.py Normal file
View File

55
glfw/backend_utils.c vendored
View File

@@ -292,7 +292,7 @@ finalizePollData(EventLoopData *eld) {
}
int
pollForEvents(EventLoopData *eld, monotonic_t 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", monotonic_t_to_s_double(timeout));
@@ -305,6 +305,7 @@ pollForEvents(EventLoopData *eld, monotonic_t 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);
@@ -319,6 +320,7 @@ pollForEvents(EventLoopData *eld, monotonic_t 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);
@@ -330,54 +332,3 @@ pollForEvents(EventLoopData *eld, monotonic_t 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;
}

View File

@@ -90,9 +90,8 @@ void toggleTimer(EventLoopData *eld, id_type timer_id, int enabled);
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);
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

@@ -529,6 +529,32 @@ static GLFWapplicationwillfinishlaunchingfun finish_launching_callback = NULL;
@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 //////
//////////////////////////////////////////////////////////////////////////

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 //////
//////////////////////////////////////////////////////////////////////////
@@ -331,6 +351,7 @@ void _glfwPollMonitorsNS(void)
_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);
@@ -360,9 +381,6 @@ void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* 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;
@@ -373,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;
@@ -392,7 +411,6 @@ void _glfwSetVideoModeNS(_GLFWmonitor* monitor, const GLFWvidmode* desired)
}
CFRelease(modes);
CVDisplayLinkRelease(link);
}
// Restore the previously saved (original) video mode
@@ -469,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));
@@ -482,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++)
{
@@ -499,20 +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)

16
glfw/cocoa_platform.h vendored
View File

@@ -65,13 +65,15 @@ typedef void* CVDisplayLinkRef;
#endif
typedef VkFlags VkMacOSSurfaceCreateFlagsMVK;
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int, unsigned long);
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;
@@ -80,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"
@@ -201,6 +212,7 @@ typedef struct _GLFWmonitorNS
CGDisplayModeRef previousMode;
uint32_t unitNumber;
id screen;
double fallbackRefreshRate;
} _GLFWmonitorNS;
@@ -227,6 +239,8 @@ void _glfwRestoreVideoModeNS(_GLFWmonitor* monitor);
float _glfwTransformYNS(float y);
void* _glfwLoadLocalVulkanLoaderNS(void);
void _glfwClearDisplayLinks(void);
void _glfwRestartDisplayLinks(void);
void _glfwDispatchTickCallback(void);

View File

@@ -410,6 +410,7 @@ static int translateKey(unsigned int key, bool apply_keymap)
K('[', LEFT_BRACKET);
K('\\', BACKSLASH);
K(']', RIGHT_BRACKET);
K('^', CIRCUMFLEX);
K('_', UNDERSCORE);
K('`', GRAVE_ACCENT);
K(PARAGRAPH_UTF_8, PARAGRAPH);
@@ -658,6 +659,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
NSTrackingArea* trackingArea;
NSMutableAttributedString* markedText;
NSRect markedRect;
NSString *input_source_at_last_key_event;
}
- (void) removeGLFWWindow;
@@ -676,6 +678,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
trackingArea = nil;
markedText = [[NSMutableAttributedString alloc] init];
markedRect = NSMakeRect(0.0, 0.0, 0.0, 0.0);
input_source_at_last_key_event = nil;
[self updateTrackingAreas];
[self registerForDraggedTypes:@[NSPasteboardTypeFileURL, NSPasteboardTypeString]];
@@ -688,6 +691,7 @@ static const NSRange kEmptyRange = { NSNotFound, 0 };
{
[trackingArea release];
[markedText release];
if (input_source_at_last_key_event) [input_source_at_last_key_event release];
[super dealloc];
}
@@ -964,12 +968,19 @@ is_ascii_control_char(char x) {
- (void)keyDown:(NSEvent *)event
{
const bool previous_has_marked_text = [self hasMarkedText];
bool input_source_changed = false;
NSTextInputContext *inpctx = [NSTextInputContext currentInputContext];
if (inpctx && (!input_source_at_last_key_event || ![input_source_at_last_key_event isEqualToString:inpctx.selectedKeyboardInputSource])) {
input_source_at_last_key_event = [inpctx.selectedKeyboardInputSource retain];
input_source_changed = true;
}
const unsigned int keycode = [event keyCode];
const NSUInteger flags = [event modifierFlags];
const int mods = translateFlags(flags);
const int key = translateKey(keycode, true);
const bool process_text = !window->ns.textInputFilterCallback || window->ns.textInputFilterCallback(key, mods, keycode, flags) != 1;
const bool previous_has_marked_text = [self hasMarkedText];
[self unmarkText];
_glfw.ns.text[0] = 0;
GLFWkeyevent glfw_keyevent;
@@ -983,6 +994,14 @@ is_ascii_control_char(char x) {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
}
} else {
if (input_source_changed) {
debug_key(@"Input source changed, clearing pre-edit text and resetting deadkey state\n");
glfw_keyevent.text = NULL;
glfw_keyevent.ime_state = 1;
window->ns.deadKeyState = 0;
_glfwInputKeyboard(window, &glfw_keyevent); // clear pre-edit text
}
static UniChar text[256];
UniCharCount char_count = 0;
const bool in_compose_sequence = window->ns.deadKeyState != 0;
@@ -1140,27 +1159,20 @@ is_ascii_control_char(char x) {
const NSUInteger count = [objs count];
if (count)
{
char** paths = calloc(count, sizeof(char*));
for (NSUInteger i = 0; i < count; i++)
{
id obj = objs[i];
if ([obj isKindOfClass:[NSURL class]]) {
paths[i] = _glfw_strdup([obj fileSystemRepresentation]);
const char *path = [obj fileSystemRepresentation];
_glfwInputDrop(window, "text/plain;charset=utf-8", path, strlen(path));
} else if ([obj isKindOfClass:[NSString class]]) {
paths[i] = _glfw_strdup([obj UTF8String]);
const char *text = [obj UTF8String];
_glfwInputDrop(window, "text/plain;charset=utf-8", text, strlen(text));
} else {
_glfwInputError(GLFW_PLATFORM_ERROR,
"Cocoa: Object is neither a URL nor a string");
paths[i] = _glfw_strdup("");
}
}
_glfwInputDrop(window, (int) count, (const char**) paths);
for (NSUInteger i = 0; i < count; i++)
free(paths[i]);
free(paths);
}
return YES;
@@ -1577,10 +1589,18 @@ void _glfwPlatformSetWindowSizeLimits(_GLFWwindow* window,
void _glfwPlatformSetWindowAspectRatio(_GLFWwindow* window, int numer, int denom)
{
if (numer == GLFW_DONT_CARE || denom == GLFW_DONT_CARE)
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
else
if (numer != GLFW_DONT_CARE && denom != GLFW_DONT_CARE)
[window->ns.object setContentAspectRatio:NSMakeSize(numer, denom)];
else
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
}
void _glfwPlatformSetWindowSizeIncrements(_GLFWwindow* window, int widthincr, int heightincr)
{
if (widthincr != GLFW_DONT_CARE && heightincr != GLFW_DONT_CARE)
[window->ns.object setResizeIncrements:NSMakeSize(widthincr, heightincr)];
else
[window->ns.object setResizeIncrements:NSMakeSize(1.0, 1.0)];
}
void _glfwPlatformGetFramebufferSize(_GLFWwindow* window, int* width, int* height)
@@ -2070,11 +2090,16 @@ const char* _glfwPlatformGetClipboardString(void)
void _glfwPlatformGetRequiredInstanceExtensions(char** extensions)
{
if (!_glfw.vk.KHR_surface || !_glfw.vk.MVK_macos_surface)
return;
extensions[0] = "VK_KHR_surface";
extensions[1] = "VK_MVK_macos_surface";
if (_glfw.vk.KHR_surface && _glfw.vk.EXT_metal_surface)
{
extensions[0] = "VK_KHR_surface";
extensions[1] = "VK_EXT_metal_surface";
}
else if (_glfw.vk.KHR_surface && _glfw.vk.MVK_macos_surface)
{
extensions[0] = "VK_KHR_surface";
extensions[1] = "VK_MVK_macos_surface";
}
}
int _glfwPlatformGetPhysicalDevicePresentationSupport(VkInstance instance UNUSED,
@@ -2090,19 +2115,6 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance,
VkSurfaceKHR* surface)
{
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101100
VkResult err;
VkMacOSSurfaceCreateInfoMVK sci;
PFN_vkCreateMacOSSurfaceMVK vkCreateMacOSSurfaceMVK;
vkCreateMacOSSurfaceMVK = (PFN_vkCreateMacOSSurfaceMVK)
vkGetInstanceProcAddr(instance, "vkCreateMacOSSurfaceMVK");
if (!vkCreateMacOSSurfaceMVK)
{
_glfwInputError(GLFW_API_UNAVAILABLE,
"Cocoa: Vulkan instance missing VK_MVK_macos_surface extension");
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
// HACK: Dynamically load Core Animation to avoid adding an extra
// dependency for the majority who don't use MoltenVK
NSBundle* bundle = [NSBundle bundleWithPath:@"/System/Library/Frameworks/QuartzCore.framework"];
@@ -2128,11 +2140,49 @@ VkResult _glfwPlatformCreateWindowSurface(VkInstance instance,
[window->ns.view setLayer:window->ns.layer];
[window->ns.view setWantsLayer:YES];
memset(&sci, 0, sizeof(sci));
sci.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK;
sci.pView = window->ns.view;
VkResult err;
if (_glfw.vk.EXT_metal_surface)
{
VkMetalSurfaceCreateInfoEXT sci;
PFN_vkCreateMetalSurfaceEXT vkCreateMetalSurfaceEXT;
vkCreateMetalSurfaceEXT = (PFN_vkCreateMetalSurfaceEXT)
vkGetInstanceProcAddr(instance, "vkCreateMetalSurfaceEXT");
if (!vkCreateMetalSurfaceEXT)
{
_glfwInputError(GLFW_API_UNAVAILABLE,
"Cocoa: Vulkan instance missing VK_EXT_metal_surface extension");
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
memset(&sci, 0, sizeof(sci));
sci.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT;
sci.pLayer = window->ns.layer;
err = vkCreateMetalSurfaceEXT(instance, &sci, allocator, surface);
}
else
{
VkMacOSSurfaceCreateInfoMVK sci;
PFN_vkCreateMacOSSurfaceMVK vkCreateMacOSSurfaceMVK;
vkCreateMacOSSurfaceMVK = (PFN_vkCreateMacOSSurfaceMVK)
vkGetInstanceProcAddr(instance, "vkCreateMacOSSurfaceMVK");
if (!vkCreateMacOSSurfaceMVK)
{
_glfwInputError(GLFW_API_UNAVAILABLE,
"Cocoa: Vulkan instance missing VK_MVK_macos_surface extension");
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
memset(&sci, 0, sizeof(sci));
sci.sType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK;
sci.pView = window->ns.view;
err = vkCreateMacOSSurfaceMVK(instance, &sci, allocator, surface);
}
err = vkCreateMacOSSurfaceMVK(instance, &sci, allocator, surface);
if (err)
{
_glfwInputError(GLFW_PLATFORM_ERROR,
@@ -2178,9 +2228,9 @@ GLFWAPI void glfwCocoaRequestRenderFrame(GLFWwindow *w, GLFWcocoarenderframefun
requestRenderFrame((_GLFWwindow*)w, callback);
}
GLFWAPI void glfwGetCocoaKeyEquivalent(int glfw_key, int glfw_mods, unsigned short *cocoa_key, int *cocoa_mods) {
*cocoa_key = 0;
GLFWAPI void glfwGetCocoaKeyEquivalent(int glfw_key, int glfw_mods, char *cocoa_key, size_t key_sz, int *cocoa_mods) {
*cocoa_mods = 0;
memset(cocoa_key, 0, key_sz);
if (glfw_mods & GLFW_MOD_SHIFT)
*cocoa_mods |= NSEventModifierFlagShift;
@@ -2193,147 +2243,156 @@ GLFWAPI void glfwGetCocoaKeyEquivalent(int glfw_key, int glfw_mods, unsigned sho
if (glfw_mods & GLFW_MOD_CAPS_LOCK)
*cocoa_mods |= NSEventModifierFlagCapsLock;
uint32_t utf_8_key = 0;
unichar utf_16_key = 0;
START_ALLOW_CASE_RANGE
switch(glfw_key) {
#define K(ch, name) case GLFW_KEY_##name: *cocoa_key = ch; break;
K('!', EXCLAM);
K('"', DOUBLE_QUOTE);
K('#', NUMBER_SIGN);
K('$', DOLLAR);
K('&', AMPERSAND);
K('\'', APOSTROPHE);
K('(', PARENTHESIS_LEFT);
K(')', PARENTHESIS_RIGHT);
K('+', PLUS);
K(',', COMMA);
K('-', MINUS);
K('.', PERIOD);
K('/', SLASH);
K('0', 0);
K('1', 1);
K('2', 2);
K('3', 3);
K('5', 5);
K('6', 6);
K('7', 7);
K('8', 8);
K('9', 9);
K(':', COLON);
K(';', SEMICOLON);
K('<', LESS);
K('=', EQUAL);
K('>', GREATER);
K('@', AT);
K('[', LEFT_BRACKET);
K('\\', BACKSLASH);
K(']', RIGHT_BRACKET);
K('_', UNDERSCORE);
K('`', GRAVE_ACCENT);
K('a', A);
K('b', B);
K('c', C);
K('d', D);
K('e', E);
K('f', F);
K('g', G);
K('h', H);
K('i', I);
K('j', J);
K('k', K);
K('l', L);
K('m', M);
K('n', N);
K('o', O);
K('p', P);
K('q', Q);
K('r', R);
K('s', S);
K('t', T);
K('u', U);
K('v', V);
K('w', W);
K('x', X);
K('y', Y);
K('z', Z);
K(PARAGRAPH_UTF_8, PARAGRAPH);
K(MASCULINE_UTF_8, MASCULINE);
K(S_SHARP_UTF_8, S_SHARP);
K(A_GRAVE_LOWER_CASE_UTF_8, A_GRAVE);
K(A_DIAERESIS_LOWER_CASE_UTF_8, A_DIAERESIS);
K(A_RING_LOWER_CASE_UTF_8, A_RING);
K(AE_LOWER_CASE_UTF_8, AE);
K(C_CEDILLA_LOWER_CASE_UTF_8, C_CEDILLA);
K(E_GRAVE_LOWER_CASE_UTF_8, E_GRAVE);
K(E_ACUTE_LOWER_CASE_UTF_8, E_ACUTE);
K(I_GRAVE_LOWER_CASE_UTF_8, I_GRAVE);
K(N_TILDE_LOWER_CASE_UTF_8, N_TILDE);
K(O_GRAVE_LOWER_CASE_UTF_8, O_GRAVE);
K(O_DIAERESIS_LOWER_CASE_UTF_8, O_DIAERESIS);
K(O_SLASH_LOWER_CASE_UTF_8, O_SLASH);
K(U_GRAVE_LOWER_CASE_UTF_8, U_GRAVE);
K(U_DIAERESIS_LOWER_CASE_UTF_8, U_DIAERESIS);
K(CYRILLIC_A_LOWER_CASE_UTF_8, CYRILLIC_A);
K(CYRILLIC_BE_LOWER_CASE_UTF_8, CYRILLIC_BE);
K(CYRILLIC_VE_LOWER_CASE_UTF_8, CYRILLIC_VE);
K(CYRILLIC_GHE_LOWER_CASE_UTF_8, CYRILLIC_GHE);
K(CYRILLIC_DE_LOWER_CASE_UTF_8, CYRILLIC_DE);
K(CYRILLIC_IE_LOWER_CASE_UTF_8, CYRILLIC_IE);
K(CYRILLIC_ZHE_LOWER_CASE_UTF_8, CYRILLIC_ZHE);
K(CYRILLIC_ZE_LOWER_CASE_UTF_8, CYRILLIC_ZE);
K(CYRILLIC_I_LOWER_CASE_UTF_8, CYRILLIC_I);
K(CYRILLIC_SHORT_I_LOWER_CASE_UTF_8, CYRILLIC_SHORT_I);
K(CYRILLIC_KA_LOWER_CASE_UTF_8, CYRILLIC_KA);
K(CYRILLIC_EL_LOWER_CASE_UTF_8, CYRILLIC_EL);
K(CYRILLIC_EM_LOWER_CASE_UTF_8, CYRILLIC_EM);
K(CYRILLIC_EN_LOWER_CASE_UTF_8, CYRILLIC_EN);
K(CYRILLIC_O_LOWER_CASE_UTF_8, CYRILLIC_O);
K(CYRILLIC_PE_LOWER_CASE_UTF_8, CYRILLIC_PE);
K(CYRILLIC_ER_LOWER_CASE_UTF_8, CYRILLIC_ER);
K(CYRILLIC_ES_LOWER_CASE_UTF_8, CYRILLIC_ES);
K(CYRILLIC_TE_LOWER_CASE_UTF_8, CYRILLIC_TE);
K(CYRILLIC_U_LOWER_CASE_UTF_8, CYRILLIC_U);
K(CYRILLIC_EF_LOWER_CASE_UTF_8, CYRILLIC_EF);
K(CYRILLIC_HA_LOWER_CASE_UTF_8, CYRILLIC_HA);
K(CYRILLIC_TSE_LOWER_CASE_UTF_8, CYRILLIC_TSE);
K(CYRILLIC_CHE_LOWER_CASE_UTF_8, CYRILLIC_CHE);
K(CYRILLIC_SHA_LOWER_CASE_UTF_8, CYRILLIC_SHA);
K(CYRILLIC_SHCHA_LOWER_CASE_UTF_8, CYRILLIC_SHCHA);
K(CYRILLIC_HARD_SIGN_LOWER_CASE_UTF_8, CYRILLIC_HARD_SIGN);
K(CYRILLIC_YERU_LOWER_CASE_UTF_8, CYRILLIC_YERU);
K(CYRILLIC_SOFT_SIGN_LOWER_CASE_UTF_8, CYRILLIC_SOFT_SIGN);
K(CYRILLIC_E_LOWER_CASE_UTF_8, CYRILLIC_E);
K(CYRILLIC_YU_LOWER_CASE_UTF_8, CYRILLIC_YU);
K(CYRILLIC_YA_LOWER_CASE_UTF_8, CYRILLIC_YA);
K(CYRILLIC_IO_LOWER_CASE_UTF_8, CYRILLIC_IO);
#define K8(ch, name) case GLFW_KEY_##name: utf_8_key = ch; break;
#define K16(ch, name) case GLFW_KEY_##name: utf_16_key = ch; break;
K8('!', EXCLAM);
K8('"', DOUBLE_QUOTE);
K8('#', NUMBER_SIGN);
K8('$', DOLLAR);
K8('&', AMPERSAND);
K8('\'', APOSTROPHE);
K8('(', PARENTHESIS_LEFT);
K8(')', PARENTHESIS_RIGHT);
K8('+', PLUS);
K8(',', COMMA);
K8('-', MINUS);
K8('.', PERIOD);
K8('/', SLASH);
K8('0', 0);
K8('1', 1);
K8('2', 2);
K8('3', 3);
K8('5', 5);
K8('6', 6);
K8('7', 7);
K8('8', 8);
K8('9', 9);
K8(':', COLON);
K8(';', SEMICOLON);
K8('<', LESS);
K8('=', EQUAL);
K8('>', GREATER);
K8('@', AT);
K8('[', LEFT_BRACKET);
K8('\\', BACKSLASH);
K8(']', RIGHT_BRACKET);
K8('^', CIRCUMFLEX);
K8('_', UNDERSCORE);
K8('`', GRAVE_ACCENT);
K8('a', A);
K8('b', B);
K8('c', C);
K8('d', D);
K8('e', E);
K8('f', F);
K8('g', G);
K8('h', H);
K8('i', I);
K8('j', J);
K8('k', K);
K8('l', L);
K8('m', M);
K8('n', N);
K8('o', O);
K8('p', P);
K8('q', Q);
K8('r', R);
K8('s', S);
K8('t', T);
K8('u', U);
K8('v', V);
K8('w', W);
K8('x', X);
K8('y', Y);
K8('z', Z);
K8(PARAGRAPH_UTF_8, PARAGRAPH);
K8(MASCULINE_UTF_8, MASCULINE);
K8(S_SHARP_UTF_8, S_SHARP);
K8(A_GRAVE_LOWER_CASE_UTF_8, A_GRAVE);
K8(A_DIAERESIS_LOWER_CASE_UTF_8, A_DIAERESIS);
K8(A_RING_LOWER_CASE_UTF_8, A_RING);
K8(AE_LOWER_CASE_UTF_8, AE);
K8(C_CEDILLA_LOWER_CASE_UTF_8, C_CEDILLA);
K8(E_GRAVE_LOWER_CASE_UTF_8, E_GRAVE);
K8(E_ACUTE_LOWER_CASE_UTF_8, E_ACUTE);
K8(I_GRAVE_LOWER_CASE_UTF_8, I_GRAVE);
K8(N_TILDE_LOWER_CASE_UTF_8, N_TILDE);
K8(O_GRAVE_LOWER_CASE_UTF_8, O_GRAVE);
K8(O_DIAERESIS_LOWER_CASE_UTF_8, O_DIAERESIS);
K8(O_SLASH_LOWER_CASE_UTF_8, O_SLASH);
K8(U_GRAVE_LOWER_CASE_UTF_8, U_GRAVE);
K8(U_DIAERESIS_LOWER_CASE_UTF_8, U_DIAERESIS);
K8(CYRILLIC_A_LOWER_CASE_UTF_8, CYRILLIC_A);
K8(CYRILLIC_BE_LOWER_CASE_UTF_8, CYRILLIC_BE);
K8(CYRILLIC_VE_LOWER_CASE_UTF_8, CYRILLIC_VE);
K8(CYRILLIC_GHE_LOWER_CASE_UTF_8, CYRILLIC_GHE);
K8(CYRILLIC_DE_LOWER_CASE_UTF_8, CYRILLIC_DE);
K8(CYRILLIC_IE_LOWER_CASE_UTF_8, CYRILLIC_IE);
K8(CYRILLIC_ZHE_LOWER_CASE_UTF_8, CYRILLIC_ZHE);
K8(CYRILLIC_ZE_LOWER_CASE_UTF_8, CYRILLIC_ZE);
K8(CYRILLIC_I_LOWER_CASE_UTF_8, CYRILLIC_I);
K8(CYRILLIC_SHORT_I_LOWER_CASE_UTF_8, CYRILLIC_SHORT_I);
K8(CYRILLIC_KA_LOWER_CASE_UTF_8, CYRILLIC_KA);
K8(CYRILLIC_EL_LOWER_CASE_UTF_8, CYRILLIC_EL);
K8(CYRILLIC_EM_LOWER_CASE_UTF_8, CYRILLIC_EM);
K8(CYRILLIC_EN_LOWER_CASE_UTF_8, CYRILLIC_EN);
K8(CYRILLIC_O_LOWER_CASE_UTF_8, CYRILLIC_O);
K8(CYRILLIC_PE_LOWER_CASE_UTF_8, CYRILLIC_PE);
K8(CYRILLIC_ER_LOWER_CASE_UTF_8, CYRILLIC_ER);
K8(CYRILLIC_ES_LOWER_CASE_UTF_8, CYRILLIC_ES);
K8(CYRILLIC_TE_LOWER_CASE_UTF_8, CYRILLIC_TE);
K8(CYRILLIC_U_LOWER_CASE_UTF_8, CYRILLIC_U);
K8(CYRILLIC_EF_LOWER_CASE_UTF_8, CYRILLIC_EF);
K8(CYRILLIC_HA_LOWER_CASE_UTF_8, CYRILLIC_HA);
K8(CYRILLIC_TSE_LOWER_CASE_UTF_8, CYRILLIC_TSE);
K8(CYRILLIC_CHE_LOWER_CASE_UTF_8, CYRILLIC_CHE);
K8(CYRILLIC_SHA_LOWER_CASE_UTF_8, CYRILLIC_SHA);
K8(CYRILLIC_SHCHA_LOWER_CASE_UTF_8, CYRILLIC_SHCHA);
K8(CYRILLIC_HARD_SIGN_LOWER_CASE_UTF_8, CYRILLIC_HARD_SIGN);
K8(CYRILLIC_YERU_LOWER_CASE_UTF_8, CYRILLIC_YERU);
K8(CYRILLIC_SOFT_SIGN_LOWER_CASE_UTF_8, CYRILLIC_SOFT_SIGN);
K8(CYRILLIC_E_LOWER_CASE_UTF_8, CYRILLIC_E);
K8(CYRILLIC_YU_LOWER_CASE_UTF_8, CYRILLIC_YU);
K8(CYRILLIC_YA_LOWER_CASE_UTF_8, CYRILLIC_YA);
K8(CYRILLIC_IO_LOWER_CASE_UTF_8, CYRILLIC_IO);
K(0x35, ESCAPE);
K('\r', ENTER);
K('\t', TAB);
K(NSBackspaceCharacter, BACKSPACE);
K(NSInsertFunctionKey, INSERT);
K(NSDeleteCharacter, DELETE);
K(NSLeftArrowFunctionKey, LEFT);
K(NSRightArrowFunctionKey, RIGHT);
K(NSUpArrowFunctionKey, UP);
K(NSDownArrowFunctionKey, DOWN);
K(NSPageUpFunctionKey, PAGE_UP);
K(NSPageDownFunctionKey, PAGE_DOWN);
K(NSHomeFunctionKey, HOME);
K(NSEndFunctionKey, END);
K(NSPrintFunctionKey, PRINT_SCREEN);
K8(0x35, ESCAPE);
K8('\r', ENTER);
K8('\t', TAB);
K16(NSBackspaceCharacter, BACKSPACE);
K16(NSInsertFunctionKey, INSERT);
K16(NSDeleteCharacter, DELETE);
K16(NSLeftArrowFunctionKey, LEFT);
K16(NSRightArrowFunctionKey, RIGHT);
K16(NSUpArrowFunctionKey, UP);
K16(NSDownArrowFunctionKey, DOWN);
K16(NSPageUpFunctionKey, PAGE_UP);
K16(NSPageDownFunctionKey, PAGE_DOWN);
K16(NSHomeFunctionKey, HOME);
K16(NSEndFunctionKey, END);
K16(NSPrintFunctionKey, PRINT_SCREEN);
case GLFW_KEY_F1 ... GLFW_KEY_F24:
*cocoa_key = NSF1FunctionKey + (glfw_key - GLFW_KEY_F1); break;
case GLFW_KEY_KP_0 ... GLFW_KEY_KP_9:
*cocoa_key = NSEventModifierFlagNumericPad | (0x52 + (glfw_key - GLFW_KEY_KP_0)); break;
K((unichar)(0x41|NSEventModifierFlagNumericPad), KP_DECIMAL);
K((unichar)(0x43|NSEventModifierFlagNumericPad), KP_MULTIPLY);
K((unichar)(0x45|NSEventModifierFlagNumericPad), KP_ADD);
K((unichar)(0x4B|NSEventModifierFlagNumericPad), KP_DIVIDE);
K((unichar)(0x4E|NSEventModifierFlagNumericPad), KP_SUBTRACT);
K((unichar)(0x51|NSEventModifierFlagNumericPad), KP_EQUAL);
#undef K
utf_16_key = NSF1FunctionKey + (glfw_key - GLFW_KEY_F1); break;
#undef K8
#undef K16
END_ALLOW_CASE_RANGE
}
if (utf_16_key != 0) {
strncpy(cocoa_key, [[NSString stringWithCharacters:&utf_16_key length:1] UTF8String], key_sz - 1);
} else {
unsigned str_pos = 0;
for (unsigned i = 0; i < 4 && str_pos < key_sz - 1; i++) {
uint8_t byte = (utf_8_key >> 24) & 0xff;
utf_8_key <<= 8;
if (byte != 0) cocoa_key[str_pos++] = byte;
}
cocoa_key[str_pos] = 0;
}
}

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 = []
@@ -166,7 +202,7 @@ def generate_wrappers(glfw_header):
GLFWcocoatogglefullscreenfun glfwSetCocoaToggleFullscreenIntercept(GLFWwindow *window, GLFWcocoatogglefullscreenfun callback)
GLFWapplicationshouldhandlereopenfun glfwSetApplicationShouldHandleReopen(GLFWapplicationshouldhandlereopenfun callback)
GLFWapplicationwillfinishlaunchingfun glfwSetApplicationWillFinishLaunching(GLFWapplicationwillfinishlaunchingfun callback)
void glfwGetCocoaKeyEquivalent(int glfw_key, int glfw_mods, void* cocoa_key, void* cocoa_mods)
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)
@@ -197,6 +233,9 @@ const char *action_text, int32_t timeout, GLFWDBusnotificationcreatedfun callbac
#include <stdint.h>
#include "monotonic.h"
#ifndef GFW_EXTERN
#define GFW_EXTERN extern
#endif
{}
typedef int (* GLFWcocoatextinputfilterfun)(int,int,unsigned int,unsigned long);
@@ -215,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>
@@ -244,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')

64
glfw/glfw3.h vendored
View File

@@ -405,6 +405,7 @@ extern "C" {
#define GLFW_KEY_LEFT_BRACKET 91 /* [ */
#define GLFW_KEY_BACKSLASH 92 /* \ */
#define GLFW_KEY_RIGHT_BRACKET 93 /* ] */
#define GLFW_KEY_CIRCUMFLEX 94 /* ^ */
#define GLFW_KEY_UNDERSCORE 95 /* _ */
#define GLFW_KEY_GRAVE_ACCENT 96 /* ` */
#define GLFW_KEY_WORLD_1 161 /* non-US #1 */
@@ -1592,19 +1593,22 @@ typedef void (* GLFWscrollfun)(GLFWwindow*,double,double,int);
*/
typedef void (* GLFWkeyboardfun)(GLFWwindow*, GLFWkeyevent*);
/*! @brief The function pointer type for path drop callbacks.
/*! @brief The function pointer type for drag and drop callbacks.
*
* This is the function pointer type for path drop callbacks. A path drop
* This is the function pointer type for drop callbacks. A drop
* callback function has the following signature:
* @code
* void function_name(GLFWwindow* window, int path_count, const char* paths[])
* int function_name(GLFWwindow* window, const char* mime, const char* text)
* @endcode
*
* @param[in] window The window that received the event.
* @param[in] path_count The number of dropped paths.
* @param[in] paths The UTF-8 encoded file and/or directory path names.
* @param[in] mime The UTF-8 encoded drop mime-type
* @param[in] data The dropped data or NULL for drag enter events
* @param[in] sz The size of the dropped data
* @return For drag events should return the priority for the specified mime type. A priority of zero
* or lower means the mime type is not accepted. Highest priority will be the finally accepted mime-type.
*
* @pointer_lifetime The path array and its strings are valid until the
* @pointer_lifetime The text is valid until the
* callback function returns.
*
* @sa @ref path_drop
@@ -1614,7 +1618,7 @@ typedef void (* GLFWkeyboardfun)(GLFWwindow*, GLFWkeyevent*);
*
* @ingroup input
*/
typedef void (* GLFWdropfun)(GLFWwindow*,int,const char*[]);
typedef int (* GLFWdropfun)(GLFWwindow*, const char *, const char*, size_t);
typedef void (* GLFWliveresizefun)(GLFWwindow*, bool);
@@ -3035,6 +3039,46 @@ GLFWAPI void glfwSetWindowSizeLimits(GLFWwindow* window, int minwidth, int minhe
*
* @ingroup window
*/
GLFWAPI void glfwSetWindowSizeIncrements(GLFWwindow* window, int widthincr, int heightincr);
/*! @brief Sets the size increments of the specified window.
*
* This function sets the size increments of the content area of the specified
* window. If the window is full screen, the size limits only take effect
* once it is made windowed. If the window is not resizable, this function
* does nothing.
*
* The size increments are applied immediately to a windowed mode window and
* may cause it to be resized.
*
* The dimension increments must be greater than zero.
*
* @param[in] window The window to set limits for.
* @param[in] widthincr The width increments, in screen coordinates, of the
* content area, or `GLFW_DONT_CARE`.
* @param[in] heightincr The height increments, in screen coordinates, of the
* content area, or `GLFW_DONT_CARE`.
*
* @errors Possible errors include @ref GLFW_NOT_INITIALIZED, @ref
* GLFW_INVALID_VALUE and @ref GLFW_PLATFORM_ERROR.
*
* @remark If you set size limits and an aspect ratio that conflict, the
* results are undefined.
*
* @remark @wayland The size limits will not be applied until the window is
* actually resized, either by the user or by the compositor.
*
* @thread_safety This function must only be called from the main thread.
*
* @sa @ref window_sizelimits
* @sa @ref glfwSetWindowSizeLimits
*
* @since Added in version 3.2.
*
* @ingroup window
*/
GLFWAPI void glfwSetWindowAspectRatio(GLFWwindow* window, int numer, int denom);
/*! @brief Sets the size of the content area of the specified window.
@@ -4148,6 +4192,7 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* window, int mode, int value);
* - `GLFW_KEY_LEFT_BRACKET`
* - `GLFW_KEY_BACKSLASH`
* - `GLFW_KEY_RIGHT_BRACKET`
* - `GLFW_KEY_CIRCUMFLEX`
* - `GLFW_KEY_UNDERSCORE`
* - `GLFW_KEY_GRAVE_ACCENT`
* - `GLFW_KEY_WORLD_1`
@@ -5524,8 +5569,9 @@ GLFWAPI int glfwVulkanSupported(void);
* returned array, as it is an error to specify an extension more than once in
* the `VkInstanceCreateInfo` struct.
*
* @remark @macos This function currently only supports the
* `VK_MVK_macos_surface` extension from MoltenVK.
* @remark @macos This function currently supports either the
* `VK_MVK_macos_surface` extension from MoltenVK or `VK_EXT_metal_surface`
* extension.
*
* @pointer_lifetime The returned array is allocated and freed by GLFW. You
* should not free it yourself. It is guaranteed to be valid only until the

6
glfw/input.c vendored
View File

@@ -350,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
@@ -510,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";

6
glfw/internal.h vendored
View File

@@ -132,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;
@@ -418,6 +419,7 @@ struct _GLFWwindow
int minwidth, minheight;
int maxwidth, maxheight;
int numer, denom;
int widthincr, heightincr;
bool stickyKeys;
bool stickyMouseButtons;
@@ -587,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;
@@ -673,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,
@@ -755,7 +759,7 @@ void _glfwInputScroll(_GLFWwindow* window, double xoffset, double yoffset, int f
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);

4
glfw/null_window.c vendored
View File

@@ -128,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)

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;

16
glfw/window.c vendored
View File

@@ -254,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))
@@ -690,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;

2
glfw/wl_init.c vendored
View File

@@ -852,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)

24
glfw/wl_platform.h vendored
View File

@@ -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
@@ -287,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
@@ -322,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*);

244
glfw/wl_window.c vendored
View File

@@ -42,7 +42,7 @@
#include <sys/mman.h>
#define URI_LIST_MIME "text/uri-list"
static bool checkScaleChange(_GLFWwindow* window)
@@ -754,6 +754,13 @@ abortOnFatalError(int last_error) {
_glfw.wl.eventLoopData.wakeup_data_read = true;
}
static void
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)
{
@@ -762,17 +769,9 @@ handleEvents(monotonic_t 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;
}
}
@@ -782,23 +781,22 @@ handleEvents(monotonic_t 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");
@@ -960,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,
@@ -1028,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)
{
@@ -1499,10 +1509,11 @@ 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;
@@ -1523,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)");
@@ -1549,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) {
@@ -1586,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]);
}
}
}
@@ -1618,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) {
@@ -1649,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;
}
}
@@ -1686,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) {
@@ -1694,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
}
@@ -1745,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;
}
}
@@ -1882,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;
}
}
@@ -1932,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;
}
}

1
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

5
glfw/x11_platform.h vendored
View File

@@ -180,6 +180,7 @@ typedef struct _GLFWwindowX11
{
Colormap colormap;
Window handle;
Window parent;
bool iconified;
bool maximized;
@@ -265,7 +266,6 @@ typedef struct _GLFWlibraryX11
Atom XdndFinished;
Atom XdndSelection;
Atom XdndTypeList;
Atom text_uri_list;
// Selection (clipboard) atoms
Atom TARGETS;
@@ -326,7 +326,8 @@ typedef struct _GLFWlibraryX11
struct {
int version;
Window source;
Atom format;
char format[128];
int format_priority;
} xdnd;
struct {

168
glfw/x11_window.c vendored
View File

@@ -68,7 +68,7 @@ static unsigned _glfwDispatchX11Events(void);
static void
handleEvents(monotonic_t timeout) {
EVDBG("starting handleEvents(%.2f)", monotonic_t_to_s_double(timeout));
int display_read_ok = pollForEvents(&_glfw.x11.eventLoopData, 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();
@@ -255,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
{
@@ -525,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);
@@ -1182,6 +1185,12 @@ static void processEvent(XEvent *event)
switch (event->type)
{
case ReparentNotify:
{
window->x11.parent = event->xreparent.parent;
return;
}
case KeyPress:
{
UPDATE_KEYMAP_IF_NEEDED;
@@ -1375,18 +1384,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;
@@ -1435,7 +1454,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;
@@ -1452,14 +1472,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) {
XGetAtomNames(_glfw.x11.display, 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)
@@ -1473,7 +1501,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];
@@ -1481,7 +1509,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);
@@ -1529,7 +1557,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
@@ -1559,14 +1587,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)
@@ -2034,6 +2055,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);
@@ -2245,7 +2274,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,
@@ -2665,7 +2694,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);
@@ -2708,8 +2737,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,

1
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); \

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,18 +3,21 @@
# 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 = {
'title': '',
@@ -23,7 +26,7 @@ formats = {
}
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)
@@ -42,7 +45,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 +53,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 +61,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 +73,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 +103,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 +114,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 +131,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,16 @@
# 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.utils import (
positive_int, python_string, to_color
)
from kitty.conf.definition import Option, Shortcut, option_func
from kitty.conf.utils import positive_int, python_string, to_color
# }}}
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 +24,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 +33,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`
@@ -121,4 +120,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

@@ -13,42 +13,54 @@ from collections import defaultdict
from contextlib import suppress
from functools import partial
from gettext import gettext as _
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
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)
@@ -58,35 +70,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:
@@ -98,7 +110,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':
@@ -107,25 +119,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)
@@ -139,7 +151,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:
@@ -150,15 +162,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)
@@ -167,23 +179,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
@@ -191,11 +203,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
@@ -215,10 +227,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:
@@ -230,7 +242,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:
@@ -243,10 +255,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()
@@ -266,30 +278,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)
@@ -310,7 +322,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)
@@ -333,19 +381,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()
@@ -356,7 +411,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()
@@ -383,7 +438,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
@@ -392,7 +447,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
@@ -402,7 +457,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:
@@ -421,7 +476,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()
@@ -434,7 +489,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
@@ -467,7 +522,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:
@@ -475,10 +530,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)
@@ -503,23 +558,28 @@ 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 = []
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 get_remote_file(path):
def get_remote_file(path: str) -> str:
if path.startswith('ssh:'):
parts = path.split(':', 2)
if len(parts) == 3:
@@ -533,16 +593,16 @@ def get_remote_file(path):
return path
def main(args):
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))
@@ -551,7 +611,7 @@ def main(args):
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
@@ -569,9 +629,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'
@@ -132,39 +146,41 @@ 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 +189,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 +198,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,7 +209,7 @@ 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)
@@ -204,7 +220,7 @@ text_bg_map = {'filler': filler_format, 'remove': removed_format, 'add': added_f
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 +231,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 +261,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 +287,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 +356,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 +366,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 +374,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 +391,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 +400,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 +410,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 +448,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 +480,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,33 +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
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', 'groupdict')
def __init__(self, index, start, end, text, groupdict):
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:
@@ -44,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
@@ -53,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)
@@ -69,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
@@ -83,47 +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()
@property
def text_matches(self):
def text_matches(self) -> List[str]:
return [m.text + self.match_suffix for m in self.chosen]
@property
def groupdicts(self):
def groupdicts(self) -> List[Any]:
return [m.groupdict for m in self.chosen]
def get_match_suffix(self, args):
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:
@@ -145,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
@@ -168,23 +187,23 @@ 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(0 if has_named_groups else pat.groups)
@@ -196,16 +215,17 @@ def regex_finditer(pat, minimum_match_length, text):
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('[')
@@ -227,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]
@@ -238,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]
@@ -248,7 +268,7 @@ 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, groupdict) in enumerate(regex_finditer(pat, args.minimum_match_length, text)):
for func in post_processors:
@@ -257,7 +277,7 @@ def mark(pattern, post_processors, text, args):
yield Mark(idx, s, e, mark_text, groupdict)
def run_loop(args, text, all_marks, index_map, extra_cli_args=()):
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)
@@ -265,21 +285,25 @@ def run_loop(args, text, all_marks, index_map, extra_cli_args=()):
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
'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':
@@ -291,9 +315,8 @@ def functions_for(args):
pattern = '[0-9a-f]{7,128}'
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']
if not chars:
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:
@@ -301,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:
@@ -315,19 +338,28 @@ 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 load_custom_processor(customize_processing):
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 config_dir
customize_processing = os.path.expandvars(os.path.expanduser(customize_processing))
if os.path.isabs(customize_processing):
@@ -338,10 +370,12 @@ def load_custom_processor(customize_processing):
return runpy.run_path(custom_path, run_name='__main__')
def run(args, text, extra_cli_args=()):
def run(args: HintsCLIOptions, text: str, extra_cli_args: Sequence[str] = ()) -> Optional[Dict[str, Any]]:
try:
text = parse_input(text)
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:
@@ -354,7 +388,7 @@ def run(args, text, extra_cli_args=()):
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)
@@ -386,12 +420,15 @@ 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 numbered group in
the regular expression only the group will be matched. This allow you to match
@@ -403,9 +440,23 @@ 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
@@ -470,58 +521,93 @@ 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(','.join(sorted(URL_PREFIXES))).format
'''.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 and not args.customize_processing:
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, items)
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:
return m['handle_result'](args, data, target_window_id, boss, data['extra_cli_args'])
m['handle_result'](args, data, target_window_id, boss, data['extra_cli_args'])
return None
programs = data['programs'] or ('default',)
matches, groupdicts = [], []
matches: List[str] = []
groupdicts = []
for m, g in zip(data['match'], data['groupdicts']):
if m:
matches.append(m), groupdicts.append(g)
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]
@@ -557,16 +643,14 @@ def handle_result(args, data, target_window_id, boss):
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,18 +9,26 @@ 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
OPTIONS = '''\
--align
@@ -93,38 +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.
'''
screen_size = None
screen_size: Optional[ScreenSizeGetter] = None
can_transfer_with_files = False
def get_screen_size_function():
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():
def get_screen_size() -> ScreenSize:
screen_size = get_screen_size_function()
return screen_size()
def options_spec():
if not hasattr(options_spec, 'ans'):
options_spec.ans = OPTIONS.format(
appname='{}-icat'.format(appname),
)
return options_spec.ans
@lru_cache(maxsize=2)
def options_spec() -> str:
return OPTIONS.format(appname='{}-icat'.format(appname))
def write_gr_cmd(cmd, payload=None):
sys.stdout.buffer.write(serialize_gr_command(cmd, payload))
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
@@ -135,7 +148,7 @@ def calculate_in_cell_x_offset(width, cell_width, align):
return (cell_width - extra_pixels) // 2
def set_cursor(cmd, width, height, align):
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))
@@ -143,9 +156,9 @@ def set_cursor(cmd, width, height, align):
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
@@ -155,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 = 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
@@ -169,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:
@@ -197,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 = get_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
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]
@@ -233,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()
@@ -257,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 = (
@@ -287,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:
@@ -305,33 +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):
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='')
@@ -340,7 +400,7 @@ 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)
@@ -350,48 +410,55 @@ def main(args=sys.argv):
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
@@ -403,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')
@@ -135,16 +142,17 @@ def main(sys_args):
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

@@ -5,8 +5,10 @@
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
@@ -17,19 +19,19 @@ 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:
with suppress(KeyboardInterrupt):
real_main(args)

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:
@@ -97,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'
@@ -109,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
@@ -117,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]
@@ -100,58 +113,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 +255,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 +265,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 +303,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

@@ -9,18 +9,23 @@ import sys
from contextlib import suppress
from functools import lru_cache
from gettext import gettext as _
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 wcswidth, is_emoji_presentation_base
from kitty.fast_data_types import is_emoji_presentation_base, wcswidth
from kitty.key_encoding import (
DOWN, ESCAPE, F1, F2, F3, F4, F12, LEFT, RELEASE, RIGHT, SHIFT, TAB, UP,
enter_key
CTRL, RELEASE, SHIFT, KeyEvent, enter_key, key_defs as K
)
from kitty.utils import get_editor
from kitty.typing import BossType
from kitty.utils import ScreenSize, get_editor
from ..tui.handler import Handler
from ..tui.handler import Handler, result_handler
from ..tui.line_edit import LineEdit
from ..tui.loop import Loop
from ..tui.operations import (
@@ -29,6 +34,17 @@ from ..tui.operations import (
)
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(
@@ -38,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])
@@ -70,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:
@@ -94,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
@@ -106,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:
@@ -127,53 +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, emoji_variation):
def __init__(self, emoji_variation: str) -> None:
self.emoji_variation = emoji_variation
self.layout_dirty = True
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):
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)
@@ -190,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)
@@ -217,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
@@ -226,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:
@@ -245,16 +267,17 @@ 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, emoji_variation='none'):
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':
@@ -263,26 +286,26 @@ class UnicodeInput(Handler):
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.emoji_variation)
self.update_prompt()
@property
def resolved_current_char(self):
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):
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
@@ -301,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:
@@ -332,7 +355,7 @@ 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'
@@ -345,22 +368,17 @@ class UnicodeInput(Handler):
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)
@@ -371,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
@@ -398,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)
@@ -430,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):
@@ -452,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'))
@@ -477,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
@@ -486,13 +517,18 @@ 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()
@@ -510,22 +546,22 @@ default form specified in the unicode standard for the symbol is used.
'''.format
def parse_unicode_input_args(args):
return parse_args(args, OPTIONS, usage, help_text, 'kitty +kitten unicode_input')
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):
def main(args: List[str]) -> Optional[str]:
try:
args, items = parse_unicode_input_args(args[1:])
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
return None
loop = Loop()
with cached_values_for('unicode-input') as cached_values:
handler = UnicodeInput(cached_values, args.emoji_variation)
handler = UnicodeInput(cached_values, cli_opts.emoji_variation)
loop.loop(handler)
if handler.current_char and loop.return_code == 0:
with suppress(Exception):
@@ -535,9 +571,11 @@ def main(args):
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)
@@ -548,6 +586,7 @@ if __name__ == '__main__':
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

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

View File

@@ -0,0 +1,15 @@
#version GLSL_VERSION
uniform sampler2D image;
uniform float opacity;
uniform float premult;
in vec2 texcoord;
out vec4 color;
void main() {
color = texture(image, texcoord);
float alpha = color.a * opacity;
vec4 premult_color = vec4(color.rgb * alpha, alpha);
color = vec4(color.rgb, alpha);
color = premult * premult_color + (1 - premult) * color;
}

42
kitty/bgimage_vertex.glsl Normal file
View File

@@ -0,0 +1,42 @@
#version GLSL_VERSION
#define left -1.0f
#define top 1.0f
#define right 1.0f
#define bottom -1.0f
#define tex_left 0
#define tex_top 0
#define tex_right 1
#define tex_bottom 1
uniform float tiled;
uniform vec4 sizes; // [ window_width, window_height, image_width, image_height ]
out vec2 texcoord;
const vec2 pos_map[] = vec2[4](
vec2(left, top),
vec2(left, bottom),
vec2(right, bottom),
vec2(right, top)
);
const vec2 tex_map[] = vec2[4](
vec2(tex_left, tex_top),
vec2(tex_left, tex_bottom),
vec2(tex_right, tex_bottom),
vec2(tex_right, tex_top)
);
float scale_factor(float window, float image) {
return window / image;
}
float tiling_factor(int i) {
return tiled * scale_factor(sizes[i], sizes[i + 2]) + (1 - tiled);
}
void main() {
vec2 tex_coords = tex_map[gl_VertexID];
texcoord = vec2(tex_coords[0] * tiling_factor(0), tex_coords[1] * tiling_factor(1));
gl_Position = vec4(pos_map[gl_VertexID], 0, 1);
}

View File

@@ -7,5 +7,4 @@ out vec4 color;
void main() {
color = texture(image, texcoord);
color = vec4(color.rgb, color.a);
}

View File

@@ -2,17 +2,18 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from enum import IntFlag
from itertools import chain
from typing import List, Optional, Sequence, Tuple
from .constants import WindowGeometry
from .fast_data_types import (
BORDERS_PROGRAM, add_borders_rect, compile_program, init_borders_program
BORDERS_PROGRAM, add_borders_rect, compile_program, init_borders_program,
os_window_has_background_image
)
from .options_stub import Options
from .utils import load_shaders
try:
from enum import IntFlag
except ImportError:
from enum import IntEnum as IntFlag
from .typing import WindowType, LayoutType
class BorderColor(IntFlag):
@@ -20,15 +21,15 @@ class BorderColor(IntFlag):
default_bg, active, inactive, window_bg, bell = ((1 << i) for i in range(5))
def vertical_edge(os_window_id, tab_id, color, width, top, bottom, left):
def vertical_edge(os_window_id: int, tab_id: int, color: int, width: int, top: int, bottom: int, left: int) -> None:
add_borders_rect(os_window_id, tab_id, left, top, left + width, bottom, color)
def horizontal_edge(os_window_id, tab_id, color, height, left, right, top):
def horizontal_edge(os_window_id: int, tab_id: int, color: int, height: int, left: int, right: int, top: int) -> None:
add_borders_rect(os_window_id, tab_id, left, top, right, top + height, color)
def draw_edges(os_window_id, tab_id, colors, width, geometry, base_width=0):
def draw_edges(os_window_id: int, tab_id: int, colors: Sequence[int], width: int, geometry: 'WindowGeometry', base_width: int = 0) -> None:
left = geometry.left - (width + base_width)
top = geometry.top - (width + base_width)
right = geometry.right + (width + base_width)
@@ -39,33 +40,35 @@ def draw_edges(os_window_id, tab_id, colors, width, geometry, base_width=0):
vertical_edge(os_window_id, tab_id, colors[2], width, top, bottom, geometry.right + base_width)
def load_borders_program():
def load_borders_program() -> None:
compile_program(BORDERS_PROGRAM, *load_shaders('border'))
init_borders_program()
Borders.program_initialized = True
class Borders:
def __init__(self, os_window_id, tab_id, opts, border_width, padding_width):
def __init__(self, os_window_id: int, tab_id: int, opts: Options):
self.os_window_id = os_window_id
self.tab_id = tab_id
self.border_width = border_width
self.padding_width = padding_width
self.draw_active_borders = opts.active_border_color is not None
def __call__(
self,
windows,
active_window,
current_layout,
extra_blank_rects,
draw_window_borders=True
):
windows: List[WindowType],
active_window: Optional[WindowType],
current_layout: LayoutType,
extra_blank_rects: Sequence[Tuple[int, int, int, int]],
padding_width: int,
border_width: int,
draw_window_borders: bool = True,
) -> None:
add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, BorderColor.default_bg)
for br in chain(current_layout.blank_rects, extra_blank_rects):
add_borders_rect(self.os_window_id, self.tab_id, *br, BorderColor.default_bg)
bw, pw = self.border_width, self.padding_width
has_background_image = os_window_has_background_image(self.os_window_id)
if not has_background_image:
for br in chain(current_layout.blank_rects, extra_blank_rects):
left, top, right, bottom = br
add_borders_rect(self.os_window_id, self.tab_id, left, top, right, bottom, BorderColor.default_bg)
bw, pw = border_width, padding_width
if bw + pw <= 0:
return
draw_borders = bw > 0 and draw_window_borders
@@ -85,8 +88,12 @@ class Borders:
colors = tuple(color if needed else window_bg for needed in next(border_data))
draw_edges(
self.os_window_id, self.tab_id, colors, bw, g, base_width=pw)
if pw > 0:
if pw > 0 and not has_background_image:
# Draw the background rectangles over the padding region
colors = (window_bg, window_bg, window_bg, window_bg)
draw_edges(
self.os_window_id, self.tab_id, colors, pw, g)
color = BorderColor.inactive
for (left, top, right, bottom) in current_layout.window_independent_borders(windows, active_window):
add_borders_rect(self.os_window_id, self.tab_id, left, top, right, bottom, color)

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
#ifdef NEEDS_BACKROUND
in vec3 background;
in float draw_bg;
#if defined(TRANSPARENT) || defined(SPECIAL)
in float bg_alpha;
#endif
@@ -76,11 +77,13 @@ vec4 blend_onto_opaque_premul(vec3 over, float over_alpha, vec3 under) {
*
* 2a) Opaque bg with images under text
* There are multiple passes, each pass is blended onto the previous using the opaque blend func (alpha, 1- alpha):
* 1) Draw only the background -- expected output is color with alpha 1
* 2) Draw the images that are supposed to be below text. This happens in the graphics shader
* 3) Draw the special cells (selection/cursor). Output is same as from step 1, with bg_alpha 1 for special cells and 0 otherwise
* 4) Draw the foreground -- expected output is color with alpha which is blended using the opaque blend func
* 5) Draw the images that are supposed to be above text again in the graphics shader
* 1) Draw background for all cells
* 2) Draw the images that are supposed to be below both the background and text, if any. This happens in the graphics shader
* 3) Draw the background of cells that don't have the default background if any images were drawn in 2 above
4) Draw the images that are supposed to be below text but not background, again in graphics shader.
* 5) Draw the special cells (selection/cursor). Output is same as from step 1, with bg_alpha 1 for special cells and 0 otherwise
* 6) Draw the foreground -- expected output is color with alpha which is blended using the opaque blend func
* 7) Draw the images that are supposed to be above text again in the graphics shader
*
* 2b) Transparent bg with images
* First everything is rendered into a framebuffer, and then the framebauffer is blended onto
@@ -126,10 +129,10 @@ void main() {
#endif
#ifdef BACKGROUND
#ifdef TRANSPARENT
#if defined(TRANSPARENT)
final_color = vec4(background.rgb * bg_alpha, bg_alpha);
#else
final_color = vec4(background.rgb, 1.0f);
final_color = vec4(background.rgb, draw_bg);
#endif
#endif

View File

@@ -5,7 +5,10 @@
#define REVERSE_SHIFT {REVERSE_SHIFT}
#define STRIKE_SHIFT {STRIKE_SHIFT}
#define DIM_SHIFT {DIM_SHIFT}
#define MARK_SHIFT {MARK_SHIFT}
#define MARK_MASK {MARK_MASK}
#define USE_SELECTION_FG
#define NUM_COLORS 256
// Inputs {{{
layout(std140) uniform CellRenderData {
@@ -16,8 +19,11 @@ layout(std140) uniform CellRenderData {
uint xnum, ynum, cursor_fg_sprite_idx;
float cursor_x, cursor_y, cursor_w;
uint color_table[256];
uint color_table[NUM_COLORS + MARK_MASK + MARK_MASK + 2];
};
#ifdef BACKGROUND
uniform uint draw_bg_bitfield;
#endif
// Have to use fixed locations here as all variants of the cell program share the same VAO
layout(location=0) in uvec3 colors;
@@ -45,6 +51,7 @@ const uvec2 cell_pos_map[] = uvec2[4](
#ifdef NEEDS_BACKROUND
out vec3 background;
out float draw_bg;
#if defined(TRANSPARENT) || defined(SPECIAL)
out float bg_alpha;
#endif
@@ -158,7 +165,11 @@ void main() {
float cell_has_cursor = is_cursor(c, r);
float is_block_cursor = step(float(cursor_fg_sprite_idx), 0.5);
float cell_has_block_cursor = cell_has_cursor * is_block_cursor;
vec3 bg = to_color(colors[bg_index], default_colors[bg_index]);
int mark = int(text_attrs >> MARK_SHIFT) & MARK_MASK;
uint has_mark = uint(step(1, float(mark)));
uint bg_as_uint = resolve_color(colors[bg_index], default_colors[bg_index]);
bg_as_uint = has_mark * color_table[NUM_COLORS + mark] + (ONE - has_mark) * bg_as_uint;
vec3 bg = color_to_vec(bg_as_uint);
// }}}
// Foreground {{{
@@ -169,12 +180,13 @@ void main() {
colored_sprite = float((sprite_coords.z & COLOR_MASK) >> 14);
// Foreground
uint resolved_fg = resolve_color(colors[fg_index], default_colors[fg_index]);
foreground = color_to_vec(resolved_fg);
uint fg_as_uint = resolve_color(colors[fg_index], default_colors[fg_index]);
fg_as_uint = has_mark * color_table[NUM_COLORS + MARK_MASK + 1 + mark] + (ONE - has_mark) * fg_as_uint;
foreground = color_to_vec(fg_as_uint);
float has_dim = float((text_attrs >> DIM_SHIFT) & ONE);
effective_text_alpha = inactive_text_alpha * mix(1.0, dim_opacity, has_dim);
float in_url = float((is_selected & TWO) >> 1);
decoration_fg = choose_color(in_url, color_to_vec(url_color), to_color(colors[2], resolved_fg));
decoration_fg = choose_color(in_url, color_to_vec(url_color), to_color(colors[2], fg_as_uint));
#ifdef USE_SELECTION_FG
// Selection
vec3 selection_color = color_to_vec(highlight_fg);
@@ -196,23 +208,29 @@ void main() {
// Background {{{
#ifdef NEEDS_BACKROUND
float cell_has_non_default_bg = step(1, float(abs(bg_as_uint - default_colors[bg_index])));
draw_bg = 1;
#if defined(BACKGROUND)
background = bg;
// draw_bg_bitfield has bit 0 set to draw default bg cells and bit 1 set to draw non-default bg cells
uint draw_bg_mask = uint(2 * cell_has_non_default_bg + (1 - cell_has_non_default_bg));
draw_bg = step(1, float(draw_bg_bitfield & draw_bg_mask));
#endif
#if defined(TRANSPARENT)
#ifdef TRANSPARENT
// Set bg_alpha to background_opacity on cells that have the default background color
// Which means they must not have a block cursor or a selection or reverse video
// On other cells it should be 1. For the SPECIAL program it should be 1 on cells with
// selections/block cursor and 0 everywhere else.
float is_special_cell = cell_has_block_cursor + float(is_selected & ONE);
#ifndef SPECIAL
is_special_cell += float(colors[bg_index] & BYTE_MASK) + float(is_reversed);
is_special_cell += cell_has_non_default_bg + float(is_reversed);
#endif
bg_alpha = step(0.5, is_special_cell);
#ifndef SPECIAL
bg_alpha = bg_alpha + (1.0f - bg_alpha) * background_opacity;
bg_alpha *= draw_bg;
#endif
#endif

View File

@@ -512,15 +512,23 @@ collect_cursor_info(CursorRenderInfo *ans, Window *w, monotonic_t now, OSWindow
ans->is_focused = os_window->is_focused;
}
static inline void
change_menubar_title(PyObject *title UNUSED) {
#ifdef __APPLE__
static PyObject *current_title = NULL;
if (title != current_title) {
current_title = title;
if (title && OPT(macos_show_window_title_in) & MENUBAR) cocoa_update_menu_bar_title(title);
}
#endif
}
static inline bool
update_window_title(Window *w, OSWindow *os_window) {
if (w->title && w->title != os_window->window_title) {
os_window->window_title = w->title;
Py_INCREF(os_window->window_title);
set_os_window_title(os_window, PyUnicode_AsUTF8(w->title));
#ifdef __APPLE__
if (os_window->is_focused && (OPT(macos_show_window_title_in) & MENUBAR)) cocoa_update_menu_bar_title(w->title);
#endif
return true;
}
return false;
@@ -554,7 +562,7 @@ prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int *
*num_visible_windows += 1;
color_type window_bg = colorprofile_to_color(WD.screen->color_profile, WD.screen->color_profile->overridden.default_bg, WD.screen->color_profile->configured.default_bg);
if (*num_visible_windows == 1) first_window_bg = window_bg;
if (first_window_bg != window_bg) all_windows_have_same_bg = false;
if (first_window_bg != window_bg) *all_windows_have_same_bg = false;
if (w->last_drag_scroll_at > 0) {
if (now - w->last_drag_scroll_at >= ms_to_monotonic_t(20ll)) {
if (drag_scroll(w, os_window)) {
@@ -633,7 +641,7 @@ draw_resizing_text(OSWindow *w) {
snprintf(text, sizeof(text), "%u x %u cells", width / w->fonts_data->cell_width, height / w->fonts_data->cell_height);
StringCanvas rendered = render_simple_text(w->fonts_data, text);
if (rendered.canvas) {
draw_centered_alpha_mask(w->gvao_idx, width, height, rendered.width, rendered.height, rendered.canvas);
draw_centered_alpha_mask(w, width, height, rendered.width, rendered.height, rendered.canvas);
free(rendered.canvas);
}
}
@@ -641,7 +649,7 @@ draw_resizing_text(OSWindow *w) {
static inline bool
no_render_frame_received_recently(OSWindow *w, monotonic_t now, monotonic_t max_wait) {
bool ans = now - w->last_render_frame_received_at > max_wait;
if (ans) log_error("No render frame received in %f seconds, re-requesting at: %f", monotonic_t_to_s_double(max_wait), monotonic_t_to_s_double(now));
if (ans) log_error("No render frame received in %.2f seconds, re-requesting at: %f", monotonic_t_to_s_double(max_wait), monotonic_t_to_s_double(now));
return ans;
}
@@ -657,9 +665,11 @@ render(monotonic_t now, bool input_read) {
for (size_t i = 0; i < global_state.num_os_windows; i++) {
OSWindow *w = global_state.os_windows + i;
w->render_calls++;
if (!w->num_tabs) continue;
if (!should_os_window_be_rendered(w)) {
update_os_window_title(w);
if (w->is_focused) change_menubar_title(w->window_title);
continue;
}
if (USE_RENDER_FRAMES && w->render_state != RENDER_FRAME_READY) {
@@ -688,7 +698,9 @@ render(monotonic_t now, bool input_read) {
if (!w->fonts_data) { log_error("No fonts data found for window id: %llu", w->id); continue; }
if (prepare_to_render_os_window(w, now, &active_window_id, &active_window_bg, &num_visible_windows, &all_windows_have_same_bg)) needs_render = true;
if (w->last_active_window_id != active_window_id || w->last_active_tab != w->active_tab || w->focused_at_last_render != w->is_focused) needs_render = true;
if (w->render_calls < 3 && w->bgimage && w->bgimage->texture_id) needs_render = true;
if (needs_render) render_os_window(w, now, active_window_id, active_window_bg, num_visible_windows, all_windows_have_same_bg);
if (w->is_focused) change_menubar_title(w->window_title);
}
last_render_at = now;
#undef TD
@@ -1555,12 +1567,23 @@ safe_pipe(PyObject *self UNUSED, PyObject *args) {
return Py_BuildValue("ii", fds[0], fds[1]);
}
static PyObject*
cocoa_set_menubar_title(PyObject *self UNUSED, PyObject *args UNUSED) {
#ifdef __APPLE__
PyObject *title = NULL;
if (!PyArg_ParseTuple(args, "U", &title)) return NULL;
change_menubar_title(title);
#endif
Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = {
METHODB(safe_pipe, METH_VARARGS),
{"add_timer", (PyCFunction)add_python_timer, METH_VARARGS, ""},
{"remove_timer", (PyCFunction)remove_python_timer, METH_VARARGS, ""},
METHODB(monitor_pid, METH_VARARGS),
{"set_iutf8_winid", (PyCFunction)pyset_iutf8, METH_VARARGS, ""},
METHODB(cocoa_set_menubar_title, METH_VARARGS),
{NULL} /* Sentinel */
};

View File

@@ -7,10 +7,20 @@ import os
import sys
from collections import defaultdict
from contextlib import contextmanager, suppress
from typing import (
DefaultDict, Dict, Generator, List, Optional, Sequence, Tuple
)
import kitty.fast_data_types as fast_data_types
from .constants import is_macos, shell_path, terminfo_dir
from .options_stub import Options
try:
from typing import TypedDict
except ImportError:
TypedDict = dict
if is_macos:
from kitty.fast_data_types import (
@@ -18,31 +28,31 @@ if is_macos:
process_group_map as _process_group_map
)
def cwd_of_process(pid):
def cwd_of_process(pid: int) -> str:
return os.path.realpath(_cwd(pid))
def process_group_map():
ans = defaultdict(list)
def process_group_map() -> DefaultDict[int, List[int]]:
ans: DefaultDict[int, List] = defaultdict(list)
for pid, pgid in _process_group_map():
ans[pgid].append(pid)
return ans
else:
def cmdline_of_process(pid):
def cmdline_of_process(pid: int) -> List[str]:
with open('/proc/{}/cmdline'.format(pid), 'rb') as f:
return list(filter(None, f.read().decode('utf-8').split('\0')))
def cwd_of_process(pid):
def cwd_of_process(pid: int) -> str:
ans = '/proc/{}/cwd'.format(pid)
return os.path.realpath(ans)
def _environ_of_process(pid):
def _environ_of_process(pid: int) -> str:
with open('/proc/{}/environ'.format(pid), 'rb') as f:
return f.read().decode('utf-8')
def process_group_map():
ans = defaultdict(list)
def process_group_map() -> DefaultDict[int, List[int]]:
ans: DefaultDict[int, List[int]] = defaultdict(list)
for x in os.listdir('/proc'):
try:
pid = int(x)
@@ -51,7 +61,7 @@ else:
try:
with open('/proc/' + x + '/stat', 'rb') as f:
raw = f.read().decode('utf-8')
except EnvironmentError:
except OSError:
continue
try:
q = int(raw.split(' ', 5)[4])
@@ -61,40 +71,44 @@ else:
return ans
def checked_terminfo_dir():
ans = getattr(checked_terminfo_dir, 'ans', None)
if ans is None:
ans = checked_terminfo_dir.ans = terminfo_dir if os.path.isdir(terminfo_dir) else None
def checked_terminfo_dir() -> Optional[str]:
q = getattr(checked_terminfo_dir, 'ans', False)
if q is False:
ans = terminfo_dir if os.path.isdir(terminfo_dir) else None
setattr(checked_terminfo_dir, 'ans', ans)
else:
ans = q
return ans
def processes_in_group(grp):
gmap = getattr(process_group_map, 'cached_map', None)
def processes_in_group(grp: int) -> List[int]:
gmap: Optional[DefaultDict[int, List[int]]] = getattr(process_group_map, 'cached_map', None)
if gmap is None:
try:
gmap = process_group_map()
except Exception:
gmap = {}
gmap = defaultdict(list)
return gmap.get(grp, [])
@contextmanager
def cached_process_data():
def cached_process_data() -> Generator[None, None, None]:
try:
process_group_map.cached_map = process_group_map()
cm = process_group_map()
except Exception:
process_group_map.cached_map = {}
cm = defaultdict(list)
setattr(process_group_map, 'cached_map', cm)
try:
yield
finally:
process_group_map.cached_map = None
delattr(process_group_map, 'cached_map')
def parse_environ_block(data):
def parse_environ_block(data: str) -> Dict[str, str]:
"""Parse a C environ block of environment variables into a dictionary."""
# The block is usually raw data from the target process. It might contain
# trailing garbage and lines that do not look like assignments.
ret = {}
ret: Dict[str, str] = {}
pos = 0
while True:
@@ -113,54 +127,69 @@ def parse_environ_block(data):
return ret
def environ_of_process(pid):
def environ_of_process(pid: int) -> Dict[str, str]:
return parse_environ_block(_environ_of_process(pid))
def remove_cloexec(fd):
def remove_cloexec(fd: int) -> None:
fcntl.fcntl(fd, fcntl.F_SETFD, fcntl.fcntl(fd, fcntl.F_GETFD) & ~fcntl.FD_CLOEXEC)
def remove_blocking(fd):
def remove_blocking(fd: int) -> None:
os.set_blocking(fd, False)
def process_env():
ans = os.environ
def process_env() -> Dict[str, str]:
ans = dict(os.environ)
ssl_env_var = getattr(sys, 'kitty_ssl_env_var', None)
if ssl_env_var is not None:
ans = ans.copy()
ans.pop(ssl_env_var, None)
return ans
def default_env():
try:
return default_env.env
except AttributeError:
def default_env() -> Dict[str, str]:
ans: Optional[Dict[str, str]] = getattr(default_env, 'env', None)
if ans is None:
return process_env()
return ans
def set_default_env(val=None):
def set_default_env(val: Optional[Dict[str, str]] = None) -> None:
env = process_env().copy()
if val:
env.update(val)
default_env.env = env
setattr(default_env, 'env', env)
def openpty():
def openpty() -> Tuple[int, int]:
master, slave = os.openpty() # Note that master and slave are in blocking mode
remove_cloexec(slave)
fast_data_types.set_iutf8_fd(master, True)
return master, slave
class ProcessDesc(TypedDict):
cwd: Optional[str]
pid: int
cmdline: Optional[Sequence[str]]
class Child:
child_fd = pid = None
child_fd: Optional[int] = None
pid: Optional[int] = None
forked = False
def __init__(self, argv, cwd, opts, stdin=None, env=None, cwd_from=None, allow_remote_control=False):
def __init__(
self,
argv: Sequence[str],
cwd: str,
opts: Options,
stdin: Optional[bytes] = None,
env: Optional[Dict[str, str]] = None,
cwd_from: Optional[int] = None,
allow_remote_control: bool = False
):
self.allow_remote_control = allow_remote_control
self.argv = argv
if cwd_from is not None:
@@ -177,8 +206,8 @@ class Child:
self.env = env or {}
@property
def final_env(self):
env = getattr(self, '_final_env', None)
def final_env(self) -> Dict[str, str]:
env: Optional[Dict[str, str]] = getattr(self, '_final_env', None)
if env is None:
env = self._final_env = default_env().copy()
env.update(self.env)
@@ -189,13 +218,14 @@ class Child:
# can use it to display the current directory name rather
# than the resolved path
env['PWD'] = self.cwd
if checked_terminfo_dir():
env['TERMINFO'] = checked_terminfo_dir()
tdir = checked_terminfo_dir()
if tdir:
env['TERMINFO'] = tdir
return env
def fork(self):
def fork(self) -> Optional[int]:
if self.forked:
return
return None
self.forked = True
master, slave = openpty()
stdin, self.stdin = self.stdin, None
@@ -206,8 +236,7 @@ class Child:
remove_cloexec(stdin_read_fd)
else:
stdin_read_fd = stdin_write_fd = -1
env = self.final_env
env = tuple('{}={}'.format(k, v) for k, v in env.items())
env = tuple('{}={}'.format(k, v) for k, v in self.final_env.items())
argv = list(self.argv)
exe = argv[0]
if is_macos and exe == shell_path:
@@ -235,60 +264,68 @@ class Child:
fast_data_types.thread_write(stdin_write_fd, stdin)
os.close(ready_read_fd)
self.terminal_ready_fd = ready_write_fd
remove_blocking(self.child_fd)
if self.child_fd is not None:
remove_blocking(self.child_fd)
return pid
def mark_terminal_ready(self):
def mark_terminal_ready(self) -> None:
os.close(self.terminal_ready_fd)
self.terminal_ready_fd = -1
@property
def foreground_processes(self):
def foreground_processes(self) -> List[ProcessDesc]:
if self.child_fd is None:
return []
try:
pgrp = os.tcgetpgrp(self.child_fd)
foreground_processes = processes_in_group(pgrp) if pgrp >= 0 else []
def process_desc(pid):
ans = {'pid': pid}
def process_desc(pid: int) -> ProcessDesc:
ans: ProcessDesc = {'pid': pid, 'cmdline': None, 'cwd': None}
with suppress(Exception):
ans['cmdline'] = cmdline_of_process(pid)
with suppress(Exception):
ans['cwd'] = cwd_of_process(pid) or None
return ans
return list(map(process_desc, foreground_processes))
return [process_desc(x) for x in foreground_processes]
except Exception:
return []
@property
def cmdline(self):
def cmdline(self) -> List[str]:
try:
assert self.pid is not None
return cmdline_of_process(self.pid) or list(self.argv)
except Exception:
return list(self.argv)
@property
def foreground_cmdline(self):
def foreground_cmdline(self) -> List[str]:
try:
assert self.pid_for_cwd is not None
return cmdline_of_process(self.pid_for_cwd) or self.cmdline
except Exception:
return self.cmdline
@property
def environ(self):
def environ(self) -> Dict[str, str]:
try:
assert self.pid is not None
return environ_of_process(self.pid)
except Exception:
return {}
@property
def current_cwd(self):
def current_cwd(self) -> Optional[str]:
with suppress(Exception):
assert self.pid is not None
return cwd_of_process(self.pid)
@property
def pid_for_cwd(self):
def pid_for_cwd(self) -> Optional[int]:
with suppress(Exception):
assert self.child_fd is not None
pgrp = os.tcgetpgrp(self.child_fd)
foreground_processes = processes_in_group(pgrp) if pgrp >= 0 else []
if len(foreground_processes) == 1:
@@ -296,16 +333,19 @@ class Child:
return self.pid
@property
def foreground_cwd(self):
def foreground_cwd(self) -> Optional[str]:
with suppress(Exception):
assert self.pid_for_cwd is not None
return cwd_of_process(self.pid_for_cwd) or None
@property
def foreground_environ(self):
def foreground_environ(self) -> Dict[str, str]:
try:
assert self.pid_for_cwd is not None
return environ_of_process(self.pid_for_cwd)
except Exception:
try:
assert self.pid is not None
return environ_of_process(self.pid)
except Exception:
pass

View File

@@ -4,8 +4,13 @@
import re
from typing import List, Generator, Any, Type
def mark(text, args, Mark, extra_cli_args, *a):
from .cli_stub import HintsCLIOptions
from .typing import MarkType
def mark(text: str, args: HintsCLIOptions, Mark: Type[MarkType], extra_cli_args: List[str], *a: Any) -> Generator[MarkType, None, None]:
for idx, m in enumerate(re.finditer(args.regex, text)):
start, end = m.span()
mark_text = text[start:end].replace('\n', '').replace('\0', '')

View File

@@ -6,9 +6,28 @@ import os
import re
import sys
from collections import deque
from typing import (
Any, Callable, Dict, FrozenSet, Iterable, Iterator, List, Match, Optional,
Sequence, Set, Tuple, Type, TypeVar, Union, cast
)
from .cli_stub import CLIOptions
from .conf.utils import resolve_config
from .config import KeyAction
from .constants import appname, defconf, is_macos, is_wayland, str_version
from .options_stub import Options as OptionsStub
from .typing import BadLineType, KeySpec, SequenceMap, TypedDict
class OptionDict(TypedDict):
dest: str
aliases: FrozenSet[str]
help: str
choices: FrozenSet[str]
type: str
default: Optional[str]
condition: bool
CONFIG_HELP = '''\
Specify a path to the configuration file(s) to use. All configuration files are
@@ -34,49 +53,49 @@ defaults for all users.
)
def surround(x, start, end):
def surround(x: str, start: int, end: int) -> str:
if sys.stdout.isatty():
x = '\033[{}m{}\033[{}m'.format(start, x, end)
return x
def emph(x):
def emph(x: str) -> str:
return surround(x, 91, 39)
def cyan(x):
def cyan(x: str) -> str:
return surround(x, 96, 39)
def green(x):
def green(x: str) -> str:
return surround(x, 32, 39)
def blue(x):
def blue(x: str) -> str:
return surround(x, 34, 39)
def yellow(x):
def yellow(x: str) -> str:
return surround(x, 93, 39)
def italic(x):
def italic(x: str) -> str:
return surround(x, 3, 23)
def bold(x):
def bold(x: str) -> str:
return surround(x, 1, 22)
def title(x):
def title(x: str) -> str:
return blue(bold(x))
def opt(text):
def opt(text: str) -> str:
return text
def option(x):
def option(x: str) -> str:
idx = x.find('-')
if idx > -1:
x = x[idx:]
@@ -84,33 +103,40 @@ def option(x):
return ' '.join(parts)
def code(x):
def code(x: str) -> str:
return x
def kbd(x):
def kbd(x: str) -> str:
return x
def env(x):
def env(x: str) -> str:
return italic(x)
def file(x):
def file(x: str) -> str:
return italic(x)
def parse_option_spec(spec=None):
OptionSpecSeq = List[Union[str, OptionDict]]
def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, OptionSpecSeq]:
if spec is None:
spec = options_spec()
NORMAL, METADATA, HELP = 'NORMAL', 'METADATA', 'HELP'
state = NORMAL
lines = spec.splitlines()
prev_line = ''
seq = []
disabled = []
seq: OptionSpecSeq = []
disabled: OptionSpecSeq = []
mpat = re.compile('([a-z]+)=(.+)')
current_cmd = None
current_cmd: OptionDict = {
'dest': '', 'aliases': frozenset(), 'help': '', 'choices': frozenset(),
'type': '', 'condition': False, 'default': None
}
empty_cmd = current_cmd
for line in lines:
line = line.rstrip()
@@ -122,7 +148,11 @@ def parse_option_spec(spec=None):
continue
if line.startswith('--'):
parts = line.split(' ')
current_cmd = {'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': ''}
current_cmd = {
'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': '',
'choices': frozenset(), 'type': '',
'default': None, 'condition': True
}
state = METADATA
continue
raise ValueError('Invalid option spec, unexpected line: {}'.format(line))
@@ -133,11 +163,17 @@ def parse_option_spec(spec=None):
current_cmd['help'] += line
else:
k, v = m.group(1), m.group(2)
if k == 'condition':
v = eval(v)
current_cmd[k] = v
if k == 'choices':
current_cmd['choices'] = {x.strip() for x in current_cmd['choices'].split(',')}
current_cmd['choices'] = frozenset(x.strip() for x in v.split(','))
else:
if k == 'default':
current_cmd['default'] = v
elif k == 'type':
current_cmd['type'] = v
elif k == 'dest':
current_cmd['dest'] = v
elif k == 'condition':
current_cmd['condition'] = bool(eval(v))
elif state is HELP:
if line:
spc = '' if current_cmd['help'].endswith('\n') else ' '
@@ -148,30 +184,30 @@ def parse_option_spec(spec=None):
else:
state = NORMAL
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
current_cmd = None
current_cmd = empty_cmd
prev_line = line
if current_cmd is not None:
if current_cmd is not empty_cmd:
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
return seq, disabled
def prettify(text):
def prettify(text: str) -> str:
role_map = globals()
def sub(m):
def sub(m: Match) -> str:
role, text = m.group(1, 2)
return role_map[role](text)
return str(role_map[role](text))
text = re.sub(r':([a-z]+):`([^`]+)`', sub, text)
return text
def prettify_rst(text):
def prettify_rst(text: str) -> str:
return re.sub(r':([a-z]+):`([^`]+)`(=[^\s.]+)', r':\1:`\2`:code:`\3`', text)
def version(add_rev=False):
def version(add_rev: bool = False) -> str:
rev = ''
from . import fast_data_types
if add_rev and hasattr(fast_data_types, 'KITTY_VCS_REV'):
@@ -179,7 +215,7 @@ def version(add_rev=False):
return '{} {}{} created by {}'.format(italic(appname), green(str_version), rev, title('Kovid Goyal'))
def wrap(text, limit=80):
def wrap(text: str, limit: int = 80) -> Iterator[str]:
NORMAL, IN_FORMAT = 'NORMAL', 'IN_FORMAT'
state = NORMAL
last_space_at = None
@@ -203,7 +239,7 @@ def wrap(text, limit=80):
last_space_at = None
chars_in_line = i - breaks[-1]
lines = []
lines: List[str] = []
for b in reversed(breaks):
lines.append(text[b:].lstrip())
text = text[:b]
@@ -212,8 +248,8 @@ def wrap(text, limit=80):
return reversed(lines)
def get_defaults_from_seq(seq):
ans = {}
def get_defaults_from_seq(seq: OptionSpecSeq) -> Dict[str, Any]:
ans: Dict[str, Any] = {}
for opt in seq:
if not isinstance(opt, str):
ans[opt['dest']] = defval_for_opt(opt)
@@ -223,81 +259,91 @@ def get_defaults_from_seq(seq):
default_msg = ('''\
Run the :italic:`{appname}` terminal emulator. You can also specify the :italic:`program`
to run inside :italic:`{appname}` as normal arguments following the :italic:`options`.
For example: {appname} /bin/sh
For example: {appname} sh -c "echo hello, world. Press ENTER to quit; read"
For comprehensive documentation for kitty, please see: https://sw.kovidgoyal.net/kitty/''').format(appname=appname)
def print_help_for_seq(seq, usage, message, appname):
from kitty.utils import screen_size_function
screen_size = screen_size_function()
try:
linesz = min(screen_size().cols, 76)
except EnvironmentError:
linesz = 76
blocks = []
a = blocks.append
class PrintHelpForSeq:
def wa(text, indent=0, leading_indent=None):
if leading_indent is None:
leading_indent = indent
j = '\n' + (' ' * indent)
lines = []
for l in text.splitlines():
if l:
lines.extend(wrap(l, limit=linesz - indent))
else:
lines.append('')
a((' ' * leading_indent) + j.join(lines))
allow_pager = True
usage = '[program-to-run ...]' if usage is None else usage
optstring = '[options] ' if seq else ''
a('{}: {} {}{}'.format(title('Usage'), bold(yellow(appname)), optstring, usage))
a('')
message = message or default_msg
wa(prettify(message))
a('')
if seq:
a('{}:'.format(title('Options')))
for opt in seq:
if isinstance(opt, str):
a('{}:'.format(title(opt)))
continue
help_text = opt['help']
if help_text == '!':
continue # hidden option
a(' ' + ', '.join(map(green, sorted(opt['aliases']))))
if not opt.get('type', '').startswith('bool-'):
blocks[-1] += '={}'.format(italic(opt['dest'].upper()))
if opt.get('help'):
defval = opt.get('default')
t = help_text.replace('%default', str(defval))
wa(prettify(t.strip()), indent=4)
if defval is not None:
wa('Default: {}'.format(defval), indent=4)
if 'choices' in opt:
wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4)
a('')
text = '\n'.join(blocks) + '\n\n' + version()
if print_help_for_seq.allow_pager and sys.stdout.isatty():
import subprocess
p = subprocess.Popen(['less', '-isRXF'], stdin=subprocess.PIPE)
def __call__(self, seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: str) -> None:
from kitty.utils import screen_size_function
screen_size = screen_size_function()
try:
p.communicate(text.encode('utf-8'))
except KeyboardInterrupt:
raise SystemExit(1)
raise SystemExit(p.wait())
else:
print(text)
linesz = min(screen_size().cols, 76)
except OSError:
linesz = 76
blocks: List[str] = []
a = blocks.append
def wa(text: str, indent: int = 0, leading_indent: Optional[int] = None) -> None:
if leading_indent is None:
leading_indent = indent
j = '\n' + (' ' * indent)
lines: List[str] = []
for l in text.splitlines():
if l:
lines.extend(wrap(l, limit=linesz - indent))
else:
lines.append('')
a((' ' * leading_indent) + j.join(lines))
usage = '[program-to-run ...]' if usage is None else usage
optstring = '[options] ' if seq else ''
a('{}: {} {}{}'.format(title('Usage'), bold(yellow(appname)), optstring, usage))
a('')
message = message or default_msg
wa(prettify(message))
a('')
if seq:
a('{}:'.format(title('Options')))
for opt in seq:
if isinstance(opt, str):
a('{}:'.format(title(opt)))
continue
help_text = opt['help']
if help_text == '!':
continue # hidden option
a(' ' + ', '.join(map(green, sorted(opt['aliases']))))
if not opt.get('type', '').startswith('bool-'):
blocks[-1] += '={}'.format(italic(opt['dest'].upper()))
if opt.get('help'):
defval = opt.get('default')
t = help_text.replace('%default', str(defval))
wa(prettify(t.strip()), indent=4)
if defval is not None:
wa('Default: {}'.format(defval), indent=4)
if opt.get('choices'):
wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4)
a('')
text = '\n'.join(blocks) + '\n\n' + version()
if print_help_for_seq.allow_pager and sys.stdout.isatty():
import subprocess
p = subprocess.Popen(['less', '-isRXF'], stdin=subprocess.PIPE)
try:
p.communicate(text.encode('utf-8'))
except KeyboardInterrupt:
raise SystemExit(1)
raise SystemExit(p.wait())
else:
print(text)
print_help_for_seq.allow_pager = True
print_help_for_seq = PrintHelpForSeq()
def seq_as_rst(seq, usage, message, appname, heading_char='-'):
def seq_as_rst(
seq: OptionSpecSeq,
usage: Optional[str],
message: Optional[str],
appname: Optional[str],
heading_char: str = '-'
) -> str:
import textwrap
blocks = []
blocks: List[str] = []
a = blocks.append
usage = '[program-to-run ...]' if usage is None else usage
@@ -334,7 +380,7 @@ def seq_as_rst(seq, usage, message, appname, heading_char='-'):
a(textwrap.indent(prettify_rst(t), ' ' * 4))
if defval is not None:
a(textwrap.indent('Default: :code:`{}`'.format(defval), ' ' * 4))
if 'choices' in opt:
if opt.get('choices'):
a(textwrap.indent('Choices: :code:`{}`'.format(', '.join(sorted(opt['choices']))), ' ' * 4))
a('')
@@ -342,8 +388,34 @@ def seq_as_rst(seq, usage, message, appname, heading_char='-'):
return text
def defval_for_opt(opt):
dv = opt.get('default')
def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, extra_fields: Sequence[str] = ()) -> str:
from itertools import chain
ans: List[str] = ['class {}:'.format(class_name)]
for opt in chain(seq, disabled):
if isinstance(opt, str):
continue
name = opt['dest']
otype = opt['type'] or 'str'
if otype in ('str', 'int', 'float'):
t = otype
if t == 'str' and defval_for_opt(opt) is None:
t = 'typing.Optional[str]'
elif otype == 'list':
t = 'typing.Sequence[str]'
elif otype in ('choice', 'choices'):
t = 'str'
elif otype.startswith('bool-'):
t = 'bool'
else:
raise ValueError('Unknown CLI option type: {}'.format(otype))
ans.append(' {}: {}'.format(name, t))
for x in extra_fields:
ans.append(' {}'.format(x))
return '\n'.join(ans) + '\n\n\n'
def defval_for_opt(opt: OptionDict) -> Any:
dv: Any = opt.get('default')
typ = opt.get('type', '')
if typ.startswith('bool-'):
if dv is None:
@@ -359,11 +431,11 @@ def defval_for_opt(opt):
class Options:
def __init__(self, seq, usage, message, appname):
def __init__(self, seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: Optional[str]):
self.alias_map = {}
self.seq = seq
self.names_map = {}
self.values_map = {}
self.names_map: Dict[str, OptionDict] = {}
self.values_map: Dict[str, Any] = {}
self.usage, self.message, self.appname = usage, message, appname
for opt in seq:
if isinstance(opt, str):
@@ -374,13 +446,13 @@ class Options:
self.names_map[name] = opt
self.values_map[name] = defval_for_opt(opt)
def opt_for_alias(self, alias):
def opt_for_alias(self, alias: str) -> OptionDict:
opt = self.alias_map.get(alias)
if opt is None:
raise SystemExit('Unknown option: {}'.format(emph(alias)))
return opt
def needs_arg(self, alias):
def needs_arg(self, alias: str) -> bool:
if alias in ('-h', '--help'):
print_help_for_seq(self.seq, self.usage, self.message, self.appname or appname)
raise SystemExit(0)
@@ -391,7 +463,7 @@ class Options:
typ = opt.get('type', '')
return not typ.startswith('bool-')
def process_arg(self, alias, val=None):
def process_arg(self, alias: str, val: Any = None) -> None:
opt = self.opt_for_alias(alias)
typ = opt.get('type', '')
name = opt['dest']
@@ -420,26 +492,19 @@ class Options:
self.values_map[name] = val
class Namespace:
def __init__(self, kwargs):
for name, val in kwargs.items():
setattr(self, name, val)
def parse_cmdline(oc, disabled, args=None):
def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: Optional[List[str]] = None) -> List[str]:
NORMAL, EXPECTING_ARG = 'NORMAL', 'EXPECTING_ARG'
state = NORMAL
if args is None:
args = sys.argv[1:]
args = deque(args)
dargs = deque(sys.argv[1:] if args is None else args)
leftover_args: List[str] = []
current_option = None
while args:
arg = args.popleft()
while dargs:
arg = dargs.popleft()
if state is NORMAL:
if arg.startswith('-'):
if arg == '--':
leftover_args = list(dargs)
break
parts = arg.split('=', 1)
needs_arg = oc.needs_arg(parts[0])
@@ -454,21 +519,23 @@ def parse_cmdline(oc, disabled, args=None):
continue
oc.process_arg(parts[0], parts[1])
else:
args = [arg] + list(args)
leftover_args = [arg] + list(dargs)
break
else:
elif current_option is not None:
oc.process_arg(current_option, arg)
current_option, state = None, NORMAL
if state is EXPECTING_ARG:
raise SystemExit('An argument is required for the option: {}'.format(emph(arg)))
ans = Namespace(oc.values_map)
for key, val in oc.values_map.items():
setattr(ans, key, val)
for opt in disabled:
setattr(ans, opt['dest'], defval_for_opt(opt))
return ans, list(args)
if not isinstance(opt, str):
setattr(ans, opt['dest'], defval_for_opt(opt))
return leftover_args
def options_spec():
def options_spec() -> str:
if not hasattr(options_spec, 'ans'):
OPTIONS = '''
--class
@@ -614,56 +681,77 @@ Print out information about the system and kitty configuration.
type=bool-set
!
'''
options_spec.ans = OPTIONS.format(
setattr(options_spec, 'ans', OPTIONS.format(
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname)
)
return options_spec.ans
))
ans: str = getattr(options_spec, 'ans')
return ans
def options_for_completion():
def options_for_completion() -> OptionSpecSeq:
raw = '--help -h\ntype=bool-set\nShow help for {appname} command line options\n\n{raw}'.format(
appname=appname, raw=options_spec())
return parse_option_spec(raw)[0]
def option_spec_as_rst(ospec=options_spec, usage=None, message=None, appname=None, heading_char='-'):
def option_spec_as_rst(
ospec: Callable[[], str] = options_spec,
usage: Optional[str] = None, message: Optional[str] = None, appname: Optional[str] = None,
heading_char: str = '-'
) -> str:
options = parse_option_spec(ospec())
seq, disabled = options
oc = Options(seq, usage, message, appname)
return seq_as_rst(oc.seq, oc.usage, oc.message, oc.appname, heading_char=heading_char)
def parse_args(args=None, ospec=options_spec, usage=None, message=None, appname=None):
T = TypeVar('T')
def parse_args(
args: Optional[List[str]] = None,
ospec: Callable[[], str] = options_spec,
usage: Optional[str] = None,
message: Optional[str] = None,
appname: Optional[str] = None,
result_class: Optional[Type[T]] = None,
) -> Tuple[T, List[str]]:
options = parse_option_spec(ospec())
seq, disabled = options
oc = Options(seq, usage, message, appname)
return parse_cmdline(oc, disabled, args=args)
if result_class is not None:
ans = result_class()
else:
ans = cast(T, CLIOptions())
return ans, parse_cmdline(oc, disabled, ans, args=args)
SYSTEM_CONF = '/etc/xdg/kitty/kitty.conf'
ShortcutMap = Dict[Tuple[KeySpec, ...], KeyAction]
def print_shortcut(key_sequence, action):
def print_shortcut(key_sequence: Iterable[KeySpec], action: KeyAction) -> None:
if not getattr(print_shortcut, 'maps', None):
from kitty.keys import defines
v = vars(defines)
mmap = {m[len('GLFW_MOD_'):].lower(): x for m, x in v.items() if m.startswith('GLFW_MOD_')}
kmap = {k[len('GLFW_KEY_'):].lower(): x for k, x in v.items() if k.startswith('GLFW_KEY_')}
krmap = {v: k for k, v in kmap.items()}
print_shortcut.maps = mmap, krmap
mmap, krmap = print_shortcut.maps
setattr(print_shortcut, 'maps', (mmap, krmap))
mmap, krmap = getattr(print_shortcut, 'maps')
keys = []
for key in key_sequence:
for key_spec in key_sequence:
names = []
mods, is_native, key = key
mods, is_native, key = key_spec
for name, val in mmap.items():
if mods & val:
names.append(name)
if key:
if is_native:
from .fast_data_types import GLFW_KEY_UNKNOWN, glfw_get_key_name
names.append(glfw_get_key_name(GLFW_KEY_UNKNOWN, key))
kn = glfw_get_key_name(GLFW_KEY_UNKNOWN, key) or 'Unknown key'
names.append(kn)
else:
names.append(krmap[key])
keys.append('+'.join(names))
@@ -671,7 +759,7 @@ def print_shortcut(key_sequence, action):
print('\t', ' > '.join(keys), action)
def print_shortcut_changes(defns, text, changes):
def print_shortcut_changes(defns: ShortcutMap, text: str, changes: Set[Tuple[KeySpec, ...]]) -> None:
if changes:
print(title(text))
@@ -679,7 +767,7 @@ def print_shortcut_changes(defns, text, changes):
print_shortcut(k, defns[k])
def compare_keymaps(final, initial):
def compare_keymaps(final: ShortcutMap, initial: ShortcutMap) -> None:
added = set(final) - set(initial)
removed = set(initial) - set(final)
changed = {k for k in set(final) & set(initial) if final[k] != initial[k]}
@@ -688,20 +776,20 @@ def compare_keymaps(final, initial):
print_shortcut_changes(final, 'Changed shortcuts:', changed)
def flatten_sequence_map(m):
ans = {}
for k, rest_map in m.items():
def flatten_sequence_map(m: SequenceMap) -> ShortcutMap:
ans: Dict[Tuple[KeySpec, ...], KeyAction] = {}
for key_spec, rest_map in m.items():
for r, action in rest_map.items():
ans[(k,) + (r)] = action
ans[(key_spec,) + (r)] = action
return ans
def compare_opts(opts):
def compare_opts(opts: OptionsStub) -> None:
from .config import defaults, load_config
print('\nConfig options different from defaults:')
default_opts = load_config()
changed_opts = [
f for f in sorted(defaults._fields)
f for f in sorted(defaults._fields) # type: ignore
if f not in ('key_definitions', 'keymap', 'sequence_map') and getattr(opts, f) != getattr(defaults, f)
]
field_len = max(map(len, changed_opts)) if changed_opts else 20
@@ -709,16 +797,16 @@ def compare_opts(opts):
for f in changed_opts:
print(title(fmt.format(f)), getattr(opts, f))
final, initial = opts.keymap, default_opts.keymap
final = {(k,): v for k, v in final.items()}
initial = {(k,): v for k, v in initial.items()}
final_, initial_ = opts.keymap, default_opts.keymap
final: ShortcutMap = {(k,): v for k, v in final_.items()}
initial: ShortcutMap = {(k,): v for k, v in initial_.items()}
final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map))
final.update(final_s)
initial.update(initial_s)
compare_keymaps(final, initial)
def create_opts(args, debug_config=False, accumulate_bad_lines=None):
def create_opts(args: CLIOptions, debug_config: bool = False, accumulate_bad_lines: Optional[List[BadLineType]] = None) -> OptionsStub:
from .config import load_config
config = tuple(resolve_config(SYSTEM_CONF, defconf, args.config))
if debug_config:

76
kitty/cli_stub.py Normal file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Sequence
class CLIOptions:
pass
LaunchCLIOptions = AskCLIOptions = ClipboardCLIOptions = DiffCLIOptions = CLIOptions
HintsCLIOptions = IcatCLIOptions = PanelCLIOptions = ResizeCLIOptions = CLIOptions
ErrorCLIOptions = UnicodeCLIOptions = RCOptions = CLIOptions
def generate_stub() -> None:
from .cli import parse_option_spec, as_type_stub
from .conf.definition import save_type_stub
text = 'import typing\n\n\n'
def do(otext=None, cls: str = 'CLIOptions', extra_fields: Sequence[str] = ()):
nonlocal text
text += as_type_stub(*parse_option_spec(otext), class_name=cls, extra_fields=extra_fields)
do(extra_fields=('args: typing.List[str]',))
from .launch import options_spec
do(options_spec(), 'LaunchCLIOptions')
from .remote_control import global_options_spec
do(global_options_spec(), 'RCOptions', extra_fields=['no_command_response: typing.Optional[bool]'])
from kittens.ask.main import option_text
do(option_text(), 'AskCLIOptions')
from kittens.clipboard.main import OPTIONS
do(OPTIONS(), 'ClipboardCLIOptions')
from kittens.diff.main import OPTIONS
do(OPTIONS(), 'DiffCLIOptions')
from kittens.hints.main import OPTIONS
do(OPTIONS(), 'HintsCLIOptions')
from kittens.icat.main import options_spec
do(options_spec(), 'IcatCLIOptions')
from kittens.panel.main import OPTIONS
do(OPTIONS(), 'PanelCLIOptions')
from kittens.resize_window.main import OPTIONS
do(OPTIONS(), 'ResizeCLIOptions')
from kittens.show_error.main import OPTIONS
do(OPTIONS(), 'ErrorCLIOptions')
from kittens.unicode_input.main import OPTIONS
do(OPTIONS(), 'UnicodeCLIOptions')
from kitty.rc.base import all_command_names, command_for_name
for cmd_name in all_command_names():
cmd = command_for_name(cmd_name)
if cmd.options_spec:
do(cmd.options_spec, cmd.__class__.__name__ + 'RCOptions')
save_type_stub(text, __file__)
if __name__ == '__main__':
import subprocess
subprocess.Popen([
'kitty', '+runpy',
'from kitty.cli_stub import generate_stub; generate_stub()'
])

View File

@@ -10,133 +10,134 @@
import sys
from contextlib import suppress
from typing import Any
CSI = '\033['
OSC = '\033]'
def write(x):
def write(x: str) -> None:
sys.stdout.write(x)
sys.stdout.flush()
def set_title(*args):
def set_title(*args: Any) -> None:
pass
def set_icon(*args):
def set_icon(*args: Any) -> None:
pass
def screen_bell():
def screen_bell() -> None:
pass
def screen_cursor_position(y, x):
def screen_cursor_position(y: int, x: int) -> None:
write(CSI + '%s;%sH' % (y, x))
def screen_cursor_forward(amt):
def screen_cursor_forward(amt: int) -> None:
write(CSI + '%sC' % amt)
def screen_cursor_back1(amt):
def screen_cursor_back1(amt: int) -> None:
write(CSI + '%sD' % amt)
def screen_designate_charset(which, to):
which = '()'[int(which)]
to = chr(int(to))
write('\033' + which + to)
def screen_designate_charset(which: int, to: int) -> None:
w = '()'[int(which)]
t = chr(int(to))
write('\033' + w + t)
def select_graphic_rendition(*a):
def select_graphic_rendition(*a: int) -> None:
write(CSI + '%sm' % ';'.join(map(str, a)))
def screen_cursor_to_column(c):
def screen_cursor_to_column(c: int) -> None:
write(CSI + '%dG' % c)
def screen_cursor_to_line(l):
def screen_cursor_to_line(l: int) -> None:
write(CSI + '%dd' % l)
def screen_set_mode(x, private):
def screen_set_mode(x: int, private: bool) -> None:
write(CSI + ('?' if private else '') + str(x) + 'h')
def screen_reset_mode(x, private):
def screen_reset_mode(x: int, private: bool) -> None:
write(CSI + ('?' if private else '') + str(x) + 'l')
def screen_set_margins(t, b):
def screen_set_margins(t: int, b: int) -> None:
write(CSI + '%d;%dr' % (t, b))
def screen_indexn(n):
def screen_indexn(n: int) -> None:
write(CSI + '%dS' % n)
def screen_erase_in_display(how, private):
def screen_erase_in_display(how: int, private: bool) -> None:
write(CSI + ('?' if private else '') + str(how) + 'J')
def screen_erase_in_line(how, private):
def screen_erase_in_line(how: int, private: bool) -> None:
write(CSI + ('?' if private else '') + str(how) + 'K')
def screen_delete_lines(num):
def screen_delete_lines(num: int) -> None:
write(CSI + str(num) + 'M')
def screen_cursor_up2(count):
def screen_cursor_up2(count: int) -> None:
write(CSI + '%dA' % count)
def screen_cursor_down(count):
def screen_cursor_down(count: int) -> None:
write(CSI + '%dB' % count)
def screen_carriage_return():
def screen_carriage_return() -> None:
write('\r')
def screen_linefeed():
def screen_linefeed() -> None:
write('\n')
def screen_backspace():
def screen_backspace() -> None:
write('\x08')
def screen_set_cursor(mode, secondary):
def screen_set_cursor(mode: int, secondary: int) -> None:
write(CSI + '%d q' % secondary)
def screen_insert_lines(num):
def screen_insert_lines(num: int) -> None:
write(CSI + '%dL' % num)
def draw(*a):
def draw(*a: str) -> None:
write(' '.join(a))
def screen_manipulate_title_stack(op, which):
def screen_manipulate_title_stack(op: int, which: int) -> None:
write(CSI + '%d;%dt' % (op, which))
def report_device_attributes(mode, char):
def report_device_attributes(mode: int, char: int) -> None:
x = CSI
if char:
x += ord(char)
x += chr(char)
if mode:
x += str(mode)
write(CSI + x + 'c')
def write_osc(code, string=''):
def write_osc(code: int, string: str = '') -> None:
if string:
string = ';' + string
write(OSC + str(code) + string + '\x07')
@@ -145,18 +146,18 @@ def write_osc(code, string=''):
set_dynamic_color = set_color_table_color = write_osc
def replay(raw):
def replay(raw: str) -> None:
for line in raw.splitlines():
if line.strip() and not line.startswith('#'):
cmd, rest = line.partition(' ')[::2]
if cmd in {'draw', 'set_title', 'set_icon', 'set_dynamic_color', 'set_color_table_color'}:
globals()[cmd](rest)
else:
rest = map(int, rest.split()) if rest else ()
globals()[cmd](*rest)
r = map(int, rest.split()) if rest else ()
globals()[cmd](*r)
def main(path):
def main(path: str) -> None:
with open(path) as f:
raw = f.read()
replay(raw)

File diff suppressed because it is too large Load Diff

View File

@@ -83,6 +83,11 @@ find_app_name(void) {
set_cocoa_pending_action(NEW_OS_WINDOW, NULL);
}
- (void)open_kitty_website_url:(id)sender {
(void)sender;
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://sw.kovidgoyal.net/kitty/"]];
}
+ (GlobalMenuTarget *) shared_instance
{
@@ -97,7 +102,7 @@ find_app_name(void) {
@end
static unichar new_window_key = 0;
static char new_window_key[32] = {0};
static NSEventModifierFlags new_window_mods = 0;
static PyObject*
@@ -105,9 +110,9 @@ cocoa_set_new_window_trigger(PyObject *self UNUSED, PyObject *args) {
int mods, key;
if (!PyArg_ParseTuple(args, "ii", &mods, &key)) return NULL;
int nwm;
get_cocoa_key_equivalent(key, mods, &new_window_key, &nwm);
get_cocoa_key_equivalent(key, mods, new_window_key, sizeof(new_window_key), &nwm);
new_window_mods = nwm;
if (new_window_key) Py_RETURN_TRUE;
if (new_window_key[0]) Py_RETURN_TRUE;
Py_RETURN_FALSE;
}
@@ -118,11 +123,10 @@ get_dock_menu(id self UNUSED, SEL _cmd UNUSED, NSApplication *sender UNUSED) {
if (!dockMenu) {
GlobalMenuTarget *global_menu_target = [GlobalMenuTarget shared_instance];
dockMenu = [[NSMenu alloc] init];
NSMenuItem *newWindowItem =
[dockMenu addItemWithTitle:@"New OS window"
action:@selector(new_os_window:)
keyEquivalent:@""];
[newWindowItem setTarget:global_menu_target];
[[dockMenu addItemWithTitle:@"New OS window"
action:@selector(new_os_window:)
keyEquivalent:@""]
setTarget:global_menu_target];
}
return dockMenu;
}
@@ -249,17 +253,17 @@ cocoa_create_global_menu(void) {
action:@selector(orderFrontStandardAboutPanel:)
keyEquivalent:@""];
[appMenu addItem:[NSMenuItem separatorItem]];
NSMenuItem* preferences_menu_item = [[NSMenuItem alloc] initWithTitle:@"Preferences..." action:@selector(show_preferences:) keyEquivalent:@","], *new_os_window_menu_item = NULL;
[preferences_menu_item setTarget:global_menu_target];
[appMenu addItem:preferences_menu_item];
if (new_window_key) {
NSString *s = [NSString stringWithCharacters:&new_window_key length:1];
new_os_window_menu_item = [[NSMenuItem alloc] initWithTitle:@"New OS window" action:@selector(new_os_window:) keyEquivalent:s];
[new_os_window_menu_item setKeyEquivalentModifierMask:new_window_mods];
[new_os_window_menu_item setTarget:global_menu_target];
[appMenu addItem:new_os_window_menu_item];
[s release];
}
[[appMenu addItemWithTitle:@"Preferences..."
action:@selector(show_preferences:)
keyEquivalent:@","]
setTarget:global_menu_target];
NSMenuItem* new_os_window_menu_item =
[appMenu addItemWithTitle:@"New OS window"
action:@selector(new_os_window:)
keyEquivalent:@(new_window_key)];
[new_os_window_menu_item setKeyEquivalentModifierMask:new_window_mods];
[new_os_window_menu_item setTarget:global_menu_target];
[appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", app_name]
@@ -313,10 +317,18 @@ cocoa_create_global_menu(void) {
setKeyEquivalentModifierMask:NSEventModifierFlagControl | NSEventModifierFlagCommand];
[NSApp setWindowsMenu:windowMenu];
[windowMenu release];
[preferences_menu_item release];
if (new_os_window_menu_item) {
[new_os_window_menu_item release];
}
NSMenuItem* helpMenuItem =
[bar addItemWithTitle:@"Help"
action:NULL
keyEquivalent:@""];
NSMenu* helpMenu = [[NSMenu alloc] initWithTitle:@"Help"];
[helpMenuItem setSubmenu:helpMenu];
[[helpMenu addItemWithTitle:[NSString stringWithFormat:@"Visit %@ website", app_name]
action:@selector(open_kitty_website_url:)
keyEquivalent:@"?"]
setTarget:global_menu_target];
[helpMenu release];
[bar release];
@@ -474,7 +486,6 @@ cleanup() {
dockMenu = nil;
if (notification_activated_callback) Py_DECREF(notification_activated_callback);
notification_activated_callback = NULL;
} // autoreleasepool
}
@@ -489,6 +500,30 @@ cocoa_hide_window_title(void *w)
} // autoreleasepool
}
void
cocoa_hide_titlebar(void *w)
{
@autoreleasepool {
cocoa_hide_window_title(w);
NSWindow *window = (NSWindow*)w;
NSButton *button;
button = [window standardWindowButton: NSWindowCloseButton];
if (button) button.hidden = true;
button = [window standardWindowButton: NSWindowMiniaturizeButton];
if (button) button.hidden = true;
button = [window standardWindowButton: NSWindowZoomButton];
if (button) button.hidden = true;
[window setTitlebarAppearsTransparent:YES];
[window setStyleMask:
[window styleMask] | NSWindowStyleMaskFullSizeContentView];
} // autoreleasepool
}
static PyMethodDef module_methods[] = {
{"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""},
{"cocoa_set_new_window_trigger", (PyCFunction)cocoa_set_new_window_trigger, METH_VARARGS, ""},

View File

@@ -6,7 +6,7 @@
*/
#define EXTRA_INIT if (PyModule_AddFunctions(module, module_methods) != 0) return false;
#include "data-types.h"
#include "state.h"
#include <structmember.h>
PyTypeObject ColorProfile_Type;
@@ -69,6 +69,9 @@ new(PyTypeObject *type, PyObject UNUSED *args, PyObject UNUSED *kwds) {
init_FG_BG_table();
memcpy(self->color_table, FG_BG_256, sizeof(FG_BG_256));
memcpy(self->orig_color_table, FG_BG_256, sizeof(FG_BG_256));
#define S(which) self->mark_foregrounds[which] = OPT(mark##which##_foreground); self->mark_backgrounds[which] = OPT(mark##which##_background)
S(1); S(2); S(3);
#undef S
self->dirty = true;
}
return (PyObject*) self;
@@ -107,6 +110,32 @@ copy_color_profile(ColorProfile *dest, ColorProfile *src) {
dest->dirty = true;
}
static inline void
patch_color_table(const char *key, PyObject *profiles, PyObject *spec, size_t which, int change_configured) {
PyObject *v = PyDict_GetItemString(spec, key);
if (v) {
color_type color = PyLong_AsUnsignedLong(v);
for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) {
ColorProfile *self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j);
self->color_table[which] = color;
if (change_configured) self->orig_color_table[which] = color;
self->dirty = true;
}
}
}
#define patch_mark_color(key, profiles, spec, array, i) { \
PyObject *v = PyDict_GetItemString(spec, key); \
if (v) { \
color_type color = PyLong_AsUnsignedLong(v); \
for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) { \
ColorProfile *self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j); \
self->array[i] = color; \
self->dirty = true; \
} } }
static PyObject*
patch_color_profiles(PyObject *module UNUSED, PyObject *args) {
PyObject *spec, *profiles, *v; ColorProfile *self; int change_configured; PyObject *cursor_text_color;
@@ -114,16 +143,12 @@ patch_color_profiles(PyObject *module UNUSED, PyObject *args) {
char key[32] = {0};
for (size_t i = 0; i < arraysz(FG_BG_256); i++) {
snprintf(key, sizeof(key) - 1, "color%zu", i);
v = PyDict_GetItemString(spec, key);
if (v) {
color_type color = PyLong_AsUnsignedLong(v);
for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) {
self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j);
self->color_table[i] = color;
if (change_configured) self->orig_color_table[i] = color;
self->dirty = true;
}
}
patch_color_table(key, profiles, spec, i, change_configured);
}
for (size_t i = 1; i <= MARK_MASK; i++) {
#define S(which, i) snprintf(key, sizeof(key) - 1, "mark%zu_" #which, i); patch_mark_color(key, profiles, spec, mark_##which##s, i)
S(background, i); S(foreground, i);
#undef S
}
#define S(config_name, profile_name) { \
v = PyDict_GetItemString(spec, #config_name); \
@@ -282,8 +307,13 @@ void
copy_color_table_to_buffer(ColorProfile *self, color_type *buf, int offset, size_t stride) {
size_t i;
stride = MAX(1u, stride);
for (i = 0, buf = buf + offset; i < sizeof(self->color_table)/sizeof(self->color_table[0]); i++, buf += stride) {
*buf = self->color_table[i];
for (i = 0, buf = buf + offset; i < arraysz(self->color_table); i++, buf += stride) *buf = self->color_table[i];
// Copy the mark colors
for (i = 0; i < arraysz(self->mark_backgrounds); i++) {
*buf = self->mark_backgrounds[i]; buf += stride;
}
for (i = 0; i < arraysz(self->mark_foregrounds); i++) {
*buf = self->mark_foregrounds[i]; buf += stride;
}
self->dirty = false;
}

View File

@@ -5,11 +5,17 @@
import os
import shlex
import sys
from functools import lru_cache
from typing import (
Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple
)
from kittens.runner import get_kitten_cli_docs, all_kitten_names
from kittens.runner import all_kitten_names, get_kitten_cli_docs
from .cli import options_for_completion, parse_option_spec
from .cmds import cmap
from .cli import (
OptionDict, OptionSpecSeq, options_for_completion, parse_option_spec
)
from .rc.base import all_command_names, command_for_name
from .shell import options_for_cmd
'''
@@ -31,27 +37,35 @@ taking the results from kitty's completion system and converting
them into something your shell will understand.
'''
parsers, serializers = {}, {}
parsers: Dict[str, Callable] = {}
serializers: Dict[str, Callable] = {}
MathGroup = Dict[str, str]
def debug(*a, **kw):
def debug(*a: Any, **kw: Any) -> None:
kw['file'] = sys.stderr
print(*a, **kw)
class Completions:
def __init__(self):
self.match_groups = {}
self.no_space_groups = set()
self.files_groups = set()
def __init__(self) -> None:
self.match_groups: Dict[str, MathGroup] = {}
self.no_space_groups: Set[str] = set()
self.files_groups: Set[str] = set()
@lru_cache(maxsize=2)
def remote_control_command_names() -> Tuple[str, ...]:
return tuple(sorted(x.replace('_', '-') for x in all_command_names()))
# Shell specific code {{{
completion_scripts = {
'zsh': '''
'zsh': '''#compdef kitty
_kitty() {
local src
# Send all words up to the word the cursor is currently on
@@ -86,40 +100,44 @@ complete -f -c kitty -a "(__kitty_completions)"
''',
}
ParseResult = Tuple[List[str], bool]
ParserFunc = Callable[[str], ParseResult]
SerializerFunc = Callable[[Completions], str]
def input_parser(func):
def input_parser(func: ParserFunc) -> ParserFunc:
name = func.__name__.split('_')[0]
parsers[name] = func
return func
def output_serializer(func):
def output_serializer(func: SerializerFunc) -> SerializerFunc:
name = func.__name__.split('_')[0]
serializers[name] = func
return func
@input_parser
def zsh_input_parser(data):
def zsh_input_parser(data: str) -> ParseResult:
new_word = data.endswith('\n\n')
words = data.rstrip().splitlines()
return words, new_word
@input_parser
def bash_input_parser(data):
def bash_input_parser(data: str) -> ParseResult:
new_word = data.endswith('\n\n')
words = data.rstrip().splitlines()
return words, new_word
@input_parser
def fish_input_parser(data):
def fish_input_parser(data: str) -> ParseResult:
return data.rstrip().splitlines(), True
@output_serializer
def zsh_output_serializer(ans):
def zsh_output_serializer(ans: Completions) -> str:
lines = []
for description, matches in ans.match_groups.items():
cmd = ['compadd', '-U', '-J', shlex.quote(description), '-X', shlex.quote(description)]
@@ -132,7 +150,7 @@ def zsh_output_serializer(ans):
cmd.extend(('-p', shlex.quote(common_prefix)))
matches = {k[len(common_prefix):]: v for k, v in matches.items()}
cmd.append('--')
for word, description in matches.items():
for word in matches:
cmd.append(shlex.quote(word))
lines.append(' '.join(cmd) + ';')
# debug('\n'.join(lines))
@@ -140,7 +158,7 @@ def zsh_output_serializer(ans):
@output_serializer
def bash_output_serializer(ans):
def bash_output_serializer(ans: Completions) -> str:
lines = []
for description, matches in ans.match_groups.items():
needs_space = description not in ans.no_space_groups
@@ -153,7 +171,7 @@ def bash_output_serializer(ans):
@output_serializer
def fish_output_serializer(ans):
def fish_output_serializer(ans: Completions) -> str:
lines = []
for matches in ans.match_groups.values():
for word in matches:
@@ -163,16 +181,16 @@ def fish_output_serializer(ans):
# }}}
def completions_for_first_word(ans, prefix, entry_points, namespaced_entry_points):
cmds = ['@' + c for c in cmap]
def completions_for_first_word(ans: Completions, prefix: str, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> None:
cmds = ['@' + c for c in remote_control_command_names()]
ans.match_groups['Entry points'] = {
k: None for k in
k: '' for k in
list(entry_points) + cmds + ['+' + k for k in namespaced_entry_points]
if not prefix or k.startswith(prefix)
}
def kitty_cli_opts(ans, prefix=None):
def kitty_cli_opts(ans: Completions, prefix: Optional[str] = None) -> None:
matches = {}
for opt in options_for_completion():
if isinstance(opt, str):
@@ -183,7 +201,7 @@ def kitty_cli_opts(ans, prefix=None):
ans.match_groups['Options'] = matches
def complete_kitty_cli_arg(ans, opt, prefix):
def complete_kitty_cli_arg(ans: Completions, opt: Optional[OptionDict], prefix: str) -> None:
prefix = prefix or ''
if not opt:
return
@@ -191,11 +209,11 @@ def complete_kitty_cli_arg(ans, opt, prefix):
if dest == 'override':
from kitty.config import option_names_for_completion
k = 'Config directives'
ans.match_groups[k] = {k+'=': None for k in option_names_for_completion() if k.startswith(prefix)}
ans.match_groups[k] = {k+'=': '' for k in option_names_for_completion() if k.startswith(prefix)}
ans.no_space_groups.add(k)
elif dest == 'config':
def is_conf_file(x):
def is_conf_file(x: str) -> bool:
if os.path.isdir(x):
return True
return x.lower().endswith('.conf')
@@ -218,14 +236,23 @@ def complete_kitty_cli_arg(ans, opt, prefix):
complete_files_and_dirs(ans, prefix[len('unix:'):], files_group_name='UNIX sockets', add_prefix='unix:')
def complete_alias_map(ans, words, new_word, option_map, complete_args=None):
CompleteArgsFunc = Callable[[Completions, Optional[OptionDict], str], None]
def complete_alias_map(
ans: Completions,
words: Sequence[str],
new_word: str,
option_map: Dict[str, OptionDict],
complete_args: Optional[CompleteArgsFunc] = None
) -> None:
expecting_arg = False
opt = None
opt: Optional[OptionDict] = None
last_word = words[-1] if words else ''
for w in words:
if expecting_arg:
if w is last_word and not new_word:
if opt is not None:
if opt is not None and complete_args is not None:
complete_args(ans, opt, w)
return
expecting_arg = False
@@ -253,7 +280,13 @@ def complete_alias_map(ans, words, new_word, option_map, complete_args=None):
ans.match_groups['Options'] = {k: opt['help'] for k, opt in option_map.items() if k.startswith(prefix)}
def complete_cli(ans, words, new_word, seq, complete_args=lambda *a: None):
def complete_cli(
ans: Completions,
words: Sequence[str],
new_word: str,
seq: OptionSpecSeq,
complete_args: Optional[CompleteArgsFunc] = None
) -> None:
option_map = {}
for opt in seq:
if not isinstance(opt, str):
@@ -262,14 +295,18 @@ def complete_cli(ans, words, new_word, seq, complete_args=lambda *a: None):
complete_alias_map(ans, words, new_word, option_map, complete_args)
def complete_remote_command(ans, cmd_name, words, new_word):
def complete_remote_command(ans: Completions, cmd_name: str, words: Sequence[str], new_word: str) -> None:
aliases, alias_map = options_for_cmd(cmd_name)
if not alias_map:
return
complete_alias_map(ans, words, new_word, alias_map)
args_completer: Optional[CompleteArgsFunc] = None
args_completion = command_for_name(cmd_name).args_completion
if args_completion and 'files' in args_completion:
args_completer = remote_files_completer(args_completion['files'])
complete_alias_map(ans, words, new_word, alias_map, complete_args=args_completer)
def path_completion(prefix=''):
def path_completion(prefix: str = '') -> Tuple[List[str], List[str]]:
prefix = prefix.replace(r'\ ', ' ')
dirs, files = [], []
base = '.'
@@ -280,7 +317,7 @@ def path_completion(prefix=''):
src = os.path.expandvars(os.path.expanduser(base))
src_prefix = os.path.abspath(os.path.expandvars(os.path.expanduser(prefix))) if prefix else ''
try:
items = os.scandir(src)
items: Iterable[os.DirEntry] = os.scandir(src)
except FileNotFoundError:
items = ()
for x in items:
@@ -299,45 +336,70 @@ def path_completion(prefix=''):
return dirs, files
def complete_files_and_dirs(ans, prefix, files_group_name='Files', predicate=None, add_prefix=None):
dirs, files = path_completion(prefix or '')
files = filter(predicate, files)
def complete_files_and_dirs(
ans: Completions,
prefix: str,
files_group_name: str = 'Files',
predicate: Optional[Callable[[str], bool]] = None,
add_prefix: Optional[str] = None
) -> None:
dirs, files_ = path_completion(prefix or '')
files = filter(predicate, files_)
if add_prefix:
dirs = (add_prefix + x for x in dirs)
dirs = list(add_prefix + x for x in dirs)
files = (add_prefix + x for x in files)
if dirs:
ans.match_groups['Directories'] = dict.fromkeys(dirs)
ans.files_groups.add('Directories'), ans.no_space_groups.add('Directories')
ans.files_groups.add('Directories')
ans.no_space_groups.add('Directories')
if files:
ans.match_groups[files_group_name] = dict.fromkeys(files)
ans.files_groups.add(files_group_name)
def complete_icat_args(ans, opt, prefix):
def complete_icat_args(ans: Completions, opt: Optional[OptionDict], prefix: str) -> None:
from mimetypes import guess_type
def icat_file_predicate(filename):
def icat_file_predicate(filename: str) -> bool:
mt = guess_type(filename)[0]
if mt and mt.startswith('image/'):
return True
return False
if opt is None:
complete_files_and_dirs(ans, prefix, 'Images', icat_file_predicate)
def config_file_predicate(filename):
def remote_files_completer(spec: Tuple[str, Tuple[str, ...]]) -> CompleteArgsFunc:
name, matchers = spec
def complete_files_map(ans: Completions, opt: Optional[OptionDict], prefix: str) -> None:
def predicate(filename: str) -> bool:
for m in matchers:
if isinstance(m, str):
from fnmatch import fnmatch
return fnmatch(filename, m)
return False
if opt is None:
complete_files_and_dirs(ans, prefix, name, predicate)
return complete_files_map
def config_file_predicate(filename: str) -> bool:
return filename.endswith('.conf')
def complete_diff_args(ans, opt, prefix):
def complete_diff_args(ans: Completions, opt: Optional[OptionDict], prefix: str) -> None:
if opt is None:
complete_files_and_dirs(ans, prefix, 'Files')
elif opt['dest'] == 'config':
complete_files_and_dirs(ans, prefix, 'Config Files', config_file_predicate)
def complete_kitten(ans, kitten, words, new_word):
def complete_kitten(ans: Completions, kitten: str, words: Sequence[str], new_word: str) -> None:
try:
cd = get_kitten_cli_docs(kitten)
except SystemExit:
@@ -357,7 +419,7 @@ def complete_kitten(ans, kitten, words, new_word):
}.get(kitten))
def find_completions(words, new_word, entry_points, namespaced_entry_points):
def find_completions(words: Sequence[str], new_word: str, entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> Completions:
ans = Completions()
if not words or words[0] != 'kitty':
return ans
@@ -370,20 +432,20 @@ def find_completions(words, new_word, entry_points, namespaced_entry_points):
if words[0] == '@':
if len(words) == 1 or (len(words) == 2 and not new_word):
prefix = words[1] if len(words) > 1 else ''
ans.match_groups['Remote control commands'] = {c: None for c in cmap if c.startswith(prefix)}
ans.match_groups['Remote control commands'] = {c: '' for c in remote_control_command_names() if c.startswith(prefix)}
else:
complete_remote_command(ans, words[1], words[2:], new_word)
return ans
if words[0].startswith('@'):
if len(words) == 1 and not new_word:
prefix = words[0]
ans.match_groups['Remote control commands'] = {'@' + c: None for c in cmap if c.startswith(prefix)}
ans.match_groups['Remote control commands'] = {'@' + c: '' for c in remote_control_command_names() if c.startswith(prefix)}
else:
complete_remote_command(ans, words[0][1:], words[1:], new_word)
if words[0] == '+':
if len(words) == 1 or (len(words) == 2 and not new_word):
prefix = words[1] if len(words) > 1 else ''
ans.match_groups['Entry points'] = {c: None for c in namespaced_entry_points if c.startswith(prefix)}
ans.match_groups['Entry points'] = {c: '' for c in namespaced_entry_points if c.startswith(prefix)}
else:
if words[1] == 'kitten':
if len(words) == 2 or (len(words) == 3 and not new_word):
@@ -398,7 +460,7 @@ def find_completions(words, new_word, entry_points, namespaced_entry_points):
ans.match_groups['Kittens'] = dict.fromkeys(all_kitten_names())
else:
prefix = words[0]
ans.match_groups['Entry points'] = {c: None for c in namespaced_entry_points if c.startswith(prefix)}
ans.match_groups['Entry points'] = {c: '' for c in namespaced_entry_points if c.startswith(prefix)}
else:
if len(words) == 2 and not new_word:
ans.match_groups['Kittens'] = dict.fromkeys(k for k in all_kitten_names() if k.startswith(words[1]))
@@ -411,11 +473,11 @@ def find_completions(words, new_word, entry_points, namespaced_entry_points):
return ans
def setup(cstyle):
def setup(cstyle: str) -> None:
print(completion_scripts[cstyle])
def main(args, entry_points, namespaced_entry_points):
def main(args: Sequence[str], entry_points: Iterable[str], namespaced_entry_points: Iterable[str]) -> None:
if not args:
raise SystemExit('Must specify completion style')
cstyle = args[0]

View File

@@ -4,11 +4,15 @@
import re
from functools import partial
from typing import (
Any, Callable, Dict, Generator, Iterable, List, Match, Optional, Sequence,
Set, Tuple, Union, cast, get_type_hints
)
from .utils import to_bool
def to_string(x):
def to_string(x: str) -> str:
return x
@@ -16,7 +20,7 @@ class Group:
__slots__ = 'name', 'short_text', 'start_text', 'end_text'
def __init__(self, name, short_text, start_text='', end_text=''):
def __init__(self, name: str, short_text: str, start_text: str = '', end_text: str = '') -> None:
self.name, self.short_text = name, short_text.strip()
self.start_text, self.end_text = start_text.strip(), end_text.strip()
@@ -25,7 +29,7 @@ class Option:
__slots__ = 'name', 'group', 'long_text', 'option_type', 'defval_as_string', 'add_to_default', 'add_to_docs', 'line'
def __init__(self, name, group, defval, option_type, long_text, add_to_default, add_to_docs):
def __init__(self, name: str, group: Group, defval: str, option_type: Any, long_text: str, add_to_default: bool, add_to_docs: bool):
self.name, self.group = name, group
self.long_text, self.option_type = long_text.strip(), option_type
self.defval_as_string = defval
@@ -33,12 +37,38 @@ class Option:
self.add_to_docs = add_to_docs
self.line = self.name + ' ' + self.defval_as_string
def type_definition(self, is_multiple: bool, imports: Set[Tuple[str, str]]) -> str:
def type_name(x: type) -> str:
ans = x.__name__
if x.__module__ and x.__module__ != 'builtins':
imports.add((x.__module__, x.__name__))
if is_multiple:
ans = 'typing.Dict[str, str]'
return ans
def option_type_as_str(x: Any) -> str:
if hasattr(x, '__name__'):
return type_name(x)
ans = repr(x)
ans = ans.replace('NoneType', 'None')
return ans
if type(self.option_type) is type:
return type_name(self.option_type)
th = get_type_hints(self.option_type)
try:
rettype = th['return']
except KeyError:
raise ValueError('The Option {} has an unknown option_type: {}'.format(self.name, self.option_type))
return option_type_as_str(rettype)
class Shortcut:
__slots__ = 'name', 'group', 'key', 'action_def', 'short_text', 'long_text', 'add_to_default', 'add_to_docs', 'line'
def __init__(self, name, group, key, action_def, short_text, long_text, add_to_default, add_to_docs):
def __init__(self, name: str, group: Group, key: str, action_def: str, short_text: str, long_text: str, add_to_default: bool, add_to_docs: bool):
self.name, self.group, self.key, self.action_def = name, group, key, action_def
self.short_text, self.long_text = short_text, long_text
self.add_to_default = add_to_default
@@ -47,15 +77,15 @@ class Shortcut:
def option(
all_options,
group,
name,
defval,
long_text='',
option_type=to_string,
add_to_default=True,
add_to_docs=True
):
all_options: Dict[str, Option],
group: Sequence[Group],
name: str,
defval: Any,
long_text: str = '',
option_type: Callable[[str], Any] = to_string,
add_to_default: bool = True,
add_to_docs: bool = True
) -> Option:
is_multiple = name.startswith('+')
if is_multiple:
name = name[1:]
@@ -80,33 +110,33 @@ def option(
def shortcut(
all_options,
group,
action_name,
key,
action_def,
short_text='',
long_text='',
add_to_default=True,
add_to_docs=True
):
all_options: Dict[str, List[Shortcut]],
group: Sequence[Group],
action_name: str,
key: str,
action_def: str,
short_text: str = '',
long_text: str = '',
add_to_default: bool = True,
add_to_docs: bool = True
) -> Shortcut:
ans = Shortcut(action_name, group[0], key, action_def, short_text, long_text, add_to_default, add_to_docs)
key = 'sc-' + action_name
all_options.setdefault(key, []).append(ans)
return ans
def option_func(all_options, all_groups):
all_groups = {k: Group(k, *v) for k, v in all_groups.items()}
group = [None]
def option_func(all_options: Dict[str, Any], all_groups: Dict[str, Sequence[str]]) -> Tuple[Callable, Callable, Callable[[str], None], Dict[str, Group]]:
all_groups_ = {k: Group(k, *v) for k, v in all_groups.items()}
group: List[Optional[Group]] = [None]
def change_group(name):
group[0] = all_groups[name]
def change_group(name: str) -> None:
group[0] = all_groups_[name]
return partial(option, all_options, group), partial(shortcut, all_options, group), change_group, all_groups
return partial(option, all_options, group), partial(shortcut, all_options, group), change_group, all_groups_
def merged_opts(all_options, opt, i):
def merged_opts(all_options: Sequence[Union[Option, Sequence[Shortcut]]], opt: Option, i: int) -> Generator[Option, None, None]:
yield opt
for k in range(i + 1, len(all_options)):
q = all_options[k]
@@ -118,21 +148,21 @@ def merged_opts(all_options, opt, i):
break
def remove_markup(text):
def remove_markup(text: str) -> str:
def sub(m):
def sub(m: Match) -> str:
if m.group(1) == 'ref':
return {
'layouts': 'https://sw.kovidgoyal.net/kitty/index.html#layouts',
'sessions': 'https://sw.kovidgoyal.net/kitty/index.html#sessions',
}[m.group(2)]
return m.group(2)
return cast(str, m.group(2))
return re.sub(r':([a-zA-Z0-9]+):`(.+?)`', sub, text, flags=re.DOTALL)
def iter_blocks(lines):
current_block = []
def iter_blocks(lines: Iterable[str]) -> Generator[Tuple[List[str], int], None, None]:
current_block: List[str] = []
prev_indent = 0
for line in lines:
indent_size = len(line) - len(line.lstrip())
@@ -149,13 +179,14 @@ def iter_blocks(lines):
yield current_block, indent_size
def wrapped_block(lines):
def wrapped_block(lines: Iterable[str]) -> Generator[str, None, None]:
wrapper = getattr(wrapped_block, 'wrapper', None)
if wrapper is None:
import textwrap
wrapper = wrapped_block.wrapper = textwrap.TextWrapper(
wrapper = textwrap.TextWrapper(
initial_indent='#: ', subsequent_indent='#: ', width=70, break_long_words=False
)
setattr(wrapped_block, 'wrapper', wrapper)
for block, indent_size in iter_blocks(lines):
if indent_size > 0:
for line in block:
@@ -168,20 +199,20 @@ def wrapped_block(lines):
yield line
def render_block(text):
def render_block(text: str) -> str:
text = remove_markup(text)
lines = text.splitlines()
return '\n'.join(wrapped_block(lines))
def as_conf_file(all_options):
def as_conf_file(all_options: Iterable[Union[Option, Sequence[Shortcut]]]) -> List[str]:
ans = ['# vim:fileencoding=utf-8:ft=conf:foldmethod=marker', '']
a = ans.append
current_group = None
current_group: Optional[Group] = None
num_open_folds = 0
all_options = list(all_options)
all_options_ = list(all_options)
def render_group(group, is_shortcut):
def render_group(group: Group, is_shortcut: bool) -> None:
nonlocal num_open_folds
if is_shortcut or '.' not in group.name:
a('#: ' + group.short_text + ' {{''{')
@@ -191,7 +222,7 @@ def as_conf_file(all_options):
a(render_block(group.start_text))
a('')
def handle_group_end(group, new_group_name='', new_group_is_shortcut=False):
def handle_group_end(group: Group, new_group_name: str = '', new_group_is_shortcut: bool = False) -> None:
nonlocal num_open_folds
if group.end_text:
a(''), a(render_block(group.end_text))
@@ -200,7 +231,7 @@ def as_conf_file(all_options):
a('#: }}''}'), a('')
num_open_folds -= 1
def handle_group(new_group, is_shortcut=False):
def handle_group(new_group: Group, is_shortcut: bool = False) -> None:
nonlocal current_group
if new_group is not current_group:
if current_group:
@@ -208,7 +239,7 @@ def as_conf_file(all_options):
current_group = new_group
render_group(current_group, is_shortcut)
def handle_shortcut(shortcuts):
def handle_shortcut(shortcuts: Sequence[Shortcut]) -> None:
handle_group(shortcuts[0].group, True)
for sc in shortcuts:
if sc.add_to_default:
@@ -216,11 +247,11 @@ def as_conf_file(all_options):
if sc.long_text:
a(''), a(render_block(sc.long_text.strip())), a('')
def handle_option(opt):
def handle_option(opt: Option) -> None:
if not opt.long_text or not opt.add_to_docs:
return
handle_group(opt.group)
mopts = list(merged_opts(all_options, opt, i))
mopts = list(merged_opts(all_options_, opt, i))
sz = max(len(x.name) for x in mopts)
for mo in mopts:
prefix = '' if mo.add_to_default else '# '
@@ -229,7 +260,7 @@ def as_conf_file(all_options):
a(render_block(opt.long_text))
a('')
for i, opt in enumerate(all_options):
for i, opt in enumerate(all_options_):
if isinstance(opt, Option):
handle_option(opt)
else:
@@ -242,18 +273,20 @@ def as_conf_file(all_options):
num_open_folds -= 1
map_groups = []
start = count = None
start: Optional[int] = None
count: Optional[int] = None
for i, line in enumerate(ans):
if line.startswith('map '):
if start is None:
start = i
count = 1
else:
count += 1
if count is not None:
count += 1
else:
if start is not None:
if start is not None and count is not None:
map_groups.append((start, count))
start = None
start = count = None
for start, count in map_groups:
r = range(start, start + count)
sz = max(len(ans[i].split(' ', 3)[1]) for i in r)
@@ -266,7 +299,9 @@ def as_conf_file(all_options):
return ans
def config_lines(all_options):
def config_lines(
all_options: Dict[str, Union[Option, Sequence[Shortcut]]],
) -> Generator[str, None, None]:
for opt in all_options.values():
if isinstance(opt, Option):
if opt.add_to_default:
@@ -275,3 +310,42 @@ def config_lines(all_options):
for sc in opt:
if sc.add_to_default:
yield sc.line
def as_type_stub(
all_options: Dict[str, Union[Option, Sequence[Shortcut]]],
special_types: Optional[Dict[str, str]] = None,
preamble_lines: Union[Tuple[str, ...], List[str], Iterable[str]] = (),
extra_fields: Union[Tuple[Tuple[str, str], ...], List[Tuple[str, str]], Iterable[Tuple[str, str]]] = (),
class_name: str = 'Options'
) -> str:
ans = ['import typing\n'] + list(preamble_lines) + ['', 'class {}:'.format(class_name)]
imports: Set[Tuple[str, str]] = set()
overrides = special_types or {}
for name, val in all_options.items():
if isinstance(val, Option):
is_multiple = ' ' in name
field_name = name.partition(' ')[0]
ans.append(' {}: {}'.format(field_name, overrides.get(field_name, val.type_definition(is_multiple, imports))))
for mod, name in imports:
ans.insert(0, 'from {} import {}'.format(mod, name))
ans.insert(0, 'import {}'.format(mod))
for field_name, type_def in extra_fields:
ans.append(' {}: {}'.format(field_name, type_def))
ans.append(' def __iter__(self) -> typing.Iterator[str]: pass')
ans.append(' def __len__(self) -> int: pass')
ans.append(' def __getitem__(self, k: typing.Union[int, str]) -> typing.Any: pass')
ans.append(' def _replace(self, **kw: typing.Any) -> {}: pass'.format(class_name))
return '\n'.join(ans) + '\n\n\n'
def save_type_stub(text: str, fpath: str) -> None:
fpath += 'i'
preamble = '# Update this file by running: ./test.py mypy\n\n'
try:
existing = open(fpath).read()
except FileNotFoundError:
existing = ''
current = preamble + text
if existing != current:
open(fpath, 'w').write(current)

View File

@@ -5,62 +5,89 @@
import os
import re
import shlex
from collections import namedtuple
from typing import (
Any, Callable, Dict, FrozenSet, Generator, Iterable, Iterator, List,
NamedTuple, Optional, Sequence, Tuple, Type, TypeVar, Union
)
from ..rgb import to_color as as_color
from ..rgb import Color, to_color as as_color
from ..utils import log_error
key_pat = re.compile(r'([a-zA-Z][a-zA-Z0-9_-]*)\s+(.+)$')
BadLine = namedtuple('BadLine', 'number line exception')
T = TypeVar('T')
def to_color(x):
return as_color(x, validate=True)
class BadLine(NamedTuple):
number: int
line: str
exception: Exception
def to_color_or_none(x):
def to_color(x: str) -> Color:
ans = as_color(x, validate=True)
if ans is None: # this is only for type-checking
ans = Color(0, 0, 0)
return ans
def to_color_or_none(x: str) -> Optional[Color]:
return None if x.lower() == 'none' else to_color(x)
def positive_int(x):
ConvertibleToNumbers = Union[str, bytes, int, float]
def positive_int(x: ConvertibleToNumbers) -> int:
return max(0, int(x))
def positive_float(x):
def positive_float(x: ConvertibleToNumbers) -> float:
return max(0, float(x))
def unit_float(x):
def unit_float(x: ConvertibleToNumbers) -> float:
return max(0, min(float(x), 1))
def to_bool(x):
def to_bool(x: str) -> bool:
return x.lower() in ('y', 'yes', 'true')
def to_cmdline(x):
return list(map(
lambda y: os.path.expandvars(os.path.expanduser(y)), shlex.split(x)))
def to_cmdline(x: str) -> List[str]:
return list(
map(
lambda y: os.path.expandvars(os.path.expanduser(y)),
shlex.split(x)
)
)
def python_string(text):
def python_string(text: str) -> str:
import ast
return ast.literal_eval("'''" + text.replace("'''", "'\\''") + "'''")
ans: str = ast.literal_eval("'''" + text.replace("'''", "'\\''") + "'''")
return ans
def choices(*choices):
def choices(*choices: str) -> Callable[[str], str]:
defval = choices[0]
choices = frozenset(choices)
uc = frozenset(choices)
def choice(x):
def choice(x: str) -> str:
x = x.lower()
if x not in choices:
if x not in uc:
x = defval
return x
return choice
def parse_line(line, type_map, special_handling, ans, all_keys, base_path_for_includes):
def parse_line(
line: str,
type_convert: Callable[[str, Any], Any],
special_handling: Callable,
ans: Dict[str, Any], all_keys: Optional[FrozenSet[str]],
base_path_for_includes: str
) -> None:
line = line.strip()
if not line or line.startswith('#'):
return
@@ -77,22 +104,32 @@ def parse_line(line, type_map, special_handling, ans, all_keys, base_path_for_in
val = os.path.join(base_path_for_includes, val)
try:
with open(val, encoding='utf-8', errors='replace') as include:
_parse(include, type_map, special_handling, ans, all_keys)
_parse(include, type_convert, special_handling, ans, all_keys)
except FileNotFoundError:
log_error('Could not find included config file: {}, ignoring'.format(val))
except EnvironmentError:
log_error('Could not read from included config file: {}, ignoring'.format(val))
log_error(
'Could not find included config file: {}, ignoring'.
format(val)
)
except OSError:
log_error(
'Could not read from included config file: {}, ignoring'.
format(val)
)
return
if all_keys is not None and key not in all_keys:
log_error('Ignoring unknown config key: {}'.format(key))
return
tm = type_map.get(key)
if tm is not None:
val = tm(val)
ans[key] = val
ans[key] = type_convert(key, val)
def _parse(lines, type_map, special_handling, ans, all_keys, accumulate_bad_lines=None):
def _parse(
lines: Iterable[str],
type_convert: Callable[[str, Any], Any],
special_handling: Callable,
ans: Dict[str, Any],
all_keys: Optional[FrozenSet[str]],
accumulate_bad_lines: Optional[List[BadLine]] = None
) -> None:
name = getattr(lines, 'name', None)
if name:
base_path_for_includes = os.path.dirname(os.path.abspath(name))
@@ -101,7 +138,10 @@ def _parse(lines, type_map, special_handling, ans, all_keys, accumulate_bad_line
base_path_for_includes = config_dir
for i, line in enumerate(lines):
try:
parse_line(line, type_map, special_handling, ans, all_keys, base_path_for_includes)
parse_line(
line, type_convert, special_handling, ans, all_keys,
base_path_for_includes
)
except Exception as e:
if accumulate_bad_lines is None:
raise
@@ -109,28 +149,35 @@ def _parse(lines, type_map, special_handling, ans, all_keys, accumulate_bad_line
def parse_config_base(
lines, defaults, type_map, special_handling, ans, check_keys=True,
accumulate_bad_lines=None
):
all_keys = defaults._asdict() if check_keys else None
_parse(lines, type_map, special_handling, ans, all_keys, accumulate_bad_lines)
lines: Iterable[str],
defaults: Any,
type_convert: Callable[[str, Any], Any],
special_handling: Callable,
ans: Dict[str, Any],
check_keys: bool = True,
accumulate_bad_lines: Optional[List[BadLine]] = None
) -> None:
all_keys: Optional[FrozenSet[str]] = defaults._asdict() if check_keys else None
_parse(
lines, type_convert, special_handling, ans, all_keys, accumulate_bad_lines
)
def create_options_class(keys):
keys = tuple(sorted(keys))
slots = keys + ('_fields',)
def create_options_class(all_keys: Iterable[str]) -> Type:
keys = tuple(sorted(all_keys))
slots = keys + ('_fields', )
def __init__(self, kw):
def __init__(self: Any, kw: Dict[str, Any]) -> None:
for k, v in kw.items():
setattr(self, k, v)
def __iter__(self):
def __iter__(self: Any) -> Iterator[str]:
return iter(keys)
def __len__(self):
def __len__(self: Any) -> int:
return len(keys)
def __getitem__(self, i):
def __getitem__(self: Any, i: Union[int, str]) -> Any:
if isinstance(i, int):
i = keys[i]
try:
@@ -138,29 +185,36 @@ def create_options_class(keys):
except AttributeError:
raise KeyError('No option named: {}'.format(i))
def _asdict(self):
def _asdict(self: Any) -> Dict[str, Any]:
return {k: getattr(self, k) for k in self._fields}
def _replace(self, **kw):
def _replace(self: Any, **kw: Dict) -> Any:
ans = self._asdict()
ans.update(kw)
return self.__class__(ans)
ans = type('Options', (), {
'__slots__': slots, '__init__': __init__, '_asdict': _asdict, '_replace': _replace, '__iter__': __iter__,
'__len__': __len__, '__getitem__': __getitem__
})
ans._fields = keys
ans = type(
'Options', (), {
'__slots__': slots,
'__init__': __init__,
'_asdict': _asdict,
'_replace': _replace,
'__iter__': __iter__,
'__len__': __len__,
'__getitem__': __getitem__
}
)
ans._fields = keys # type: ignore
return ans
def merge_dicts(defaults, newvals):
def merge_dicts(defaults: Dict, newvals: Dict) -> Dict:
ans = defaults.copy()
ans.update(newvals)
return ans
def resolve_config(SYSTEM_CONF, defconf, config_files_on_cmd_line):
def resolve_config(SYSTEM_CONF: str, defconf: str, config_files_on_cmd_line: Sequence[str]) -> Generator[str, None, None]:
if config_files_on_cmd_line:
if 'NONE' not in config_files_on_cmd_line:
yield SYSTEM_CONF
@@ -171,8 +225,15 @@ def resolve_config(SYSTEM_CONF, defconf, config_files_on_cmd_line):
yield defconf
def load_config(Options, defaults, parse_config, merge_configs, *paths, overrides=None):
ans = defaults._asdict()
def load_config(
Options: Type[T],
defaults: Any,
parse_config: Callable[[Iterable[str]], Dict[str, Any]],
merge_configs: Callable[[Dict, Dict], Dict],
*paths: str,
overrides: Optional[Iterable[str]] = None
) -> T:
ans: Dict = defaults._asdict()
for path in paths:
if not path:
continue
@@ -185,61 +246,70 @@ def load_config(Options, defaults, parse_config, merge_configs, *paths, override
if overrides is not None:
vals = parse_config(overrides)
ans = merge_configs(ans, vals)
return Options(ans)
return Options(ans) # type: ignore
def init_config(default_config_lines, parse_config):
def init_config(default_config_lines: Iterable[str], parse_config: Callable) -> Tuple[Type, Any]:
defaults = parse_config(default_config_lines, check_keys=False)
Options = create_options_class(defaults.keys())
defaults = Options(defaults)
return Options, defaults
def key_func():
ans = {}
def key_func() -> Tuple[Callable[..., Callable], Dict[str, Callable]]:
ans: Dict[str, Callable] = {}
def func_with_args(*names):
def func_with_args(*names: str) -> Callable:
def w(f):
def w(f: Callable) -> Callable:
for name in names:
if ans.setdefault(name, f) is not f:
raise ValueError('the args_func {} is being redefined'.format(name))
raise ValueError(
'the args_func {} is being redefined'.format(name)
)
return f
return w
return func_with_args, ans
def parse_kittens_shortcut(sc):
def parse_kittens_shortcut(sc: str) -> Tuple[Optional[int], str, bool]:
from ..key_encoding import config_key_map, config_mod_map, text_match
if sc.endswith('+'):
parts = list(filter(None, sc.rstrip('+').split('+') + ['+']))
else:
parts = sc.split('+')
mods = parts[:-1] or None
if mods is not None:
qmods = parts[:-1]
if qmods:
resolved_mods = 0
for mod in mods:
for mod in qmods:
m = config_mod_map.get(mod.upper())
if m is None:
raise ValueError('Unknown shortcut modifiers: {}'.format(sc))
resolved_mods |= m
mods = resolved_mods
mods: Optional[int] = resolved_mods
else:
mods = None
is_text = False
rkey = parts[-1]
tkey = text_match(rkey)
if tkey is None:
rkey = rkey.upper()
rkey = config_key_map.get(rkey)
if rkey is None:
q = config_key_map.get(rkey)
if q is None:
raise ValueError('Unknown shortcut key: {}'.format(sc))
rkey = q
else:
is_text = True
rkey = tkey
return mods, rkey, is_text
def parse_kittens_func_args(action, args_funcs):
KittensKeyAction = Tuple[str, Tuple[str, ...]]
def parse_kittens_func_args(action: str, args_funcs: Dict[str, Callable]) -> KittensKeyAction:
parts = action.strip().split(' ', 1)
func = parts[0]
if len(parts) == 1:
@@ -260,15 +330,20 @@ def parse_kittens_func_args(action, args_funcs):
raise ValueError('Unknown key action: {}'.format(action))
if not isinstance(args, (list, tuple)):
args = (args,)
args = (args, )
return func, tuple(args)
def parse_kittens_key(val, funcs_with_args):
KittensKey = Tuple[str, Optional[int], bool]
def parse_kittens_key(
val: str, funcs_with_args: Dict[str, Callable]
) -> Optional[Tuple[KittensKeyAction, KittensKey]]:
sc, action = val.partition(' ')[::2]
if not sc or not action:
return
return None
mods, key, is_text = parse_kittens_shortcut(sc)
action = parse_kittens_func_args(action, funcs_with_args)
return action, key, mods, is_text
ans = parse_kittens_func_args(action, funcs_with_args)
return ans, (key, mods, is_text)

View File

@@ -6,32 +6,46 @@ import json
import os
import re
import sys
from collections import namedtuple
from contextlib import contextmanager
from contextlib import suppress
from contextlib import contextmanager, suppress
from functools import partial
from typing import (
Any, Callable, Dict, Generator, Iterable, List, Match, NamedTuple,
Optional, Sequence, Set, Tuple, Type, Union
)
from . import fast_data_types as defines
from .conf.definition import as_conf_file, config_lines
from .conf.utils import (
init_config, key_func, load_config as _load_config, merge_dicts,
BadLine, init_config, key_func, load_config as _load_config, merge_dicts,
parse_config_base, python_string, to_bool, to_cmdline
)
from .config_data import all_options, parse_mods, type_map
from .config_data import all_options, parse_mods, type_convert
from .constants import cache_dir, defconf, is_macos
from .utils import log_error
from .key_names import get_key_name_lookup, key_name_aliases
from .options_stub import Options as OptionsStub
from .typing import TypedDict
from .utils import log_error
KeySpec = Tuple[int, bool, int]
KeyMap = Dict[KeySpec, 'KeyAction']
KeySequence = Tuple[KeySpec, ...]
SubSequenceMap = Dict[KeySequence, 'KeyAction']
SequenceMap = Dict[KeySpec, SubSequenceMap]
def parse_shortcut(sc):
class InvalidMods(ValueError):
pass
def parse_shortcut(sc: str) -> Tuple[int, bool, Optional[int]]:
parts = sc.split('+')
mods = 0
if len(parts) > 1:
mods = parse_mods(parts[:-1], sc)
if mods is None:
return None, None, None
key = parts[-1].upper()
key = getattr(defines, 'GLFW_KEY_' + key_name_aliases.get(key, key), None)
mods = parse_mods(parts[:-1], sc) or 0
if not mods:
raise InvalidMods('Invalid shortcut')
q = parts[-1].upper()
key: Optional[int] = getattr(defines, 'GLFW_KEY_' + key_name_aliases.get(q, q), None)
is_native = False
if key is None:
q = parts[-1]
@@ -39,13 +53,18 @@ def parse_shortcut(sc):
with suppress(Exception):
key = int(q, 16)
else:
key = get_key_name_lookup()(q)
key = get_key_name_lookup()(q, False)
is_native = key is not None
return mods, is_native, key
KeyAction = namedtuple('KeyAction', 'func args')
class KeyAction(NamedTuple):
func: str
args: Sequence[str]
func_with_args, args_funcs = key_func()
FuncArgsType = Tuple[str, Sequence[Any]]
@func_with_args(
@@ -53,12 +72,12 @@ func_with_args, args_funcs = key_func()
'new_window_with_cwd', 'new_tab_with_cwd', 'new_os_window_with_cwd',
'launch'
)
def shlex_parse(func, rest):
def shlex_parse(func: str, rest: str) -> FuncArgsType:
return func, to_cmdline(rest)
@func_with_args('combine')
def combine_parse(func, rest):
def combine_parse(func: str, rest: str) -> FuncArgsType:
sep, rest = rest.split(maxsplit=1)
parts = re.split(r'\s*' + re.escape(sep) + r'\s*', rest)
args = tuple(map(parse_key_action, filter(None, parts)))
@@ -66,19 +85,21 @@ def combine_parse(func, rest):
@func_with_args('send_text')
def send_text_parse(func, rest):
def send_text_parse(func: str, rest: str) -> FuncArgsType:
args = rest.split(maxsplit=1)
if len(args) > 0:
mode = ''
data = b''
if len(args) > 1:
mode = args[0]
try:
args[1] = parse_send_text_bytes(args[1])
data = parse_send_text_bytes(args[1])
except Exception:
log_error('Ignoring invalid send_text string: ' + args[1])
args[1] = ''
return func, args
return func, [mode, data]
@func_with_args('run_kitten', 'run_simple_kitten', 'kitten')
def kitten_parse(func, rest):
def kitten_parse(func: str, rest: str) -> FuncArgsType:
if func == 'kitten':
args = rest.split(maxsplit=1)
else:
@@ -88,74 +109,75 @@ def kitten_parse(func, rest):
@func_with_args('goto_tab')
def goto_tab_parse(func, rest):
def goto_tab_parse(func: str, rest: str) -> FuncArgsType:
args = (max(0, int(rest)), )
return func, args
@func_with_args('detach_window')
def detach_window_parse(func, rest):
def detach_window_parse(func: str, rest: str) -> FuncArgsType:
if rest not in ('new', 'new-tab', 'ask'):
log_error('Ignoring invalid detach_window argument: {}'.format(rest))
rest = 'new'
return func, (rest,)
@func_with_args('detach_tab')
def detach_tab_parse(func, rest):
def detach_tab_parse(func: str, rest: str) -> FuncArgsType:
if rest not in ('new', 'ask'):
log_error('Ignoring invalid detach_tab argument: {}'.format(rest))
rest = 'new'
return func, (rest,)
@func_with_args('set_background_opacity', 'goto_layout', 'kitty_shell')
def simple_parse(func, rest):
def simple_parse(func: str, rest: str) -> FuncArgsType:
return func, [rest]
@func_with_args('set_font_size')
def float_parse(func, rest):
def float_parse(func: str, rest: str) -> FuncArgsType:
return func, (float(rest),)
@func_with_args('change_font_size')
def parse_change_font_size(func, rest):
def parse_change_font_size(func: str, rest: str) -> Tuple[str, Tuple[bool, Optional[str], float]]:
vals = rest.strip().split(maxsplit=1)
if len(vals) != 2:
log_error('Invalid change_font_size specification: {}, treating it as default'.format(rest))
args = [True, None, 0]
else:
args = [vals[0].lower() == 'all', None, 0]
amt = vals[1]
if amt[0] in '+-':
args[1] = amt[0]
amt = amt[1:]
args[2] = float(amt.strip())
return func, args
return func, (True, None, 0)
c_all = vals[0].lower() == 'all'
sign: Optional[str] = None
amt = vals[1]
if amt[0] in '+-':
sign = amt[0]
amt = amt[1:]
return func, (c_all, sign, float(amt.strip()))
@func_with_args('clear_terminal')
def clear_terminal(func, rest):
def clear_terminal(func: str, rest: str) -> FuncArgsType:
vals = rest.strip().split(maxsplit=1)
if len(vals) != 2:
log_error('clear_terminal needs two arguments, using defaults')
args = ['reset', 'active']
args: List[Union[str, bool]] = ['reset', 'active']
else:
args = [vals[0].lower(), vals[1].lower() == 'active']
return func, args
@func_with_args('copy_to_buffer')
def copy_to_buffer(func, rest):
def copy_to_buffer(func: str, rest: str) -> FuncArgsType:
return func, [rest]
@func_with_args('paste_from_buffer')
def paste_from_buffer(func, rest):
def paste_from_buffer(func: str, rest: str) -> FuncArgsType:
return func, [rest]
@func_with_args('neighboring_window')
def neighboring_window(func, rest):
def neighboring_window(func: str, rest: str) -> FuncArgsType:
rest = rest.lower()
rest = {'up': 'top', 'down': 'bottom'}.get(rest, rest)
if rest not in ('left', 'right', 'top', 'bottom'):
@@ -165,7 +187,7 @@ def neighboring_window(func, rest):
@func_with_args('resize_window')
def resize_window(func, rest):
def resize_window(func: str, rest: str) -> FuncArgsType:
vals = rest.strip().split(maxsplit=1)
if len(vals) > 2:
log_error('resize_window needs one or two arguments, using defaults')
@@ -186,39 +208,40 @@ def resize_window(func, rest):
@func_with_args('move_window')
def move_window(func, rest):
def move_window(func: str, rest: str) -> FuncArgsType:
rest = rest.lower()
rest = {'up': 'top', 'down': 'bottom'}.get(rest, rest)
prest: Union[int, str] = rest
try:
rest = int(rest)
prest = int(prest)
except Exception:
if rest not in ('left', 'right', 'top', 'bottom'):
if prest not in ('left', 'right', 'top', 'bottom'):
log_error('Invalid move_window specification: {}'.format(rest))
rest = 0
return func, [rest]
prest = 0
return func, [prest]
@func_with_args('pipe')
def pipe(func, rest):
def pipe(func: str, rest: str) -> FuncArgsType:
import shlex
rest = shlex.split(rest)
if len(rest) < 3:
r = shlex.split(rest)
if len(r) < 3:
log_error('Too few arguments to pipe function')
rest = ['none', 'none', 'true']
return func, rest
r = ['none', 'none', 'true']
return func, r
@func_with_args('set_colors')
def set_colors(func, rest):
def set_colors(func: str, rest: str) -> FuncArgsType:
import shlex
rest = shlex.split(rest)
if len(rest) < 1:
r = shlex.split(rest)
if len(r) < 1:
log_error('Too few arguments to set_colors function')
return func, rest
return func, r
@func_with_args('nth_window')
def nth_window(func, rest):
def nth_window(func: str, rest: str) -> FuncArgsType:
try:
num = int(rest)
except Exception:
@@ -228,7 +251,7 @@ def nth_window(func, rest):
@func_with_args('disable_ligatures_in')
def disable_ligatures_in(func, rest):
def disable_ligatures_in(func: str, rest: str) -> FuncArgsType:
parts = rest.split(maxsplit=1)
if len(parts) == 1:
where, strategy = 'active', parts[0]
@@ -241,7 +264,67 @@ def disable_ligatures_in(func, rest):
return func, [where, strategy]
def parse_key_action(action):
@func_with_args('layout_action')
def layout_action(func: str, rest: str) -> FuncArgsType:
parts = rest.split(maxsplit=1)
if not parts:
raise ValueError('layout_action must have at least one argument')
return func, [parts[0], tuple(parts[1:])]
def parse_marker_spec(ftype: str, parts: Sequence[str]) -> Tuple[str, Union[str, Tuple[Tuple[int, str], ...]], int]:
flags = re.UNICODE
if ftype in ('text', 'itext', 'regex', 'iregex'):
if ftype.startswith('i'):
flags |= re.IGNORECASE
if not parts or len(parts) % 2 != 0:
raise ValueError('No color specified in marker: {}'.format(' '.join(parts)))
ans = []
for i in range(0, len(parts), 2):
try:
color = max(1, min(int(parts[i]), 3))
except Exception:
raise ValueError('color {} in marker specification is not an integer'.format(parts[i]))
sspec = parts[i + 1]
if 'regex' not in ftype:
sspec = re.escape(sspec)
ans.append((color, sspec))
ftype = 'regex'
spec: Union[str, Tuple[Tuple[int, str], ...]] = tuple(ans)
elif ftype == 'function':
spec = ' '.join(parts)
else:
raise ValueError('Unknown marker type: {}'.format(ftype))
return ftype, spec, flags
@func_with_args('toggle_marker')
def toggle_marker(func: str, rest: str) -> FuncArgsType:
parts = rest.split(maxsplit=1)
if len(parts) != 2:
raise ValueError('{} if not a valid marker specification'.format(rest))
ftype, spec = parts
parts = spec.split()
return func, list(parse_marker_spec(ftype, parts))
@func_with_args('scroll_to_mark')
def scroll_to_mark(func: str, rest: str) -> FuncArgsType:
parts = rest.split()
if not parts or not rest:
return func, [True, 0]
if len(parts) == 1:
q = parts[0].lower()
if q in ('prev', 'previous', 'next'):
return func, [q != 'next', 0]
try:
return func, [True, max(0, min(int(q), 3))]
except Exception:
raise ValueError('{} is not a valid scroll_to_mark destination'.format(rest))
return func, [parts[0] != 'next', max(0, min(int(parts[1]), 3))]
def parse_key_action(action: str) -> Optional[KeyAction]:
parts = action.strip().split(maxsplit=1)
func = parts[0]
if len(parts) == 1:
@@ -251,29 +334,30 @@ def parse_key_action(action):
if parser is not None:
try:
func, args = parser(func, rest)
except Exception:
log_error('Ignoring invalid key action: {}'.format(action))
except Exception as err:
log_error('Ignoring invalid key action: {} with err: {}'.format(action, err))
else:
return KeyAction(func, args)
return None
all_key_actions = set()
all_key_actions: Set[str] = set()
sequence_sep = '>'
class KeyDefinition:
def __init__(self, is_sequence, action, mods, is_native, key, rest=()):
def __init__(self, is_sequence: bool, action: KeyAction, mods: int, is_native: bool, key: int, rest: Tuple[KeySpec, ...] = ()):
self.is_sequence = is_sequence
self.action = action
self.trigger = mods, is_native, key
self.rest = rest
def resolve(self, kitty_mod):
def resolve(self, kitty_mod: int) -> None:
self.trigger = defines.resolve_key_mods(kitty_mod, self.trigger[0]), self.trigger[1], self.trigger[2]
self.rest = tuple((defines.resolve_key_mods(kitty_mod, mods), is_native, key) for mods, is_native, key in self.rest)
def resolve_kitten_aliases(self, aliases):
def resolve_kitten_aliases(self, aliases: Dict[str, Sequence[str]]) -> None:
if not self.action.args:
return
kitten = self.action.args[0]
@@ -289,7 +373,7 @@ class KeyDefinition:
self.action = self.action._replace(args=[kitten + (' ' + rest).rstrip()])
def parse_key(val, key_definitions):
def parse_key(val: str, key_definitions: List[KeyDefinition]) -> None:
parts = val.split(maxsplit=1)
if len(parts) != 2:
return
@@ -299,10 +383,13 @@ def parse_key(val, key_definitions):
return
is_sequence = sequence_sep in sc
if is_sequence:
trigger = None
rest = []
trigger: Optional[Tuple[int, bool, int]] = None
restl: List[Tuple[int, bool, int]] = []
for part in sc.split(sequence_sep):
mods, is_native, key = parse_shortcut(part)
try:
mods, is_native, key = parse_shortcut(part)
except InvalidMods:
return
if key is None:
if mods is not None:
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
@@ -310,10 +397,13 @@ def parse_key(val, key_definitions):
if trigger is None:
trigger = mods, is_native, key
else:
rest.append((mods, is_native, key))
rest = tuple(rest)
restl.append((mods, is_native, key))
rest = tuple(restl)
else:
mods, is_native, key = parse_shortcut(sc)
try:
mods, is_native, key = parse_shortcut(sc)
except InvalidMods:
return
if key is None:
if mods is not None:
log_error('Shortcut: {} has unknown key, ignoring'.format(sc))
@@ -327,16 +417,18 @@ def parse_key(val, key_definitions):
if paction is not None:
all_key_actions.add(paction.func)
if is_sequence:
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], trigger[2], rest))
if trigger is not None:
key_definitions.append(KeyDefinition(True, paction, trigger[0], trigger[1], trigger[2], rest))
else:
assert key is not None
key_definitions.append(KeyDefinition(False, paction, mods, is_native, key))
def parse_symbol_map(val):
def parse_symbol_map(val: str) -> Dict[Tuple[int, int], str]:
parts = val.split()
symbol_map = {}
symbol_map: Dict[Tuple[int, int], str] = {}
def abort():
def abort() -> Dict[Tuple[int, int], str]:
log_error('Symbol map: {} is invalid, ignoring'.format(
val))
return {}
@@ -345,17 +437,16 @@ def parse_symbol_map(val):
return abort()
family = ' '.join(parts[1:])
def to_chr(x):
def to_chr(x: str) -> int:
if not x.startswith('U+'):
raise ValueError()
x = int(x[2:], 16)
return x
return int(x[2:], 16)
for x in parts[0].split(','):
a, b = x.partition('-')[::2]
b = b or a
a_, b_ = x.partition('-')[::2]
b_ = b_ or a_
try:
a, b = map(to_chr, (a, b))
a, b = map(to_chr, (a_, b_))
except Exception:
return abort()
if b < a or max(a, b) > sys.maxunicode or min(a, b) < 1:
@@ -364,36 +455,36 @@ def parse_symbol_map(val):
return symbol_map
def parse_send_text_bytes(text):
def parse_send_text_bytes(text: str) -> bytes:
return python_string(text).encode('utf-8')
def parse_send_text(val, key_definitions):
def parse_send_text(val: str, key_definitions: List[KeyDefinition]) -> None:
parts = val.split(' ')
def abort(msg):
def abort(msg: str) -> None:
log_error('Send text: {} is invalid ({}), ignoring'.format(
val, msg))
return {}
if len(parts) < 3:
return abort('Incomplete')
mode, sc = parts[:2]
text = ' '.join(parts[2:])
key_str = '{} send_text {} {}'.format(sc, mode, text)
return parse_key(key_str, key_definitions)
parse_key(key_str, key_definitions)
special_handlers = {}
SpecialHandlerFunc = Callable[[str, str, Dict[str, Any]], None]
special_handlers: Dict[str, SpecialHandlerFunc] = {}
def special_handler(func):
def special_handler(func: SpecialHandlerFunc) -> SpecialHandlerFunc:
special_handlers[func.__name__.partition('_')[2]] = func
return func
def deprecated_handler(*names):
def special_handler(func):
def deprecated_handler(*names: str) -> Callable[[SpecialHandlerFunc], SpecialHandlerFunc]:
def special_handler(func: SpecialHandlerFunc) -> SpecialHandlerFunc:
for name in names:
special_handlers[name] = func
return func
@@ -401,38 +492,65 @@ def deprecated_handler(*names):
@special_handler
def handle_map(key, val, ans):
def handle_map(key: str, val: str, ans: Dict[str, Any]) -> None:
parse_key(val, ans['key_definitions'])
@special_handler
def handle_symbol_map(key, val, ans):
def handle_symbol_map(key: str, val: str, ans: Dict[str, Any]) -> None:
ans['symbol_map'].update(parse_symbol_map(val))
class FontFeature(str):
def __new__(cls, name: str, parsed: bytes) -> 'FontFeature':
ans: FontFeature = str.__new__(cls, name) # type: ignore
ans.parsed = parsed # type: ignore
return ans
@special_handler
def handle_kitten_alias(key, val, ans):
def handle_font_features(key: str, val: str, ans: Dict[str, Any]) -> None:
if val != 'none':
parts = val.split()
if len(parts) < 2:
log_error("Ignoring invalid font_features {}".format(val))
else:
features = []
for feat in parts[1:]:
try:
parsed = defines.parse_font_feature(feat)
except ValueError:
log_error('Ignoring invalid font feature: {}'.format(feat))
else:
features.append(FontFeature(feat, parsed))
if features:
ans['font_features'][parts[0]] = tuple(features)
@special_handler
def handle_kitten_alias(key: str, val: str, ans: Dict[str, Any]) -> None:
parts = val.split(maxsplit=2)
if len(parts) >= 2:
ans['kitten_aliases'][parts[0]] = parts[1:]
@special_handler
def handle_send_text(key, val, ans):
def handle_send_text(key: str, val: str, ans: Dict[str, Any]) -> None:
# For legacy compatibility
parse_send_text(val, ans['key_definitions'])
@special_handler
def handle_clear_all_shortcuts(key, val, ans):
def handle_clear_all_shortcuts(key: str, val: str, ans: Dict[str, Any]) -> None:
if to_bool(val):
ans['key_definitions'] = [None]
@deprecated_handler('x11_hide_window_decorations', 'macos_hide_titlebar')
def handle_deprecated_hide_window_decorations_aliases(key, val, ans):
def handle_deprecated_hide_window_decorations_aliases(key: str, val: str, ans: Dict[str, Any]) -> None:
if not hasattr(handle_deprecated_hide_window_decorations_aliases, key):
handle_deprecated_hide_window_decorations_aliases.key = True
setattr(handle_deprecated_hide_window_decorations_aliases, 'key', True)
log_error('The option {} is deprecated. Use hide_window_decorations instead.'.format(key))
if to_bool(val):
if is_macos and key == 'macos_hide_titlebar' or (not is_macos and key == 'x11_hide_window_decorations'):
@@ -440,9 +558,9 @@ def handle_deprecated_hide_window_decorations_aliases(key, val, ans):
@deprecated_handler('macos_show_window_title_in_menubar')
def handle_deprecated_macos_show_window_title_in_menubar_alias(key, val, ans):
def handle_deprecated_macos_show_window_title_in_menubar_alias(key: str, val: str, ans: Dict[str, Any]) -> None:
if not hasattr(handle_deprecated_macos_show_window_title_in_menubar_alias, key):
handle_deprecated_macos_show_window_title_in_menubar_alias.key = True
setattr(handle_deprecated_macos_show_window_title_in_menubar_alias, 'key', True)
log_error('The option {} is deprecated. Use macos_show_window_title_in menubar instead.'.format(key))
macos_show_window_title_in = ans.get('macos_show_window_title_in', 'all')
if to_bool(val):
@@ -458,9 +576,9 @@ def handle_deprecated_macos_show_window_title_in_menubar_alias(key, val, ans):
ans['macos_show_window_title_in'] = macos_show_window_title_in
def expandvars(val, env):
def expandvars(val: str, env: Dict[str, str]) -> str:
def sub(m):
def sub(m: Match) -> str:
key = m.group(1)
result = env.get(key)
if result is None:
@@ -473,33 +591,37 @@ def expandvars(val, env):
@special_handler
def handle_env(key, val, ans):
def handle_env(key: str, val: str, ans: Dict[str, Any]) -> None:
key, val = val.partition('=')[::2]
key, val = key.strip(), val.strip()
ans['env'][key] = expandvars(val, ans['env'])
def special_handling(key, val, ans):
def special_handling(key: str, val: str, ans: Dict[str, Any]) -> bool:
func = special_handlers.get(key)
if func is not None:
func(key, val, ans)
return True
defaults = None
def option_names_for_completion():
def option_names_for_completion() -> Generator[str, None, None]:
yield from defaults
yield from special_handlers
def parse_config(lines, check_keys=True, accumulate_bad_lines=None):
ans = {'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': [], 'env': {}, 'kitten_aliases': {}}
def parse_config(lines: Iterable[str], check_keys: bool = True, accumulate_bad_lines: Optional[List[BadLine]] = None) -> Dict[str, Any]:
ans: Dict[str, Any] = {
'symbol_map': {}, 'keymap': {}, 'sequence_map': {}, 'key_definitions': [],
'env': {}, 'kitten_aliases': {}, 'font_features': {}
}
defs: Optional[OptionsStub] = None
if check_keys:
defs = defaults
parse_config_base(
lines,
defaults,
type_map,
defs,
type_convert,
special_handling,
ans,
check_keys=check_keys,
@@ -508,12 +630,14 @@ def parse_config(lines, check_keys=True, accumulate_bad_lines=None):
return ans
def parse_defaults(lines, check_keys=False):
def parse_defaults(lines: Iterable[str], check_keys: bool = False) -> Dict[str, Any]:
ans = parse_config(lines, check_keys)
return ans
Options, defaults = init_config(config_lines(all_options), parse_defaults)
xc = init_config(config_lines(all_options), parse_defaults)
Options: Type[OptionsStub] = xc[0]
defaults: OptionsStub = xc[1]
actions = frozenset(all_key_actions) | frozenset(
'run_simple_kitten combine send_text goto_tab goto_layout set_font_size new_tab_with_cwd new_window_with_cwd new_os_window_with_cwd'.
split()
@@ -521,7 +645,7 @@ actions = frozenset(all_key_actions) | frozenset(
no_op_actions = frozenset({'noop', 'no-op', 'no_op'})
def merge_configs(defaults, vals):
def merge_configs(defaults: Dict, vals: Dict) -> Dict:
ans = {}
for k, v in defaults.items():
if isinstance(v, dict):
@@ -534,18 +658,18 @@ def merge_configs(defaults, vals):
return ans
def build_ansi_color_table(opts=defaults):
def build_ansi_color_table(opts: OptionsStub = defaults) -> List[int]:
def as_int(x):
def as_int(x: Tuple[int, int, int]) -> int:
return (x[0] << 16) | (x[1] << 8) | x[2]
def col(i):
def col(i: int) -> int:
return as_int(getattr(opts, 'color{}'.format(i)))
return list(map(col, range(256)))
def atomic_save(data, path):
def atomic_save(data: bytes, path: str) -> None:
import tempfile
fd, p = tempfile.mkstemp(dir=os.path.dirname(path), suffix='.tmp')
try:
@@ -563,9 +687,9 @@ def atomic_save(data, path):
@contextmanager
def cached_values_for(name):
def cached_values_for(name: str) -> Generator[Dict, None, None]:
cached_path = os.path.join(cache_dir(), name + '.json')
cached_values = {}
cached_values: Dict = {}
try:
with open(cached_path, 'rb') as f:
cached_values.update(json.loads(f.read().decode('utf-8')))
@@ -585,14 +709,14 @@ def cached_values_for(name):
err))
def initial_window_size_func(opts, cached_values):
def initial_window_size_func(opts: OptionsStub, cached_values: Dict) -> Callable[[int, int, float, float, float, float], Tuple[int, int]]:
if 'window-size' in cached_values and opts.remember_window_size:
ws = cached_values['window-size']
try:
w, h = map(int, ws)
def initial_window_size(*a):
def initial_window_size(*a: Any) -> Tuple[int, int]:
return w, h
return initial_window_size
except Exception:
@@ -601,7 +725,7 @@ def initial_window_size_func(opts, cached_values):
w, w_unit = opts.initial_window_width
h, h_unit = opts.initial_window_height
def get_window_size(cell_width, cell_height, dpi_x, dpi_y, xscale, yscale):
def get_window_size(cell_width: int, cell_height: int, dpi_x: float, dpi_y: float, xscale: float, yscale: float) -> Tuple[int, int]:
if not is_macos:
# scaling is not needed on Wayland, but is needed on macOS. Not
# sure about X11.
@@ -614,12 +738,12 @@ def initial_window_size_func(opts, cached_values):
height = cell_height * h / yscale + (dpi_y / 72) * (opts.window_margin_width + opts.window_padding_width) + 1
else:
height = h
return width, height
return int(width), int(height)
return get_window_size
def commented_out_default_config():
def commented_out_default_config() -> str:
ans = []
for line in as_conf_file(all_options.values()):
if line and line[0] != '#':
@@ -628,7 +752,7 @@ def commented_out_default_config():
return '\n'.join(ans)
def prepare_config_file_for_editing():
def prepare_config_file_for_editing() -> str:
if not os.path.exists(defconf):
d = os.path.dirname(defconf)
with suppress(FileExistsError):
@@ -638,20 +762,20 @@ def prepare_config_file_for_editing():
return defconf
def finalize_keys(opts):
defns = []
for d in opts.key_definitions:
def finalize_keys(opts: OptionsStub) -> None:
defns: List[KeyDefinition] = []
for d in getattr(opts, 'key_definitions'):
if d is None: # clear_all_shortcuts
defns = []
else:
defns.append(d)
kitten_aliases = opts.kitten_aliases
kitten_aliases: List[Dict[str, Sequence[str]]] = getattr(opts, 'kitten_aliases')
for d in defns:
d.resolve(opts.kitty_mod)
if kitten_aliases and d.action.func == 'kitten':
d.resolve_kitten_aliases(kitten_aliases)
keymap = {}
sequence_map = {}
keymap: KeyMap = {}
sequence_map: SequenceMap = {}
for defn in defns:
is_no_op = defn.action.func in no_op_actions
@@ -674,7 +798,7 @@ def finalize_keys(opts):
opts.sequence_map = sequence_map
def load_config(*paths, overrides=None, accumulate_bad_lines=None):
def load_config(*paths: str, overrides: Optional[Iterable[str]] = None, accumulate_bad_lines: Optional[List[BadLine]] = None) -> OptionsStub:
parser = parse_config
if accumulate_bad_lines is not None:
parser = partial(parse_config, accumulate_bad_lines=accumulate_bad_lines)
@@ -684,3 +808,19 @@ def load_config(*paths, overrides=None, accumulate_bad_lines=None):
log_error('Cannot use both macos_titlebar_color and background_opacity')
opts.macos_titlebar_color = 0
return opts
class KittyCommonOpts(TypedDict):
select_by_word_characters: str
open_url_with: List[str]
url_prefixes: Tuple[str, ...]
def common_opts_as_dict(opts: Optional[OptionsStub] = None) -> KittyCommonOpts:
if opts is None:
opts = defaults
return {
'select_by_word_characters': opts.select_by_word_characters,
'open_url_with': opts.open_url_with,
'url_prefixes': opts.url_prefixes,
}

View File

@@ -5,9 +5,13 @@
# Utils {{{
import os
from gettext import gettext as _
from typing import (
Any, Dict, FrozenSet, Iterable, List, Optional, Sequence, Set, Tuple,
TypeVar, Union
)
from . import fast_data_types as defines
from .conf.definition import option_func
from .conf.definition import Option, Shortcut, option_func
from .conf.utils import (
choices, positive_float, positive_int, to_bool, to_cmdline, to_color,
to_color_or_none, unit_float
@@ -15,7 +19,7 @@ from .conf.utils import (
from .constants import config_dir, is_macos
from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE
from .layout import all_layouts
from .rgb import color_as_int, color_as_sharp, color_from_int
from .rgb import Color, color_as_int, color_as_sharp, color_from_int
from .utils import log_error
MINIMUM_FONT_SIZE = 4
@@ -25,9 +29,9 @@ mod_map = {'CTRL': 'CONTROL', 'CMD': 'SUPER', '⌘': 'SUPER',
'': 'ALT', 'OPTION': 'ALT', 'KITTY_MOD': 'KITTY'}
def parse_mods(parts, sc):
def parse_mods(parts: Iterable[str], sc: str) -> Optional[int]:
def map_mod(m):
def map_mod(m: str) -> str:
return mod_map.get(m, m)
mods = 0
@@ -36,25 +40,28 @@ def parse_mods(parts, sc):
mods |= getattr(defines, 'GLFW_MOD_' + map_mod(m.upper()))
except AttributeError:
log_error('Shortcut: {} has unknown modifier, ignoring'.format(sc))
return
return None
return mods
def to_modifiers(val):
def to_modifiers(val: str) -> int:
return parse_mods(val.split('+'), val) or 0
def uniq(vals, result_type=list):
seen = set()
T = TypeVar('T')
def uniq(vals: Iterable[T]) -> List[T]:
seen: Set[T] = set()
seen_add = seen.add
return result_type(x for x in vals if x not in seen and not seen_add(x))
return [x for x in vals if x not in seen and not seen_add(x)]
# }}}
# Groups {{{
all_options = {}
all_options: Dict[str, Union[Option, Sequence[Shortcut]]] = {}
o, k, g, all_groups = option_func(all_options, {
@@ -84,19 +91,21 @@ as color16 to color255.''')
'shortcuts': [
_('Keyboard shortcuts'),
_('''\
For a list of key names, see: :link:`GLFW keys
<https://www.glfw.org/docs/latest/group__keys.html>`. The name to use is the part
after the :code:`GLFW_KEY_` prefix. For a list of modifier names, see:
For a list of key names, see: :link:`the GLFW key macros
<https://github.com/kovidgoyal/kitty/blob/master/glfw/glfw3.h#L349>`.
The name to use is the part after the :code:`GLFW_KEY_` prefix.
For a list of modifier names, see:
:link:`GLFW mods <https://www.glfw.org/docs/latest/group__mods.html>`
On Linux you can also use XKB key names to bind keys that are not supported by
GLFW. See :link:`XKB keys
<https://github.com/xkbcommon/libxkbcommon/blob/master/xkbcommon/xkbcommon-keysyms.h>`
for a list of key names. The name to use is the part after the :code:`XKB_KEY_`
prefix. Note that you should only use an XKB key name for keys that are not present
in the list of GLFW keys.
prefix. Note that you can only use an XKB key name for keys that are not known
as GLFW keys.
Finally, you can use raw system key codes to map keys. To see the system key code
Finally, you can use raw system key codes to map keys, again only for keys that are not
known as GLFW keys. To see the system key code
for a key, start kitty with the :option:`kitty --debug-keyboard` option. Then kitty will
output some debug text for every key event. In that text look for ``native_code``
the value of that becomes the key name in the shortcut. For example:
@@ -144,7 +153,8 @@ For example::
_('Tab management'), '',
_('''\
You can also create shortcuts to go to specific tabs, with 1 being the first
tab, 2 the second tab and -1 being the previously active tab::
tab, 2 the second tab and -1 being the previously active tab, and any number
larger than the last tab being the last tab::
map ctrl+alt+1 goto_tab 1
map ctrl+alt+2 goto_tab 2
@@ -217,14 +227,31 @@ o('italic_font', 'auto')
o('bold_italic_font', 'auto')
def to_font_size(x):
def to_font_size(x: str) -> float:
return max(MINIMUM_FONT_SIZE, float(x))
o('font_size', 11.0, long_text=_('Font size (in pts)'), option_type=to_font_size)
o('force_ltr', False, long_text=_("""
kitty does not support BIDI (bidirectional text), however, for RTL scripts,
words are automatically displayed in RTL. That is
to say, in an RTL script, the words "HELLO WORLD" display in kitty as "WORLD
HELLO", and if you try to select a substring of an RTL-shaped string, you will
get the character that would be there had the the string been LTR. For example,
assuming the Hebrew word ירושלים, selecting the character that on the screen
appears to be ם actually writes into the selection buffer the character י.
def adjust_line_height(x):
kitty's default behavior is useful in conjunction with a filter to reverse the
word order, however, if you wish to manipulate RTL glyphs, it can be very
challenging to work with, so this option is provided to turn it off.
Furthermore, this option can be used with the command line program
:link:`GNU FriBidi <https://github.com/fribidi/fribidi#executable>` to get BIDI
support, because it will force kitty to always treat the text as LTR, which
FriBidi expects for terminals."""))
def adjust_line_height(x: str) -> Union[int, float]:
if x.endswith('%'):
ans = float(x[:-1].strip()) / 100.0
if ans < 0:
@@ -245,7 +272,7 @@ o('adjust_column_width', 0, option_type=adjust_line_height)
o(
'+symbol_map',
'U+E0A0-U+E0A2,U+E0B0-U+E0B3 PowerlineSymbols',
'U+E0A0-U+E0A3,U+E0C0-U+E0C7 PowerlineSymbols',
add_to_default=False,
long_text=_('''
Map the specified unicode codepoints to a particular font. Useful if you need
@@ -260,7 +287,7 @@ Syntax is::
'''))
def disable_ligatures(x):
def disable_ligatures(x: str) -> int:
cmap = {'never': 0, 'cursor': 1, 'always': 2}
return cmap.get(x.lower(), 0)
@@ -276,14 +303,59 @@ or by defining shortcuts for it in kitty.conf, for example::
map alt+1 disable_ligatures_in active always
map alt+2 disable_ligatures_in all never
map alt+3 disable_ligatures_in tab cursor
'''))
o('font_features', 'none', long_text=_('''
Choose exactly which OpenType features to enable or disable. This is useful as
some fonts might have features worthwhile in a terminal. For example, Fira
Code Retina includes a discretionary feature, :code:`zero`, which in that font
changes the appearance of the zero (0), to make it more easily distinguishable
from Ø. Fira Code Retina also includes other discretionary features known as
Stylistic Sets which have the tags :code:`ss01` through :code:`ss20`.
Note that this code is indexed by PostScript name, and not the font
family. This allows you to define very precise feature settings; e.g. you can
disable a feature in the italic font but not in the regular font.
To get the PostScript name for a font, use :code:`kitty + list-fonts --psnames`::
$ kitty + list-fonts --psnames | grep Fira
Fira Code
Fira Code Bold (FiraCode-Bold)
Fira Code Light (FiraCode-Light)
Fira Code Medium (FiraCode-Medium)
Fira Code Regular (FiraCode-Regular)
Fira Code Retina (FiraCode-Retina)
The part in brackets is the PostScript name.
Enable alternate zero and oldstyle numerals::
font_features FiraCode-Retina +zero +onum
Enable only alternate zero::
font_features FiraCode-Retina +zero
Disable the normal ligatures, but keep the :code:`calt` feature which (in this
font) breaks up monotony::
font_features TT2020StyleB-Regular -liga +calt
In conjunction with :opt:`force_ltr`, you may want to disable Arabic shaping
entirely, and only look at their isolated forms if they show up in a document.
You can do this with e.g.::
font_features UnifontMedium +isol -medi -fina -init
'''))
def box_drawing_scale(x):
ans = tuple(float(x.strip()) for x in x.split(','))
def box_drawing_scale(x: str) -> Tuple[float, float, float, float]:
ans = tuple(float(q.strip()) for q in x.split(','))
if len(ans) != 4:
raise ValueError('Invalid box_drawing scale, must have four entries')
return ans
return ans[0], ans[1], ans[2], ans[3]
o(
@@ -308,7 +380,7 @@ cshapes = {
}
def to_cursor_shape(x):
def to_cursor_shape(x: str) -> int:
try:
return cshapes[x.lower()]
except KeyError:
@@ -319,9 +391,9 @@ def to_cursor_shape(x):
)
def cursor_text_color(x):
def cursor_text_color(x: str) -> Optional[Color]:
if x.lower() == 'background':
return
return None
return to_color(x)
@@ -331,6 +403,10 @@ Choose the color of text under the cursor. If you want it rendered with the
background color of the cell underneath instead, use the special keyword: background'''))
o('cursor_shape', 'block', option_type=to_cursor_shape, long_text=_(
'The cursor shape can be one of (block, beam, underline)'))
o('cursor_beam_thickness', 1.5, option_type=positive_float, long_text=_(
'Defines the thickness of the beam cursor (in pts)'))
o('cursor_underline_thickness', 2.0, option_type=positive_float, long_text=_(
'Defines the thickness of the underline cursor (in pts)'))
o('cursor_blink_interval', -1, option_type=float, long_text=_('''
The interval (in seconds) at which to blink the cursor. Set to zero to disable
blinking. Negative values mean use system default. Note that numbers smaller
@@ -346,14 +422,14 @@ inactivity. Set to zero to never stop blinking.
g('scrollback') # {{{
def scrollback_lines(x):
x = int(x)
if x < 0:
x = 2 ** 32 - 1
return x
def scrollback_lines(x: str) -> int:
ans = int(x)
if ans < 0:
ans = 2 ** 32 - 1
return ans
def scrollback_pager_history_size(x):
def scrollback_pager_history_size(x: str) -> int:
ans = int(max(0, float(x)) * 1024 * 1024)
return min(ans, 4096 * 1024 * 1024 - 1)
@@ -406,11 +482,11 @@ The color and style for highlighting URLs on mouse-over.
:code:`url_style` can be one of: none, single, double, curly'''))
def url_style(x):
return url_style.map.get(x, url_style.map['curly'])
def url_style(x: str) -> int:
return url_style_map.get(x, url_style_map['curly'])
url_style.map = dict(
url_style_map = dict(
((v, i) for i, v in enumerate('none single double curly'.split()))
)
@@ -427,7 +503,15 @@ The special value :code:`default` means to use the
operating system's default URL handler.'''))
def copy_on_select(raw):
def url_prefixes(x: str) -> Tuple[str, ...]:
return tuple(a.lower() for a in x.replace(',', ' ').split())
o('url_prefixes', 'http https file ftp', option_type=url_prefixes, long_text=_('''
The set of URL prefixes to look for when detecting a URL under the mouse cursor.'''))
def copy_on_select(raw: str) -> str:
q = raw.lower()
# boolean values special cased for backwards compat
if q in ('y', 'yes', 'true', 'clipboard'):
@@ -542,7 +626,7 @@ number of cells instead of pixels.
'''))
def window_size(val):
def window_size(val: str) -> Tuple[int, str]:
val = val.lower()
unit = 'cells' if val.endswith('c') else 'px'
return positive_int(val.rstrip('c')), unit
@@ -552,9 +636,9 @@ o('initial_window_width', '640', option_type=window_size)
o('initial_window_height', '400', option_type=window_size)
def to_layout_names(raw):
def to_layout_names(raw: str) -> List[str]:
parts = [x.strip().lower() for x in raw.split(',')]
ans = []
ans: List[str] = []
for p in parts:
if p in ('*', 'all'):
ans.extend(sorted(all_layouts))
@@ -569,7 +653,8 @@ def to_layout_names(raw):
o('enabled_layouts', '*', option_type=to_layout_names, long_text=_('''
The enabled window layouts. A comma separated list of layout names. The special
value :code:`all` means all layouts. The first listed layout will be used as the
startup layout. For a list of available layouts, see the :ref:`layouts`.
startup layout. Default configuration is all layouts in alphabetical order.
For a list of available layouts, see the :ref:`layouts`.
'''))
o('window_resize_step_cells', 2, option_type=positive_int, long_text=_('''
@@ -623,8 +708,18 @@ Fade the text in inactive windows by the specified amount (a number between
zero and one, with zero being fully faded).
'''))
o('hide_window_decorations', False, long_text=_('''
Hide the window decorations (title-bar and window borders).
def hide_window_decorations(x: str) -> int:
if x == 'titlebar-only':
return 0b10
if to_bool(x):
return 0b01
return 0b00
o('hide_window_decorations', 'no', option_type=hide_window_decorations, long_text=_('''
Hide the window decorations (title-bar and window borders) with :code:`yes`.
On macOS, :code:`titlebar-only` can be used to only hide the titlebar.
Whether this works and exactly what effect it has depends on the
window manager/operating system.
'''))
@@ -636,7 +731,7 @@ operating system sends events corresponding to the start and end
of a resize, this number is ignored.'''))
def resize_draw_strategy(x):
def resize_draw_strategy(x: str) -> int:
cmap = {'static': 0, 'scale': 1, 'blank': 2, 'size': 3}
return cmap.get(x.lower(), 0)
@@ -649,13 +744,20 @@ A value of :code:`blank` means draw a blank window.
A value of :code:`size` means show the window size in cells.
'''))
o('resize_in_steps', False, long_text=_('''
Resize the OS window in steps as large as the cells, instead of with the usual pixel accuracy.
Combined with an :opt:`initial_window_width` and :opt:`initial_window_height` in number of cells,
this option can be used to keep the margins as small as possible when resizing the OS window.
Note that this does not currently work on Wayland.
'''))
# }}}
g('tabbar') # {{{
default_tab_separator = ''
def tab_separator(x):
def tab_separator(x: str) -> str:
for q in '\'"':
if x.startswith(q) and x.endswith(q):
x = x[1:-1]
@@ -665,11 +767,11 @@ def tab_separator(x):
return x
def tab_bar_edge(x):
def tab_bar_edge(x: str) -> int:
return {'top': 1, 'bottom': 3}.get(x.lower(), 3)
def tab_font_style(x):
def tab_font_style(x: str) -> Tuple[bool, bool]:
return {
'bold-italic': (True, True),
'bold': (True, False),
@@ -689,7 +791,12 @@ In the fade style, each tab's edges fade into the background color, in the separ
separated by a configurable separator, and the powerline shows the tabs as a continuous line.
'''))
o('tab_bar_min_tabs', 2, option_type=lambda x: max(1, positive_int(x)), long_text=_('''
def tab_bar_min_tabs(x: str) -> int:
return max(1, positive_int(x))
o('tab_bar_min_tabs', 2, option_type=tab_bar_min_tabs, long_text=_('''
The minimum number of tabs that must exist before the tab bar is shown
'''))
@@ -701,7 +808,7 @@ of :code:`last` will switch to the right-most tab.
'''))
def tab_fade(x):
def tab_fade(x: str) -> Tuple[float, ...]:
return tuple(map(unit_float, x.split()))
@@ -717,11 +824,21 @@ o('tab_separator', '"{}"'.format(default_tab_separator), option_type=tab_separat
The separator between tabs in the tab bar when using :code:`separator` as the :opt:`tab_bar_style`.'''))
def active_tab_title_template(x):
def tab_title_template(x: str) -> str:
if x:
for q in '\'"':
if x.startswith(q) and x.endswith(q):
x = x[1:-1]
break
return x
def active_tab_title_template(x: str) -> Optional[str]:
x = tab_title_template(x)
return None if x == 'none' else x
o('tab_title_template', '{title}', long_text=_('''
o('tab_title_template', '"{title}"', option_type=tab_title_template, long_text=_('''
A template to render the tab title. The default just renders
the title. If you wish to include the tab-index as well,
use something like: :code:`{index}: {title}`. Useful
@@ -753,7 +870,8 @@ o('background_opacity', 1.0, option_type=unit_float, long_text=_('''
The opacity of the background. A number between 0 and 1, where 1 is opaque and
0 is fully transparent. This will only work if supported by the OS (for
instance, when using a compositor under X11). Note that it only sets the
default background color's opacity. This is so that things like the status bar
background color's opacity in cells that have the same background color as
the default terminal background. This is so that things like the status bar
in vim, powerline prompts, etc. still look good. But it means that if you use
a color theme with a background color in your editor, it will not be rendered
as transparent. Instead you should change the default background color in your
@@ -765,20 +883,49 @@ of windows set :opt:`dynamic_background_opacity` to :code:`yes` (this is off by
default as it has a performance cost)
'''))
def config_or_absolute_path(x: str) -> Optional[str]:
if x.lower() == 'none':
return None
x = os.path.expanduser(x)
x = os.path.expandvars(x)
if not os.path.isabs(x):
x = os.path.join(config_dir, x)
return x
o('background_image', 'none', option_type=config_or_absolute_path, long_text=_('''
Path to a background image. Must be in PNG format.'''))
o('background_image_layout', 'tiled', option_type=choices('tiled', 'scaled', 'mirror-tiled'), long_text=_('''
Whether to tile or scale the background image.'''))
o('background_image_linear', False, long_text=_('''
When background image is scaled, whether linear interpolation should be used.'''))
o('dynamic_background_opacity', False, long_text=_('''
Allow changing of the :opt:`background_opacity` dynamically, using either keyboard
shortcuts (:sc:`increase_background_opacity` and :sc:`decrease_background_opacity`)
or the remote control facility.
'''))
o('background_tint', 0.0, option_type=unit_float, long_text=_('''
How much to tint the background image by the background color. The tint is applied
only under the text area, not margin/borders. Makes it easier to read the text.
Tinting is done using the current background color for each window. This setting
applies only if :opt:`background_opacity` is set and transparent windows are supported
or :opt:`background_image` is set.
'''))
o('dim_opacity', 0.75, option_type=unit_float, long_text=_('''
How much to dim text that has the DIM/FAINT attribute set. One means no dimming and
zero means fully dimmed (i.e. invisible).'''))
def selection_foreground(x):
def selection_foreground(x: str) -> Optional[Color]:
if x.lower() != 'none':
return to_color(x)
return None
o('selection_foreground', '#000000', option_type=selection_foreground, long_text=_('''
@@ -811,6 +958,12 @@ o('color14', '#14ffff', option_type=to_color)
o('color7', '#dddddd', long_text=_('white'), option_type=to_color)
o('color15', '#ffffff', option_type=to_color)
o('mark1_foreground', 'black', long_text=_('Color for marks of type 1'), option_type=to_color)
o('mark1_background', '#98d3cb', long_text=_('Color for marks of type 1 (light steel blue)'), option_type=to_color)
o('mark2_foreground', 'black', long_text=_('Color for marks of type 2'), option_type=to_color)
o('mark2_background', '#f2dcd3', long_text=_('Color for marks of type 1 (beige)'), option_type=to_color)
o('mark3_foreground', 'black', long_text=_('Color for marks of type 3'), option_type=to_color)
o('mark3_background', '#f274bc', long_text=_('Color for marks of type 1 (violet)'), option_type=to_color)
dfctl = defines.default_color_table()
for i in range(16, 256):
o('color{}'.format(i), color_as_sharp(color_from_int(dfctl[i])), option_type=to_color, add_to_docs=False)
@@ -841,7 +994,7 @@ terminal can fail silently because their stdout/stderr/stdin no longer work.
'''))
def allow_remote_control(x):
def allow_remote_control(x: str) -> str:
if x != 'socket-only':
x = 'y' if to_bool(x) else 'n'
return x
@@ -879,17 +1032,7 @@ The default is to check every 24 hrs, set to zero to disable.
'''))
def startup_session(x):
if x.lower() == 'none':
return
x = os.path.expanduser(x)
x = os.path.expandvars(x)
if not os.path.isabs(x):
x = os.path.join(config_dir, x)
return x
o('startup_session', 'none', option_type=startup_session, long_text=_('''
o('startup_session', 'none', option_type=config_or_absolute_path, long_text=_('''
Path to a session file to use for all kitty instances. Can be overridden
by using the :option:`kitty --session` command line option for individual
instances. See :ref:`sessions` in the kitty documentation for details. Note
@@ -897,7 +1040,12 @@ that relative paths are interpreted with respect to the kitty config directory.
Environment variables in the path are expanded.
'''))
o('clipboard_control', 'write-clipboard write-primary', option_type=lambda x: frozenset(x.lower().split()), long_text=_('''
def clipboard_control(x: str) -> FrozenSet[str]:
return frozenset(x.lower().split())
o('clipboard_control', 'write-clipboard write-primary', option_type=clipboard_control, long_text=_('''
Allow programs running in kitty to read and write from the clipboard. You can
control exactly which actions are allowed. The set of possible actions is:
write-clipboard read-clipboard write-primary read-primary. You can
@@ -923,7 +1071,7 @@ key-presses, to colors, to various advanced features may not work.
g('os') # {{{
def macos_titlebar_color(x):
def macos_titlebar_color(x: str) -> int:
x = x.strip('"')
if x == 'system':
return 0
@@ -944,7 +1092,7 @@ probably better off just hiding the titlebar with :opt:`hide_window_decorations`
'''))
def macos_option_as_alt(x):
def macos_option_as_alt(x: str) -> int:
x = x.lower()
if x == 'both':
return 0b11
@@ -1037,7 +1185,8 @@ to zero for all mappings, including the builtin ones.
g('shortcuts.clipboard') # {{{
k('copy_to_clipboard', 'kitty_mod+c', 'copy_to_clipboard', _('Copy to clipboard'), long_text=_('''
There is also a :code:`copy_or_interrupt` action that can be optionally mapped to :kbd:`Ctrl+c`.
It will copy only if there is a selection and send an interrupt otherwise.'''))
It will copy only if there is a selection and send an interrupt otherwise. Similarly, :code:`copy_and_clear_or_interrupt`
will copy and clear the selection or send an interrupt if there is no selection.'''))
if is_macos:
k('copy_to_clipboard', 'cmd+c', 'copy_to_clipboard', _('Copy to clipboard'), add_to_docs=False)
k('paste_from_clipboard', 'kitty_mod+v', 'paste_from_clipboard', _('Paste from clipboard'))
@@ -1228,6 +1377,11 @@ k('insert_selected_hash', 'kitty_mod+p>h', 'kitten hints --type hash --program -
Select something that looks like a hash and insert it into the terminal.
Useful with git, which uses sha1 hashes to identify commits'''))
k('goto_file_line', 'kitty_mod+p>n', 'kitten hints --type linenum', _('Open the selected file at the selected line'), long_text=_('''
Select something that looks like :code:`filename:linenum` and open it in vim at
the specified line number.'''))
# }}}
g('shortcuts.misc') # {{{
@@ -1287,4 +1441,9 @@ the line (same as pressing the Home key)::
# }}}
# }}}
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

@@ -2,45 +2,67 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
import errno
import os
import pwd
import sys
import errno
from collections import namedtuple
from contextlib import suppress
from functools import lru_cache
from typing import NamedTuple, Optional, Set
appname = 'kitty'
version = (0, 15, 1)
str_version = '.'.join(map(str, version))
from .options_stub import Options
class Version(NamedTuple):
major: int
minor: int
patch: int
appname: str = 'kitty'
version: Version = Version(0, 17, 1)
str_version: str = '.'.join(map(str, version))
_plat = sys.platform.lower()
is_macos = 'darwin' in _plat
is_macos: bool = 'darwin' in _plat
base = os.path.dirname(os.path.abspath(__file__))
ScreenGeometry = namedtuple('ScreenGeometry', 'xstart ystart xnum ynum dx dy')
WindowGeometry = namedtuple('WindowGeometry', 'left top right bottom xnum ynum')
class ScreenGeometry(NamedTuple):
xstart: float
ystart: float
xnum: int
ynum: int
dx: float
dy: float
def kitty_exe():
ans = getattr(kitty_exe, 'ans', None)
if ans is None:
rpath = sys._xoptions.get('bundle_exe_dir')
if not rpath:
items = filter(None, os.environ.get('PATH', '').split(os.pathsep))
seen = set()
for candidate in items:
if candidate not in seen:
seen.add(candidate)
if os.access(os.path.join(candidate, 'kitty'), os.X_OK):
rpath = candidate
break
else:
rpath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'launcher')
ans = kitty_exe.ans = os.path.join(rpath, 'kitty')
return ans
class WindowGeometry(NamedTuple):
left: int
top: int
right: int
bottom: int
xnum: int
ynum: int
def _get_config_dir():
@lru_cache(maxsize=2)
def kitty_exe() -> str:
rpath = sys._xoptions.get('bundle_exe_dir')
if not rpath:
items = filter(None, os.environ.get('PATH', '').split(os.pathsep))
seen: Set[str] = set()
for candidate in items:
if candidate not in seen:
seen.add(candidate)
if os.access(os.path.join(candidate, 'kitty'), os.X_OK):
rpath = candidate
break
else:
rpath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'launcher')
return os.path.join(rpath, 'kitty')
def _get_config_dir() -> str:
if 'KITTY_CONFIG_DIRECTORY' in os.environ:
return os.path.abspath(os.path.expanduser(os.environ['KITTY_CONFIG_DIRECTORY']))
@@ -58,12 +80,12 @@ def _get_config_dir():
if os.access(q, os.W_OK) and os.path.exists(os.path.join(q, 'kitty.conf')):
return q
def make_tmp_conf():
def make_tmp_conf() -> None:
import tempfile
import atexit
ans = tempfile.mkdtemp(prefix='kitty-conf-')
def cleanup():
def cleanup() -> None:
import shutil
with suppress(Exception):
shutil.rmtree(ans)
@@ -89,7 +111,8 @@ del _get_config_dir
defconf = os.path.join(config_dir, 'kitty.conf')
def _get_cache_dir():
@lru_cache(maxsize=2)
def cache_dir() -> str:
if 'KITTY_CACHE_DIRECTORY' in os.environ:
candidate = os.path.abspath(os.environ['KITTY_CACHE_DIRECTORY'])
elif is_macos:
@@ -101,25 +124,11 @@ def _get_cache_dir():
return candidate
def cache_dir():
ans = getattr(cache_dir, 'ans', None)
if ans is None:
ans = cache_dir.ans = _get_cache_dir()
return ans
def get_boss():
return get_boss.boss
def set_boss(m):
from .fast_data_types import set_boss as set_c_boss
get_boss.boss = m
set_c_boss(m)
def wakeup():
get_boss.boss.child_monitor.wakeup()
def wakeup() -> None:
from .fast_data_types import get_boss
b = get_boss()
if b is not None:
b.child_monitor.wakeup()
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -135,11 +144,11 @@ except KeyError:
shell_path = '/bin/sh'
def glfw_path(module):
def glfw_path(module: str) -> str:
return os.path.join(base, 'glfw-{}.so'.format(module))
def detect_if_wayland_ok():
def detect_if_wayland_ok() -> bool:
if 'WAYLAND_DISPLAY' not in os.environ:
return False
if 'KITTY_DISABLE_WAYLAND' in os.environ:
@@ -169,11 +178,11 @@ def detect_if_wayland_ok():
return ans == b'YES'
def is_wayland(opts=None):
def is_wayland(opts: Optional[Options] = None) -> bool:
if is_macos:
return False
if opts is None:
return is_wayland.ans
return bool(getattr(is_wayland, 'ans'))
if opts.linux_display_server == 'auto':
ans = detect_if_wayland_ok()
else:
@@ -183,3 +192,9 @@ def is_wayland(opts=None):
supports_primary_selection = not is_macos
def running_in_kitty(set_val: Optional[bool] = None) -> bool:
if set_val is not None:
setattr(running_in_kitty, 'ans', set_val)
return bool(getattr(running_in_kitty, 'ans', False))

View File

@@ -92,10 +92,10 @@ font_descriptor_to_python(CTFontDescriptorRef descriptor) {
NSString *style = (NSString *) CTFontDescriptorCopyAttribute(descriptor, kCTFontStyleNameAttribute);
NSDictionary *traits = (NSDictionary *) CTFontDescriptorCopyAttribute(descriptor, kCTFontTraitsAttribute);
unsigned int straits = [traits[(id)kCTFontSymbolicTrait] unsignedIntValue];
NSNumber *weightVal = traits[(id)kCTFontWeightTrait];
NSNumber *widthVal = traits[(id)kCTFontWidthTrait];
float weightVal = [traits[(id)kCTFontWeightTrait] floatValue];
float widthVal = [traits[(id)kCTFontWidthTrait] floatValue];
PyObject *ans = Py_BuildValue("{ssssssss sOsOsO sfsfsI}",
PyObject *ans = Py_BuildValue("{ssssssss sOsOsOsOsOsO sfsfsI}",
"path", [[url path] UTF8String],
"postscript_name", [psName UTF8String],
"family", [family UTF8String],
@@ -104,9 +104,12 @@ font_descriptor_to_python(CTFontDescriptorRef descriptor) {
"bold", (straits & kCTFontBoldTrait) != 0 ? Py_True : Py_False,
"italic", (straits & kCTFontItalicTrait) != 0 ? Py_True : Py_False,
"monospace", (straits & kCTFontMonoSpaceTrait) != 0 ? Py_True : Py_False,
"expanded", (straits & kCTFontExpandedTrait) != 0 ? Py_True : Py_False,
"condensed", (straits & kCTFontCondensedTrait) != 0 ? Py_True : Py_False,
"color_glyphs", (straits & kCTFontColorGlyphsTrait) != 0 ? Py_True : Py_False,
"weight", [weightVal floatValue],
"width", [widthVal floatValue],
"weight", weightVal,
"width", widthVal,
"traits", straits
);
[url release];
@@ -457,11 +460,12 @@ render_simple_text_impl(PyObject *s, const char *text, unsigned int baseline) {
CTFontGetGlyphsForCharacters(font, chars, glyphs, num_chars);
CTFontGetAdvancesForGlyphs(font, kCTFontOrientationDefault, glyphs, advances, num_chars);
CGRect bounding_box = CTFontGetBoundingRectsForGlyphs(font, kCTFontOrientationDefault, glyphs, boxes, num_chars);
StringCanvas ans = { .width = 0, .height = (size_t)(2 * bounding_box.size.height) };
for (size_t i = 0, y = 0; i < num_chars; i++) {
positions[i] = CGPointMake(ans.width, y);
ans.width += advances[i].width; y += advances[i].height;
CGFloat x = 0, y = 0;
for (size_t i = 0; i < num_chars; i++) {
positions[i] = CGPointMake(x, y);
x += advances[i].width; y += advances[i].height;
}
StringCanvas ans = { .width = (size_t)ceil(x), .height = (size_t)(2 * bounding_box.size.height) };
ensure_render_space(ans.width, ans.height);
render_glyphs(font, ans.width, ans.height, baseline, num_chars);
ans.canvas = malloc(ans.width * ans.height);

View File

@@ -58,9 +58,9 @@ static PyObject*
redirect_std_streams(PyObject UNUSED *self, PyObject *args) {
char *devnull = NULL;
if (!PyArg_ParseTuple(args, "s", &devnull)) return NULL;
if (freopen(devnull, "r", stdin) == NULL) return PyErr_SetFromErrno(PyExc_EnvironmentError);
if (freopen(devnull, "w", stdout) == NULL) return PyErr_SetFromErrno(PyExc_EnvironmentError);
if (freopen(devnull, "w", stderr) == NULL) return PyErr_SetFromErrno(PyExc_EnvironmentError);
if (freopen(devnull, "r", stdin) == NULL) return PyErr_SetFromErrno(PyExc_OSError);
if (freopen(devnull, "w", stdout) == NULL) return PyErr_SetFromErrno(PyExc_OSError);
if (freopen(devnull, "w", stderr) == NULL) return PyErr_SetFromErrno(PyExc_OSError);
Py_RETURN_NONE;
}
@@ -251,6 +251,8 @@ PyInit_fast_data_types(void) {
PyModule_AddIntConstant(m, "STRIKETHROUGH", STRIKE_SHIFT);
PyModule_AddIntConstant(m, "DIM", DIM_SHIFT);
PyModule_AddIntConstant(m, "DECORATION", DECORATION_SHIFT);
PyModule_AddIntConstant(m, "MARK", MARK_SHIFT);
PyModule_AddIntConstant(m, "MARK_MASK", MARK_MASK);
PyModule_AddStringMacro(m, ERROR_PREFIX);
#ifdef KITTY_VCS_REV
PyModule_AddStringMacro(m, KITTY_VCS_REV);

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